AI agent 工具一旦进入真实业务,就不会停留在第一版 schema。字段会增加,命名会调整,枚举会拆分,某些参数会从可选变成必填。问题不在于升级本身,而在于升级经常发生在系统仍然有旧 Run、旧 prompt、旧回放样本、旧评测集的情况下。
很多团队第一次踩坑,都不是因为升级失败,而是因为升级“部分成功”:新请求能跑,旧回放开始报错,某些长任务在中途继续执行时突然读不懂新结构。看起来像偶发 bug,本质却是 schema 演进策略缺位。
建议先结合 AI Agent 工具注册表治理、AI agent 工具结果标准化、AI agent 灰度发布与功能开关 和 AI Agent Session Replay 调试指南 一起看。
先给结论:schema 演进要同时照顾 4 类消费者
| 消费者 | 为什么会受影响 |
|---|---|
| 新请求 | 需要理解新字段和新规则 |
| 旧 Run 恢复 | 可能还带着旧参数继续执行 |
| 回放与评测 | 历史样本需要按旧版本复现 |
| 观测与审计 | 日志、告警、报表要读懂新旧结构 |
如果你只关心“线上新请求能否跑通”,schema 升级一定会在别处留下坑。
一、先分清三种 schema 变化
不是每种变更都同样危险。至少分三类:
| 变化类型 | 示例 | 风险等级 | 推荐策略 |
|---|---|---|---|
| 向后兼容 | 新增可选字段 | 低 | 直接发布,但保留默认值 |
| 条件兼容 | 字段改名、枚举扩展 | 中 | 双写双读,灰度迁移 |
| 破坏性变更 | 必填新增、结构重组 | 高 | 新旧版本并存,逐步切换 |
最危险的是把破坏性变更伪装成“只是字段改了一下”。
二、工具 schema 不要只存一份“当前版本”
很多系统的工具定义只有当前 schema。这样做的后果是:历史 Run 无法知道自己当时到底用的是哪版结构。
更稳的做法是把版本作为一等公民:
{
"toolName": "createDraft",
"schemaVersion": "v2",
"status": "active",
"compatibility": {
"reads": ["v1", "v2"],
"writes": ["v2"]
}
}
这里有两个关键点:
- 工具当前写什么版本
- 工具还能读什么版本
如果只记录 current=v2,恢复旧任务时系统仍然不知道该如何解释旧 payload。
三、双写双读是过渡期的主策略
当 schema 从 v1 升级到 v2,最稳的过渡不是强切,而是一个短暂的双写双读窗口:
| 阶段 | 读 | 写 |
|---|---|---|
| 阶段 1 | v1 | v1 |
| 阶段 2 | v1 + v2 | v1 + v2 |
| 阶段 3 | v1 + v2 | v2 |
| 阶段 4 | v2 | v2 |
这意味着:
- parser 能同时解释 v1 和 v2
- 新产生的 artifact 可以暂时同时保存旧结构和新结构
- 回放和恢复不会立刻断掉
双写双读看起来麻烦,但比半夜修历史 Run 要便宜得多。
还有一个经常被忽略的现实问题:双写窗口如果没有明确结束条件,就会无限期存在,最后把系统拖成“什么都兼容、什么都不敢删”的状态。因此每次进入双写双读前,就要先定义退出门槛,例如:
- 新版本写入占比连续 7 天超过 95%
- 历史回放样本全部通过 v2 adapter
- checkpoint 恢复在新旧版本下都通过演练
- 审计报表已经切换到新字段口径
只有退出条件提前写清,双写双读才是迁移手段,而不是永久负债。
四、改名和重组时,最好显式提供 compatibility adapter
比如原来的参数是:
{ "invoiceId": "inv_123" }
新版本改成:
{
"invoice": {
"id": "inv_123",
"country": "CN"
}
}
不要指望模型自己猜旧字段如何映射到新结构,而应该在工具层提供 adapter:
function normalizeCreateInvoiceInput(input: unknown): CreateInvoiceV2Input {
if (isV2(input)) return input
if (isV1(input)) {
return {
invoice: {
id: input.invoiceId,
country: 'CN'
}
}
}
throw new Error('unsupported schema version')
}
兼容逻辑应该放在工具边界,而不是 prompt 里。prompt 只能影响新生成的参数,不能修复历史数据。
如果工具输入不只来自模型,还来自工作流引擎、批量回放脚本、人工修复控制台,那么 adapter 最好输出一份统一的“解析结果”,而不是仅仅返回业务对象:
interface ParsedToolInput<T> {
schemaVersion: 'v1' | 'v2'
normalized: T
migrated: boolean
warnings: string[]
}
function parseCreateInvoiceInput(input: unknown): ParsedToolInput<CreateInvoiceV2Input> {
if (isV2(input)) {
return {
schemaVersion: 'v2',
normalized: input,
migrated: false,
warnings: []
}
}
if (isV1(input)) {
return {
schemaVersion: 'v1',
normalized: {
invoice: {
id: input.invoiceId,
country: 'CN'
}
},
migrated: true,
warnings: ['country defaulted to CN for legacy payload']
}
}
throw new Error('unsupported schema version')
}
这样做的价值是:后续日志、审计和告警可以清楚知道本次执行到底是原生 v2,还是从旧结构迁移过来的“兼容成功”。
五、版本协商最好显式化,不要靠隐式猜测
不少系统把“schemaVersion”藏在文档或者注释里,实际运行时靠 parser 猜。短期能跑,长期会让行为越来越不可解释。
更稳的方式是把版本协商写成明确协议:
{
"toolName": "createDraft",
"requestedSchemaVersion": "v2",
"acceptedSchemaVersions": ["v1", "v2"],
"selectedSchemaVersion": "v1",
"adapterVersion": "2026-05-07",
"selectionReason": "checkpoint_resume_from_legacy_run"
}
这个结构至少解决 3 个问题:
- 新请求为什么没有直接走最新版
- 恢复任务当前到底按哪版协议执行
- 故障复盘时该看哪套 adapter 和测试基线
很多团队的问题不是没有版本,而是版本只存在于代码里,不存在于运行事实里。
六、长任务恢复时,schema 版本必须跟着 checkpoint 走
一个常见误区是:任务恢复时重新读取“当前最新 schema”。这会让长任务在中途切换协议。
恢复信息至少要保存:
{
"runId": "run_123",
"toolName": "createDraft",
"schemaVersion": "v1",
"checkpointId": "cp_03",
"inputSnapshot": { "invoiceId": "inv_123" }
}
恢复时先按当时的 schemaVersion 解释 payload,再决定是否需要迁移。不要让“继续执行”变成“顺便升级协议”。
七、兼容矩阵要成为发布门禁,不只是文档附件
最容易被低估的是测试范围。schema 演进不是单测改过就算结束,真正需要验证的是“谁写、谁读、何时恢复、如何回放”的交叉组合。
一个最小兼容矩阵通常至少包括下面这些维度:
| 写入方 | 读取方 | 运行场景 | 预期结果 |
|---|---|---|---|
| v1 | v1 parser | 老任务正常执行 | 通过 |
| v1 | v2 parser + adapter | 历史 Run 恢复 | 通过并产出迁移告警 |
| v2 | v1 parser | 回滚后读取新数据 | 明确失败或受控降级 |
| v2 | v2 parser | 新请求实时执行 | 通过 |
| v1 checkpoint | v2 runtime | 断点恢复 | 不丢上下文,不重复副作用 |
| v2 artifact | replay loader | 历史回放 | 通过并保持结果可解释 |
如果发布单里没有这张表,团队就很容易只测“新写新读”,完全漏掉恢复与回放。
更进一步,兼容矩阵最好直接落到 CI 或回归脚本,而不是贴在文档里就算完成:
const compatibilityCases = [
{ writer: 'v1', reader: 'v2', scenario: 'resume' },
{ writer: 'v2', reader: 'v2', scenario: 'replay' },
{ writer: 'v2', reader: 'v1', scenario: 'rollback-read' }
]
for (const testCase of compatibilityCases) {
expect(runCompatibilityCase(testCase)).toMatchObject({ ok: true })
}
这里的重点不是测试代码写成什么样,而是兼容性要从“人为记得测”变成“系统强制测”。
八、回滚不是只切 flag,还要考虑历史数据可读性
如果你发布 v2 后发现有问题,回滚时最容易漏掉两件事:
- 已经写出的 v2 数据,旧版本是否还能读取
- 旧 prompt 是否会继续生成 v1 风格参数
一个更完整的回滚清单是:
- 关闭新 schema 的写入。
- 保留对新写入数据的兼容读取。
- 暂停相关回放任务,避免读写混乱。
- 标记受影响的 Run 和 artifact。
- 把失败样本加入回归集。
九、上线后要盯兼容指标,而不是只盯成功率
很多 schema 变更上线后表面成功率没掉,但兼容成本正在悄悄抬高。真正值得盯的不是单一错误率,而是这些迁移相关指标:
| 指标 | 为什么要看 |
|---|---|
| 旧版本 payload 占比 | 判断双写窗口是否可以收口 |
| adapter 命中率 | 判断历史数据依赖面是否仍然很大 |
| adapter 失败率 | 发现未覆盖的旧结构 |
| checkpoint 恢复成功率 | 判断长任务是否被版本切换打断 |
| replay 兼容通过率 | 判断审计和复盘链路是否可用 |
| 迁移告警量 | 判断是否出现默认值兜底过多 |
如果只看“接口 200 比例”,你会错过很多已经在恢复链路里发生的结构性问题。
建议给每次兼容解析打上统一日志:
{
"event": "tool_schema_parse",
"toolName": "createDraft",
"selectedSchemaVersion": "v1",
"adapterVersion": "2026-05-07",
"migrated": true,
"warningCount": 1,
"runId": "run_123"
}
这类日志不是为了“多记一点”,而是为了在双写窗口结束前,知道系统到底还有多少历史包袱没有处理完。
十、失败案例:字段改名后,回放全挂但线上新请求正常
一个审核 Agent 原来写 reviewerId,后来统一成 reviewer_id。线上新请求没问题,因为 prompt 和新 parser 一起更新了。但历史 replay 还保留旧事件,回放平台直接报解析失败。
更糟的是,团队一开始没意识到这算生产问题,因为“真实流量没报错”。直到事故复盘需要回放上周 Run,才发现关键样本全读不出来。
修复方式不是把回放平台临时兼容一下,而是:
- 给工具 registry 加 schemaVersion
- replay loader 支持旧事件 adapter
- 任何字段改名都必须附带 compatibility test
十一、把迁移节奏写进发布单,避免团队各自理解
一个成熟的 schema 演进流程,通常不是一次发布,而是至少 4 步:
- 先发布新 parser 和 adapter,只增加读取能力。
- 再灰度新写入,让少量新请求产出 v2 数据。
- 观察恢复、回放、报表、审计是否稳定。
- 最后关闭 v1 写入,确认收口窗口和删除计划。
这 4 步如果没有明确负责人,现实里往往会演变成:开发以为已经切完,数据团队还在读旧字段,运维以为双写是临时,最后没人敢删。
因此发布单里最好明确写出:
| 项目 | 负责人 | 退出条件 |
|---|---|---|
| parser 双读 | 平台工程 | 历史回放通过率 100% |
| v2 灰度写入 | 业务开发 | 新写入占比 > 95% |
| 审计口径切换 | 数据/风控 | 报表字段完成对齐 |
| 删除 v1 写入 | 平台工程 | 连续一周无兼容告警 |
当 schema 演进开始被当成跨团队发布节奏管理,它才真的从“代码改动”升级成“协议治理”。
十二、Schema 演进 Checklist
- 是否明确区分向后兼容、条件兼容和破坏性变更
- 工具 registry 是否记录 schemaVersion
- 运行时是否显式记录 selectedSchemaVersion 和 adapterVersion
- parser 是否支持双读或 adapter
- 是否有写入方 x 读取方 x 场景的兼容矩阵测试
- 长任务 checkpoint 是否保存版本信息
- 回滚后是否还能读新旧数据
- 是否监控旧版本占比、adapter 命中率和恢复成功率
- 回放和评测样本是否跑过兼容测试
- 变更是否附带灰度和关闭条件
结语
AI agent 的工具 schema 演进,不是“改个 JSON”这么简单。真正要治理的是版本、恢复、回放、审计和回滚。只有把 schema 升级当成系统协议演进,而不是单次代码改动,工具体系才不会越长越脆。
延伸阅读:


