工具 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 年使用的任何分类法中,与智能体应该首先思考哪些参数毫无关系。
- 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
