跳到主要内容
查看文档索引

Tool Call 成功率低?别只换模型,先修好这 6 层链路

一次 Agent 工具调用优化复盘:先拆清失败发生在哪,再收敛 Tool、Schema、错误反馈、重试、状态与观测,最后才比较模型。

Agent 的 Tool Call 成功率低,最顺手的动作是什么?换一个更强的模型。

这个动作有时有效。更强的模型通常更会选工具,也更容易生成复杂参数。但我们最近重新梳理了一遍工具调用链路,发现不少失败其实发生在模型输出之后:Tool 名称被另一层校验拒绝、JSON Schema 和运行时规则不一致、错误结果没有告诉模型哪里要改、一次安全的重试变成重复执行,或者 Tool 已经成功,后续状态却丢了。

这些问题,换模型解决不了。

全文只想留下一个判断:

Tool Call 成功率不是单纯的模型指标,而是一条从“选对工具”到“拿结果继续工作”的系统指标。先把路修好,再比较司机。

先重新定义一次“成功”

很多监控只看模型有没有输出 tool_call。这离真正成功还很远。

一次完整的 Tool Call 至少要经过下面几步:

用户任务
  -> 模型选中正确 Tool
  -> 参数符合 Schema
  -> 应用接受并执行
  -> 结果与原 call_id 正确配对
  -> 结果进入下一轮模型上下文
  -> Agent 根据结果完成任务

模型生成了一个格式正确的调用,但服务端没有这个 Tool,不算成功。Tool 执行完成了,结果却没有进入下一轮上下文,也不算成功。Agent 最后根据旧数据回答,更不能因为中间出现过一个绿色状态就算成功。

所以我们先把失败拆开,而不是先换模型:

阶段常见失败
选择没调用、选错 Tool、同时调用了互相冲突的 Tool
参数缺字段、类型错误、枚举错误、误填可选值
执行权限、依赖、领域规则或副作用失败
协议Tool 名称、call ID、流式事件或结果配对失败
继续Tool Result 丢失、顺序错乱、模型不知道下一步怎么做
任务单次调用成功,但用户目标没有完成

失败属于哪一层,决定了应该改 Schema、运行时、Prompt、网关,还是模型。把它们都记成“模型调用失败”,只会让后面的优化靠猜。

第一层:减少模型面对的选择

一个 Agent 可以拥有很多 Tool,不代表每一轮都要把全部目录摊在模型面前。

Tool 越多,名称越相似,模型越容易在相邻能力之间摇摆。长描述和大型 Schema 还会挤占上下文,让真正重要的区别变得不显眼。我们的处理方式是保留一份短能力索引,再按任务加载当前需要的 Tool。对于大型目录,可以使用 Tool Search 或自己的渐进式加载器。

Tool 描述也不需要写成产品说明书。它应该回答四件事:什么时候用、关键输入从哪里来、是否有副作用、返回结果能做什么。字段类型、枚举和是否必填,交给 Schema 表达。重复写两遍,反而可能写出两套互相打架的规则。

如果一轮工作只能顺序执行一个 Tool,就明确关闭并行调用。并行适合相互独立的读取,不适合需要审批、依赖上一步结果或可能产生副作用的流程。

OpenAI 的 Function calling 指南也把 Tool Search、严格 Schema、Tool Choice 和并行控制作为不同的工具。它们解决的是不同问题,不应该被一条“请正确调用工具”的 Prompt 代替。

第二层:让 Schema 和运行时只讲一套规则

我们遇到过一种很典型的失败:模型生成的参数完全符合它看到的 JSON Schema,应用运行时却拒绝了。原因不在模型,而在工程里同时维护了两份结构。

更稳的做法是让一个类型定义同时生成:

  • 模型看到的 JSON Schema;
  • 服务端真正使用的运行时校验;
  • 测试里的成功与失败样例。

对象尽量关闭额外字段,枚举、长度、数值范围和嵌套结构写清楚。properties 表示“可以填写”,required 才表示“必须填写”。可选字段没有值时,优先让模型省略,而不是发明空字符串、null 或“当前”之类的占位词。

Provider 支持时,可以给兼容的函数开启 strict: true。但 Strict 只能保证形状,不会替你验证权限、库存、余额或业务状态。服务端校验仍然是最后一道门。

第三层:让失败结果教会模型怎么改

早期实现里,参数错了只返回一句:

{"ok": false, "error": "invalid_tool_call"}

这句话对模型几乎没有帮助。它只知道自己错了,不知道错在哪里。下一轮往往重新猜一遍,或者把原来的错误换一种写法再发回来。

现在我们更倾向于返回有边界的结构化结果:

{
  "ok": false,
  "error": {
    "code": "invalid_arguments",
    "message": "Please correct the rejected fields.",
    "issues": [
      {"path": "range.end", "code": "too_small", "message": "Must be later than range.start"}
    ]
  }
}

这里有三个关键点:错误码稳定、字段路径明确、修复建议能执行。同时不要把原始私密值、完整请求、堆栈或内部异常直接塞回模型。

当一轮只有一个 Tool 因参数校验失败,而且可以确定副作用还没有开始,下一次请求可以只保留这个 Tool,并用命名 tool_choice 强制它修复一次。这样模型不用重新做“选哪个 Tool”的题,只需要改好这张表单。

修复次数仍然要有上限。自动修复不是把死循环包装成可靠性。

第四层:把“可重试”和“不能重试”分开

网络超时、限流和短暂的网关故障,确实可以自动重试。但重试有一个前提:系统必须知道上一轮没有产生可见输出,也没有开始副作用。

一旦已经输出文字、发出 Tool Call,或者写操作可能已经执行,再自动重放整次请求,就可能得到两条消息、两笔订单或两次通知。此时正确状态不是“失败,请重试”,而是“结果暂时无法确认”,然后查询权威状态。

一个相对安全的重试策略通常包含:

  • 只重试明确分类的瞬时错误;
  • 在任何模型事件或副作用发生前重试;
  • 尊重 Retry-After,并使用有上限的指数退避和随机抖动;
  • 区分连接超时、长时间无活动和整次请求的硬超时;
  • 为写操作设置幂等键,并保留权威执行记录;
  • 达到预算后明确失败,不静默切换成另一个行为。

重试提高的是链路抗抖动能力。它不能修复错误的 Tool 名、矛盾的 Schema 或永久无效的凭证。

第五层:保证每个调用都有且只有一个结果

多轮 Agent 依赖一条严格的时间线:Assistant 发出 Tool Call,应用在同一个 call_id 下返回 Tool Result,模型再继续。

实际系统里,浏览器刷新、流式断线、用户中断、审批等待、Worker 重启和重复事件都可能打断这条线。我们最后把下面几条当成协议不变量:

  1. 每个已结束的 Tool Call 必须有且只有一个 JSON Result;
  2. Result 必须与原 call_id 配对,并保留原来的时间顺序;
  3. 用户拒绝、中断和结果未知,都要有不同的明确状态;
  4. 重复事件可以幂等接收,冲突事件必须失败;
  5. 会话恢复不能重新执行可能已经发生的副作用;
  6. Tool 名称的定义、流式解析、审批判断和执行注册表必须来自同一份目录。

最后一条尤其容易被忽略。模型选对了 Tool,运行时也实现了它,但某个旧的事件白名单不认识这个名字,整次 Run 仍然会失败。表面看是 Tool Call 成功率低,实际上是内部注册表没有同步。

第六层:用同一组任务验证,最后才比较模型

完成前面五层后,模型差异才开始变得容易测量。

我们现在至少分别观察这些指标:

  • 正确 Tool 选择率;
  • 参数 Schema 首次通过率;
  • Tool 实际执行成功率;
  • 一次修复后的成功率;
  • 每个用户任务产生的模型请求数和 Tool Call 数;
  • 最终任务完成率;
  • P50、P95 延迟,以及超时、限流和重试比例。

每条 Trace 要能串起用户任务、物理模型请求、Tool Call、Tool Result 和下一轮模型请求。Trace 可以理解成请求的行程单:如果只看到“出发”和“到达”,中间在哪里堵住仍然不知道。

验证顺序也很重要。先用单元测试和契约测试覆盖 Schema、Tool 名称、结果配对、重试边界与副作用保护;再用固定任务集做回放;最后才在相同 Prompt、Tool 目录、Schema、网关和预算下比较不同模型。

否则,新模型接到的是一条已经修过的路,旧模型接到的却是旧路。这个对比不会告诉你模型谁更好,只会告诉你两套系统不一样。

什么时候确实应该换模型

不是说换模型没用。下面几种情况,模型选择很重要:

  • 在相同 Tool 和 Schema 下,模型仍然经常选错能力;
  • 参数结构虽然合法,但语义关系持续出错;
  • 工作流需要模型或 Provider 并不支持的 Strict、Tool Choice、长上下文或流式能力;
  • 更好的任务完成率足以覆盖新增的延迟和成本。

但如果失败来自内部 Tool 白名单、两份 Schema、丢失的 Tool Result、错误的重试或未配对的 call_id,升级模型只是在坏路上换了一辆更好的车。

我们这轮优化真正改变的,不是某一句神奇 Prompt,而是处理问题的顺序:先把成功定义清楚,找到失败层,修正契约和状态,再让模型在更小、更明确、可修复的空间里做判断。

模型当然重要。只是它应该负责判断,不应该每天替系统补洞。

完 / 继续实践