你在没告诉智能体的情况下修改了工具 Schema
一位后端工程师重命名了一个字段。user_id 变成了 customer_id,因为团队终于在所有服务中统一了 “customer” 这个术语。他们还增加了一个参数 region,因为计费系统现在需要它。这次变更通过一个包含两个批准的普通拉取请求(pull request)发布。每一个调用该端点的下游服务都在同一个发布版本中进行了更新。集成测试全部通过。按照后端团队衡量的一切标准,这是一次常规且执行良好的 API 变更。
一周后,支持工单开始增加。负责下单的智能体偶尔会在没有关联客户的情况下下单,或者将其关联到错误的区域。没有人改动过智能体。没有人改动过提示词(prompt)。模型的版本与上个月完全相同。然而,智能体现在却出现了一种以前从未有过的错误。
原因既不是模型中的 Bug,也不是后端中的 Bug。而是工具 Schema(tool schema)有两个消费者,但在审查变更时,只有其中一个在场。
Schema 是包含两个读者的契约
当你向智能体暴露一个函数时,你会编写一个工具定义:名称、描述以及参数的 JSON Schema。你很容易将该 Schema 视为 OpenAPI 规范——一个供你自己的代码进行验证的机器可读描述。这种心理模型只对了一半,而缺失的那一半正是让你栽跟头的原因。
第一个消费者是编写集成代码的开发人员。他们阅读字段名称,连接调用点并处理响应。如果后端重命名了字段,这个消费者会立即发现:构建失败,类型不再匹配,代码检查工具(linter)报错。对于人类来说,这种破坏性变更是“响亮”的,因为人类与 Schema 的关系是由编译器介导的。
第二个消费者是模型。当你将 tools 数组传递给模型 API 时,供应商并不会将 Schema 交给某个独立的验证子系统。它会将你的工具定义——名称、描述、参数 Schema 以及任何示例——序列化为系统提示词,并将该提示词喂给模型。Schema 并不是放在模型旁边的配置。它就是提示词文本。模型读取它的方式与读取其他指令完全相同:将其作为塑造下一个 token 的自然语言上下文。
这种区别正是问题的症结所在。模型与 Schema 的关系不是由编译器介导的。它是由对提示词的注意力介导的。当后端将 user_id 重命名为 customer_id 时,模型不会收到构建错误。它什么也收不到。它会继续输出 user_id,因为那是它锚定的字段名称,而在整个技术栈中,没有任何机制可以告诉它事实并非如此。
为什么模型会悄无声息地失效
API 的人类消费者会快速且彻底地失败。模型消费者则会缓慢且部分地失败,这两点都使得这种故障更难被察觉。
失败是缓慢的,因为模型并不会在每次请求时重新推演。无论它学到了什么形状——无论是从工具描述、少样本示例(few-shot examples),还是从其训练数据中偏向常见字段名(如 user_id)而非罕见字段名(如 customer_id)的统计引力中——这种形状都已经固化在它生成调用的方式中。后端可以在周二更改 Schema,而模型会继续生成周一的参数,直到有人更新它读取的提示词和示例。
失败是部分的,因为工具调用的准确性是统计性的,而非二进制的。模型不会从 100% 正确切换到 0% 正确。它会发生漂移。也许 85% 的调用恰好仍然正确,因为新的字段名与旧的足够接近,或者模型有时会从更新后的描述中复制正确的名称,有时又退回到之前的状态。故障率从接近于零上升到 15%,并不会触发旨在捕捉宕机的警报。它表现为一种模糊的质量退化,有人会在一周后、数据已经变脏时才注意到。
漂移的分类值得明确命名,因为每种变体都以其特有的安静方式失效:
- 重命名字段。 模型输出旧的键。你的工具接收到的新键为
undefined。如果你不进行验证,调用就会在缺少参数的情况下继续。 - 增加必填参数。 模型锚定的上下文中没有任何内容提到新的参数,因此它会完全忽略它。调用在结构上是不完整的。
- 更改枚举值。 模型从旧集合中输出一个值。这是一个看似合理但已不再有效的字 符串。
- 更改类型。 模型在需要数字的地方发送了字符串。宽松的后端会对其进行强制转换;严格的后端会拒绝它;无论哪种方式,意图都被破坏了。
- 删除字段。 模型仍然发送已停用的参数,而它会被默默丢弃。
每一项按照 API 版本控制的标准定义都是破坏性变更——而标准定义在制定时考虑到的是人类消费者。模型也是消费者,而且它是更脆弱的那一个,因为它针对旧的形状进行了微调,并且没有任何契约测试在守护它。
借鉴你已有的词汇
- https://www.anthropic.com/engineering/advanced-tool-use
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/implement-tool-use
- https://developers.openai.com/api/docs/guides/function-calling
- https://openai.com/index/introducing-structured-outputs-in-the-api/
- https://gorilla.cs.berkeley.edu/leaderboard.html
- https://semver.org/
- https://docs.pact.io/
- https://modelcontextprotocol.io/specification/versioning
- https://arxiv.org/html/2502.18990v1
- https://medium.com/@Micheal-Lanham/stop-blaming-the-llm-json-schema-is-the-cheapest-fix-for-flaky-ai-agents-00ebcecefff8
