For the complete documentation index, see llms.txt. This page is also available as Markdown.

14.5 AI Agent 与工具调用:让模型从“说”到“做”

14.4 节拓宽的是模型能读入和生成的模态。但模态再多,模型交付的仍然只是内容本身;让外部系统真的动起来,是另一回事。

本节回答一个容易被应用层叙事掩盖的问题:工具调用在词元层到底发生了什么。答案是——没有新的模型结构。工具调用只是一套聊天模板约定加上相应的微调数据,模型生成的仍然是普通词元,执行由宿主完成。弄清这一点之后,Agent 的成本账、失效模式和对推理引擎的要求都会变得可算。

14.5.1 为什么需要工具

裸模型的输出是词元序列。浏览网页、查询数据库、执行代码依赖宿主系统暴露的接口。工具弥补的是三类不同性质的缺口:

  • 知识的时间边界:训练数据有截止时间,实时信息不在权重里。

  • 精确计算:模型逐词元预测,大数乘法这类需要精确进位的运算不可靠。

  • 副作用:模型可以写出代码,但运行代码、发送邮件、修改文件都发生在权重之外。

后两条不是“知识”的缺陷,而是“模型只能生成词元”这一事实的直接后果。Agent 的核心思想是把 LLM 当作推理与决策引擎,由宿主执行它选中的动作,再把结果回灌。

14.5.2 一次工具调用在词元层发生了什么

工具调用(Function Calling / Tool Use)的全部机制可以用一个公开的聊天模板讲完。下面以 Qwen3 的 tokenizer_config.json 里的 chat_template 为例,它的每一步都能在文件里找到对应的 Jinja 分支。

第 1 步:工具定义被渲染进提示。 模板在有 tools 参数时,把工具的 JSON Schema 逐个写进系统段:

<|im_start|>system
# Tools

You may call one or more functions to assist with the user query.

You are provided with function signatures within <tools></tools> XML tags:
<tools>
{"type": "function", "function": {"name": "get_weather", ...}}
</tools>

For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
<tool_call>
{"name": <function-name>, "arguments": <args-json-object>}
</tool_call><|im_end|>

要点有三。工具定义就是普通文本,要占词元;每一轮请求都要重新渲染一遍,因为模型无状态;放在哪里由模板决定,Qwen3 放系统段,别的模型可能另开一段,所以“注入到系统提示词中”只是其中一种做法。

第 2 步:模型生成调用片段。 模型照常逐词元生成,只是生成出的是:

<tool_call> 与 </tool_call> 在 Qwen3 的 added_tokens_decoder 里各是一个新增词元(special 字段为 false,该文件里标 true 的只有 <|im_start|> 与 <|im_end|> 一类控制词元),不是逐字符拼出来的。这一步与生成任何其他文本没有区别——同一套 logits、同一套采样。

第 3 步:生成停止。 模型在这一轮末尾生成 <|im_end|>,它是 Qwen3 的 eos_token。请求到此结束,引擎把整段文本返回给宿主。

第 4 步:宿主解析并执行。 宿主从返回文本里切出 <tool_call> 与 </tool_call> 之间的 JSON,校验参数,执行,拿到结果。这一步完全在模型之外。

第 5 步:结果以新一轮回灌。 结果作为一条 role = "tool" 的消息加进消息列表。模板的渲染方式值得单看:Qwen3 把连续的 tool 消息合并进一个 user 轮次,每条包在 <tool_response> 与 </tool_response> 之间。

所以“工具角色”存在于 API 的消息列表里,未必存在于最终的词元流里。末尾的 <|im_start|>assistant\n 是生成提示,下一轮从这里继续。

第 6 步:重复。 带着更长的前缀重新走一次完整的前向。

这六步里没有任何一步需要改模型结构。能力来自训练数据——模板见过、格式学会了。图 14-9 把同样的流程画成时序图。

图 14-9:一次工具调用的完整往返。模型侧只有“生成”一件事,解析与执行全在宿主。

约束解码的角色

工具调用要求输出可被程序解析,这正是 9.4 节约束解码的典型场景:JSON Schema 编译成文法,每步为 logits 生成一个掩码,只允许文法接受的词元。

它保证的与不保证的要分清。它保证输出形状合法——括号配对、字段名在集合内、类型正确。它不保证参数语义正确(城市名写成邻市、单位写错),不保证调用被授权,也不保证操作幂等。工具执行前仍需要权限校验、参数审计、回滚设计和高风险操作确认。约束解码本身还有代价,见 9.4.5:逐步归一化会改变分布。

14.5.3 Agent 循环的成本账

工具调用的机制简单,但循环起来的成本增长很不直观。三笔账应当会算,图 14-10 先给出上下文逐轮变长的形状。

Agent 循环的上下文增长与前缀缓存

图 14-10:Agent 循环里上下文怎样逐轮变长,前缀缓存省掉了哪一块(生成脚本)。设定为工具定义 1 万词元、每轮新增 2048 词元。

第一笔:工具定义的固定开销。 每一轮都要重新渲染全部工具的 schema。一个工具的 JSON Schema 含名称、描述、参数类型与说明,约 200 词元是常见量级。10 个工具是 2000 词元,50 个是 1 万,100 个是 2 万。这笔开销与任务难度无关,只与挂了多少工具有关,且每一轮都付一次。这是“按需暴露工具子集”这类做法的直接动机。

第二笔:预填充随轮次累积。 设工具定义按第一笔取 1 万词元,每轮新增 2048 词元(模型的调用片段加工具返回结果),共 30 轮。第 $k$ 轮的前缀长度是 10,000 + 2048k。若每轮都从头预填充,30 轮合计:

∑k=130(10,000+2048k)=300,000+2048×465=1,252,320 词元\sum_{k=1}^{30}\left(10{,}000 + 2048k\right) = 300{,}000 + 2048 \times 465 = 1{,}252{,}320\ \text{词元}

若前缀缓存全命中,工具定义与已有对话都不必重算,每轮只算增量:

2048×30=61,440 词元2048 \times 30 = 61{,}440\ \text{词元}

相差 20.4 倍。把轮数记作 $R$,这个比值是 $10{,}000/2048 + (R+1)/2$,随轮数近似线性增长:30 轮 20.4 倍,60 轮 35.4 倍。常数项 $10{,}000/2048 = 4.9$ 是工具定义带来的,轮数再多也不消失。所以前缀缓存(11.5 节)对 Agent 不是锦上添花,而是能不能跑的分界。

第三笔:解码的 FLOPs。 按 3.8.8 节的口径,解码一个词元约 2 × 激活参数量 次浮点运算。Llama 3 8B 约 16 GFLOPs/词元,DeepSeek-V3(37B 激活)约 74 GFLOPs/词元。30 轮各输出 500 词元共 1.5 万词元,两者分别约 0.24 和 1.11 PFLOPs。与预填充的 125 万词元相比,解码的词元数小两个数量级,但解码是访存受限的,墙钟时间上往往反而是它占大头。

14.5.4 误差怎样逐步累积

单步可靠不等于全程可靠。设每一步(选对工具、填对参数、正确理解返回结果)的成功率为 $p$,$k$ 步串行且任一步出错即失败,则整体成功率是 $p^k$。表 14-11 代入三档单步成功率。

单步成功率
10 步
20 步
50 步

0.99

0.904

0.818

0.605

0.95

0.599

0.358

0.077

0.90

0.349

0.122

0.005

表 14-11:串行多步任务的整体成功率 $p^k$。

0.95 这个看起来相当不错的单步成功率,20 步之后只剩 0.358。反过来算:要让 20 步任务整体达到 0.9,单步需要 $0.9^{1/20} = 0.9947$,即单步错误率要低于 0.53%,约每 190 步才允许错一次。

这个指数衰减解释了 Agent 系统的几乎全部工程设计。缩短链路(一次多调几个工具而不是串起来)把指数的底数变少;验证与重试把单步的 $p$ 拉高;检查点让失败只回退一步而不是全程重来;人工确认在高风险步骤上把 $p$ 直接提到 1。把 $p^k$ 写在纸上,这些做法就不再是经验之谈。

需要说明的是这只是一个上界模型:真实任务的各步并不独立,失败也未必立刻终止。但它给出的量级感是对的——链路长度是最强的一阶因素。

14.5.5 Agent 架构模式

ReAct:推理与行动交替

ReAct(Reasoning + Acting)让模型交替生成推理过程和工具调用:

优势是把“为什么调用某个工具”的依据显式写出来,便于调试和审计。但产品中通常只向用户展示摘要化的计划、进度和证据,不默认暴露模型的原始隐藏思维链;审计日志也应按权限和隐私边界保存。

按 14.5.4 的模型看,ReAct 的思考步本身也会失败,它是在增加链路长度换取单步成功率——只有当思考带来的 $p$ 提升超过多一步的代价时才划算。这解释了为什么简单任务上显式推理常常没有收益。

规划-执行分离

复杂任务先整体分解为有序子任务,再逐步执行并按中间结果调整。它避免了“边想边做”的短视决策,也把一次长链路切成若干条短链路。代价是计划本身可能在第一步就错,而错误要到执行很久之后才暴露。

多 Agent 协作

任务复杂度超过单个 Agent 的边界时,可以分工:不同角色(研究员、程序员、审查员)各自擅长不同工具;主 Agent 分解任务并分派,子 Agent 具体执行;Agent 之间通过消息协商。

它的实际收益主要来自上下文隔离——每个子 Agent 只看自己那段,避免 14.5.3 第二笔账的无限膨胀。代价是消息传递本身会丢信息,且 $p^k$ 的链路并没有真的变短,只是被拆到了不同的上下文里。

14.5.6 标准化协议:MCP

每个工具提供者用不同的接口定义方式,Agent 与工具之间的集成成本极高。MCP(Model Context Protocol)是 Anthropic 在 2024 年提出的开放标准,为 LLM 应用与外部工具、数据源之间建立统一协议。图 14-11 给出它的客户端-服务器分工:应用一侧是 MCP 客户端,每类工具由一个 MCP 服务器暴露,两者之间只有“发现工具”与“调用工具”两类消息。

图 14-11:MCP 的客户端-服务器架构。

协议骨架。 消息格式是 JSON-RPC 2.0。三类角色在规范里分得很清:宿主(Host)是发起连接的 LLM 应用,客户端(Client)是宿主里的连接器,服务器(Server)提供上下文与能力。服务器可提供的三类原语是 resources(数据)、prompts(模板)、tools(可执行函数)。

工具发现与调用是两个方法。 客户端发 tools/list 拿到工具清单,每个工具含 name、description 和 inputSchema——最后这个字段就是 14.5.2 第 1 步要渲染进提示的那份 JSON Schema。调用时发 tools/call,参数是 name 与 arguments:

返回里的 content 是结果内容,isError 标记工具自身是否出错。工具执行出错与协议层出错是两件事:前者用 isError 让模型看见并自行纠正,后者才是 JSON-RPC 错误。

传输与版本。 标准传输只有两种:标准输入输出(stdio)与 Streamable HTTP。更早的 HTTP+SSE 传输已被 Streamable HTTP 取代并标为弃用,规范只保留一节讲怎样与它保持向后兼容。协议修订版本号与具体字段集合是易过期事实,应以规范的当前修订为准,不宜背下来。

MCP 降低的是集成复杂度。它不自动解决身份认证、最小权限、数据驻留、速率限制和高风险工具确认等生产治理问题。从安全角度看,MCP 服务器返回的工具描述、资源内容和工具输出都应视为不可信输入。宿主相应地需要做四件事:固定批准的服务器与工具集合,限制凭据与文件权限的透传,隔离会话状态,对提示注入、工具投毒和数据外传建立审计与回滚机制。

14.5.7 工具调用能力怎样训练出来

14.5.2 说明了推理时没有新结构,能力全在权重里。这些权重来自两类训练。

多轮轨迹的监督微调。 训练样本是完整的交互过程:“用户问题 → 工具调用 → 工具结果 → 最终回答”,按同一套聊天模板渲染成一条词元序列。关键的实现细节是损失掩码:工具返回的结果是环境给的,不是模型该学着生成的内容,因此这些位置要从损失里排除,与 8.1.2 节对提示词位置做掩码是同一手法。漏掉这一步,模型会学着自己编造工具输出。

训练数据还要包含两类容易被忽略的样本:多轮调用链,即需要连续调用多个工具才能完成的任务;拒绝样本,即模型判断不需要调用工具而直接回答的场景,用来抑制过度调用。

生产系统的真实调用轨迹不应直接原样进入训练集:需要用户授权、数据最小化、敏感字段脱敏、工具副作用标注和权限边界保留,否则模型可能学到越权调用、泄漏私有结果或复现内部工作流。

面向环境的强化学习。 监督微调只能模仿已有轨迹。让模型在可验证环境里自己试错、按任务是否完成给奖励,是当前提升 Agent 能力的主线。它的技术难点是轨迹级的信用分配——一条 30 步的轨迹最后失败了,该罚哪一步。具体算法见 14.6 节。

14.5.8 Agent 对推理引擎的要求

Agent 的工作模式与常规对话差别很大,对引擎(第十一章)提出了几条具体要求。

前缀缓存是第一位的。 14.5.3 的 20.4 倍说明了原因。这里要纠正一个常见误解:主流引擎并不在工具等待期间挂起请求。第 3 步生成 <|im_end|> 之后请求就正常结束了,资源被释放;第 6 步是一个新请求,只是前缀更长。省掉重算靠的是前缀缓存命中(11.5 节的 RadixAttention 一类机制),不是引擎里有个暂停的状态机。这个区别很实际:它意味着 Agent 的性能高度依赖缓存命中率,而任何打乱前缀的改动(比如在提示开头插入时间戳)都会让命中率归零。

上下文管理。 数十轮之后上下文迅速膨胀。压缩历史、把长期信息放进外部存储按需检索,都是在控制 14.5.3 的第二笔账。注意压缩会改变前缀,从而使缓存失效——两个优化目标在这里打架。

并行工具调用由模型和模板决定。 一次生成里输出多个 <tool_call> 片段,是模型学出来的行为、模板支持的格式;引擎负责的是把整段文本原样返回。宿主解析出多个调用后并行执行,再把多条结果合并回灌。把这条能力算到引擎头上是主语错位。

流式输出。 Agent 任务耗时长,流式让用户实时看到可展示的进度、工具调用状态和中间结果摘要。它不改变上面任何一条机制。

14.5.9 评测与常见失效

能力侧的失效模式与 14.5.4 的模型对得上:

  • 工具选择错误:可选工具多、描述相近时尤其常见,等价于降低单步 $p$。

  • 幻觉出不存在的工具或参数:约束解码可以挡住参数名不在 schema 里的情况,挡不住语义错误的取值。

  • 循环:反复调用同一个工具而不推进,常见于工具返回了模型看不懂的错误。

  • 过早终止:任务未完成就给出最终回答。

  • 上下文膨胀导致的退化:轮次多了之后早期指令被稀释,与 14.7 节讨论的长上下文问题同源。

公开评测各有侧重:BFCL 侧重函数调用的格式与选择正确性,$\tau$-bench 侧重多轮对话中遵守策略地完成任务,SWE-bench 侧重在真实代码仓库上完成可验证的修改。三者测的是链路的不同长度,分数不可互相换算。

14.5.10 RAG、工作流与 Agent 的边界

检索增强生成(Retrieval-Augmented Generation,RAG)把外部资料检索出来,作为生成答案的上下文;一次检索和回答本身不更新模型权重。RAG 原论文研究了检索器与生成器的联合微调,但应用也可直接组合现成模型。RAG 描述知识如何进入回答,工作流描述预设步骤如何执行,Agent 则由模型根据中间结果选择后续动作:三者可以组合。例如“本部门出差住宿能报销多少”可走固定 RAG 工作流;若模型发现目的地缺失、决定追问并重新查询,就是把检索放进 Agent 循环。下面是一条企业制度问答链路,工程依据见 Microsoft RAG 文档:

  1. 离线建库:按条款切分制度,保存版本、适用范围、权限和原文位置;用 embedding 模型将片段编码成向量并建索引,制度更新时同步更新。

  2. 在线检索:先限定用户可读范围,编码问题,结合向量或关键词检索候选,再重排选出相关条款。向量、双塔与重排机制见 12.3.6 节。

  3. 组装与生成:将问题和选中的原文片段拼入提示,要求按适用地区、职级和版本回答,并附条款引文;资料不足时说明缺口。

  4. 分别验证:检索评估相关条款召回率,回答评估每项结论是否有原文支持。检索正确不保证模型正确使用证据,有引文也不等于引文支持结论。外部文档始终是不可信输入,其中的“忽略规则”等文字不能取得指令权限。

最后更新于