与工具本身脱节的工具描述
一名后端工程师将参数从 user_id 重命名为 account_id,因为两者在半年前就不再是同一个东西了,而且一个支持工单终于让这种歧义变得无法忍受。JSON schema 在发布重命名的 pull request 中得到了更新。该工具的文本描述——即模型实际阅读以决定是否调用该工具以及如何调用的那个段落——却存在于另一个代码库中,由另一个团队负责,通过工单队列更新,并且仍然写着“传递 user_id 以查找账户”。没有人标记出这一点。模型尽职尽责地使用正确的 schema 调用工具,填充正确的字段,并在每一个快乐路径(happy-path)查询中获得正确答案。这个 bug 是隐形的,直到有一天用户输入的内容中,他们经过身份验证的 user_id 与他们询问的 account_id 是两个不同的实体,而智能体自信地返回了别人的数据。
这种失败属于这样一种类型:系统正在完全按照你告诉它的去做——做了两次,但在两个互不一致的地方。工具调用格式正确。参数通过了 schema 验证。评测套件(eval suite)显示为绿色。模型的推理轨迹(reasoning trace)如果你去看的话,会引用文本描述作为其行 为的依据,而文本描述是错误的。你的模型一直在一个你已经停止维护的规范上悄悄运行。
两个真相来源,却都没有权威性
Schema 和描述从不同角度描述同一个事物,但它们位于代码库的不同部分,由不同的人以不同的频率更新。JSON schema 与实现代码放在一起。后端工程师重命名一个字段,类型系统或 API 生成器就会强制 schema 随之更新——如果不更新 schema,就无法发布代码,因为运行时(runtime)不会接受旧的形状。Schema 以一种最糟糕的方式承担着重任:如果不一致就会崩溃,因此它保持了真实性。
文本描述则完全没有这种压力。它存在于工具注册表、文档网站、平台团队维护的 YAML 文件,或者某人在三个季度前粘贴到 Slack 线程中的一个段落里。只有当有人记得提交工单,或者发布经理在评审期间发现了它,或者根本没有人管它时,它才会更新。在运行时路径上,没有任何机制强制它保持准确。在 CI 中,没有任何东西会因为它的错误而失败。Schema 被视为代码;描述被视为文档,而软件领域的每个团队都明白,文档是会脱节的。
模型对此一无所知。模型被赋予了两个旨在描述同一个工具的产物,并被要求自行调和它们。当它们一致时,模型的行为符合你的预期。当它们不一致时,模型的行为取决于它在选择过程中更看重哪一个产物,而对于调用决策——“我应该使用哪个工具,它的参数是什么意思?”——文本描述占据了压倒性的地位。Schema 告诉模型存在哪些字 段;描述则告诉模型这些字段的含义。含义胜过形式。
描述是你提示词的一部分,只是你没有这样对待它
那些绝不允许系统提示词(system prompt)在未经代码评审的情况下发生更改的工程团队,却会乐于让工具描述通过维基编辑来更改。这种不对称性与技术重要性无关,而完全取决于该产物恰好存放在哪里。系统提示词中的描述会经过评审,因为系统提示词在 git 中。你的 MCP 服务器工具定义中的描述只有在 MCP 服务器代码经过评审时才会被评审。而你挂载到智能体中的第三方工具的描述,则取决于供应商在任何一个周二心血来潮的决定。
这很重要,因为从功能上讲,描述就是运行时的系统提示词。每当工具可用时,模型都会将其连接到上下文中,在选择过程中对其进行权衡,并使用它来推断参数的含义。描述如果说“使用此工具查找用户的账户”,就会将该工具界定为账户查找;描述如果说“当用户询问过去的购买记录时,使用此工具检索账户的交易历史”,则会将同一个端点界定为交易检索,并改变模型路由到该端点的查询类型。在这两种情况下,schema 是完全相同的。但模型的行为则不然。
因此,当描述发生脱节时——当 user_id 在 schema 字段变成 account_id 后仍保留在文本中时——模型不仅仅是在阅读过时的文档。它是在生产环境中被告知,字段语义与实际不符。模型将使 用运行时不再遵循的定义来推理工具,并一直持续下去,直到描述被更新,或者直到这种偏差产生了一个足以引起调查的明显故障。在常见路径上,这两者都不会发生。
故障在生产环境中的表现
这种故障具有一种特殊的特征,使其在常规代码审查中很难被发现。工具调用成功了,参数通过了校验,响应格式也正确。模型的下游行为——总结结果、规划下一步——看起来都很合理。如果你对对话进行抽样,你会认为 Agent 做了正确的事情。这个 Bug 只会出现在那一小部分流量中:当描述文本中的错误定义导致了错误的调用时,而在设计良好的 Schema 中,这恰恰是因为两个概念被混淆而导致 Schema 重命名的那类情况。
对于回归测试来说,这是最糟糕的选样偏差:故障集中在那些由于重命名所试图修复的底层实体区分起关键作用的查询中。你的健康用户——那些因为具有一对一关系而导致 user_id 等于 account_id 的用户——不会察觉到任何异常。你的企业级客户——他们在一个身份下管理多个账户,而这正是最初进行重命名的原因——则会看到看起来像权限 Bug 的跨账户数据泄露。你的评估集如果是在重命名之前构建的,那么它包含的关于这种区分的例子数量恰好为零,并且会将模型评定为健康。
更糟糕的是,模型的置信度不会受到影响。没有模棱两可的话语,没有“我不确定这是否是正确的账户”之类的开场白,因为从模型的角度来看,没有任何歧义:它读到了一个写着 user_id 的描述,查找了 user_id,并返回了结果。推理链读起来连贯且理由充分,而在事故后复盘中,团队会花很长时间讨论这究竟应该算作模型错误还是工具错误,最后才会意识到这是一个在推理时起关键作用的文档错误。
弥合差距究竟需要什么
解决方法并不是“对描述更加细心”。几十年来,团队一直试图对文档更加细心,但文档依然在不断偏离,因为更新文档的压力总是小于交付底层变更的压力。解决方法是取消描述作为独立产物的地位,将其变为 Schema 的派生视图。
最强的模式是生成:描述文本是根据 Schema 注解本身生成的,其参数名称、类型和逐字段描述都来自运行时校验所依据的同一个数据源。参数重命名变成了对 Schema 的单一变更,而发送给模型的描述会在每次构建时从该 Schema 重新生成。真相不再有第二个存储位置,因为第二个产物并不作为一个可写的实体存在。这是 OpenAPI 到 MCP 生成器所采用的模式,也是任何规模大到足以让偏离产生统计风险的工具层面的正确默认选择。
当无法实现完全生成时——因为文本中包含 Schema 注解无法捕获的教学式引导,或者描述是与非工程师共同创作的——次优的模式是对描述中引用的 Schema 字段进行契约测试。测试会解析文本,提取每一个看起来像参数名称的 Token(每一个带有反引号的标识符,每一个符合字段命名规范的 snake_case 单词),并断言这些 Token 中的每一个都是 Schema 中的当前字段。当 Schema 的字段已变为 account_id 而描述中仍提到 user_id 时,测试就会失败。它不要求描述尽善尽美,只要求描述在所引用的名称上保持诚实。
注册表侧的验证从发布方向弥合了同样的差距:一个 MCP 注册表、一个内部工具目录或一个函数调用网关可以拒绝接受描述中引用了 Schema 未声明字段的工具定义。这种检查成本很低,失败模式很明显,它将负担从“记得更新两者”转移到了“你无法发布错误状态”。这种模式与运行时已经用于防止错误类型工具调用的 Schema 校验步骤完全相同——只是它被应用在了作为输入的描述上,而不是作为输出的参数上。
对于第三方工具的描述——供应商的 MCP 服务、托管的函数调用 API,以及任何你不控制的东西——正确的做法是将每个工具描述视为一个固定版本的依赖。你接受的描述版本会进入你的工具清单(manifest),下个月到达的新版本在发布到生产环境之前需要经过评审,文本的变更应像 Schema 的变更一样被严肃对待。供应商对描述引导语的重命名,对于你的模型行为来说,是一个没有提升版本号的破坏性变更(breaking change),你应该像捕获任何其他隐蔽的原始变更一样,准备好捕获它。
隐藏在文档 Bug 背后的领导力问题
这个问题还有一个领导层面的版本,技术修复手段无法完全解决。描述之所以会发生偏离,最初的原因是两个团队拥有描述同一事物的两个产物,且两个团队的评审流程都没有将对方的产物视为自己交付物的一部分。后端工程师 按照其专业标准完美地完成了字段重命名。维护描述的平台工程师从未被告知发生了重命名。他们之间的交接环节才是断裂的地方。
将工具描述视为系统提示词的一部分——从而也将其视为交付给用户的产品界面的一部分——会将其纳入与提示词其余部分相同的评审边界内。这意味着需要有一个代码所有者来批准描述变更,需要有一个 CI 检查在工具描述与其 Schema 不一致时报错,还需要一个发布流程,在该流程中,除非描述已根据 Schema 重新推导并经过重新评审,否则工具变更就不算完成。这些机制在技术上都不难。难点在于组织决策:认定描述是产品而非文档,并将其置于该认定所蕴含的纪律之下。
能够处理好这一点的团队往往会趋向于同一个运营原则:模型在推理时读取的每一个产物都是提示词的一部分,而提示词的每一个部分都是与用户签订的契约。 Schema、描述、系统提示词、工具结果格式化程序、检索上下文——所有这些都决定了模型会说什么,所有这些都值得同等权重的评审。描述并不是其中最微不足道的部分;它是定义 Schema 含义的部分。任由其偏离就是任由工具层面的含义偏离,而模型——作为模型——会遵循含义而非类型。
值得关注的迹象
如果你想在客户发现之前在自己的系统中找到这种失效模式,有三项检查可以快速发现它。首先,解析每个工具描述中的标识符标记 (identifier tokens),并针对实时模式 (live schema) 进行 grep 检查;这种方式的误报率很低,而正报的情况恰恰就是那些发生偏移 (drift) 的案例。其次,采样一周的生产环境工具调用,并将模型传递的参数与散文描述中对该字段用途的框架定义进行对比;两者之间的不匹配,正是描述引导模型的理解偏离了模式意图的情况。第三,观察不同用户间故障的基数 (cardinality)——描述偏移导致的 Bug 会集中在那些数据违反了旧描述所假设的简化方案的用户身上,其故障率表现为特定细分群体的剧增,而非平滑的基准线上升。
工具描述是你提示词中团队里没人会将其视为提示词的那一部分,而这恰恰是它现在最有可能对你的模型撒谎的原因。模式是诚实的,因为它承担不起撒谎的代价。描述则没有这种约束,除非你为它建立一种约束,否则它会像文档一向发生的那样产生偏移——悄无声息地、不对称地,并且恰恰发生在真相最为关键的情况下。
- https://www.anthropic.com/engineering/writing-tools-for-agents
- https://dev.to/nesquikm/my-mcp-tools-broke-silently-schema-drift-is-the-new-dependency-hell-5c49
- https://dev.to/aws-heroes/mcp-tool-design-why-your-ai-agent-is-failing-and-how-to-fix-it-40fc
- https://www.speakeasy.com/mcp/tool-design/generate-mcp-tools-from-openapi
- https://blog.christianposta.com/semantics-matter-exposing-openapi-as-mcp-tools/
- https://workos.com/blog/mcp-registry-architecture-technical-overview
- https://nordicapis.com/why-openapi-should-be-the-foundation-for-your-mcp-server/
- https://apxml.com/courses/building-advanced-llm-agent-tools/chapter-1-llm-agent-tooling-foundations/tool-specifications-descriptions
- https://www.llamaindex.ai/blog/building-better-tools-for-llm-agents-f8c5a6714f11
- https://composio.dev/content/ai-agent-tool-calling-guide
- https://arxiv.org/pdf/2602.20426
