工具调用
第 22 课:工具调用
上一课我们确认了 Agent 循环里最关键的一件东西是工具——它是模型唯一能改变世界的接口。但模型只会输出文本,它到底是怎么「调用」一个函数的?这一课从最底层的机制讲起:schema → JSON → 执行 → 回填;再看 Function Calling 的硬约束、参数出错与「工具幻觉」怎么兜;接着回答两个规模化的问题:桌面上摆了 20 个工具时模型凭什么挑对、一次要调 5 个工具时结果会不会把 context 撑爆;最后落到一个好工具到底长什么样。上一课里和「接入生态」有关的那部分(MCP)已经移到下一课单独讲——本课只讲一次调用的机制与设计。
② 用 JSON Schema 与 strict 约束把参数错误率压下去,并用 1−(1−p)^(k+1) 算重试收益(单次 0.9 时,1 次重试即到 0.99)
③ 解释工具变多为什么更难选(可见工具 25 → 150 个,选择正确率掉约 10 个百分点),并算出 5 个工具各回填 2,000 token = 10,000 token 的上下文账、以及怎么压缩到 250 token
上一课的结论是「工具是唯一能改变世界的接口」。这一张先回答最底层的问题:模型究竟怎么「调用」它。
因为模型只会输出文本、也不会执行任何东西,所谓「调用」只能拆成四步:① 宿主把工具定义送进请求 → ② 模型输出结构化的调用意图 → ③ 宿主代码执行 → ④ 结果回填给模型。就像你让助理去办事:你先告诉他「有哪些部门可以找、各自负责什么」,他回你一张「我要找财务、需要这些材料」的单子,是你去跑腿,跑完再把结果告诉他。所以下面用一笔 token 账,把「四步往返」的代价算出来。
更自然的做法是「让模型直接生成一段代码字符串,宿主拿去执行」。但自由文本没有固定结构,宿主只能靠字符串匹配去猜「哪个是工具名、哪些是参数」;模型只要换个说法(例如把调用写成一句自然语言,而不是「get_order(id=1)」),解析就失败。所以协议要把「调用意图」限定成结构化字段——这一步换来的是宿主能无条件解析。
| 项 | 数值 |
|---|---|
| 单个工具定义(名字 + 说明 + JSON Schema) | ≈ 200 token(示意) |
| 发送时机 | 每一次模型请求都要带 |
| 20 个工具的定义 | 20 × 200 = 4,000 token |
| 项 | 数值 |
|---|---|
| 每轮工具的固定开销 | 4,000 token(20 个工具) |
| 轮数 | 10 |
| 仅工具定义的累计输入 | 4,000 × 10 = 40,000 token |
| 工具调用次数 | 模型往返次数 | 说明 |
|---|---|---|
| 1 | 2 | 第 1 次决定调用,第 2 次读结果继续 |
| 3 | 6 | 每次调用都多一次往返 |
工具调用不是模型在跑代码,而是一条四步协议:定义 → 意图 → 执行 → 回填。理解这一点,后面所有工程问题都会变清楚:为什么要 schema(约束「意图」的格式)、为什么要重试(「执行」会失败)、为什么要挑工具与压结果(后两张卡片)、以及为什么接入方式需要一套统一协议(这正是下一课要单独讲的 MCP)。
上一张卡片把调用拆成了「定义 → 意图 → 执行 → 回填」。这一张盯住第二步:模型吐出来的那段「意图」,凭什么能直接被程序当参数用。
因为模型的原生输出是自由文本,程序没办法拿它去调用接口——所以要先用 schema 把「参数长什么样」写进模型的 context,让它按结构生成。就像填表:先规定好「姓名必填、金额只能是数字、状态只能从三个词里挑」,填的人才有章可循、机器才敢直接入库。所以下面用一笔账,看约束能把「全对率」抬多少。
更笨的做法是「让模型随便吐一段 JSON,宿主自己容错解析」。但自由 JSON 的字段名、类型、嵌套都不受约束,设单个参数正确的概率 0.95,10 个参数一次全对的概率只有 0.95¹⁰ ≈ 0.599;用 schema 把单参数正确率抬到 0.99,10 个参数全对就升到 0.99¹⁰ ≈ 0.904。schema 买的不是「模型更聪明」,而是「错的机会更少」。(口径提示:下面把「一次调用」也视作一个「参数位」——无论按参数计还是按调用计,连乘公式都不变。)
| 约束 | 作用 | 不加会怎样 |
|---|---|---|
| 类型 | 金额必须是数字 | 模型可能给「三百元」 |
| 必填 | 关键字段不能缺 | 调用时少参数、直接报错 |
| 枚举 | 状态只能取固定几个值 | 出现不存在的取值 |
| 禁额外字段 | 不许自创参数 | 多传字段被接口拒绝 |
| 单次参数正确率 | 20 次全对 = p²⁰ | 至少错一次 |
|---|---|---|
| 0.95(无 strict,示意) | 0.359 | 约 64% |
| 0.99(有 strict,示意) | 0.818 | 约 18% |
| 错误类型 | schema 能拦吗 | 例子 |
|---|---|---|
| 类型/必填/枚举违例 | 能 | 金额传了字符串 |
| 语义错误 | 不能 | 金额类型对,但把 1000 填成 10000 |
| 工具选择错 | 不能 | 该查订单却调了查库存 |
Function Calling 的硬约束有两层:① 结构层——JSON Schema + strict,保证参数符合声明(不合规的 schema 直接被拒);② 语义层——schema 管不到,只能靠校验、重试与幂等。把这两层分清楚,你就不会误以为「加了 strict 就万事大吉」。
上一张卡片给参数套上了 schema。这一张处理它兜不住的那部分:出错之后怎么办。
因为失败是常态,关键在于「错了能不能被纠正」。做法是把校验器/接口返回的错误信息,当作一次观察拼回 context,让模型看到「刚才错在哪」再重试——就像填表被打回:窗口把「金额栏你填了文字」写清楚,你才知道改哪一格,而不是重填整张表。所以下面算一笔重试的账,同时看看它的代价。
| 失败类型 | 典型表现 | 兜法 |
|---|---|---|
| 结构违例 | 类型错、缺必填、枚举外取值 | 校验器报错 → 回填重试 |
| 语义错误 | 值合法但业务不对 | 业务校验 / 二次确认 |
| 工具幻觉 | 调不存在的工具名 | 白名单校验 → 返回可选列表 |
| 重试次数 k | 总尝试次数 k+1 | 至少成功一次 = 1−(1−p)^(k+1),p=0.9 |
|---|---|---|
| 0 | 1 | 0.90 |
| 1 | 2 | 0.99 |
| 2 | 3 | 0.999 |
| 3 | 4 | 0.9999 |
| 代价 | 数值/影响 |
|---|---|
| 额外模型往返 | k=3 → 最多 4 次调用、4 份 token 与延迟 |
| 重复副作用 | 下单失败重试 → 可能下两单 |
| 兜法 | 幂等键(同一请求只生效一次)+ 重试上限 |
工具出错的兜法是一条三步链:校验(能否执行)→ 回填错误(让模型知道错在哪)→ 重试(带上限与幂等)。关键认知是:只读工具重试只是浪费;会写数据的工具,重试必须有幂等保护——否则「更稳的重试」会变成「更多的重复下单」。
上一张卡片解决了「错了怎么兜」——校验、回填、重试、幂等。这一张换一个更靠前的问题:工具不止一个时,模型到底怎么挑。
因为模型看不到工具的源码,它做选择时唯一能看到的只有名字、描述和参数表,所以选择质量几乎完全由这几行文字决定。就像一排长得差不多的电灯开关:只有 3 个时闭着眼也能摸对,变成 20 个、标签又都写着「灯光」,就得一个个辨认——标签越像,按错的概率越高。所以下面用公开评测的数字,看「工具变多」到底让正确率掉了多少。
更省事的做法是「给每个工具起个名字、配一句话描述就完事」。但模型看不到代码,做选择时只能读到这几行文字:可见工具 25 个时选择正确率 86.7%,涨到 150 个时掉到 76.7%——掉的这 10.0 个百分点,正是「描述区分度不够」在规模上的放大。工具越多,写描述的成本越低,选错的代价越高。
| 模型能看到的信息 | 作用 | 缺了会怎样 |
|---|---|---|
| name(工具名) | 第一层线索,供粗筛 | 名字含糊 → 选到错误的域 |
| description(描述) | 决定「何时用、何时不用」的最重要因素 | 描述太短 → 模型只能靠猜 |
| JSON Schema(参数表) | 决定参数怎么填 | 描述与 schema 不一致 → 填错 |
| 可见工具数 | 选择正确率(示意:单模型 Grok 4.1 Fast,60 条 prompt) | 相对 25 个 |
|---|---|---|
| 25 | 86.7% | 基准 |
| 50 | 83.3% | −3.4 |
| 75 | 80.0% | −6.7 |
| 100 | 83.3% | −3.4 |
| 150 | 76.7% | −10.0 |
| 最常被混淆的服务对 | 典型模糊问法 |
|---|---|
| Datadog vs Grafana | 「查一下监控告警」 |
| Notion vs Confluence | 「搜一下文档」 |
| Linear vs Jira | 「给这个 issue 加条评论」 |
| GitHub vs GitLab | 「看看有哪些 open issue」 |
模型做选择的唯一依据,就是每个工具的 name / description / schema。有区分度 → 它能在 20 个里挑对;没有区分度 → 近义工具互相干扰,工具越多越像在猜。由此得到两条工程纪律:① 大工具集不平铺——按业务域分组、统一命名前缀,必要时先「检索出候选集」再让模型选;② 与其堆工具,不如把每个 description 写清楚——这一步的投入产出比,通常高于调提示词。
上一张卡片让模型在 20 个工具里挑对了。这一张处理挑对之后的下一件事:一次要调好几个工具,它们的返回结果塞回来会不会把上下文撑爆。
因为模型可以在一轮里同时要 5 个工具(并行调用),执行端也能并发跑,所以这 5 份结果会几乎同时回填进 context。就像一次叫了 5 个外卖骑手:配送确实更快了,但门口一下堆 5 大箱,放不下就得出事。所以下面先算并行省了多少往返,再算回填这笔长度账。
更自然的做法是「把工具返回的原始 JSON 原样塞回上下文」,因为这样最完整。但一次并行 5 个工具、每个回 2,000 token,就是 10,000 token,占 128K 窗口的约 7.8%;这些结果还会留在历史里,10 轮累计到 100,000 token。把「完整」换成「够用」——截断、摘要、只回引用——同一批结果能从 10,000 token 压到 250。
| 调用方式 | 模型往返次数 | 说明 |
|---|---|---|
| 串行 5 个工具 | 10 | 每个工具各 1 次「决定」+ 1 次「读结果」,5 × 2 |
| 并行 1 批 5 个工具 | 2 | 1 次「决定」(一次返回 5 个调用)+ 1 次「读全部结果」 |
| 省下 | 8 | 10 − 2 |
| 项 | 数值 |
|---|---|
| 单个工具返回结果 | 2,000 token(示意:一页 JSON 或一段日志) |
| 5 个工具一起回填 | 5 × 2,000 = 10,000 token |
| 占 128K 上下文窗口 | 10,000 ÷ 128,000 ≈ 7.8% |
| 若连续 10 轮都带着这些结果 | 10,000 × 10 = 100,000 token |
| 策略 | 5 个工具回填合计 | 相对原始 |
|---|---|---|
| 原样回填 | 5 × 2,000 = 10,000 | 1× |
| 截断(每个保留前 500 token) | 5 × 500 = 2,500 | 1/4 |
| 摘要(每个压到 200 token) | 5 × 200 = 1,000 | 1/10 |
| 外部存储 + 只回引用(每个 50 token) | 5 × 50 = 250 | 1/40 |
并行调用把 5 个工具从 10 次往返压到 2 次,代价是 5 份结果同时涌入 context。所以「并行」必须和「收口」一起用:截断、摘要、外部存储回引用,把回填长度从 10,000 token 量级压到 250–1,000。记住一条口径:上下文里要放的是「模型下一步要用的信息」,而不是「工具返回的全部内容」。
前面五张卡片解决了「怎么调、怎么兜错、怎么选、怎么回填」。最后一张回到最影响成功率的一环:工具本身设计得对不对。
因为模型能看到的只有名字、说明和参数表——写得含糊,它就选错、填错。就像给同事一张写着「处理一下」的便签:他要么问你三遍,要么直接办错;写清「把 X 单的金额从 A 改成 B」,他才一次做对。所以下面先算一笔「工具越多越难选」的账,再给一套可执行的设计原则。
| 工具数 | schema 占用(200 token/个) |
|---|---|
| 5 | 1,000 token |
| 20 | 4,000 token |
| 50 | 10,000 token |
| 工具数 | 候选空间 | 对模型的要求 |
|---|---|---|
| 5 | 小 | 描述略模糊也能选对 |
| 50 | 放大 10 倍 | 必须靠精确描述区分近义工具 |
| # | 原则 | 为什么 |
|---|---|---|
| 1 | 描述即文档 | 模型只看得到 name/description/schema |
| 2 | 参数最小化 | 参数越少越不易填错 |
| 3 | 防呆(poka-yoke) | 让「用错」在设计上就很难发生 |
| 4 | 错误可读可行动 | 错误信息本身就是给模型的观察 |
| 5 | 幂等、可重放 | 配合重试不产生重复副作用 |
| 6 | 最小权限 + 用户同意 | 工具即任意代码执行 |
本课收口:工具是模型与真实世界的唯一接口,所以它的设计质量直接决定 Agent 的成功率。四步往返(定义→意图→执行→回填)决定成本结构,schema 与 strict 决定结构正确性,校验/重试/幂等决定出错后的鲁棒性,工具选择与结果回填决定规模化后的成功率,而工具设计决定「模型第一遍能不能做对」。至于「接入生态」那个更大的问题——每个宿主都要为每个工具重写一套适配——下一课用 MCP 单独讲:它怎么把 M×N 压成 M+N。