工具契约设计

提出问题

工具契约是模型和业务系统之间的接口协议。它不是 SDK 文档的精简版,也不是把后端函数签名翻译成 JSON。一个好的工具契约要同时服务三件事:

  • 帮模型判断“什么时候该用这个工具”。
  • 帮程序校验“这次调用是否合法”。
  • 帮人复盘“为什么调用、结果是什么、失败如何处理”。

如果契约设计不好,模型会误选工具、乱填参数、误读返回值;程序会拿到半结构化数据;运营和研发在事故后只能从自然语言日志里猜原因。

场景

继续看发票工单场景。系统有一个查询发票状态的工具:

{
  "name": "get_invoice_status",
  "description": "Query the invoice status for one order that belongs to the current customer.",
  "input": {
    "orderId": "E20260509018"
  }
}

这个工具看起来简单,但真实产品里会遇到很多边界:

  • 用户可能只说“昨天买的企业版”,没有订单号。
  • 用户可能给了多个订单号。
  • 订单号可能属于同租户的另一个部门,也可能完全不属于当前用户。
  • 发票可能不存在、开票中、已开票、被驳回、需要补充税号。
  • 工具可能超时,或发票系统维护中。
  • 模型需要知道“未找到订单”和“未开票”不是一回事。

如果契约只写“查询发票”,模型就容易把它用于所有发票相关问题;如果 output 只返回一段文本,模型就容易把状态解释错;如果 error 只返回 failed,调用方无法决定重试、追问还是转人工。

分析问题

问题类型

工具契约主要处理五类问题。

问题类型常见表现契约要解决什么
工具误选用户只是咨询政策,模型却调用退款工具用 name 和 description 限定适用场景
参数错填把邮箱填到订单号,把自然语言时间填成无效日期用 input schema、枚举、格式和必填规则约束
参数过宽允许模型传任意 SQL、任意 URL、任意用户 ID缩小参数能力,避免越权和注入
返回误读工具返回 pending_review,模型说“已开票”用 output schema 和状态说明稳定语义
失败不可处理只返回 error: true 或一段异常文本用 error schema 区分可重试、需补参、无权限和系统故障

模型适合从自然语言中抽取候选参数、根据 description 选择工具、把 output 转成用户可读解释。程序应该负责 schema 校验、权限校验、业务状态映射、错误分流和日志。

收益和成本判断

工具契约的收益包括:

  • 降低工具误选率。
  • 降低参数解析和校验失败。
  • 让工具返回值可被 UI、工作流和模型稳定消费。
  • 让错误能进入重试、追问、降级或人工队列。
  • 让工具版本变更可以被管理。

成本也很实际:

  • 需要维护契约和后端实现的一致性。
  • 需要为状态、枚举和错误码命名。
  • 需要处理版本兼容。
  • 需要写工具级测试和评估样本。
  • 复杂 schema 会增加模型调用难度。

所以契约设计要避免两个极端:一个极端是“只有函数名和自由文本”;另一个极端是“一次暴露几十个字段和复杂嵌套”。工具契约应该围绕一个清晰业务动作,尽量窄、稳定、可解释。

Name 怎么设计

name 是模型选择工具时最先看到的信号。它应该表达具体业务动作,而不是内部实现。

好的名字:

  • get_invoice_status
  • search_customer_orders
  • create_refund_request
  • send_invoice_email_preview
  • append_ticket_note

不好的名字:

  • query
  • handle_invoice
  • do_action
  • order_api
  • update

命名建议:

  • 使用动词 + 业务对象。
  • 查询类用 getlistsearch
  • 写入类用 createappendsendupdate
  • 高风险动作避免伪装成普通更新,例如退款用 create_refund_requestupdate_order 更清楚。
  • 一个工具只做一个风险等级的动作。

Description 怎么设计

description 不是营销文案,而是模型的适用边界说明。它应该回答:

  • 这个工具能做什么。
  • 什么时候应该调用。
  • 什么时候不应该调用。
  • 必须具备哪些输入。
  • 是否只读、是否写入、是否需要确认。

示例:

Query the invoice status for a single order that belongs to the current authenticated customer.
Use this only when the user asks about an existing invoice or invoice progress and an order ID is available.
Do not use it to create, resend, cancel, or refund invoices.

对高风险工具,description 要更明确:

Create a refund request for manual review. This tool does not immediately refund money.
Use only after the user explicitly asks for a refund and the order has been verified.
The runtime must require human approval before the request is submitted.

description 不能替代权限控制,但能降低模型误选概率。

Input Schema 怎么设计

input schema 的目标不是把后端 API 全部暴露出来,而是给模型一个最小可用参数集合。

以发票查询为例:

{
  "type": "object",
  "required": ["orderId"],
  "properties": {
    "orderId": {
      "type": "string",
      "description": "The order ID mentioned by the user, such as E20260509018."
    }
  },
  "additionalProperties": false
}

设计原则:

  • 必填字段越少越好,但必须能完成动作。
  • 字段命名使用业务语言,不暴露内部数据库字段。
  • 枚举要用稳定值,并说明含义。
  • 时间、金额、邮箱、订单号要有格式约束。
  • 对象 ID 必须在运行时做归属校验,不能相信模型。
  • 默认禁止 additionalProperties
  • 不让模型传任意 SQL、URL、文件路径、用户 ID 或权限标识。

当参数缺失时,工具不应该让模型编造。更好的方式是返回结构化错误,或让模型追问用户。

对 Function / Tool Calling 来说,input schema 还承担“让模型少犯错”的职责。字段描述要写给模型看,而不只是写给后端看。

字段设计更稳的写法容易出错的写法
订单号orderId: 用户提供的订单号,例如 E20260509018id: 内部主键或任意字符串
时间范围startDate / endDate: ISO 日期,必须来自用户请求或默认业务规则time: 自由文本
操作原因reason: 枚举或短文本,用于审计,不作为权限依据adminReason: 暗示模型可绕过权限
收件人recipientEmail: 必须在运行时校验归属email: 没有说明来源和限制

schema 可以降低错参,但不能证明参数可信。即使模型传入的 orderId 符合格式,runtime 仍然要检查它是否属于当前用户、当前租户和当前业务流程。

Output Schema 怎么设计

output schema 的目标是让模型、UI 和工作流都能稳定消费工具结果。

示例:

{
  "type": "object",
  "required": ["orderId", "invoiceStatus", "updatedAt", "canResend"],
  "properties": {
    "orderId": { "type": "string" },
    "invoiceStatus": {
      "type": "string",
      "enum": ["not_requested", "pending_review", "issued", "rejected"]
    },
    "updatedAt": {
      "type": "string",
      "format": "date-time"
    },
    "canResend": {
      "type": "boolean"
    },
    "rejectionReason": {
      "type": ["string", "null"]
    }
  },
  "additionalProperties": false
}

设计原则:

  • 返回业务状态,不返回难以解释的内部状态码。
  • 状态枚举要稳定,并在工具说明中解释。
  • 返回值要区分“没有”“未知”“无权限”“系统失败”。
  • 给模型解释用户问题所需的信息即可,不要返回全部数据库记录。
  • 敏感字段默认不返回,必要时脱敏。
  • 如果结果会驱动 UI 或工作流,字段必须可校验。

不要把 output 设计成一段自然语言摘要。自然语言适合最终回复,不适合作为工具结果的唯一形态。

Error Schema 怎么设计

错误也是契约的一部分。没有 error schema,调用方就不知道该重试、追问、拒绝还是转人工。

示例:

{
  "type": "object",
  "required": ["code", "message", "retryable", "userAction"],
  "properties": {
    "code": {
      "type": "string",
      "enum": [
        "VALIDATION_FAILED",
        "MISSING_REQUIRED_INPUT",
        "NOT_FOUND",
        "PERMISSION_DENIED",
        "RATE_LIMITED",
        "TIMEOUT",
        "UPSTREAM_UNAVAILABLE"
      ]
    },
    "message": {
      "type": "string"
    },
    "retryable": {
      "type": "boolean"
    },
    "userAction": {
      "type": "string",
      "enum": ["ask_user", "try_later", "contact_support", "none"]
    }
  },
  "additionalProperties": false
}

错误设计要特别注意:

  • 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 化和评估就越容易落地。

决策判断

方案选型表

场景条件推荐方案不推荐方案判断理由额外成本
内部确定性调用普通函数 + 类型定义完整工具契约模型不参与,风险低常规单元测试
模型只需查询单一状态name + description + input/output schema自由文本返回需要模型选对并解释对schema 维护
参数来自用户自然语言input schema + 字段级校验让模型传任意对象需要拦截错参和缺参校验、追问逻辑
返回值驱动 UI 或流程output schema + 枚举说明字符串摘要程序需要稳定消费状态映射、兼容
工具可能失败且需恢复error schema + runtime 分流抛异常给模型失败类型决定后续动作错误码、日志
高风险工具严格契约 + 确认要求 + 审计字段宽泛 update 工具必须明确责任和边界审批和版本治理
多客户端复用版本化契约 + MCP 暴露每端自定义 schema需要统一能力描述兼容测试

反模式

第一种反模式:工具名过宽。

manage_order 可以查询、退款、改地址、重发邮件。模型无法从名字判断风险,审计时也看不出实际动作。

第二种反模式:description 只写“调用订单 API”。

模型不知道何时调用、何时不要调用、是否需要订单号、是否写入状态。description 必须写业务边界。

第三种反模式:input schema 复刻内部 API。

内部 API 可能包含 tenantIdroleadminOverriderawSql 等字段。模型不应该有机会填写这些字段。

第四种反模式:output 返回整条数据库记录。

这会增加 token 成本、泄露敏感字段,也让模型被无关字段干扰。工具应该返回完成任务所需的最小信息。

第五种反模式:错误只有字符串。

"Something went wrong" 无法指导模型追问,也无法指导运行时重试。错误必须结构化。

第六种反模式:把 schema 当权限。

schema 能限制字段形状,不能证明当前用户有权访问 orderId。权限必须在运行时和业务层执行。

工程清单

  • Name:是否表达一个具体业务动作,而不是内部实现或宽泛操作。
  • Description:是否说明适用场景、不适用场景、输入前提和风险等级。
  • Input schema:字段、类型、必填、枚举、格式和 additionalProperties 是否明确。
  • 参数边界:是否避免模型传 SQL、URL、文件路径、权限字段和任意用户 ID。
  • Output schema:是否返回最小必要字段,状态枚举是否稳定。
  • 敏感信息:返回值是否脱敏,是否避免泄露内部记录。
  • Error schema:错误码、retryable、用户动作和字段级错误是否明确。
  • 版本管理:字段新增、删除、枚举变更是否有兼容策略。
  • 契约测试:是否有样本覆盖误选、缺参、错参、失败和边界状态。
  • 文档同步:契约、实现、监控和评估是否使用同一套状态和错误码。

结论收束

工具契约是工具封装的第一道边界。namedescription 帮模型选对工具,input schema 防止错参和越权入口,output schema 让结果稳定消费,error schema 让失败可以被处理。

简单函数适合确定性、低风险、内部调用。只要模型参与工具选择或参数生成,就应该把函数升级为明确契约。契约不是为了形式完整,而是为了让工具调用从“模型试试看”变成可评估、可追踪、可治理的系统行为。