工具 Schema 版本演进与兼容:Agent 项目如何不被一次改字段搞崩
多模型 Agent 项目跑到一定阶段,工具集合不会一直不变。产品要给某个工具加个可选参数、把某个字段从字符串改成枚举、或者干脆废弃一个旧工具换新的——这些改动在传统 REST API 里有成熟的版本化套路,但放到 LLM 工具调用场景,很多团队是第一次遇到,容易踩坑:模型可能还在用旧的调用习惯生成参数,也可能因为 prompt 里残留的历史 few-shot 示例而”学会”了已经废弃的字段。
本文不讨论跨厂商 Schema 格式差异(那部分可参考《OpenAI、Anthropic、Gemini 工具 Schema 兼容层设计》),而是聚焦单个工具在时间维度上的演进:新增字段、修改类型、废弃参数、整体替换,怎么让线上流量平滑过渡而不是突然报错。
一、为什么工具 Schema 的”向后兼容”比 REST API 更难
传统 REST API 的版本兼容主要防两类问题:客户端发送的请求字段服务端不认识、服务端返回的字段客户端解析不出来。这两类问题都是代码在处理,出错会直接抛异常,容易发现。
工具调用的兼容性问题多了一层:中间隔着一个模型的推理过程。模型看到 Schema 后自己决定填什么值,如果 Schema 变了但模型的”记忆”(system prompt 里的历史示例、上一轮对话里模型自己生成过的调用)没变,模型可能会:
- 继续用旧字段名,因为 few-shot 示例里是旧名字
- 对新增的必填字段瞎填一个值,而不是报错或询问用户
- 把已经废弃但 Schema 里还留着描述文字的字段当真
这些错误往往不会在类型校验层被拦下来——因为模型生成的值本身类型是对的,只是语义已经不对了。等到业务层发现数据不对劲,可能已经产生了脏数据。
二、三类改动的风险分级
不是所有 Schema 改动风险都一样,先分级再决定要不要走灰度流程。
| 改动类型 | 风险等级 | 典型例子 | 是否需要版本号 |
|---|---|---|---|
| 新增可选字段 | 低 | 给”创建订单”工具加一个可选的 note 备注 | 不需要,模型不填就用默认值 |
| 收窄约束(原本自由字符串改枚举) | 中 | status 从任意字符串改成 enum: [pending, done] | 建议加版本号,防止模型仍传旧值 |
| 修改字段语义(名字不变但含义变了) | 高 | amount 原本是”元”,改成”分” | 必须加版本号 + 强制走新工具名 |
| 删除必填字段 / 整体替换工具 | 高 | 废弃 create_order,替换成 create_order_v2 | 必须给旧工具设废弃期 |
改动语义但不改字段名是最危险的一类,因为它不会被任何自动化校验发现——Schema 校验只看类型对不对,不看单位或含义。这类改动永远不要原地改,而是新增一个字段或新建一个工具版本。
三、给工具命名加版本号:什么时候值得
一个实用判断标准:如果新旧两版工具在同一个对话窗口里可能同时被模型看到(比如灰度期间部分流量用新 Schema、部分用旧 Schema),就需要显式版本号;如果是整个平台一次性切换、旧版本立刻下线,直接原地改 Schema 反而更简单,不用背版本号的技术债。
版本号放在工具名里比放在参数里更稳妥:
{
"name": "create_order_v2",
"description": "创建订单(v2:amount 单位为分,原 v1 的 amount 为元,已废弃)",
"input_schema": {
"type": "object",
"properties": {
"amount_cents": { "type": "integer", "description": "订单金额,单位:分" },
"currency": { "type": "string", "enum": ["CNY", "USD"] }
},
"required": ["amount_cents", "currency"]
}
}
注意 description 里显式标注了”原 v1 的 amount 为元,已废弃”——这不是给人看的注释,是给模型看的上下文提示。实测下来,在废弃期同时暴露 v1 和 v2 两个工具给模型时,如果 description 里不写清楚差异,模型混用两个工具的概率明显更高;写清楚之后,模型基本能按 v2 的语义正确调用。
四、废弃流程:给旧工具一个体面的退场
直接删除旧工具名,线上依赖旧调用逻辑的客户端代码(如果是把工具定义存在自己数据库里再动态拼给模型的架构)会直接报错。参考 REST API 的废弃套路,拆成三步:
- 标记废弃,但保留可用:在
description开头加[已废弃,请使用 create_order_v2],工具本身还能正常调用,同时后端记录调用日志,统计还有多少流量在用旧版本 - 观察期(建议 2-4 周):结合《Agent 工具调用的超时预算设计》里提到的调用监控思路,给旧工具的调用量设置告警阈值,如果观察期结束流量还没降到接近零,说明有遗留调用方没升级,需要人工介入
- 下线:从工具列表里彻底移除,同时清理 system prompt 和历史 few-shot 示例里残留的旧字段引用——这一步经常被漏掉,导致模型”记得”一个已经不存在的工具
五、灰度切换的实操:双工具并存期怎么防止模型选错
如果新旧工具必须并存一段时间(比如 A/B 测试新 Schema 的效果),有两个实操技巧能显著降低模型选错工具的概率:
- 互斥描述:在两个工具的
description里互相引用对方,明确说明”何时该用这个、何时该用另一个”,而不是让模型靠字段名猜。例如 v1 写”仅用于兼容旧客户端,新调用请优先使用 create_order_v2”;v2 写”当前推荐版本”。 - 上下文单一化:同一次对话里,尽量只暴露一个版本给模型(通过灰度开关按会话分流),而不是把两个版本都塞进 tools 列表让模型自己选。同时暴露两个功能重叠的工具是模型选错的头号原因,这一条比任何 description 措辞优化都更有效。
六、用 Schema 校验层兜底,而不是完全依赖模型自觉
即使 Schema 和 description 都写得很清楚,仍然要在业务代码里对模型返回的工具调用参数做一层独立校验,不能假设模型一定按新 Schema 的语义填值。校验层至少要覆盖:
- 类型和范围校验(JSON Schema 层面的基本校验)
- 语义哨兵:对于像”金额单位从元改成分”这种改动,校验层可以加一条启发式规则——如果
amount_cents的值小于 100 且看起来像是”元”而不是”分”(比如一个明显的整数金额如 500),触发人工复核而不是直接执行 - 版本一致性:如果请求里同时出现了 v1 和 v2 的字段名,直接拒绝并记录日志,这通常意味着 few-shot 示例污染
这层兜底不是为了替代前面几节的版本管理和灰度流程,而是防止”流程都做对了,但模型依然犯错”的长尾情况——毕竟模型的输出本质上是概率生成,不是确定性的类型系统能完全约束住的。