工具契约设计
提出问题
工具契约是模型和业务系统之间的接口协议。它不是 SDK 文档的精简版,也不是把后端函数签名翻译成 JSON。一个好的工具契约要同时服务三件事:
- 帮模型判断“什么时候该用这个工具”。
- 帮程序校验“这次调用是否合法”。
- 帮人复盘“为什么调用、结果是什么、失败如何处理”。
如果契约设计不好,模型会误选工具、乱填参数、误读返回值;程序会拿到半结构化数据;运营和研发在事故后只能从自然语言日志里猜原因。
场景
继续看发票工单场景。系统有一个查询发票状态的工具:
这个工具看起来简单,但真实产品里会遇到很多边界:
- 用户可能只说“昨天买的企业版”,没有订单号。
- 用户可能给了多个订单号。
- 订单号可能属于同租户的另一个部门,也可能完全不属于当前用户。
- 发票可能不存在、开票中、已开票、被驳回、需要补充税号。
- 工具可能超时,或发票系统维护中。
- 模型需要知道“未找到订单”和“未开票”不是一回事。
如果契约只写“查询发票”,模型就容易把它用于所有发票相关问题;如果 output 只返回一段文本,模型就容易把状态解释错;如果 error 只返回 failed,调用方无法决定重试、追问还是转人工。
分析问题
问题类型
工具契约主要处理五类问题。
模型适合从自然语言中抽取候选参数、根据 description 选择工具、把 output 转成用户可读解释。程序应该负责 schema 校验、权限校验、业务状态映射、错误分流和日志。
收益和成本判断
工具契约的收益包括:
- 降低工具误选率。
- 降低参数解析和校验失败。
- 让工具返回值可被 UI、工作流和模型稳定消费。
- 让错误能进入重试、追问、降级或人工队列。
- 让工具版本变更可以被管理。
成本也很实际:
- 需要维护契约和后端实现的一致性。
- 需要为状态、枚举和错误码命名。
- 需要处理版本兼容。
- 需要写工具级测试和评估样本。
- 复杂 schema 会增加模型调用难度。
所以契约设计要避免两个极端:一个极端是“只有函数名和自由文本”;另一个极端是“一次暴露几十个字段和复杂嵌套”。工具契约应该围绕一个清晰业务动作,尽量窄、稳定、可解释。
Name 怎么设计
name 是模型选择工具时最先看到的信号。它应该表达具体业务动作,而不是内部实现。
好的名字:
get_invoice_statussearch_customer_orderscreate_refund_requestsend_invoice_email_previewappend_ticket_note
不好的名字:
queryhandle_invoicedo_actionorder_apiupdate
命名建议:
- 使用动词 + 业务对象。
- 查询类用
get、list、search。 - 写入类用
create、append、send、update。 - 高风险动作避免伪装成普通更新,例如退款用
create_refund_request比update_order更清楚。 - 一个工具只做一个风险等级的动作。
Description 怎么设计
description 不是营销文案,而是模型的适用边界说明。它应该回答:
- 这个工具能做什么。
- 什么时候应该调用。
- 什么时候不应该调用。
- 必须具备哪些输入。
- 是否只读、是否写入、是否需要确认。
示例:
对高风险工具,description 要更明确:
description 不能替代权限控制,但能降低模型误选概率。
Input Schema 怎么设计
input schema 的目标不是把后端 API 全部暴露出来,而是给模型一个最小可用参数集合。
以发票查询为例:
设计原则:
- 必填字段越少越好,但必须能完成动作。
- 字段命名使用业务语言,不暴露内部数据库字段。
- 枚举要用稳定值,并说明含义。
- 时间、金额、邮箱、订单号要有格式约束。
- 对象 ID 必须在运行时做归属校验,不能相信模型。
- 默认禁止
additionalProperties。 - 不让模型传任意 SQL、URL、文件路径、用户 ID 或权限标识。
当参数缺失时,工具不应该让模型编造。更好的方式是返回结构化错误,或让模型追问用户。
对 Function / Tool Calling 来说,input schema 还承担“让模型少犯错”的职责。字段描述要写给模型看,而不只是写给后端看。
schema 可以降低错参,但不能证明参数可信。即使模型传入的 orderId 符合格式,runtime 仍然要检查它是否属于当前用户、当前租户和当前业务流程。
Output Schema 怎么设计
output schema 的目标是让模型、UI 和工作流都能稳定消费工具结果。
示例:
设计原则:
- 返回业务状态,不返回难以解释的内部状态码。
- 状态枚举要稳定,并在工具说明中解释。
- 返回值要区分“没有”“未知”“无权限”“系统失败”。
- 给模型解释用户问题所需的信息即可,不要返回全部数据库记录。
- 敏感字段默认不返回,必要时脱敏。
- 如果结果会驱动 UI 或工作流,字段必须可校验。
不要把 output 设计成一段自然语言摘要。自然语言适合最终回复,不适合作为工具结果的唯一形态。
Error Schema 怎么设计
错误也是契约的一部分。没有 error schema,调用方就不知道该重试、追问、拒绝还是转人工。
示例:
错误设计要特别注意:
NOT_FOUND不等于PERMISSION_DENIED。TIMEOUT不等于业务失败。- 参数错误应该暴露字段级信息,方便模型追问或修复。
- 可重试错误要标明 retryable,但重试次数由运行时控制。
- 面向用户的错误解释和面向日志的错误细节要分开。
契约版本和 MCP 暴露
工具一旦被模型、Workflow、Agent 或 MCP 客户端复用,契约变化就会影响多个调用方。版本管理不是平台化之后才需要考虑,而是从第一个生产工具开始就要留出空间。
常见兼容策略:
- 新增可选字段通常兼容,但要确认模型不会被无关字段干扰。
- 删除字段、改字段类型、改枚举含义通常不兼容,应新开版本。
- 工具行为改变比字段改变更危险,例如
create_refund_request从“创建申请”变成“直接退款”必须视为新工具。 - 高风险工具的 description、确认要求和审计字段也属于契约,不能只在文档里悄悄改。
- MCP Server 暴露工具时,应只暴露稳定契约,不把实验性内部函数直接发布给所有客户端。
MCP 让工具更容易被发现和复用,也让契约漂移的影响更大。适合 MCP 化的工具,应该已经具备清晰 name、description、schema、错误码、权限策略和版本说明。
列举方案
方案阶梯
工具契约可以逐级增强。
第一层只有函数名和文本说明,适合内部 demo 和低风险实验。
第二层补齐 name 和 description,让模型更容易选对工具。
第三层引入 input schema,约束参数类型、必填项、枚举和格式。
第四层引入 output schema,让工具结果能被程序和模型稳定消费。
第五层引入 error schema,让失败可以被重试、追问、降级和审计。
第六层管理版本。工具字段新增、枚举变更、行为变化都要考虑兼容性。
第七层做契约测试和 Eval,用样本验证模型是否选对工具、填对参数、正确解释结果。
什么时候简单函数足够
以下情况可以不做完整契约:
- 模型不直接调用这个函数。
- 函数只在一个后端流程里使用。
- 参数来自数据库或当前会话状态,不来自模型抽取。
- 返回值只被程序内部消费,不需要模型解释。
- 失败可以由普通异常处理覆盖。
- 该能力还在原型阶段,尚未进入用户流程。
但即便是简单函数,也建议保留清晰命名和常规类型定义,避免后续升级时成本过高。
什么时候需要工程封装
以下情况需要设计工具契约:
- 模型需要在多个工具之间选择。
- 工具参数来自自然语言。
- 返回值会进入模型后续推理或最终回复。
- 工具会写入业务系统。
- 工具失败后需要追问、重试、降级或转人工。
- 工具会被多个客户端、工作流或 agent 使用。
- 需要记录和评估工具调用质量。
契约越早清楚,后续 runtime、审计、MCP 化和评估就越容易落地。
决策判断
方案选型表
反模式
第一种反模式:工具名过宽。
manage_order 可以查询、退款、改地址、重发邮件。模型无法从名字判断风险,审计时也看不出实际动作。
第二种反模式:description 只写“调用订单 API”。
模型不知道何时调用、何时不要调用、是否需要订单号、是否写入状态。description 必须写业务边界。
第三种反模式:input schema 复刻内部 API。
内部 API 可能包含 tenantId、role、adminOverride、rawSql 等字段。模型不应该有机会填写这些字段。
第四种反模式:output 返回整条数据库记录。
这会增加 token 成本、泄露敏感字段,也让模型被无关字段干扰。工具应该返回完成任务所需的最小信息。
第五种反模式:错误只有字符串。
"Something went wrong" 无法指导模型追问,也无法指导运行时重试。错误必须结构化。
第六种反模式:把 schema 当权限。
schema 能限制字段形状,不能证明当前用户有权访问 orderId。权限必须在运行时和业务层执行。
工程清单
- Name:是否表达一个具体业务动作,而不是内部实现或宽泛操作。
- Description:是否说明适用场景、不适用场景、输入前提和风险等级。
- Input schema:字段、类型、必填、枚举、格式和
additionalProperties是否明确。 - 参数边界:是否避免模型传 SQL、URL、文件路径、权限字段和任意用户 ID。
- Output schema:是否返回最小必要字段,状态枚举是否稳定。
- 敏感信息:返回值是否脱敏,是否避免泄露内部记录。
- Error schema:错误码、retryable、用户动作和字段级错误是否明确。
- 版本管理:字段新增、删除、枚举变更是否有兼容策略。
- 契约测试:是否有样本覆盖误选、缺参、错参、失败和边界状态。
- 文档同步:契约、实现、监控和评估是否使用同一套状态和错误码。
结论收束
工具契约是工具封装的第一道边界。name 和 description 帮模型选对工具,input schema 防止错参和越权入口,output schema 让结果稳定消费,error schema 让失败可以被处理。
简单函数适合确定性、低风险、内部调用。只要模型参与工具选择或参数生成,就应该把函数升级为明确契约。契约不是为了形式完整,而是为了让工具调用从“模型试试看”变成可评估、可追踪、可治理的系统行为。