关于这个系列

作为 Lynxe(原JManus)的开发者,我在业余时间投入了大量精力来打磨这个Func-Agent框架,也因此对ReAct Based Agent有了更深入的认识。

之所以要整理这些内容,是因为这个项目的根本目标就是探索Agent领域的前沿实践,目前已经取得了一定成果,Lynxe能够处理我日常工作中80%以上的问题,因此我认为值得将实践中验证有效的方法分享出来,帮助大家快速上手。

你可以访问 Lynxe(菱科斯) 查看完整源代码,学习agent相关的实践方法。这是一个相当成熟的产品级 Func-Agent框架。

系列计划

正文开始

在之前的文章中,我们已经阐述了 ReAct Agent 的概念,以及 Agent 与传统编程、Workflow 之间的根本差异。

接下来我们要讨论一个备受关注的话题:AI Agent 的工具能力究竟指什么?Function Calling、MCP 和 Skills 这三者之间有何不同?它们背后的底层原理又是什么?

一句话总结

  • Function Calling:这是 AI Agent 调用工具的根本能力,同时也是后两者得以存在的前提。

  • MCP (Model Context Protocol):Anthropic 主导推动的一项开放标准,旨在为 LLM 应用提供统一的接口标准,用于连接和交互外部数据源和工具,目前已经捐赠给 Linux 基金会。

  • Skills:Anthropic Claude 推出的一项新功能,允许用户通过文字更精细地定义指令、脚本和资源,与 MCP 存在竞合关系,我们将在后文从多个维度分析这种竞合关系(尽管很多人认为它们是互补的,但实际上竞争关系更为明显)

为什么需要这些技术:理解工具调用的基础

为了说明这几个概念为何是竞合关系,我们首先需要简要了解 AI Agent 工具调用的底层原理。

AI Agent工具调用的基本流程

典型的 AI Agent 工具调用流程如下:

  1. LLM 接收用户请求和工具描述

    • 用户提出需求(例如"帮我查一下北京今天的天气")
    • 系统向 LLM 提供可用工具的清单和说明(例如"天气查询工具:可以查询指定城市的天气信息")
  2. LLM 决定是否需要调用工具

    • LLM 基于用户需求和工具描述,判断是否需要调用工具
    • 如果需要,LLM 会生成结构化的工具调用请求

    关键在于 LLM 返回的是结构化的 JSON 格式,而非自然语言。例如用户说"帮我查一下北京今天的天气",LLM 可能会返回:

    {
      "id": "chatcmpl-abc123",
      "object": "chat.completion",
      "choices": [
        {
          "index": 0,
          "message": {
            "role": "assistant",
            "content": null,
            "tool_calls": [
              {
                "id": "call_abc123",
                "type": "function",
                "function": {
                  "name": "get_weather",
                  "arguments": "{\"city\": \"北京\", \"date\": \"today\"}"
                }
              }
            ]
          },
          "finish_reason": "tool_calls"
        }
      ]
    }
    

    这种结构化的输出格式,正是 Function Calling 的核心机制。它使得系统能够可靠地解析 LLM 的意图,无需复杂的文本解析逻辑。需要注意的关键字段包括:

    • tool_calls:当需要调用工具时,此处包含工具调用的相关信息
    • function.name:需要调用的工具名称
    • function.arguments:工具的参数(采用 JSON 字符串格式)
  3. 系统解析并执行工具调用

    • 系统解析 LLM 生成的工具调用请求
    • 执行对应的工具函数(例如调用天气 API)
    • 获取工具执行结果

继续以上面的 LLM 返回为例:

上述 JSON 格式会被系统解析并转换为实际的函数调用。以 JavaScript 为例:

// 1. 从LLM响应中提取工具调用信息
const toolCall = response.choices[0].message.tool_calls[0];
const functionName = toolCall.function.name;  // "get_weather"
const functionArgs = JSON.parse(toolCall.function.arguments);  // {city: "北京", date: "today"}

// 2. 根据工具名称找到对应的函数
const tools = {
  get_weather: (city, date) => {
    // 执行天气查询逻辑
    return `北京今天天气:25°C,晴天`;
  },
  // ... 其他工具
};

// 3. 执行工具调用
const result = tools[functionName](functionArgs.city, functionArgs.date);
// 实际调用:tools["get_weather"]("北京", "today")

这个过程是自动完成的:系统根据 function.name 定位对应的函数,解析 function.arguments 获取参数,随后执行调用。这就是 Function Calling 使工具调用变得可预测和可靠的核心机制。

  1. 将结果返回给 LLM
    • 工具执行结果被返回给 LLM
    • LLM 根据结果决定下一步行动(继续调用工具,或者生成最终回答)

小结:工具调用的本质

这个流程的核心在于:LLM 需要将用户的非结构化需求(一段自然语言文本)转换为结构化的函数调用(函数名和参数),随后与其他应用程序交互,再将结构化结果返回给模型,使模型能够基于这些结果进行下一步决策。

问题的本质在于,历史上其他系统(数据库、API、文件系统等)只能处理结构化信息,而 LLM 擅长处理非结构化信息(文本)。因此,LLM 必须在两种信息形式之间建立桥梁:将非结构化的用户需求转换为结构化的函数调用,这样才能与外部系统交互。

这就是 Function Calling 的本质,同时也是 MCP 和 Skills 能够存在的前提条件。

既然已经有了工具调用,为什么还会出现 MCP 和 Skills?

Function Calling 确实解决了核心问题:使 LLM 能够稳定地输出结构化的工具调用请求,实现了"非结构化→结构化"的转换。这是 AI Agent 工具能力的基础。

但在实际应用中,开发者很快发现了新的问题:工具集成成本过高

Function Calling 存在工具集成成本高的问题

现实世界中,存在大量的既有系统和数据:数据库存储着业务数据,文件系统包含各种文档和代码,GitHub 上有项目仓库和 Issue,钉钉里有团队沟通记录,还有各种 API 服务提供实时数据。这些既有系统中蕴含着丰富的信息,如果能让 LLM 直接使用这些系统和数据,AI Agent 的能力将显著增强。

但问题是:如何让 LLM 能够使用这些既有系统?

在 Function Calling 的框架下,每个既有系统都需要单独集成到应用中。每个组织或公司都有自己的 API、认证方式、数据格式,开发者需要为每个组织或公司编写对应的函数实现。这就是 MCP 产生的原因:提供一种服务,可以让既有系统快速集成到 LLM 中。

MCP 的核心实际上还是基于 Function Calling 的。它做的事情很简单:将 Function Calling 的调用,在客户端转换成一套 JSON+HTTP 的请求。然后提供一套 Server 来响应这个 JSON+HTTP 请求,这样就能实现各类应用都可以被 LLM 使用的效果。

LLM -> Function Calling -> MCP Client -> JSON+HTTP请求 -> MCP Server -> 既有系统(GitHub/Slack/数据库等)
                                                                    ↓
LLM <- Function Calling结果 <- MCP Client <- JSON响应 <- MCP Server <- 既有系统返回结果

但 MCP 解决了工具集成的问题后,又出现了另一个问题。

Function Calling 和 MCP 都存在任务流程定义困难的问题

在实际使用中,用户经常需要让 AI Agent 按照特定的方式执行任务。例如,格式化 Excel 表格要按照公司的品牌指南,法律审查要遵循特定的合规性要求,数据分析要按照组织的工作流程。这些任务往往需要复杂的提示词和多个步骤的组合。

但在 Function Calling 和 MCP 的框架下,用户面临一个两难的问题:当前的大模型很难仅仅依托自己的模型能力就做出最优的工具调用步骤。很多任务需要特定的执行顺序、规则和约束,但把这些步骤全部写成代码又不太现实。就像我们在第一篇文章里讲的,模型的核心优势是面对不确定性时可以走一步看一步,动态调整策略。如果全部落成程序,就会丧失模型的核心优势。

举个例子,我们以 Lynxe 实际在运行的一个 new_branch 流程定义为例,这个流程用文字写到一个 markdown 里面,每次都让模型遵照执行:

1) 确认本地的 VERSION 与 pom.xml 与 本地branch 中的版本一致,不一致的话以pom.xml为准
2) mvn package 
3) 进入 ui-vue3 运行pnpm lint 
4) 退回项目目录, git merge upstream/main
5) 项目目录,运行 make ui-deploy
6) git 提交 branch到origin
7) git 打包 tag名字与pom的版本号一致,先删除远程tag(如果存在):git push upstream :refs/tags/v{版本号},然后上传tag到 upstream (上传之前请先用git remote 看一下upstream是哪里,确认是spring-ai-alibaba/JManus)

这个流程有 7 个步骤,每个步骤都有特定的顺序、条件和规则。如果完全写成代码,每一步都要处理各种异常情况(例如版本不一致、tag 已存在、upstream 地址不对等),代码会变得非常复杂。但如果只给模型一个简单的提示词"帮我创建新分支",模型可能无法按照这个精确的流程执行,或者执行顺序不对。
而用文字表达,非常直接简单,而且实际运行过程中只有很小的概率会出错,非常爽。

这就是这个问题的本质:如何在尽可能准确的前提下,能让用户用文字(而非代码)指导模型按照特定的流程和规则执行任务?

这就是 Skills 产生的原因(其实也是 Lynxe 的 Func-Agent 产生的核心原因):提供一种方式,让用户可以用文字定义指令、脚本和资源,形成可复用的任务流程。

Skills 的核心实际上也是基于 Function Calling 的。它做的事情很巧妙:通过一个固化的函数和参数,让模型去查找和加载固定的 skills 文档。

这里的关键是,Skills 完全依赖于 Function Calling 这个基础能力。 如果没有 Function Calling,Skills 就无法工作。Skills 只是在 Function Calling 之上的一个巧妙应用:将"加载文档"这个操作封装成一个函数,然后让 Claude 在需要时自动调用。

具体工作流程如下:

  1. 初始化阶段:用户用文字定义指令、脚本和资源,打包成 Skills(包含 SKILL.md 和可选的脚本、参考资料等)。Claude 在启动时会读取所有 Skills 的元数据(名称和描述),这些元数据被加载到模型的上下文中(每个约 100 token)。

  2. 发现阶段:当用户发起请求时,Claude 会根据请求内容,对比已加载的 Skills 元数据,判断是否需要使用某个 Skill。这个判断过程本质上就是 LLM 根据上下文做决策,跟 Function Calling 中判断是否需要调用工具是一样的。

  3. 加载阶段(Function Calling):如果 Claude 判断需要某个 Skill,它会通过 Function Calling 机制调用一个专门的加载函数(类似 load_skill(skill_name)),将对应的 SKILL.md 文档内容读取并加载到当前上下文中。这一步完全依赖 Function Calling 的能力。

  4. 执行阶段(继续使用 Function Calling)SKILL.md 的内容(包含指令、流程、示例等)被加入到上下文后,Claude 按照文档中定义的指令执行任务。如果 SKILL.md 中定义了需要执行脚本(例如 scripts/rotate_pdf.py),Claude 还是会通过 Function Calling调用执行脚本的函数。如果需要加载参考资料,同样是通过 Function Calling调用读取文件的函数。

可以看到,整个 Skills 的运行过程,从加载文档、执行脚本到读取资源,每一步都离不开 Function Calling。Skills 并没有创造新的能力,它只是将 Function Calling 这个基础能力组织成了一个更易用的形式:让用户可以用文字定义流程,让 Claude 自动发现和加载相关知识。从本质来说,它替代的是 MCP 调用的函数里面,过去可能会用代码写的一套串接各种 API 的逻辑流程,用这种方式,可以增强流程的适应性,其实也是呼应了我们第二篇文章的核心观点:Agent 将决策权完全下放给了 Agent 和 Prompt,能够解决原有写程序不能解决的问题——例如处理不确定性、动态调整策略、理解自然语言意图等。

[初始化]
用户定义Skills(SKILL.md + 脚本 + 资源) -> 打包成.skill文件 -> Claude启动时加载所有Skills元数据到上下文

[运行时]
用户发起请求 -> Claude判断是否需要Skill(skills 对暴露description 和skill_name)
                              ↓ (需要)
          Function Calling: 调用load_skill(skill_name) -> 将SKILL.md加载到上下文
                              ↓
          Claude按照SKILL.md中的指令执行任务
                              ↓
          Function Calling: 调用bash执行脚本 / 调用read_file读取资源 -> 完成任务
                              ↓
                         返回结果给用户

Function Calling、MCP、Skills的核心定位

通过前面的分析,我们可以看到 Function Calling、MCP 和 Skills 三者之间的本质关系:MCP 和 Skills 都是基于 Function Calling 的,它们只是在 Function Calling 这个基础能力之上的不同应用方式。

MCP 的核心是解决与既有系统的接驳问题:实际上,与外部系统接驳的方法并不只有 MCP 这一种——我们完全可以用 curl、bash 等传统方式来与程序接驳。MCP 的价值在于它提供了一套标准化的接驳协议,让不同的工具和数据源能够以统一的方式被 LLM 使用。通过 JSON-RPC 协议和标准化的工具描述格式,MCP 降低了工具集成的成本,让开发者不需要为每个系统单独编写集成代码。但本质上,MCP 更偏重是一套接驳标准,而不是唯一的接驳方式。

Skills 则实际上是一个 sub-agent 的包装:它让用户可以用文字来写流程,替代了过去在 MCP 调用的函数里用代码写的一套串接各种 API 的逻辑流程。这种方式可以增强流程的环境适应性——因为模型可以根据实际情况动态调整策略,处理不确定性,理解自然语言意图。这正是我们第二篇文章提到的核心观点:Agent 将决策权完全下放给了模型和 Prompt,能够解决原有写程序不能解决的问题。但代价就是不可能 100% 准确,因为模型的行为存在不确定性,无法像传统代码那样保证完全可预测的执行结果。

从本质来说,Function Calling 是基础能力,MCP 是在这个基础上提供标准化接驳方案,而 Skills 是在这个基础上提供文字化流程定义方案。三者共同构成了 AI Agent 工具能力的完整体系。

三者的总结性对比表

维度 Function Calling MCP (Model Context Protocol) Skills (Claude Skills)
定位/本质 AI Agent调用工具的基础能力,将非结构化需求转换为结构化函数调用 标准化接驳协议,提供统一的方式让LLM与外部系统交互 sub-agent的包装,让用户用文字定义可复用的任务流程
解决的问题 将用户的非结构化需求(自然语言)转换为结构化的函数调用(函数名和参数),实现LLM与外部系统的交互桥梁 工具集成成本高:每个既有系统都需要单独集成,每个组织都有自己的API、认证方式、数据格式 任务流程定义困难:需要特定执行顺序、规则和约束,但写成代码会丧失模型灵活性优势
实现方式 LLM生成结构化的JSON格式输出(tool_calls字段包含function.namefunction.arguments),系统解析并执行对应函数 基于Function Calling,将调用转换为JSON-RPC协议和HTTP请求;采用MCP Client/Server架构,通过标准化的工具描述格式(JSON Schema)定义工具 基于Function Calling,通过固化函数load_skill()查找和加载SKILL.md文档;采用渐进式披露机制(元数据→SKILL.md→资源),支持自动发现和按需加载
适用场景 所有工具调用的基础,任何需要LLM调用外部功能的场景 与既有系统接驳:GitHub、Slack、数据库、文件系统、各种API服务等需要统一接入的场景 需要特定流程和规则的任务:代码审查、部署流程、文档格式化、合规性检查等需要文字化流程定义的场景
替代方案 可以要求模型用固定格式输出替代,但因为模型对func-call做过很多优化,实际遵循程度来说FC还是目前最优解 可以用curl、bash调用等传统方式与调用既有程序,MCP更偏重是一套接驳标准而非唯一方式(这也是目前的实践,用bash掉python/java/ts其实比mcp更简单) 可以用代码替代(在MCP调用的函数里写代码串接各种API),但会失去处理不确定性、动态调整策略的能力

Lynxe 的实践与总结

首先,我们也认为 Agent 这种将决策委托给 LLM 的方式,是一种更有潜力的、能够提供完全不同体验的、面向未来的方案。通过 Function Calling、MCP 和 Skills 这些技术,我们看到了 AI Agent 工具能力的完整体系正在形成。

但我们也并不认为 Skills 就是终局。在 Lynxe 的开发实践中,我们发现 Skills 仍然有两个核心问题没有解决:

1. Skills 的需求描述部分不够结构化

Skills 仅仅通过 description 字段来描述 sub-agent 的要求,这会导致模型生成的信息非常容易不准确,从而导致 sub-agent(也就是 Skill)无法获得充分的信息,最终导致 sub-agent 无法达成用户期望。当任务复杂度增加时,纯文本描述的不确定性会放大,模型可能误解需求,或者遗漏关键信息。

2. Agent 无法与既有的系统接驳

Agent 仅仅只能通过聊天的方式与既有系统接驳,这种方式最后无论怎么做,都只能是个对话框。但真实的系统,远远不止有对话框这一种输入的方式。我们的大量表单都不是只有一个 textarea 的。现有的 Agent 方案很难集成到复杂的业务系统中,例如需要多步骤表单、需要实时数据展示、需要与现有 UI 组件交互的场景。

这就是 Lynxe 这套 Func-Agent 思路的原因。如果一句话来表达,就是:一切都是函数,函数才是第一公民

在 Lynxe 的设计中,我们让每个 Agent 能力都通过函数的方式暴露,这样就能更好地把 Agent 集成到既有的系统中,让它不再仅仅是一个对话框。通过函数化的接口,Lynxe 的 Func-Agent 可以:

  • 接收结构化的参数输入,而不是依赖纯文本描述
  • 返回结构化的结果,方便与现有系统集成
  • 支持多种调用方式,不仅仅是聊天界面
  • 与现有的业务逻辑、表单、API 无缝对接

这种方式既保留了 Agent 处理不确定性的核心优势,又解决了结构化输入输出和系统集成的问题,为 Agent 在实际业务场景中的应用提供了更可行的路径。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐