Agent 工具调用的隐形成本

工具调用的成本远不止 API 费用——每次错误调用带来的时间损耗、Token 浪费和系统不稳定才是真正的隐形成本。

你做了一个 Agent。配置了 LLM,注册了五六个工具,跑了一次 Demo。Agent 精准地调用了搜索、查了数据库、生成了报告。你觉得自己已经搞定了 Agent 开发的核心问题。

然后你把它放到真实的业务流程里。跑了几百次之后发现——工具调用确实在工作,但整个系统又慢、又贵、还时不时抽风。

这不是你的 Agent 架构有问题。是你没算清楚工具调用的隐形成本。

延迟的乘法效应

先说第一个,也是最容易被忽视的一个:延迟的乘法效应

一个工具调用不是"发个请求等响应"这么简单。拆开来看,Agent 调一次工具要经过:LLM 推理出需要调工具 → 生成 Tool Call 的 JSON → 宿主解析参数 → 发起实际调用 → 等待返回 → 把结果塞回上下文 → LLM 再次推理判断下一步。

每多一次工具调用,就多一次完整的 LLM 推理。如果你的 Agent 要完成一个任务需要调 5 次工具,那它至少做了 5+1=6 次 LLM 推理。每次推理 2-5 秒,6 次就是 12-30 秒。这还没算工具本身执行的时间——查数据库、调外部 API、读文件,每一项都额外加时间。

结果就是:Demo 里看起来 3 秒出结果的任务,在真实负载下可能要 20 秒甚至更久。用户等不了 20 秒。所以你得做流式输出,让用户看到 Agent 正在"思考",而不是白屏等。

但流式输出也有坑。你的 LLM 在推理过程中随时可能决定调工具——一旦调工具,流式输出就断了,等工具返回再继续。这对用户体验来说是灾难性的:用户看到一半,输出停了,过了几秒又开始输出新的内容。

上下文污染

延迟的乘法效应是第一个坑。第二个更隐蔽:上下文污染

每次工具调用的返回结果都会被追加到上下文里。大部分场景下,工具返回的数据量远大于你真正需要的信息。

想象一个场景:Agent 调了搜索 API 查"2025 年新能源汽车销量"。搜出来的可能是 20 条结果,每条带标题、摘要、链接。Agent 真正需要的可能只是"总销量 1200 万辆"这一个数字。但剩下的 19 条结果的文本都被喂进了上下文。

一次两次没什么。十次八次之后,上下文里充满了噪声。模型不得不在几千 Token 的无关信息里找那几百 Token 的关键数据。这时候推理质量开始下降——不是模型变弱了,是它的注意力被稀释了。

更麻烦的是,有些工具返回的结果天然就大。查数据库可能返回几百行记录,读文件可能返回几千行的日志。你不可能把这些全文塞进上下文。所以大部分框架会做截断——但截断也可能截掉关键信息。

比较好的做法是对工具返回做预处理:结构化摘要、只保留关键字段、或者让 Agent 在调工具前先明确"我需要什么"。但后者的代价是额外一次推理调用。

工具选择失效率

第三个成本:工具选择失效率

你注册了 8 个工具,每个都有 JSON Schema 描述。LLM 在推理时要从 8 个里选一个来调。你以为是"匹配名字和描述就行",实际上 LLM 经常选错。

最常见的情况:两个工具功能接近,一个查 MySQL,一个查 PostgreSQL。Schema 里都写了"查数据库"。LLM 可能选错,或者两个都不选,直接凭自己的训练数据瞎编一个答案出来。

更坑的是参数幻觉。工具定义的参数有必填项、可选型、类型约束。但 LLM 经常填错——传一个不存在的表名、时间格式写错、甚至虚构一个参数名。OpenAI 的 Function Calling 做了 Strict Mode,强制模型按 Schema 输出,能解决一部分问题。但 Strict Mode 不支持嵌套 JSON,也不支持递归 Schema。

你在 Schema 里写清楚了 time_range 必须是 HH:MM-HH:MM 格式,LLM 可能给你传一个 2025-01-01 09:00——一个在另一个场景下完全合理、但在这个工具里跑不通的格式。

这些问题在 Demo 里看不出来,因为 Demo 数据是设计好的。一旦面对真实用户的海量自由输入,失效率会显著上升。

错误恢复复杂度

第四个成本:错误恢复的复杂度

工具调用失败时——超时、网络错误、参数非法、权限不足——Agent 需要决定下一步。这个决定本身也是 LLM 推理。

很多时候 Agent 的处理方式是:再试一次。这又引入一次完整的工具调用循环。如果是我写代码,我可以设置退避策略:第一次失败等 1 秒重试,第二次等 2 秒,第三次抛出异常。但 LLM 没有这个内置逻辑。它可能尝试三次同样的操作,间隔基本为零,浪费三次推理再加上三次工具执行。

更糟的情况是 Agent 陷入"修复循环":工具返回了错误 → LLM 尝试修复 → 调了另一个工具查配置 → 查到的信息和直觉不符 → 又调回原来的工具 → 又失败了。

这在传统编程里是明确的 try-catch-finally 分支。在 Agent 里,你得靠提示词约束行为。"如果工具调用失败超过两次,就向用户报告错误并停止。"听起来很简单对吧?但真实场景下,LLM 不一定遵守这条规则。你的提示词可能被上下文中的其他信息稀释,或者 LLM 的"积极性"让它觉得再多试一次就能成功。

聊了这么多问题,总不能只说问题不给方案。

一些想法

我自己的做法,目前比较靠谱的有几个方向。

第一,减少不必要的工具调用。 不是所有问题都需要工具。用户问"今天天气怎么样",你不需要先调 API 查城市再查天气——如果用户说了城市名,直接查天气就行。在 System Prompt 里写清楚:能直接回答的问题不要调工具。

第二,工具返回做摘要层。 不要直接把原始结果塞上下文。用一个专门的 Summarizer(可以是小模型,也可以用简单的规则模板)把工具返回压缩成 2-3 句话的关键信息。这样上下文干净,LLM 推理质量稳定。

第三,显式的错误路由。 别把错误处理交给 LLM 自己决定。在工具调用层做硬编码的 fallback 逻辑:第一次失败自动重试一次,第二次失败直接返回"此工具暂时不可用"的固定消息。不要在上下文里让 LLM 自己决定是否重试。

第四,限制工具调用轮次上限。 大部分场景 5-6 次工具调用就够了。设一个硬上限(比如 10 次),到了直接退出循环,告诉用户任务没完成。这能避免"失智循环"的无限消耗。

第五,善用 Streaming + 状态反馈。 每个工具调用开始时,流式输出一句提示给用户,比如"正在查询数据库…"、"正在分析结果…"。用户看到进度,耐心会大幅提升。这不解决技术问题,但解决体验问题——而体验问题在很多时候比技术问题更致命。

工具调用是 Agent 的核心能力,但不是魔法。把它当成一个 RPC 系统来设计——有超时、有重试、有熔断、有摘要——你的 Agent 才有可能从 Demo 走到生产环境。

好,就说这些。


评论

此博客中的热门博文

我写了半年 prompt,最后发现最好的技巧就三个