跳到主要内容

逐渐腐化的工具描述:当你的 Agent 仍在盲目调用时

· 阅读需 11 分钟
Tian Pan
Software Engineer

你的智能体已经悄悄出错六个月了,而你的错误率看起来却很正常。底层 API 发布了一个重命名的错误代码,将一个可选字段改为必填,并开始拒绝没有幂等性请求头(idempotency header)的调用。你智能体系统提示词(system prompt)中的工具描述 —— 那是去年第四季度从 Notion 页面粘贴过来的 —— 完全没有描述这些变化。智能体不断调用旧的参数结构,编排层不断捕获失败并使用同样错误的参数进行重试,而你遥测系统中的唯一信号只是略微升高的重试次数,且没有任何值班人员有足够的背景信息去调查它。

工具描述是接口契约。底层 API 发生变化的时刻,它们就开始老化。与强类型 SDK 不同,它们的失效是无声无息的 —— 模型只会发出更糟糕的调用。

这种失败模式之所以普遍存在,是因为团队中没有人将这些描述视为受版本控制的产物。发布 API 变更的后端工程师更新了 OpenAPI 规范,运行了 SDK 代码生成,并合并了迁移。智能体平台的工具定义存在于不同的文件中 —— 有时在不同的仓库,通常是提示词模板中的一个 YAML 块,偶尔是某人入职文档中的一个 Markdown 块。它是从六个月前的草稿中粘贴过来的。代码生成从未触及它。PR 模板没有提及它。值班手册(runbook)也没提到它。这种偏移随着一次次悄无声息的 schema 变更而累积,直到有一天客户投诉升级,迫使某人朗读提示词,才发现描述的是一个早已不存在的 API。

为什么这里的偏移是无声的,而不是震耳欲聋的

带有陈旧类型签名的强类型 SDK 会在编译时报错。针对已更改契约的 HTTP 客户端会抛出 400 错误或反序列化错误,从而被监控捕获。LLM 的工具调用层面没有任何这类关口。

当描述说某个字段是可选的,而 API 后来将其改为必填时,模型会省略该字段,API 拒绝调用,编排层将其视为瞬时故障,重试策略会再次发出同样的错误参数。你的仪表盘显示 “工具 X 的重试率升高”。它们不会显示 “工具描述正在对模型撒谎”。模型无法在会话中从失败中学习 —— 它看到 400 错误,推断是服务器问题,向用户道歉,然后继续。

当 API 添加了一个描述中未提及的新可选参数时,模型永远不会传递它,API 的行为会发生细微变化 —— 参数默认值可能是六个月前合理的设置,但现在对客户的租户(tenant)来说是错误的。智能体会表现得很 “专业” 地做错事。没有错误,只有由于用户投诉而逐渐浮现的错误结果,且没有人能追踪到根本原因。

当 API 将错误代码从 RATE_LIMITED 重命名为 THROTTLED_BY_TENANT_POLICY 时,描述中 “如果你看到 RATE_LIMITED,请等待并重试” 的指令会悄无声息地停止触发。模型看到一个无法识别的错误代码,无法应用描述中编码的重试逻辑,要么放弃,要么幻化(hallucinate)出另一条恢复路径。编排层的指标最终会察觉到,但检测时间是以周计的,因为这种失败表现为用户报告的不稳定性,而不是警报。

这三者的共同属性是:描述是智能体推理层面(reasoning surface)的一部分,错误的描述在调用真正发生之前就污染了推理。

甚至不涉及 API 变更的偏移模式

这个问题更难的版本是,当 API 本身保持不变时,周围的系统可能会发生偏移。大多数团队直到被坑了之后才会考虑到这一类问题。

下游服务引入了幂等键(idempotency-key)要求。智能体调用的 API 仍然接受旧的请求结构,但其背后的服务在缺失该键时的重试方式发生了变化,产生了重复的状态更改,看起来就像智能体在进行 “双重支付”(double-spending)。从未提及幂等性的工具描述不可能提示模型发送该键。修复工作在模型的上游,但症状看起来却像是一个智能体 bug。

速率限制(rate limit)收紧了。智能体并不知道每分钟的配额现在只有以前的一半,它继续发出工具描述中 “你可以随意调用” 语言所暗示的突发调用。一半的调用被退回。另一半以不可预测的顺序成功。用户看到的是一个 “有时有用” 的会话。

一项新的合规政策意味着调用现在需要在请求头中包含同意令牌(consent token)。API 在没有该令牌的情况下接受了调用,但记录了违规。团队在六周后审计调取日志时才发现。描述从来没有任何理由提及 “同意”,因为编写描述时 “同意” 机制还不存在。

在每种情况下,工具描述作为 API 契约的描述在技术上是正确的。但作为智能体在当前运行现实中应该进行的调用的描述,它是错误的。智能体进行推理的接口比 OpenAPI 规范更广泛,相应的偏移面(drift surface)也更广。

将描述视为编译产物的版本管理准则

解决方法是停止将工具描述视为文档,并开始将其视为代码。具体而言:

从与 SDK 相同的单一事实来源生成描述。 如果 API 有 OpenAPI 规范,工具描述应在生成类型化客户端的同一个 CI 步骤中从中生成。描述的参数列表、类型约束和必填/可选标志都源自规范,而不是由人工阅读规范后转录。人工撰写的内容是 正文(prose)——该工具的 用途、Agent 何时应调用它、Agent 对结果应采取什么操作——这些正文与规范并列,并处于相同的审核流程中。

将描述与 API 一同版本化。 工具应遵循语义化版本(semver)。对描述形状(shape)的破坏性变更——重命名的参数、删除的字段、更严格的类型——应触发主版本号升级和新的工具名称(createInvoice_v2),而不是对现有描述进行静默修改。尚未升级的 Agent 会继续针对它们测试过的相同形状调用 createInvoice_v1。平台同时提供这两个版本,直到旧版本通过标准渠道被弃用。

将描述正文的变更视为破坏性变更。 这是最反直觉的一点。即使 JSON schema 完全相同,描述 正文 的改变也可能改变模型的工具选择决策。以前为“查找我最近的购买记录”调用 searchOrders 的模型,现在可能因为描述措辞的调整而调用 listInvoices。从 API 的角度来看,什么都没有改变;但从 Agent 的角度来看,其表面积(surface area)发生了偏移。CI 应将任何描述编辑视为潜在的破坏性变更,并在允许部署前运行评测(eval),证明在留存的 trace 语料库上工具选择的分布没有发生偏移。

添加契约偏移(contract-drift)的 CI 门控。 一个计划任务拉取线上 API 的 OpenAPI 规范,将其与生成已部署工具描述的规范进行比对,并在出现任何差异时使构建失败(或呼叫负责团队)。其信号是“API 变了而描述没变”,该门控强制团队要么重新生成描述,要么显式确认延迟处理并设置 TTL。

针对当前的 API 行为重放 Trace。 捕捉描述陈旧(rot)的评测不是一组静态提示词,而是针对今天的 API 重放的近期 Agent trace 语料库。重放会产生每次调用的增量(delta):“此参数形状不再通过验证”、“此参数现在是必填的”、“此响应字段已不再存在”。这些增量就是需要交付的描述更新清单。重放还能捕捉到静默失败模式,即调用仍然成功但语义行为发生了变化——评测对结果进行评分,而不仅仅是调用形状。

本地化(Vendor)描述文件。 将生成的描述存储在自己的仓库中(而不是在运行时从供应商的 MCP 服务器获取),意味着上游变更不会自动应用。你在 PR 中看到差异,刻意接受它,这样发生破坏的时刻是一个代码审查事件,而不是凌晨 3 点的报警。权衡之处在于陈旧性,而上述的契约偏移门控正是为了限定这种陈旧性。

契约归谁所有

使这类问题变得棘手的组织性失败在于,没有一个单一团队端到端地拥有工具描述。后端团队拥有 API,平台团队拥有 Agent 运行时,产品团队拥有提示词和面向用户的行为。描述夹在三者中间,只有当某些东西坏到足以触发复盘时,才会有人关注它的归属。

我见过的有效模式是像对待 SDK 一样对待工具描述——将其视为一种生成的产物,其来源在 API 仓库中,其审核在 API 团队的 PR 模板中,其部署由阻断错误 SDK 发布的相同 CI 检查来门控。Agent 平台像消费类型化 SDK 的用户一样,将描述作为版本化的包来消费。当 API 团队发布破坏性变更时,描述的升级和 SDK 的升级是在同一个 PR 中完成的。当消费者使用旧版描述时,他们也在使用旧版 SDK,弃用时间表统一适用。

这种转变虽小但影响深远:描述不再是由最后编辑 Agent 系统提示词的人所拥有的提示词内容,而是成为了由拥有 API 的人所拥有的接口契约。发布 API 变更的团队现在负责在同一个提交(commit)中发布描述变更,而消费它的团队则获得了与 SDK 相同的版本保证。

这对你的路线图意味着什么

如果你的 Agent 平台今天还没有契约偏移 CI 门控,那么你的描述在某种程度上已经陈旧了,而你并不知道陈旧了多少。第一件事是衡量:拉取你 Agent 暴露的每个工具的线上 OpenAPI 规范,重新生成描述,并将其与已部署的内容进行比对。每一个红色的差异都是一个潜在的 Agent 失败模式,你正在为此付出重试、错误路由调用以及用户投诉的代价,这些问题的根源看起来像是“模型搞错了”,但实际上是“描述告诉了模型错误的信息”。

更深层次的认识是,LLM 工具调用表面是接口契约存在的新场所,而行业尚未建立起像对待 SDK、gRPC 存根或 protobuf schema 那样对待它的肌肉记忆。早期做对这一点的团队,其 Agent 的失败模式将在 API 演进过程中平滑降级。而没做对的团队,未来两年将花在调试 Agent 上,而这些问题的根源在于模型上游,在于一个无人负责、针对已不存在的 API 编写的描述中。

工具描述是你 Agent 表面积的一部分。一个没有版本管理准则的 Agent 表面积,就是一个你选择不拥有其契约的表面积——最终,总有人会在你最不希望被发现的时候,发现这个选择带来的后果。

References:Let's stay in touch and Follow me for more thoughts and updates