文章

Tool Contract Testing 生产实战:用契约用例治理函数调用 Schema 演进

系统讲解大模型 Agent 工具调用中的契约测试方法,覆盖函数 Schema 版本化、兼容性规则、契约用例设计、灰度回放、CI 阻断与上线检查清单,帮助工程团队提前暴露 Schema 演进风险。

背景问题:Agent 失败不一定是模型能力问题

在大模型应用中,函数调用(Function Calling)Tool Use 通常被看作把模型接入真实业务系统的标准方式。模型根据用户意图选择工具,生成结构化参数,应用层执行外部 API,然后把工具结果返回给模型继续推理。

这个链路看起来清晰,但生产环境中经常出现一种隐蔽问题:模型没有明显”胡说”,工具也没有宕机,调用格式甚至符合 JSON Schema,可最终业务还是失败了。

常见场景包括:

  • 工具新增了一个必填字段,旧 Agent Prompt 没有生成该字段
  • 参数枚举从 standard / premium 改成 basic / pro,旧调用路径仍然传旧值
  • 字段名保持不变,但语义从”城市名称”变成”城市 ID”
  • 工具返回结构增加了嵌套层,后续推理提示词仍然按旧路径读取
  • 错误码从业务错误变成平台错误,Agent 的 retry / fallback 策略失效
  • 一个工具被多个 Agent 共用,某个团队的小改动破坏了另一个团队的隐式依赖

这类问题的本质不是”模型不会调用工具”,而是工具契约没有被工程化管理。Schema 只是契约的一部分,真正的生产契约还包括调用意图、参数语义、默认值、错误处理、返回字段、权限边界、成本约束和兼容性承诺。

因此,Agent 工具链不能只做”能不能生成 JSON”的测试,而要建立 Tool Contract Testing:用可执行的契约用例,持续验证工具 Schema、Agent Prompt、工具实现和下游业务接口是否仍然满足共同约定。

核心原理:把工具定义当成版本化 API 契约

OpenAI 的函数调用文档把 function tools 定义为由 JSON Schema 描述输入参数的工具;strict: true 可以让函数调用更可靠地符合函数 Schema,并要求对象关闭额外字段、属性全部声明为 required 等约束。Anthropic 的 Tool Use 文档也明确,开发者需要为自定义工具传入 input_schema,模型返回 tool_use 后由应用代码执行工具并返回 tool_result

这说明工具调用不是单纯的 Prompt 技巧,而是一个跨边界的接口协议:

User Intent → Agent Prompt → Tool Selection → Tool Arguments
    → Tool Runtime → External API / Database → Tool Result → Agent Continuation

只要存在跨边界接口,就需要契约。Pact 对 contract testing 的定义是:通过检查每个应用在隔离环境中发送或接收的消息是否符合共享理解,来测试集成点。Google AIP-180 也强调,API 本质上是与用户之间的契约,用户会基于 API 编写生产代码,因此必须区分兼容变更和不兼容变更。

放到 Agent 工具调用中,契约至少包括四层:

1. Schema 契约

Schema 契约描述工具能接收什么参数。它包括字段名、类型、枚举、嵌套结构、是否必填、是否允许额外字段、字段说明和默认值。

示例:

{
  "name": "create_refund_request",
  "description": "Create a refund request for a paid order.",
  "strict": true,
  "parameters": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "order_id": {
        "type": "string",
        "description": "The internal order identifier."
      },
      "reason_code": {
        "type": "string",
        "enum": ["duplicate_payment", "customer_request", "service_failure"]
      },
      "amount_cents": {
        "type": ["integer", "null"],
        "description": "Refund amount in cents. Null means full refund."
      }
    },
    "required": ["order_id", "reason_code", "amount_cents"]
  }
}

这类定义能解决”参数结构不稳定”的问题,但无法单独解决语义兼容问题。例如 amount_cents 是否允许部分退款、order_id 是否可以是外部订单号、reason_code 是否影响审批流程,都不只是 JSON Schema 能表达的内容。

2. 语义契约

语义契约描述字段真正代表什么,以及哪些输入组合是合法的。

例如:

  • amount_cents = null 表示全额退款,而不是 0 元退款
  • service_failure 必须携带工单 ID,否则进入人工审核
  • 已结算订单只能发起退款申请,不能直接退款
  • 超过 30 天的订单需要更高权限
  • 工具返回 status=pending_review 时,Agent 不能告诉用户”退款成功”

这部分需要通过契约用例表达,不能只靠字段描述。

3. 行为契约

行为契约描述工具在不同状态下应该如何响应。它覆盖成功路径、业务失败、权限失败、限流、超时、幂等和重试。

例如:

case_id: refund_partial_requires_amount
intent: "用户要求退还部分金额"
input:
  order_id: "ord_123"
  reason_code: "customer_request"
  amount_cents: 5000
expected:
  tool_status: "created"
  agent_next_action: "tell_user_request_submitted"
must_not_say:
  - "退款已经到账"

这种用例既验证工具返回,也验证 Agent 如何解释工具结果。

4. 演进契约

演进契约定义什么修改可以直接上线,什么修改必须升版本,什么修改必须双写或灰度。

可以把工具变更分为三类:

分类典型变更处理策略
兼容变更新增可选字段、新增返回字段(旧消费者可忽略)、放宽字段长度或数值范围、新增非默认启用的能力、新增错误码但保留旧映射可直接上线,升 minor 版本
高风险变更新增必填字段、删除字段、字段改名、字段类型改变、枚举值删除或重命名、默认值改变、同名字段语义改变、返回结构路径改变、错误码分类改变需契约回放验证,CI 阻断
必须升主版本旧 Agent 无法自动补齐的新参数、改变用户可见结果的业务语义、改变权限/安全/资金流转的参数、影响多 Agent 共用工具的行为、导致历史会话无法继续的结构变化升 major 版本或双轨运行

工程落地:从 Schema Diff 到回放测试

Tool Contract Testing 不需要一开始就做成复杂平台。更实际的做法是先建立四个最小闭环:Schema RegistryContract CasesReplay RunnerRelease Gate

1. 建立工具 Schema Registry

每个工具都应该有稳定 ID 和版本号,而不是只在代码里散落一段 JSON。

建议字段:

tool_id: refund.create_request
version: 2.1.0
owner: payment-platform
runtime: internal-http
schema_file: schemas/refund.create_request.v2.1.0.json
changelog: docs/tools/refund.create_request/changelog.md
compatibility:
  compatible_with:
    - 2.0.x
  deprecated_versions:
    - 1.x
risk_level: high

其中 tool_id 不应频繁变化。版本号用于表达契约变化,不要把所有变更都塞进一个”最新版工具”。

2. 对 Schema 做自动 Diff

每次工具 Schema 变化时,CI 应自动对比旧版本和新版本,给出风险判断。

伪代码如下:

def classify_schema_change(old_schema, new_schema):
    changes = diff_json_schema(old_schema, new_schema)
    risks = []
    for change in changes:
        if change.kind in ["remove_property", "rename_property", "change_type"]:
            risks.append("breaking")
        elif change.kind == "add_required_property":
            risks.append("breaking")
        elif change.kind == "narrow_enum":
            risks.append("breaking")
        elif change.kind == "add_optional_property":
            risks.append("compatible")
        elif change.kind == "add_response_field":
            risks.append("usually_compatible")
        else:
            risks.append("needs_review")
    return max_risk(risks)

这一步不能完全替代人工评审,但能把显性的破坏性变更提前拦住。

3. 用契约用例覆盖真实 Agent 调用

契约用例不应只写”工具参数合法”。更有价值的是覆盖真实用户意图到工具调用的路径。

一个可执行契约用例建议包括:

case_id: refund_pending_review_not_success
source: production_replay
agent: customer_service_agent
user_intent: "用户要求退款,但订单超过自动退款期限"
tool:
  id: refund.create_request
  version: 2.1.0
expected_tool_arguments:
  order_id: "ord_123"
  reason_code: "customer_request"
  amount_cents: null
mock_tool_result:
  status: "pending_review"
  review_eta_hours: 24
expected_agent_behavior:
  must_include:
    - "退款申请已提交"
    - "需要人工审核"
  must_not_include:
    - "退款成功"
    - "款项已经原路退回"

这里测试的不是模型”会不会说中文”,而是 Agent 是否把工具返回的状态解释成正确业务含义。

4. 建立回放测试 Runner

上线前应把新 Schema、新 Prompt、新工具实现和一批历史用例放到隔离环境中回放。

推荐回放分三类:

  • Golden Cases:核心业务路径,必须全部通过
  • Regression Cases:历史线上问题沉淀,一旦失败必须阻断
  • Shadow Cases:来自最近生产流量的脱敏样本,用于发现未知风险

回放 Runner 的输出不应只有 pass/fail,还要给出变化摘要:

Tool: refund.create_request v2.1.0 -> v2.2.0
Schema diff: add required field approval_channel
Risk: breaking
Replay result:
  - golden:   48 / 50 passed
  - regression: 31 / 31 passed
  - shadow:   894 / 1000 passed
Top failures:
  1. Missing approval_channel in old prompt path: 72 cases
  2. Agent misread pending_review as success: 19 cases
  3. Enum value deprecated: 15 cases
Decision: block release

这种报告比单纯”模型评测分数下降 2%“更适合工程团队行动。

5. Release Gate:让工具变更必须过闸

工具发布不应只由工具提供方合并代码。对于被 Agent 依赖的工具,发布流程应增加 Release Gate:

Schema Diff → Compatibility Classification → Contract Replay
    → Owner Review → Canary Agent Traffic → Version Promotion
    → Deprecation Window

高风险工具还应增加人工确认,例如支付、保单、订单、权限、邮件发送、数据库写入、文件删除等。

适用场景

Tool Contract Testing 特别适合以下场景:

  1. 多 Agent 共用工具。一个 CRM 查询工具可能同时被销售助手、客服助手、运营助手使用。工具字段的小改动可能只在某个 Agent 上暴露问题。

  2. 工具背后连接真实业务系统。只要涉及资金、订单、合同、权限、通知、审批、数据库写操作,就不能依赖”模型大概率会生成正确参数”。

  3. 工具 Schema 高频变化。早期 Agent 项目经常快速新增字段、调整枚举、修改错误结构,如果没有契约测试,很容易形成隐性技术债。

  4. Prompt 和工具由不同团队维护。Prompt 团队认为工具语义稳定,平台团队认为 Schema 兼容,业务团队认为字段含义没变,最后线上问题才暴露。

  5. 需要支持历史会话恢复。Agent 会话可能跨天继续执行。如果工具只保留最新版 Schema,旧会话中的中间状态可能无法继续。

常见误区

误区一:有 JSON Schema 就等于有契约

JSON Schema 只能表达结构约束的一部分。它无法完整表达业务状态、权限、幂等、成本、错误恢复和用户可见承诺。

正确做法是:Schema 管结构,契约用例管语义和行为

误区二:strict mode 可以替代测试

strict mode 能减少格式错误,但不能判断”该不该调用这个工具""调用后该不该继续执行""工具结果该怎么解释”。生产测试仍然需要覆盖工具选择、参数生成、工具执行和结果解释。

误区三:只测最新 Prompt 和最新工具

真实线上环境经常存在旧会话、旧缓存、旧客户端、旧 Agent 配置和灰度用户。契约测试必须覆盖多个版本组合,而不是只测最新版。

误区四:把所有失败都归因于模型

很多 Agent 失败不是模型本身的问题,而是工具契约变化没有被发布流程捕获。把问题归因于模型会导致团队反复调 Prompt,却忽略真正的接口演进风险。

误区五:契约测试只适合微服务,不适合 LLM Agent

Agent 工具调用本质上也是跨系统集成。区别只是传统消费者是代码,Agent 消费者同时包括 Prompt、模型行为、工具 Schema 和工具结果解释。因此它更需要契约测试,而不是更少。

上线检查清单

工具定义检查

  • 工具是否有稳定 tool_id
  • Schema 是否进入版本管理
  • 是否标记 owner、risk_level、deprecated version
  • 是否关闭不必要的额外字段
  • 字段描述是否说明单位、默认值、权限和边界条件
  • 枚举是否有明确语义,而不是只写短字符串
  • 错误码是否有稳定分类

兼容性检查

  • 是否新增必填字段
  • 是否删除字段
  • 是否改名或改变类型
  • 是否收窄枚举
  • 是否改变默认值
  • 是否改变返回结构路径
  • 是否改变错误码语义
  • 是否影响旧 Agent Prompt 或历史会话

契约用例检查

  • 是否覆盖核心成功路径
  • 是否覆盖业务失败路径
  • 是否覆盖权限失败
  • 是否覆盖超时、限流、重试
  • 是否覆盖历史线上缺陷
  • 是否覆盖高风险参数组合
  • 是否覆盖工具结果解释

发布检查

  • Schema Diff 是否通过
  • Contract Replay 是否通过
  • 灰度流量是否无异常
  • 是否配置回滚到旧版本
  • 是否保留旧工具版本一段时间
  • 是否向 Agent 应用方发布 changelog
  • 是否记录谁批准了高风险变更

一个可落地的目录结构

agent-tools/
  refund.create_request/
    schemas/
      v2.1.0.json
      v2.2.0.json
    contracts/
      golden/
        refund_full_success.yaml
        refund_pending_review.yaml
      regression/
        old_prompt_missing_reason_code.yaml
      shadow/
        2026-07-traffic-sample.yaml
    changelog.md
    owner.yaml
    release-policy.yaml

每个工具都把 Schema、契约用例、变更记录和发布策略放在一起。这样做的好处是:当某个工具变更时,不需要全局搜索 Prompt 和测试文件,CI 可以直接定位受影响的契约。

总结

Agent 工具调用的稳定性,不只取决于模型是否会生成 JSON。真正决定生产可靠性的,是工具契约能否被版本化、测试化和发布流程化。

Tool Contract Testing 的核心思想很简单:把工具 Schema 当成 API,把 Agent Prompt 当成消费者,把工具实现当成提供者,把真实业务路径沉淀为可执行契约。每次工具变更前,先做 Schema Diff,再跑契约回放,再灰度上线。

这套方法不会让 Agent 变得”绝对可靠”,但能把大量隐蔽的 Schema 演进风险提前暴露出来,避免团队在上线后才发现旧 Prompt、旧会话、旧工具调用路径已经被破坏。

主要参考资料

常见问题

函数调用 Schema 已经用了 strict mode,为什么还需要契约测试?
strict mode 主要约束模型输出是否符合 Schema,但不能保证业务语义、工具版本兼容、旧 Agent Prompt 和历史回放流量仍然正确。契约测试关注的是端到端集成边界。
工具 Schema 演进时,哪些修改最容易破坏线上 Agent?
最危险的是删除字段、字段改名、枚举收窄、必填字段新增、字段语义改变、默认值改变和错误码结构改变。这些变化可能在静态检查中不暴露,但会破坏旧提示词和旧调用路径。
契约用例应该由谁维护?
工具提供方维护 Schema 和真实执行语义,Agent 应用方维护关键用户意图、调用参数和期望结果。生产环境中应由双方共同沉淀契约用例,并在 CI 与灰度阶段自动执行。