本文由Zishan Ali Khan用英文撰写;此为机器翻译。AI发展速度快于任何语言,因此某些技术术语特意保留英文,带有虚线底线的术语在悬停或点击时显示原文。 阅读原文 →
工具契约:为什么智能体跳过你的工具
工具规范是确定性系统与概率性调用者之间的契约。大多数团队为错误的读者编写它。
2026年4月15日 · 17 min · 更新于 2026年7月11日 · 原始 .md
这份文档的起因是,一次评估运行乍一看很健康。我正在对一个生产级代理进行企业价值验证的黄金集评估:这是一组精心策划的任务,每项任务都有预期的工具调用序列,针对代理进行重放并自动评分。推理追踪确实很好。模型正确地分解了问题,合理地进行了规划,并给出了清晰的最终答案。然而,在超过20%的情况下,它跳过了本应调用的工具。它没有调用失败,也没有使用错误的参数调用。只是悄悄地凭空回答,仿佛工具不存在一样。
我的第一反应是惯常的做法:更换模型,然后重写系统提示。两者都没有显著改变这个数字。一个更昂贵的模型以13倍的成本将跳过率降低了几点。提示词修改(“你必须使用可用的工具”)却导致了相反的失败,代理开始调用它不需要的工具。最终解决问题的方法很无聊:我重写了工具描述。我说明了每个工具何时应该使用,何时不应该使用,以及它返回什么。我将约束从散文中移到了模式中。跳过率降至个位数。相同的模型,相同的系统提示,相同的评估集。
这个结果在我之后评估的每个代理中都重现了,它改变了我对工具调用失败的看法。主要原因不是模型弱点。而是工具契约规范不足:模糊的描述、重叠的工具、接受垃圾数据的模式、以及只存在于开发者脑海中的约束。团队为能阅读源代码的开发者编写工具规范。实际的读者是一个概率模型,它只看到你注册的JSON,别无其他。本指南是我现在处理这个问题的方法,包括我构建的确定性代码检查器,以使其在CI中可强制执行。
工具调用失败的分类
当我评估评估记录时,每个与工具相关的失败都归入五个类别之一。有用的做法是将每个类别追溯到导致它的规范缺陷,因为这会告诉你需要修复什么。指责“模型”并不能给你任何可操作的信息。
| 失败模式 | 在记录中表现为何 | 规范中的根本原因 | 如何在评估中检测 |
|---|---|---|---|
| 跳过 | 在需要工具调用时没有调用;代理从参数化知识或猜测中回答 | 描述没有说明工具何时适用,或者没有明确其数据是实时且权威的 | 黄金追踪期望工具X;记录中没有调用X。按工具报告跳过,而不仅仅是总体跳过 |
| 错误工具 | 代理调用了一个看似合理的邻近工具:search_documents 而不是 search_customer_accounts |
描述几乎相同且没有消歧条款的重叠工具 | 期望工具A,观察到工具B。在矩阵中聚类混淆;重叠对立即显现 |
| 参数格式错误 | 调用未能通过模式验证或API返回400;代理盲目重试或放弃 | 模式过于宽松:无类型参数,没有 required 数组,存在格式但却是自由字符串字段 |
在追踪中统计每个工具的模式验证失败和4xx响应 |
| 参数幻觉 | 参数在语法上有效但却是凭空捏造的:一个在对话中从未出现的账户ID | 规范缺少约束和示例,因此模型用看似合理的值填充空白 | 将参数值与对话中或先前工具结果中实际存在的值进行比较 |
| 过早回答 | 代理在没有调用支撑工具的情况下给出了自信的最终答案 | 描述没有告诉模型其知识在此处已过时(“账户数据每日变化;即使你认为你知道,也要调用此工具”) | 最终答案存在,所需调用缺失;检查答案声明是否可追溯到工具输出 |
关于这张表,有两点通过重复验证而确立了其地位。首先,跳过是最常见的失败,也是最不显眼的失败。格式错误的调用会抛出你可以报警的错误。跳过会产生一个流畅、自信但错误的答案,而你的日志中没有任何标记。只有当你的评估编码了预期哪些工具调用时,你才能看到跳过。其次,五个类别中有四个是规范缺陷。只有在真正良好分离的工具集中发生的错误工具选择,才可能是模型的局限性,即便如此,规范通常也是帮凶。
你的工具描述就是一个提示词。它在每一个回合中都会被注入到上下文窗口。大多数团队花费数周时间迭代系统提示词,却对随之发布的十五个工具提示词不花一分钟。
工具契约的本质
我刻意使用了“契约”这个词。工具位于两个截然不同的系统边界:一个确定性实现,它严格按照代码执行;一个概率性调用者,它纯粹根据你注册的文本和模式来决定是否以及如何调用它。模型无法阅读你的源代码。它无法询问同事。规范是整个接口,它包含六个部分:
- 名称。 工具选择最强的单一信号。模型在阅读任何描述文字之前,会先根据名称进行模式匹配。
- 描述。 何时使用工具,何时不使用,以及它返回什么。这是以模型为读者的文档,这改变了编写规则。
- 模式。 输入的机器可检查形状:类型、必填字段、枚举、格式、范围。
- 约束。 模式无法结构化表达的一切,明确说明:顺序依赖(“先调用
search_customer_accounts以获取account_id”)、速率预期、数据新鲜度。 - 示例。 至少一个具体的、有效的调用示例。模型从示例中进行泛化的可靠性远高于从抽象描述中。
- 错误语义。 失败表现为何,以及调用者应如何处理。一个知道
STATEMENT_NOT_READY意味着“停止并告知用户”的代理,其行为与盯着堆栈追踪的代理截然不同。
最具杠杆作用的习惯是编写面向行为的描述,而不是面向实现的描述。面向实现的描述说明代码做了什么:“通过账户服务查询客户数据库。”面向行为的描述说明调用者应该做什么:“按姓名、电子邮件或账户号码搜索客户账户。当用户询问特定客户且你尚未拥有其账户ID时使用此工具。”第一个描述准确但对模型无用。第二个描述读起来像一个路由规则,因为模型正是这样理解它的。我现在发布的每个描述都包含字面短语“在以下情况使用”以及,对于任何有合理邻近工具的,包含“不要用于此目的”。
可衡量的重要性规则
这些是我在审查工具规范时应用的规则,大致按照每条规则对我的评估中跳过率和错误率的影响程度排序。这些都不是什么稀奇古怪的东西。但它们却经常在生产环境的 MCP 服务器中缺失。
每个工具只做一件事,且没有重叠的邻近工具
如果两个工具都能合理地处理同一个请求,模型就会在它们之间分配调用,你的评估将显示出模糊的混淆模式,而不是清晰的失败。我看到最糟糕的例子是像 get_customer / fetch_customer_details / lookup_client 这样由不同团队自然演变而来的工具对。合并它们,或者给每个工具一个清晰的边界,并在描述中通过名称相互引用(“对于交易历史,请使用 list_account_transactions”)。模型在工具之间做决策是一个分类任务;你的工作是使这些类别可分离。
说明何时不使用它
正向描述定义了一个模糊区域。负向条款定义了其边界。在我开篇示例中降低跳过率的描述,不仅仅说明了工具的功能;它还说“即使你认为你已经知道答案,也要使用此工具;持有数据每日变化。”这一句话直接解决了跳过和过早回答的失败,因为模型的默认假设是其参数化知识是足够的。反之,“不要用于X”的条款则解决了错误工具选择的问题。这两个方向都很重要。
将约束放入模式中,而不是散文中
散文约束是建议。模式约束是可检查的,模型更可靠地遵守它们,因为它们是结构性的。每次你在描述中写“应该是一个有效的日期”时,你都可以在模式中写一个 pattern 或 format。这是一个真实审查的修改前后对比,经过泛化处理:
{
"name": "get_statement",
"description": "Gets a statement. Date should be in the right format and not in the future.",
"inputSchema": {
"type": "object",
"properties": {
"date": { "type": "string" },
"acct": { "type": "string" }
}
}
}
这里的所有错误在评估运行之前都是不可见的:没有必填数组(因此模型可能会省略这两个字段),date 没有格式(我在追踪中观察到“上个月”、“2026/05/01”和纪元毫秒),而 acct 没有给模型任何可依据的基础,这就是幻觉账户ID的来源。重写后:
{
"name": "get_monthly_statement",
"description": "Retrieves one closed monthly account statement as a PDF link. Use this after you have an account_id from search_customer_accounts. Do not use this for real-time balances; use get_account_balance. Returns error code STATEMENT_NOT_READY if the requested month is not yet closed; do not retry, tell the user when it will be available.",
"inputSchema": {
"type": "object",
"properties": {
"account_id": {
"type": "string",
"pattern": "^ACC-[0-9]{8}$",
"description": "Account identifier, e.g. ACC-00417233. Obtain via search_customer_accounts; never guess this value."
},
"month": {
"type": "string",
"pattern": "^[0-9]{4}-(0[1-9]|1[0-2])$",
"description": "Statement month as YYYY-MM, e.g. 2026-05. Must be a fully closed month, not the current month."
}
},
"required": ["account_id", "month"],
"additionalProperties": false
},
"annotations": { "readOnlyHint": true }
}
枚举值得特别提及。任何具有有限合法值集的参数都应该是一个枚举,句号。描述中写着“之一:active, dormant, closed”的自由字符串最终会收到“inactive”,而你的后端将对此进行处理。
诚实对待必填与可选
两个失败方向。将真正必填的字段标记为可选,会诱使调用省略它,然后模型从错误消息中学习,浪费一个回合。将可选字段标记为必填,会迫使模型凭空捏造一个值,这是你通过规定制造的幻觉。我经常在从后端 DTO 生成的规范中看到第二种情况,其中每个字段都是数据库所需的,而不是调用者所需的。规则是:required 反映了工具完成有用工作所需的内容,并且每个可选字段的描述都说明了省略它会发生什么(“省略以搜索所有状态”)。
用代码而非散文表示失败
错误消息是契约的一部分,因为代理会读取它们并决定下一步做什么。原始堆栈追踪或通用的“内部错误”给模型两个选择:盲目重试或道歉。带有机器可操作提示的确定性错误代码给它一个分支可走。我使用的模式是:一个稳定的 code 字段,一个单句的人类可读消息,以及在适用情况下的一条指令(retry_after_seconds,“首先获取一个有效的 account_id”)。在评估中,这会将一类死胡同的记录转换为恢复,并使你的测试套件中的失败断言变得易于编写。
为调用者命名,而非为代码库命名
动宾结构,snake_case,无内部术语。search_customer_accounts 优于 queryCustDB、cst_lkp_v2 和 CustomerAccountSearchServiceEndpoint。内部系统名称尤其代价高昂:模型不知道“Falcon”是你的定价引擎,所以 query_falcon 就像没有标签一样。名称应该让一个没有任何组织上下文的模型预测工具的功能。这听起来像是表面功夫。但事实并非如此;名称是工具选择的第一道过滤器,在混淆矩阵中,我可以看到命名不佳的工具将调用输给命名更好但做错事的邻近工具。
尊重长度预算
描述长度有一个最佳点。少于大约十个词,工具规范不足,会被跳过或误用。超过几百个词,信号就会被淹没:何时使用的条款埋在第四段,模型在同一个上下文窗口中还有十五个其他工具描述争夺注意力。在我的审查中,表现最佳的描述通常是三到六句话:它做什么,何时使用,何时不使用,它返回什么,以及错误行为。如果你需要更多内容,那么这个工具可能承担了过多的任务(参见规则一),或者多余的材料应该放在模式中。
确定性地评估规范
在进行了足够多的这些审查之后,我注意到我每次都在应用相同的清单,而一个你手动应用的清单,就是一个你尚未编写的代码检查器。所以我构建了一个:Tool Credit Score,它是 Agent Contract Studio 的一部分,开源并在 agentic-contract-studio.vercel.app 上实时运行。你粘贴一个 MCP 工具规范(或从 GitHub 导入,或从运行中的 MCP 服务器发现),它会根据一个确定性评分标准给每个工具打分0到100分,然后自动修复它能修复的部分。
评分标准有五个加权类别,它们直接映射到上述的失败分类:
| 类别 | 权重 | 检查内容 | 针对的失败模式 |
|---|---|---|---|
| 描述 | 25% | 存在,10个词以上,以动词开头,200个词以内 | 跳过,过早回答 |
| 参数 | 25% | 模式是 type: object;每个参数都有类型和描述;存在一个 required 数组且真实 |
参数格式错误和幻觉 |
| 最佳实践 | 20% | MCP 注解(readOnlyHint, destructiveHint, idempotentHint);additionalProperties: false;描述提及返回和错误 |
错误工具,死胡同错误处理 |
| 命名 | 15% | snake_case,动词前缀,64个字符以内 | 错误工具 |
| 示例 | 15% | 至少一个有效的调用示例 | 参数格式错误和幻觉 |
分数汇总为字母等级(90分以上为A,60分以下为F),自动修复器确定性地处理机械性修复:重命名为带有动词前缀的 snake_case,添加缺失的参数类型,搭建示例和注解。有一个可选的LLM润色描述措辞的步骤,但它确实是可选的、仅限本地的,并且绝不触及参数名称、类型或模式结构。核心管道不需要模型,也不需要API密钥。
最后一个设计决策是关键,值得辩护,因为“让LLM审查规范”是显而易见的替代方案。我选择在CI中使用确定性代码检查有四个原因。它是可复现的:每次运行相同的规范都会得到相同的分数,因此拉取请求中的分数下降意味着规范发生了变化,而不是评审者的心情。它快速且免费,因此你可以在每次提交时运行它,而无需讨论令牌预算。它是可解释的:每个扣分都对应一个有具体修复措施的命名规则,这正是你在代码审查评论中想要的。而且它无可辩驳,这一点比它应有的重要性更高;我曾看到团队为一个LLM评审的判决争论一个小时,但没有人会和一个正则表达式争论。
诚实的提醒:代码检查器检查的是形式,而不是意义。Tool Credit Score 每次都会捕获缺失的 required 数组;它无法告诉你你的两个工具在语义上重叠,或者你的“何时使用”条款对你自己的系统来说是事实错误的。这些缺陷只会在评估(测量的跳过率和混淆率)或人工审查中浮现。代码检查器的作用是确保廉价的、机械性的缺陷永远不会达到那个阶段,从而将昂贵的审查精力投入到真正需要的地方。
Tool Credit Score 达到90分以上并不能保证代理会很好地使用你的工具。但分数低于60分几乎可以保证它们不会。代码检查是底线,而不是上限。
MCP 服务器中这些失败的集中点
MCP 使工具变得可移植,这确实很有用,但它也使糟糕的规范变得可移植。我审查过的大多数低分服务器都存在三种模式。
从 OpenAPI 生成的服务器。 将转换器指向一个 OpenAPI 文档,你会在几分钟内得到一个 MCP 服务器:每个端点一个工具,参数名称直接取自后端,描述(如果写了的话)是为阅读 API 参考文档的开发者编写的。结果是一个形状像你的数据库,而不是像你的用户任务的规范界面。一个面对 post_v2_accounts_search、get_accounts_by_id 和 get_accounts_search_legacy 的代理将产生你预期的混淆矩阵。生成的服务器是一个很好的起点;但它们不是一个可交付的契约。预算一个整理过程,将端点形状的工具合并成任务形状的工具。
大杂烩服务器。 一个封装了整个 SaaS 产品并暴露80个工具的服务器,因为API有80种能力。这些工具定义中的每一个都会在每个回合中注入到模型的上下文中,因此无论本次对话是否需要,你都要为整个界面支付令牌费用,并且随着候选集增大,工具选择的准确性会下降,尤其当候选工具共享词汇时。我的工作经验是:对于给定任务,实际暴露给代理的工具数量保持在20个左右。这个数字是我从评估中观察到的,不是定律,而且更好的模型会不断将其推高。这种影响的方向在我测试过的所有模型中都保持不变。
没有渐进式披露。 解决大杂烩问题的方法不是删除能力,而是分阶段暴露它。为常见路径暴露一小部分核心工具。对于长尾能力,使用发现模式:一个 search_tools 风格的工具,按需返回完整的契约;如果你的客户端支持,则延迟模式加载;或者专用的子代理,每个子代理只看到其工具集的一部分。MCP 的设计支持这一点:客户端选择要暴露哪些工具,并且根据任务上下文进行服务器端工具过滤也很容易实现。契约规范在每一层都适用;一个描述模糊的发现工具与预加载的工具失败方式相同,只是晚了一个回合。
MCP 的 annotations 块(readOnlyHint、destructiveHint、idempotentHint、openWorldHint)值得比现在更多的使用。除了安全价值之外,注解允许客户端在不解析描述的情况下应用通用策略(自动批准只读工具,在确认后才允许破坏性工具),它们是代码检查器中最便宜的得分改进之一。
通过评估闭环
上述规则作为观点价值不大。但作为衡量过的差异,它们价值巨大,而衡量方法与我在 评估代理系统 中描述的相同:一组带有预期工具调用序列的黄金任务集,针对代理进行重放,并自动评分。工具契约的特定循环是:
- 基线。 针对当前规范运行黄金集。记录每个工具的跳过率、错误工具混淆和参数验证失败。按工具统计很重要:8%的总跳过率可能隐藏了一个被跳过一半时间的工具。
- 修复规范。 首先进行代码检查(机械缺陷),然后应用语义规则:边界、何时不使用条款、模式约束。
- 重新运行相同的黄金集。 模型、提示词和任务都没有改变,因此差异可归因于规范。这是你在代理工程中能获得的最清晰的A/B测试,也是我如何知道描述重写将本指南开篇所述的20%以上的跳过率降低到个位数的原因。
- 在CI中把关。 工具规范就是代码。对其进行版本控制、差异比较,并将描述更改视为行为更改,因为它确实是;它编辑了每个回合都会运行的提示词。如果任何工具的 Tool Credit Score 低于阈值,我的管道就会使拉取请求失败,并在合并前标记任何规范差异以进行评估重新运行。
同样的财富管理项目产生了另一个我经常引用的数字:一旦工具契约得到修复,一个中等模型在我们的黄金集上达到了高级模型的准确性,成本大约是后者的1/13,而且工具跳过率是经过测量的,而不是假设的。糟糕的规范不仅会导致失败。它们还会悄无声息地增加你的模型费用,因为昂贵模型的额外能力被用于弥补那些如果编写得当,廉价模型也能执行的契约。
我仍然会做不同的事情:我的黄金集是重放的对话,而不是真实用户,而真实用户表达请求的方式比精心策划的任务更能考验工具选择。我发布的所有代理在生产环境中的跳过率都高于评估中的跳过率。将评估数字视为下限,并继续根据相同的分类法抽样生产追踪。
一句话版本
生产代理中大多数工具调用失败是规范缺陷,而非模型缺陷:跳过、错误工具选择、参数格式错误、参数幻觉和过早回答,所有这些都可追溯到为开发者而非实际读取它们的模型编写的契约。修复契约:每个工具只做一件事且没有重叠的邻近工具,面向行为的描述,包含明确的何时使用和何时不使用条款,约束以枚举、格式和范围的形式在模式中表达而非散文,一个真实的 required 数组,至少一个示例,代理可以分支的确定性错误代码,以及没有内部术语的动宾结构名称。在CI中确定性地检查所有这些(我的开源 Tool Credit Score 可以做到,0到100分并自动修复),因为可复现的分数在把关方面优于LLM评审,然后通过评估前后测量每个工具的跳过率来证明语义修复的有效性。在我的企业评估工作中,这个循环将20%以上的工具跳过率降低到个位数,而无需触及模型或系统提示词,这也是一个中等模型以大约1/13的成本与高级模型匹配的原因。
参考文献
- Model Context Protocol 规范,包括工具定义和注解
- Anthropic:工具使用文档,关于模型如何消费工具规范的实用参考
- Anthropic 工程:为代理编写有效工具
- Agent Contract Studio (Tool Credit Score),上述确定性 MCP 规范代码检查器,实时运行于 agentic-contract-studio.vercel.app
- 评估代理系统,我的现场指南,本文档的测量数据来源于此评估工具