工具清单的谎言:当你的 Agent 信任一个后端已不再遵循的 Schema 时
生产环境 Agent 中最危险的 Bug 不是那些会抛错的 Bug。而是这种:工具描述写着 returns user_id,但后端在两个 Sprint 前悄悄开始返回 account_id,而模型在后续推理中仍在愉快地凭空捏造 user_id —— 因为清单(manifest)是这么写的,Few-shot 历史加强了这一点,而且循环中没有任何环节去获取真实情况(ground truth)。
这就是清单漂移(manifest drift):工具描述所声称的内容与端点实际行为之间缓慢且无声的分歧。它很少产生堆栈跟踪(stack traces)。它产生的是带有干净审计线索的错误决策 —— 这是 Agent 系统中最糟糕的一类 Bug。
每一个在生产环境中使用 Agent 的团队都有这个问题,无论他们是否意识到。清单是契约,后端是现实,在正常的软件演进过程中,两者有时会出现分歧是预料之中的。Agent 的不同之处在于,模型不知道它们产生了分歧。它根据工具名称、描述、参数文档以及之前的“动作-观察”(action-observation)对构建了一个隐式的世界模型 —— 然后基于该模型 充满信心地进行推理,即便这些内容早已不再真实。
为什么清单漂移对 Agent 而言格外糟糕
传统的 API 客户端会“大声地”崩溃。重命名的字段会产生 KeyError,类型不匹配会抛出反序列化异常,弃用的端点返回 410,CI 会捕捉到它。信号是快速、精确且可归因的。
Agent 的失败方式则不同。当清单声称 transaction_amount 的单位是分,而后端悄悄切换到元时,模型不会崩溃 —— 它会根据偏差 100 倍的阈值进行推理,发出语法正确的查询,撰写极其流畅的总结,并将错误的数据发送到人类明天会查阅的仪表盘上。当状态枚举增加了一个新值(如 pending_review),Agent 的决策逻辑(根据文档中的三个值和 Few-shot 示例中的五个值训练而成)会悄悄将新情况归入错误的决策分支,或者直接忽略它们。
这种不对称性是结构性的。语言模型是一个出色的模仿者。当它的上下文充满了过去使用 customer_id 的“动作-观察”对时,即使后端将字段重命名为 account_id,它仍会继续输出 customer_id —— 因为上下文中的模式比任何单一的描述更改都更有力。模型对 API 的心理模型比任何单一的 Schema 更新都更持久,并且随着 Few-shot 历史的堆积,它会变得 更加 自信。
有三类漂移值得关注:
- 表层漂移(Surface drift)。字段被重命名或移动。Schema 在宽泛校验下依然通过,但后续推理引 用的字段名称已不存在。后端返回
null或填充默认值;Agent 则将 null 视为具有实际意义的信息。 - 语义漂移(Semantic drift)。形状相同但含义改变了。单位、时区、货币、枚举语义、默认值、或者什么才算作“活跃(active)”。这些是最难检测的,因为没有任何 Schema 检查器会标记它们。
- 行为漂移(Behavioral drift)。端点仍然接受相同的输入并返回相同的形状,但其副作用发生了变化。它现在访问不同的下游系统,执行新的频率限制类别,具有不同的幂等性保证,或者汇总结果的方式有所不同。
只有表层漂移会出现在 JSON-schema 的 diff 中。语义漂移和行为漂移对 CI 运行的每一次静态检查来说都是不可见的。
模型的心理模型比你的 Schema 更新更持久
这里有一个违反直觉的地方。你 可以 更新清单。你推送一个新的工具描述,将其发布到 Agent 框架,并发布一个版本。问题在于,模型的行为是由其上下文窗口中的 合集 塑造的:工具描述确实是其中之一,但还包括系统提示词(system prompt)、任务指令、检索上下文、前几轮的工具调用、你作为 Few-shot 包含的追踪示例,以及对早期步骤的任何总结。更新其中一项并不会清除其他项。
具体来说:你在清单中将 user_status 重命名为 account_state。下一次 Agent 运行时会看到新的描述。但在同一对话的 前几轮中,它仍然引用 user_status。它的 Few-shot 示例仍然显示 user_status。你从中提取文档的检索语料库尚未重新索引。前二十轮的压缩总结中还在讨论 user_status。模型作为一个模式匹配器,会遵循占主导地位的信号 —— 并在下一次工具调用中输出旧的字段名称,或者更糟的是,写出一段将新旧名称混杂在一起、听起来合乎逻辑但实际上错误的叙述。
这就是为什么清单漂移不能通过“仅仅更新工具描述”来解决。描述只是房间里的一个声音,而模型正在倾听整个人群的声音,而人群已经积累了多年关于旧惯例的共识。
实际的结果是:任何 Schema 更改都需要被视为一次全面的上下文清理事件(context hygiene event),而不仅仅是单行编辑。Few-shot 示例需要更新。检索索引需要重建。存储的轨迹需要重新总结或标记为过时。测试提示词需要重新生成。否则,清单更新只是表面功夫 —— 模型看到了它,但先前的上下文会覆盖它。
工具的契约测试:真正有效的方案
静态 Schema 验证只能捕获表层漂移。除此之外,它什么也捕获不了。在生产环境中有效的模式是契约测试(Contract-test)套件,它在 Manifest 或端点的每次部署时针对 实时 后端运行,并在 Manifest 的声明与后端的实际情况不一致时发出警报。
通过三种探测可以覆盖大部分的漂移场景:
实时 Schema 探测(Live schema probe)。 对于 Manifest 中的每个工具,使用预设的、幂等的输入调用端点,并将响应外层(字段名、类型、必填项、嵌套结构)与 Manifest 的承诺进行比较。这能在发布瞬间捕获表层漂移。它的成本很低。团队通常会跳过它,因为觉得它与 OpenAPI 重复,但 OpenAPI 记录的是意图,而探测记录的是现实。当两者发生分歧时,探测会告诉你哪一个是谎言。
调用时的响应形态验证(Response-shape validation)。 在运行时,每个工具的响应都会通过 Manifest 声明的 Schema 以严格模式进行解析。未知字段、缺失字段或类型不匹配都会记录工具名称和租户。你并不是要阻断调用——你是在尝试在现实与契约发生分歧的瞬间点亮仪表盘,这样漂移就能在数小时内被发现,而不是拖上几个季度。
针对每个工具的行为漂移检测(Behavioral drift detection)。 对于每个高风险工具,保留一组黄金输入/输出对——不需要面面俱到,只需几个代表性案例。每晚针对实时端点重新运行它们。如果响应值(而不仅仅是形态)漂移超出了可接受的增量,就发出警报。这是唯一能捕获语义和行为漂移的探测,也是当上游团队悄悄将单位从分改为元时,真正能救你命的探测。它需要有人维护这些配对,这就是为什么大多数团队会跳过它。但他们不应该跳过。
这三种探测共同构成了针对 Agent 的消费者驱动契约测试(Consumer-driven contract testing):Agent 的 Manifest 是消费者契约,而探测则验证后端是否仍在履行该契约。当探测失败时,你可以在 Agent 在生产环境中悄悄产生一千个微妙错误输出 之前 修复它。
有一项准则比工具更重要:将工具描述视为版本化的契约,而非文档。如果描述的更改改变了 Agent 的选择逻辑,那么即使 JSON Schema 完全相同,这 也是一个破坏性变更(Breaking change)。请按版本固定描述。在 CI 中对它们进行 Diff。要求明确的审批才能更改它们。对描述进行一个词的修改,可能会使 Agent 对该工具的调用率翻倍,或将流量重定向到 Manifest 中的竞争工具——而模型不会告诉你发生了这种情况。
Manifest 的所有权问题
技术层面的问题只是一半。难点在于组织架构。
编写工具描述的团队几乎从来不是维护后端端点的团队。平台团队拥有端点。AI 团队拥有 Manifest。描述存在于 AI 团队的代码库中,使用经过优化的语言来引导 LLM;而端点存在于平台团队的代码库中,受制于早在有人将“提示模型”视为部署关注点之前就已经存在的发布流程。
大多数后端团队根本不知道他们的端点正在被 Agent 调用,更不用说在别人的代码库中对他们 API 的自由文本描述是支撑产品的关键逻辑了。因此,他们发布了一个完全常规的变更——收紧验证规则、重命名内部字段、调整默认值——却完全没有意识到这个变更破坏了一个没人告诉过他们存在的契约。
这就是漂移事故的来源。不是因为恶意,也不是因为无能。而是由于结构性的不可见。
三种组织实践可以弥补这一鸿沟:
- 在 Manifest 注册时声明工具所有权。 Manifest 中的每个工具都要注明主要维护者和后端服务。当后端服务发布变更时,其 CI 流水线会为任何依赖它的 Manifest 运行契约测试套件。平台团队通过其流水线中失败的测试,像了解任何其他下游消费者一样了解 Agent 消费者。
- 像对待代码一样评审描述。 工具描述是一个会改变模型行为的 Prompt 片段。对待修改描述的 PR,应像对待修改生产环境 Prompt 的 PR 一样:同行评审、在留存集上进行行为评估、版本升级、部署日志。一旦描述变更不再经过评审,实习生就可能悄悄重定向你 Agent 的流量。
- 针对描述变更运行行为评估,而不只是 Schema 变更。 在合并描述编辑之前,针对代表性的任务集运行 Agent,并将调用率、参数质量和下游结果指标与先前版本进行比较。如果描述更具说服力,Agent 就会更多地调用该工具——而这通常不是你想要的。
这些工作都不显眼。但正是这些枯燥的集成工作,将 Agent 原型与你可以放心交付生产流量的系统区分开来。
本周行动指南
如果你运行的 Agent 系统拥有超过几个工具,你可能已经存在 Manifest 漂移了。问题在于有多少。
第一周可以开展的具体工作:
- 列举你的 Agent 可以调用的每个工具。针对每个工具,识别其后端端点和负责的团队。该列表中的空白就是你的爆炸半径。
- 挑选五个最高风险的工具。为每个工具编写三种探测(实时 Schema、响应形态日志、黄金对行为重放)。将它们接入 CI,以便 Manifest 代码库和后端服务代码库都能触发它们。
- 在生产环境中为所有工具调用开启响应形态验证。先不要拦截失败——只需记录日志并建立仪表盘。在一周内,你就会知道哪些工具正在漂移,以及漂移的严重程度。
- 根据当前的 Manifest 审计你的 Few-shot 示例、检索语料库和存储的轨迹。任何使用重命名字段或弃用枚举值的内容都需要重新生成——否则你的下一次 Manifest 更新将只是表面工夫。
- 要求 Manifest 中的每次描述变更都必须附带行为评估增量。如果你无法衡量 Agent 行为的变化,你就不知道你发布了什么。
目标不是防止漂移。Schema 会发生变化;那是健康的。目标是确保当它们发生变化时,Agent 会在你的用户发现之前察觉到。
- https://modelcontextprotocol.io/specification/versioning
- https://minherz.medium.com/the-silent-breakage-a-versioning-strategy-for-production-ready-mcp-tools-fbb998e3f71f
- https://medium.com/@binarEx/your-mcp-servers-tool-descriptions-changed-last-night-nobody-noticed-e3ad93cf6bc7
- https://medium.com/@kumaran.isk/evolvable-mcp-a-guide-to-mcp-tool-versioning-ae9a612f7710
- https://medium.com/@Modexa/when-one-field-drift-breaks-the-agent-b93638330c31
- https://medium.com/@2nick2patel2/llm-function-calling-pitfalls-nobody-mentions-a0a0575888b1
- https://agenta.ai/blog/the-guide-to-structured-outputs-and-function-calling-with-llms
- https://treblle.com/blog/api-contracts-in-llm-workflows
- https://circleci.com/blog/building-llm-agents-to-validate-tool-use-and-structured-api/
- https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus
