Agent 工具调用成长史:理清楚 Function Calling、MCP、CLI 的关系

Agent 工具调用成长史

工具的重要性

Agent 是一个能够感知环境并通过行动影响环境的实体。

Agent = LLM + 上下文 + 工具 + 约束 + 验证 + 纠正 = Model + Harness

工具是 Agent 的手脚:它不只是几个可调用的 API 函数,而是 Agent 能做的所有事情的集合

在 AI Coding 的时候,你是希望 Agent 直接来改代码,而不是复制 AI 的代码到处 ctrl cv,那么 Tools 系统就可以实现这个能力。

Toolformer(2023年初,meta)🚩

Toolformer 的目标是让语言模型自主学习如何调用外部工具(如计算器、搜索引擎、翻译系统、日历等),而无需大量人工标注数据。

核心是:让模型自己教自己用工具,回答的是:模型能否学会用工具?(能力问题)

回答的问题:模型怎么知道此时需要使用 Tool 、怎么学会使用工具

Toolformer、Gorilla、HuggingGPT 的具体方法,没有一个被今天的产品直接采用。 但这个阶段集体完成了一个关键认知转变

复制代码
 之前的假设:  模型不会用工具 → 需要精心设计人工规则
 之后的共识:  模型本身就有能力理解工具 → 关键是给它好的接口

OpenAI 的判断是:既然模型已经足够聪明到能理解工具描述,那就不需要 Toolformer 那种复杂的自监督训练了,直接给一个结构化接口就行。

基本原理

Toolformer 不改变模型的基础架构,它的核心做法是:将 API 调用和返回结果,都视为一种特殊的文本标记,插入到原始的训练语料中。

比如,一段普通的文本:"巴黎是法国的首都,拥有 210 万人口。" 经过 Toolformer 处理后,会变成:"巴黎是法国的首都,拥有 [Calculator(2.1 * 1000000) -> 2100000] 210 万人口。"

模型在学习预测下一个词时,不仅学会了自然语言,也学会了在合适的位置生成 [Calculator(...)] 这样的魔法咒语,来召唤工具并利用其结果。

整个过程就像让模型进行一场大规模的"自我对话",分四步走:

1. 探索与采样:用"试错"寻找工具的用武之地

首先,准备一个只有纯文本、没标注过工具的大数据集。然后,让一个"种子模型"(就是我们要训练增强的那个基础模型)在这个数据集上"通读"一遍。

关键在于,我们会用一种带"缺陷"的方式,随机在文本句子的头、中、尾部 ,强行插入 [Tool(...)] 标记,让模型补全具体的 API 调用参数,并拿到返回结果。

比如,随机抽取句子"奥运会每四年举办一次",在它前面加上 [Search(],让模型尝试补全,它可能会输出"奥运会)"并得到搜索结果 -> 奥林匹克运动会,是国际...。这样我们就收集到了成千上万个 [Search(奥运会) -> ...] 的生成片段。

2. 过滤筛选:留下真正能帮上忙的"好样本"

上一步是撒网,会捞出很多"没用"甚至"有害"的调用。比如,给一个常识句子强行加个搜索,搜出来的结果和模型自己知道的一样,这个调用就是多余的。

Toolformer 设计了一个巧妙的过滤函数,核心是衡量交叉熵损失

  • 有工具更好吗? 计算模型在看到工具返回结果后,预测后续真实文本的难度(困惑度)。
  • 没有工具会怎样? 再计算模型在没有工具帮助下,预测同样后续文本的难度。

如果"有工具"时的预测难度,显著低于"没有工具"时的难度,就说明这个工具调用真的帮模型减少了困惑,提供了有价值的信息。再通过一个预设的权重阈值,只保留那些能显著提升预测准确性的样本。

3. 数据增强:将成功的"咒语"植入原始文本

筛选出高质量的工具调用文本后,就把这些 [Tool(参数) -> 结果] 文本块,缝合进原始语料的对应位置,形成全新的、带有工具调用和结果的增强语料。

4. 模型微调:学习在语言中自然地使用工具

最后一步,用增强后的庞大数据集,对原始的种子模型进行标准的语言模型任务微调。模型就会学会:

  • 何时用工具 :在需要事实、计算、翻译等场景,自动生成 [Calculator(21 * 2)] 这样的调用。
  • 如何用结果 :在生成调用标记后,会自然地读取 -> 42 的结果,并把它作为上下文,继续生成后续的文本。
现代大模型如何学会工具调用:两阶段训练
训练阶段 训练目标 具体方法 解决的问题
监督微调 (SFT) 学会基本操作模式 使用大量"用户问题-工具调用-工具结果-最终回复"的四段式对话数据进行训练 让模型学会: 1. 何时切换输出模式(自然语言→工具调用) 2. 如何生成正确的工具调用格式 3. 如何整合工具结果生成回复
强化学习 (RLHF) 优化决策"分寸感" 基于人类反馈或环境奖励,优化工具调用的准确性 解决边界情况: 1. 该调工具时没调 2. 不该调工具时调了 3. 参数填错 4. 调用错工具

训练数据示例:

python 复制代码
 {
   "messages": [
     {"role": "user", "content": "今天北京天气怎么样?"},
     {"role": "assistant", "content": "[tool_call]{"name": "get_weather", "args": {"city": "北京"}}[/tool_call]"},
     {"role": "tool", "content": "{"temp": 25, "condition": "晴"}"},
     {"role": "assistant", "content": "今天北京天气晴朗,气温25度。"}
   ]
 }

模型通过大量这样的样本,学会了"用户问题涉及可调用的工具→输出工具调用格式"的模式

Function Calling(OpenAI,2023.6)🚩

OpenAI 通过微调(Fine-tuning),让模型在底层"原生"支持了调用外部工具的能力,这就是 Function Calling。

Function Calling 是一种 LLM 与外部工具之间的契约(contract):你定义哪些操作可用及其参数结构,模型决定何时调用它们并输出结构化请求,你的代码负责执行并返回结果。模型本身永远不直接执行任何操作。

回答的问题:工具调用的可靠性

将工具调用从"研究问题"变成"开发者 API"

它同时解决了三个问题:

  • 可靠性:结构化输出远比自由文本解析稳定
  • 通用性:任何能描述成 JSON Schema 的工具都能接入
  • 简洁性:开发者定义一个 schema 就完成注册,零训练成本
实现原理
  • 在调用LLM前,开发者需要预先定义好一系列可用的函数
  • 这些定义以JSON Schema的格式,通过系统提示词(System Prompt)提供给LLM,作为它的"工具手册"。
  • LLM 会决策是否调用 以及调用哪个函数
  • 如果LLM决定调用函数,它不会生成自然语言回复,而是输出一个严格结构化的JSON对象
  • 你的程序在本地或远程实际调用对应的函数代码,将函数执行的结果作为新的上下文,再次发送给LLM。
  • LLM将原始问题、函数调用结果整合,生成面向用户的自然语言回复
关键:结构化输入输出
json 复制代码
 // 调用 LLM 时的输入:工具清单
 {
   "name": "get_weather",
   "description": "获取指定城市的当前天气",
   "parameters": {
     "type": "object",
     "properties": {
       "city": { "type": "string", "description": "城市名" }
     },
     "required": ["city"]
   }
 }
 ​
 // LLM 的输出:如果LLM决定调用函数,它不会生成自然语言回复,而是一个**严格结构化**的JSON对象
 {
   "name": "get_weather",
   "arguments": { "city": "北京" }
 }
逐渐成为现代工具系统的标准规范

这一个设计,现在是事实标准。 OpenAI、Anthropic、Google、Mistral、阿里、字节------所有主流 LLM 厂商的工具调用都采用几乎相同的模式。

不同厂商有一些细微的区别。

不同厂商的特殊 Token

example:tool_call{"name":"get_weather"}/tool_call

厂商 开始标签 结束标签 特点
OpenAI <function_call> </function_call> XML 风格
Claude <tool_use> </tool_use> XML 风格
某些国产模型 [TOOL_CALL] [/TOOL_CALL] 方括号风格
本质都是 function calling,但没有统一

2023 年 OpenAI 推出功能时,行业还没有"标准"可言。 它只是 OpenAI 自家 API 的一个特性,不是 RFC,不是 W3C 标准,也不是行业联盟协议。其他家不可能也没义务直接照搬------尤其是直接竞争对手(如 Anthropic),照搬就意味着把自己的 API 设计主导权拱手让人。

OpenAI 与 Anthropic 的消息架构完全不同

OpenAI 的早期路线(Chat Completion API)

  • 消息内容是纯文本 content: "string" 或 null。
  • 工具调用是一个独立于 content 的顶层字段 tool_calls
  • 当模型要调工具时,content 经常是 null,工具信息完全外挂在消息体上。
  • 这导致"文本回复"和"工具调用"在架构上是割裂的。

Anthropic 的 Messages API 设计哲学

  • 消息 content 从来就不是纯文本,而是一个内容块(Content Block)数组
  • 从一开始就支持 text 块、image 块...... 工具调用自然也应该是这个数组里的一种块:tool_use
  • 当模型要调工具时,它可能先输出一段思考文本(text 块),然后紧接着在同一个 content 数组里给出 tool_use。两者可以自然交错、组合。
  • 工具结果也是用 tool_result 块回传,同样是内容数组的一部分。

Anthropic 消息协议就是"多模态内容流"架构,工具只是其中一种内容类型。强行套用 OpenAI 那种"纯文本 + 外挂工具调用"的模式,会破坏它自身的设计一致性。

一些模型(尤其是开源或早期国产模型)没有能力或不想动底层 API 结构,就直接在文本流里插入特殊的 token 或字符串,用类似模板注入的方式来实现工具调用。这本质上是在"纯文本流"上打补丁,优点是实现简单,缺点是结构化差、容易解析错误、和真正的 API 集成存在天然鸿沟。

模型与应用的解耦:模型网关🚩

我们现在知道,OpenAI 和 Anthropic 的模型,在使用工具时交互协议是不一样的,那为什么 Claude Code 也能用 GPT 模型呢?答案是模型网关

模型网关是架在应用各种大模型 API 之间的一个统一代理层。它做三件核心事:

  1. 统一入口:应用只对接网关,不用改代码换 API 地址。
  2. 协议转换:把应用的请求/响应格式,转换成各个后端模型需要的格式。
  3. 增强治理:负载均衡、故障转移、限流、日志、计费等。

网关 example:OpenRouter & LLM Gateway

网关实现核心原理
1、消息格式转换

Claude API 的消息体是:

json 复制代码
 {
   "messages": [
     {"role": "user", "content": "..."},
     {"role": "assistant", "content": [{"type": "tool_use", ...}]},
     {"role": "user", "content": [{"type": "tool_result", ...}]}
   ]
 }

OpenAI API 的消息体是:

json 复制代码
 {
   "messages": [
     {"role": "user", "content": "..."},
     {"role": "assistant", "tool_calls": [...]},
     {"role": "tool", "tool_call_id": "...", "content": "..."}
   ]
 }

网关会做这样的映射:

  • tool_use 块 ↔ tool_calls 数组
  • tool_result 用户消息 ↔ role: "tool" 消息
  • 工具定义的 input_schemaparameters
2、工具调用 ID 对齐

Claude 的 tool_use_id 和 OpenAI 的 tool_call_id 都由模型生成,网关负责保管对应关系,确保返回结果时能精确匹配。

3、流式响应重封装

Claude 用 content_block_start/delta/stop 来增量传输工具调用参数;OpenAI 用 tool_calls 数组的增量 delta。网关需要在这两种流式结构间实时转译。

MCP:标准化协议🚩

mcp.so/ is a third-party MCP Marketplace with 22904 MCP Servers collected.

回答的问题:Tool 生态 & 用户自定义 Tool

Tool 生态问题,各平台工具不互通

假如你是 Google,你希望 Claude Code 和 Codex 在需要进行网页搜索时,使用你提供的 Google web search,需要怎么做呢?

在过去,我们需要为 Claude Code 和 Codex 各适配一份

每个组合(Agent × 工具)都要写胶水代码、处理认证、管理上下文窗口、转换格式,成本巨大且生态割裂。

还有一点,Claude Code 没办法内置全部工具,总有些 Tool 是用户需要自己去定制的,直接去改 Claude Code (闭源)源码文件几乎是不可能的

至少在 Claude Code 被意外开源前是不可能的

因此针对用户自定义 Tool 的能力,MCP 诞生了。

MCP 角色:Client & Server

  • MCP Host:真正的 AI 应用,比如 Claude Desktop、Claude Code、自研的 Agent 平台。
  • MCP Client:嵌入在 Host 里的协议实现,负责与 Server 建立一对一连接。一般就是指 Cursor,Claude Code,Cherry Studio 这样的你使用的工具。
  • MCP Server:独立进程,封装了具体工具或数据源,通过标准协议暴露能力。

MCP 的协议设计是 基于 JSON-RPC 的方法调用,而不是 RESTful 风格的 URL 路径。

MCP 两种官方 transport

1、stdio ------ "server 是 client 的子进程"
example:chrome-devtools-mcp(Google 官方 MCP)

需要 Client 侧注册:

json 复制代码
 {
   "mcpServers": {
     "chrome-devtools": {
       "command": "npx",
       "args": ["-y", "chrome-devtools-mcp@latest"]
     }
   }
 }
 ​
 // The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser.

可以大概看一下,没有什么神奇的地方。

scss 复制代码
 ┌─────────────────────────── 用户的笔记本 ───────────────────────────┐
 │                                                                    │
 │   ┌─────────────┐     spawn()      ┌──────────────────────────┐   │
 │   │             │ ───────────────▶ │                          │   │
 │   │   Cursor    │                  │  chrome-devtools-mcp     │   │
 │   │ (MCP Client)│  stdin (请求)    │     (MCP Server)         │   │
 │   │             │ ───────────────▶ │                          │   │
 │   │             │  stdout (响应)   │   ┌────────────────┐     │   │
 │   │             │ ◀─────────────── │   │ puppeteer 拉起 │     │   │
 │   └─────────────┘                  │   │   Chrome 浏览器 │ ──▶│   │
 │         父进程                      │   └────────────────┘     │   │
 │                                    └──────────────────────────┘   │
 │                                              子进程                 │
 └────────────────────────────────────────────────────────────────────┘
                           全程在本地,没网络
                           // spawn:启动运行子进程

npx 简介

npx 是 Node.js 自带的命令行工具 (跟着 npm 一起装的,装了 Node 就有)。全名是 "Node Package eXecute"------ 执行一个 npm 包里的命令 。

它干一件事:给我一个包名,我帮你 找到 / 下载 / 运行 这个包里声明的可执行命令

除了 npx,其他的 stdio 接入(code-review-graph)

stdio 模式下,client 配置文件里的 command 字段写啥都行, 只要能产出一个能读 stdin/写 stdout 的进程。Anthropic 官方有一堆 Python 写的 MCP server(filesystem、git、postgres 之类),用 uvx 启动。

再比如:github.com/tirth8205/c...

这本质是对你的代码仓库做索引,包括一个函数有哪些地方做了调用,特点是实时更新。可以解决大仓库 AI 用 grep 读不准的问题。

AI 编码工具在审查任务中可能会反复读取代码库的大量内容。code-review-graph 解决了这个问题。它使用 Tree-sitter 构建代码的结构化映射,增量跟踪变更,并通过 MCP 为 AI 助手提供精准的上下文,使其只读取真正需要的内容。

这种方案下,在初始化 install 命令时,它会执行一个脚本,在本地启动 MCP Server。

怎么判断一个 MCP server 是哪种:看有没有 url

看它的安装文档:

  • 让你在 client 配置里写 command + args → stdio ,本地子进程。
  • 让你写 url (或 transport: "http" )→ HTTP ,远程服务。
  • 让你 pip install / npm install -g 然后配 command → 还是 stdio。
perl 复制代码
 // gitlab 这是 stdio 传输方式。
 // 如果是 HTTP(即 Streamable HTTP 或 SSE)传输方式,配置中会使用 "url" 字段指向一个已运行的服务端点,而不是用 "command" 来启动进程。
 ​
 {
   "mcpServers": {
     "gitlab": {
       "command": "npx",
       "args": [
         "-y",
         "@modelcontextprotocol/server-gitlab"
       ],
       "env": {
           // "用什么身份" 去访问 "哪个 GitLab"。
         "GITLAB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>", // MCP 权限系统
         "GITLAB_API_URL": "https://gitlab.com/api/v4" // 连接的是 GitLab.com 公共实例
       }
     }
   }
 }
2、Streamable HTTP ------ "server 是远程 HTTP 服务"
监听 HTTP Path:/mcp

server 是个常驻 HTTP 服务,监听端口(比如 api.example.com/mcp ),可以在 远程服务器、云上、Docker 里 ,一份 server 服务多个用户。

标准并未强制要求端点必须是 /mcp。Server 可以提供任何路径(如 //api),只要在配置时告知 Client 即可。不过 /mcp 已经是事实上的社区约定。

client 通过 HTTP POST + SSE(Server-Sent Events)跨网络连过去。

消息协议:JSON

注意区分消息格式 vs. 传输通道

dart 复制代码
// req
POST /mcp HTTP/1.1
Host: api.example.com
Content-Type: application/json

{"jsonrpc":"2.0","method":"tools/list","params":{},"id":1}

// resp
HTTP/1.1 200 OK
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
  • 传输层 是 HTTP,负责把请求路由到服务器、携带认证头、处理状态码等。
  • 消息体 依然是 JSON-RPC 2.0 格式,和 stdio 模式下的一模一样。
为什么不是 WebSocket

HTTP + SSE 比 WebSocket 更容易穿透防火墙、做负载均衡、无状态扩展,而且兼容现有的 SaaS 认证体系(OAuth 等)。

典型场景:后端官方 MCP

GitHub 官方 MCP、Notion MCP、Linear MCP 这种"云端 SaaS 接 MCP",你不可能让用户在本机跑他们的后端。

Client 侧实现:Claude Code

Client 侧一般需要注册 MCP,像下面这样:

json 复制代码
 {
   "mcpServers": {
     "chrome-devtools": {
       "command": "npx",
       "args": ["-y", "chrome-devtools-mcp@latest"]
     }
   }
 }
 ​
 // The MCP server will start the browser automatically once the MCP client uses a tool that requires a running browser instance. Connecting to the Chrome DevTools MCP server on its own will not automatically start the browser.

这本质上时 MCP Client 会创建一个 chrome-devtools-mcp@latest 的子进程,启动起来,并通过 stdin out 与它交互

能力 提供方 调用方 说明
tools/list Server Client 列出可用工具
resources/list Server Client 列出可读资源
prompts/list Server Client 列出提示模板
roots/list Client Server 获取工作区根目录,让 Server 能够针对项目根目录做资源扫描、文件搜索等操作,而不需要用户手动输入路径。 Client 在初始化时声明自己支持 roots 能力(capabilities.roots)。 Server 如果也支持 roots,就可以在需要时通过传输层向 Client 发送 roots/list 请求。 请求体里携带 "method": "roots/list",服务器(如果是 Client 端)根据方法名路由到相应处理函数,而不是根据 URL 路由。

Server 侧实现:Google 官方 MCP

github.com/ChromeDevTo...

比如 click 是模拟在页面上点击某个元素的行为

CLI:大模型更喜欢的 Tool🚩

MCP 的不足 & CLI 的优势

2026 年起,Perplexity、飞书、钉钉等头部厂商公开宣布放弃或降低 MCP 优先级,转向 CLI(命令行)与 Skills(技能文件)方案。CLI 凭借其透明度高、组合力强、可直接复用存量系统工具等优势,被视为更符合 AI Agent"母语"的交互方式。Skills 采用的"渐进式披露"设计,仅在需要时加载详细内容,极大节省了上下文资源。

MCP(Model Context Protocol,模型上下文协议)是 Anthropic 在 2024 年底推出的、旨在为大模型与外部工具之间提供通用通信标准的协议。它曾被誉为"AI 界的 USB 接口",但到 2026 年,业界普遍认为其已"过时"或"失宠",主要原因如下:

1、上下文窗口消耗 & Token 消耗

MCP 需要将所有工具的名称、描述、参数结构等全量注入 Agent 的上下文窗口。

一个 MCP 可能有 70 行描述信息,MCP 工具一多就很烧 Token

实测显示,仅连接 3 个 MCP Server 就可能消耗 20 万 token 窗口中约 72% 的空间,导致 Agent 实际可用的推理上下文严重不足。这不仅显著增加 token 成本,更引发"上下文腐烂"(context rot)------工具越多,模型注意力越分散,工具选择准确率可从 43% 骤降至 14% 以下。

CLI 怎么省 Token 的

CLI 分两部分

  • 常见的命令行工具,在训练阶段模型就已经学会了,CLI 只要给一个 bash 工具,十几行足矣,能省 token 和上下文
  • 对用户自定义的 CLI,通常是用 skill 的形式给他用,简单说就是说明文档(skill 渐进式披露)
CLI 可以组合工具调用

比如1、查看有哪些文件,2、找出其中符合条件的文件

  • MCP 要两步,对应两次工具调用
  • CLI 只要一步,因为步骤一的输出就是步骤二的输入,CLI 天生支持把这两个调用合在一起
2、开发成本

MCP 协议复杂度过高,开发体验差​

为接入一个工具,开发者需编写独立的 MCP Server、定义 Schema、处理错误等,而直接调用 API 往往只需几行代码。MCP 的架构涉及多个独立进程与网络边界,初始化不稳定、认证繁琐(每个工具需单独认证),调试时需在多层日志中定位问题,体验远不如直接的 API/CLI 调用清晰。

示例:Lark-CLI(飞书官方 CLI)

github.com/larksuite/c...

css 复制代码
 npx @larksuite/cli@latest install

npx 一行命令安装即可,会自动把 Skill 注入到 Claude Code 等 AI 工具。

运行形态:子进程

本质:对飞书 OpenAPI 的封装
Example:CreateDoc 创建飞书文档
go 复制代码
 var DocsCreate = common.Shortcut{
     Service:     "docs",
     Command:     "+create",
     Description: "Create a Lark document",
     Risk:        "write",
     AuthTypes:   []string{"user", "bot"},
     Scopes:      []string{"docx:document:create"},
     PostMount:   installDocsShortcutHelp("+create"),
     Flags: concatFlags(
         []common.Flag{
             docsAPIVersionCompatFlag(),
         },
         v2CreateFlags(),
         v1CreateFlags(),
     ),
     Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
         return validateCreateV2(ctx, runtime)
     },
     // 不发请求,只 dump method + URL + body,e2e 测试
     // 测试的时候你不想真的去调 LARK OPEN API(创建项目、修改文件之类的),但你想验证程序拼出来的请求对不对。DryRun 就是干这个的
     DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
         return dryRunCreateV2(ctx, runtime)
     },
     Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
         return executeCreateV2(ctx, runtime)
     },

next

之后有机会讲讲工具调用可能遇到的问题,比如执行环境,沙箱,权限系统等等

相关推荐
LucianaiB2 小时前
毕业老学长给我留的最后一句话是:“AI 写的,别挂我名。” 我直接 神(Seed Evolving) 来!助我!
后端
IvanCodes2 小时前
RAG 实战教程(一):RAG 工作原理与完整流程——分片、索引、召回、重排和生成
人工智能·后端·agent
CodeSheep3 小时前
稚晖君公司人事大变动,来了!
前端·后端·程序员
小满zs3 小时前
Go语言第八章(函数)
后端·go
stark张宇4 小时前
实战Go高级特性:Context超时控制、defer资源回收与Channel通信的关键避坑点
后端·go
凤山老林6 小时前
Spring Boot @Async 线上实战:从默认配置到生产级线程池治理
java·spring boot·后端
IT_陈寒6 小时前
Vue的响应式让我原地破防,原来问题出在这
前端·人工智能·后端
思考着亮7 小时前
1.RabbitMQ基本使用
后端
XuCoder7 小时前
面试官:Redis 单线程为什么还这么快?这道题答错的人真不少
后端
子兮曰7 小时前
Bun vs Node.js 深度对决:跑分快 4 倍,真实业务只剩 3%,2026 年到底该怎么选?
前端·后端·typescript