工具 Schema 是提示词,而非 API 合约
你智能体代码库中最昂贵的一行,就是从现有的 OpenAPI 规范中自动生成工具 Schema(tool schemas)的那一行。这看起来是一个利落的工程选择——单一事实来源、无重复、每次 API 变更时自动同步。但这也是为什么你的智能体在应该选择 searchUsersV3 时却选择了 searchUsersV2,因为你的规范示例中写了 limit=20 于是它就填了 20,并且悄无声息地丢掉了 tenant_id,因为它被埋在第七个参数槽位里。
单元测试中不会显现任何这类迹象。Schema 是有效的。端点(endpoint)是存在的。智能体的调用是格式正确的 JSON。然而,模型每次都会用错工具,且是以你的 QA 流水线永远察觉不到的方式,因为 QA 测试的是 API,而不是智能体对 API 的理解。
这个 Bug 是观念性的。OpenAPI 的设计初衷是向编写 SDK 代码的人类描述 API;而工具 Schema 则是作为 Prompt 的一部分,在每次调用时由 LLM 读取。将它们视为同一种产物,就好比根据数据库列名自动生成面向用户的文案一样,犯了同类型的错误。
OpenAPI 负责 描述;工具 Schema 负责指令
OpenAPI 规范是一份合约。它的职责是让代码生成器输出一个类型化客户端,并让阅读 Swagger UI 的开发者明白该发送什么。描述性的文字是文档——人类扫一眼,然后编写遵守类型的代码。参数顺序是装饰性的;SDK 暴露命名参数,键值对进入 JSON 正文,在那里顺序无关紧要。
工具 Schema 则完全不同。每当智能体考虑调用工具时,它就是一段被拼接进模型上下文的文本。模型没有可以依靠的 SDK,没有自动补全来提醒它存在哪些字段,也没有编译错误能在运行时之前捕获缺失的必填字段。它对工具的所有了解都来自于描述、参数名、参数描述、类型标注和默认值。这个产物本质上是一个 Prompt。
一旦你理解了这种框架,模型的几种“糟糕”行为就不再像是模型的失败,而像是 Prompt 的失败。模型填错了字段?那是因为字段名含糊不清,且描述没有消除歧义。模型选错了工具?那是因为这两个工具的描述近乎相同,都是从为人类快速浏览而写的 API 摘要字段自动转换过来的。模型漏掉了可选的安全参数?因为它出现在包含 15 个参数列表的末尾,且描述全文仅写着:“可选。参见文档。”
你的 OpenAPI 规范没问题。它只是不是一个 Prompt。
描述即合约
在 OpenAPI 规范中,description 字段是文档。在工具 Schema 中,它是模型在决 定是否调用工具、在相似工具中选择哪一个以及在每个参数中填入什么时,首先读取的内容——通常也是唯一仔细读取的内容。
一个好的工具描述会告诉模型四件事:工具的作用、何时使用(以及何时不使用)、返回什么,以及调用者必须知道的、未在参数类型中隐含的信息。在自动转换为描述字段的 OpenAPI 摘要中,这些信息都无法可靠地存在。OpenAPI 摘要是为那些已经知道端点用途、只需要一行提醒的工程师准备的;而 LLM 工具描述则是为可能面临 8 个相似工具并需要消除歧义的智能体准备的。
对比同一个端点的两种描述。自动生成的描述写着:“搜索用户”。这是一个不错的 OpenAPI 摘要。经过人工调优的工具描述则写着:“通过姓名、电子邮件或员工 ID 查找用户。返回按相关性排序的最多 50 条匹配结果。当需要根据部分信息查找个人时使用此工具;不要使用此工具枚举租户中的所有用户——请使用 listTenantUsers 为此目的。如果没有匹配结果,返回空数组;未命中时绝不会报错。”
第二种描述承担了 OpenAPI 规范假设人类会通过阅读周围文档、相关端点和页面标题来完成的工作。模型无法获得任何周围的上下文。如果周围的上下文不在描述中,它就不存在。
参数顺序是优先级信号
在类型化的 SDK 中,函数签名中的参数顺序是易用性问题:必填在前,可选在后,相关参数分组。一旦编译,调用处使用命名参数,顺序就消失了。在 JSON 请求体中,在请求离开客户端之前,顺序就已经消失了。
LLM 自上而下地读取工具 Schema。出现在前面的参数比出现在后面的参数获得更多的关注——这种位置偏差(positional bias)已在智能体失败分析中得到证实,实践者在重新排列 Schema 并观察到智能体行为变化的那一刻也会注意到这一点。如果你最重要的消除歧义的参数排在列表的第七位,模型会以自动驾驶模式填完前六个,并把第七个视为大概可以跳过的内容。
这并不是一个可以通过更好的 Prompting 来修补的 Bug。这是一个将指令作为 Token 序列消费的系统的可预测行为。修复方法是将参数列表设计为一个按优先级排序的 Prompt:模型必须思考的参数放在最前面,具有安全默认值的参数放在最后面,而仅为了向后兼容性而存在的参数则根本不要放入 Schema 中。
这与 OpenAPI 的惯例直接冲突。OpenAPI 规范按合理的文档顺序罗列参数——路径参数、查询参数,然后是请求体字段,通常按资源子对象分组。自动生成保留了这种顺序。其结果是一个 Schema,模型需要思考的参数分散在 API 设计者在 2021 年使用的任何分类法中,与智能体应该首先思考哪些参数毫无关系。
默认值即 Few-Shot 示例
在 OpenAPI 规范中,默认值(default)是一种回退机制(fallback)。如果客户端没有发送该字段,服务器就会使用默认值;默认值的存在是为了让 API 在不破坏旧客户端的情况下进行演进。默认值很少能传达典型用法的任何信息——它通常是一个宽容的值(limit=100)或是一个向后兼容的值(format="legacy")。
在工具 schema 中,每一个默认值都是一个 One-Shot 示例。模型读取 "limit (number, default: 100)" 时,会把 100 视为“在没有其他提示的情况下,这里应该填什么数字”的规范答案。即使该默认值的存在原因与智能体的用例完全无关,情况也是如此——例如,该默认值可能是多年前为了匹配一个早已不存在的旧客户端的分页行为而选定的。
枚举排序和嵌套对象内部的字段级默认值也会发生同样的情况。如果一个枚举被列为 ["pending", "active", "archived"],当模型没有其他信号时,pending 被选中的概率会不成比例地增加,因为第一个列出的枚举值是自然的回退选择。一个默认值为 "web" 的嵌套字段 metadata.source,即使对话上下文强烈暗示来源应该是智能体正在运行的集成环境,智能体也会填入 "web"。
如果你是从 OpenAPI 规范中自动生成这些默认值,那么你就是在继承长达十年的偶然默认值作为上下文示例(in-context examples),而从未决定模型应该相信什么是“正常”的。这里的工程纪律是设置能够反映智能体预期行为的默认值,而不是 API 历史上的宽容性设定。
这种工程纪律在实践中是什么样的
认真对待此事的团队不再将工具 schema 视为生成的产物,而是将其视为一个版本化的提示词资产(prompt asset)。这会带来几个实际的后果。
首先,工具 schema 与 API 规范存放在不同的 文件中,由设计提示词的人(而不只是设计 API 的人)进行审查,并与系统提示词(system prompts)通过相同的评估流水线进行交付。对工具描述的修改需要运行评估(eval),就像修改智能体的指令一样,因为这本质上就是对智能体指令的修改。
其次,智能体看到的工具表面积要比 API 表面积小。一个真实的 API 可能在某个资源下有 40 个端点;但智能体只得到 7 个工具,每个工具都是在单一描述下精心策划的操作组合,参数也被简化为智能体可能会填充的字段。剩下的 33 个端点虽然存在,但并未暴露给模型,因为暴露它们会增加上下文负担,削弱选择准确性,并迫使智能体去推理那些人类 SDK 使用者通过查阅文档就能解决的细微差别。
第三,工具设计变成了一项以智能体行为而非 API 完整性为衡量标准的迭代纪律。你会观察智能体针对特定的用户意图选择了哪个工具,观察它正确填充了哪些字段以及跳过了哪些字段,然后调整描述和参数顺序,直到行为符合你的预期。OpenAPI 规范保持不变。工具 schema 是一个独立的层,根据模型的实际表现进行编辑。
这种纪律同样能将系统提示词从“某人在第一天写的一段话”转变为“一个版本化的、经过评估的、刻意编辑的产物”。工具 schema 是提示词的一部分,它们理应受到同等的对待。
自动生成的反模式
陷入这个问题最常见的路径往往是出于好意。一个团队拥有 OpenAPI 规范,他们有了一个新的智能体功能,并且想要避免维护两份 schema。因此,他们接入了一个生成器,在构 建时从规范中导出工具定义。这听起来很有纪律性——单一事实来源、自动更新、无需手动同步。
但他们实际构建的是一条流水线,它将为一类受众编写的文案作为提示词发送给另一类完全不同的受众,且没有任何人工审查这种转换。每当 API 团队增加一个端点,智能体就会得到一个新工具,而该工具的描述在编写时假设读者是一个在另一个标签页打开了其余文档的开发人员。每当 API 团队为了符合人类工程学而重命名一个字段,智能体对该工具的心理模型就会发生变化,而这种变化在任何测试中都不会体现出来。每当 API 团队为了智能体永远不会触发的用例增加一个参数,智能体的上下文就会膨胀,其工具选择的准确性也会随之下降。
解决方法不是放弃 OpenAPI 规范,而是在规范和智能体之间插入一个精心策划的层——这个层负责选择哪些端点成为工具,用提示词风格的文案重写描述,按推理优先级重新排列参数,并设置符合智能体典型用例而非 API 历史默认值的默认值。这一层由负责智能体行为的人员拥有,与系统提示词一起审查,并针对智能体评估集(eval set)而非 API 合约测试进行评估。
这些工作是实实在在的。跳过它的后果就是:你拥有一群看起来在正确调用 API 实际上却并非如此的智能体。
明天该做什么
如果你在生产环境中运行着使用自动生成的工具 schema 的智能体 (agent),一个廉价的诊断方法是将你的工具描述大声朗读出来,就像它们是提示词片段一样。那些读起来像 API 文档的描述——例如 GET /users/{id}: Retrieves a user resource ——正是模型最容易误用的部分。而那些读起来像是在给一位从未见过这个系统的同事下达指令的描述,才是能按预期工作的。
挑选三个流量最高的工具,用提示词风格的描述性文字重写它们的描述,重新排列参数顺序,让那些需要推理的参数排在前面,根据智能体实际应该填写的内容审计其默认值,并针对这些改动运行你的智能体评估集 (agent eval set)。如果评估指标发生了变化,你就有了证据证明 schema 层是起关键作用的“承重型提示词”——这也将为你优化其余部分提供路线图。
这样做可以防止一种隐蔽的失败模式:智能体看起来在正常工作,并已发布到生产环境,但在长尾输入上的准确率却在缓慢下降,而所有的仪表盘却都显示 “工具调用成功”。Schema 并没有验证出错误;它验证了错误的东西。
- https://platform.claude.com/docs/en/agents-and-tools/tool-use/overview
- https://www.anthropic.com/engineering/advanced-tool-use
- https://developers.openai.com/api/docs/guides/function-calling
- https://medium.com/percolation-labs/how-llm-apis-use-the-openapi-spec-for-function-calling-f37d76e0fef3
- https://dev.to/samchon/i-made-openapi-and-llm-schema-definitions-1mn0
- https://www.binwang.me/2025-04-27-Use-OpenAPI-Instead-of-MCP-for-LLM-Tools.html
- https://martinfowler.com/articles/function-call-LLM.html
- https://modelcontextprotocol.io/specification/2025-06-18/server/tools
- https://www.snaplogic.com/blog/unlocking-llms-with-openapi-tool-integration
- https://arxiv.org/pdf/2503.13657
- https://dev.to/docat0209/3-patterns-that-fix-llm-api-calling-stop-getting-hallucinated-parameters-4n3b
