数据契约与版本演进
本节目标
为触发事件、步骤输入输出和检查点定义可验证合同,并在不破坏运行中实例的前提下演进版本。
为什么“共享一个大字典”会失控
初学者常让所有步骤读写同一个 context。这样做很快,但调用关系隐藏在字段名里:上游把 amount 从整数改成字符串,下游可能在数小时后才失败;某个 LLM 节点写入多余字段,还可能被后续高权限步骤误用。
把数据分成四层更安全:
- 原始事件:收到后不可变,用于审计和重新解析。
- 规范化领域对象:把多版本输入转换成内部统一模型。
- 步骤输入/输出:每条边只传下游真正需要的字段。
- 运行元数据:attempt、状态、定义版本、幂等键、结果引用,不和业务数据混写。
一个完整的步骤合同
| 项目 | 问题 | 示例 |
|---|---|---|
| 输入 schema | 接受哪些字段和类型? | order_id: string, amount_cents: integer |
| 业务前置条件 | 结构合法后还要满足什么? | 金额大于 0、订单属于当前租户 |
| 输出 schema | 下游可依赖哪些字段? | reservation_id, expires_at |
| 错误分类 | 暂时、永久还是业务拒绝? | UPSTREAM_BUSY, INVALID_ORDER |
| 副作用 | 是否改变外部世界? | 预留库存 |
| 幂等策略 | 重复执行如何识别同一意图? | 实例 + 步骤 + 订单 + v1 |
| 版本 | 哪个 handler/schema 产生结果? | reserve_inventory@2 |
| 数据级别 | 能否持久化、记录或发送给模型? | 内部、机密、受限 |
| 可信主体与授权 | 谁在当前时刻获准对哪个资源执行? | 服务端派生的 actor/tenant、策略版本、资源 ACL |
| 回执与终态 | 下游接受了什么,最后业务结果是什么? | receipt_id、outcome: succeeded | failed | unknown |
JSON Schema Draft 2020-12 可描述 JSON 结构,$schema 应声明方言。它能检查 required、类型、枚举和额外字段,却无法自行证明“订单属于该用户”或“引用的文档真实存在”;这些仍是业务验证。需要语法与 profile 的前置练习时,先读 JSON Schema 基础契约。
还要避免把几个不同层次的 ID 混成一个 id:
| 标识 | 回答的问题 | 不能替代什么 |
|---|---|---|
event_id + source | 这是哪一次输入投递? | 发送方认证;未经意图绑定的业务动作去重 |
instance_id | 这是哪个工作流实例? | 自动成为每个外部副作用的幂等键 |
operation_id / action_id | 这一次允许的业务意图是什么? | 每次执行的 attempt 记录 |
attempt_id | 第几次实际执行/领取? | 幂等结果或最终业务结果 |
trace_id | 如何关联诊断信号? | 身份、租户或授权 |
高风险动作的合同应把 operation_id 与不可变意图(动作、目标资源、参数摘要、定义/策略版本)绑定。下游的回执只证明它接受或记录了该意图;若调用超时、异步处理或状态未知,工作流仍须通过查询、对账或人工处理得到可验证的 outcome,不能把“收到 202/200”写成“业务已成功”。
事件 envelope 与业务 payload
CloudEvents 的 id/source/type/specversion 属于 envelope。data 是业务 payload,可通过 dataschema 指向其 schema。不要把个人信息、密钥或完整提示塞进可被路由器和日志系统读取的上下文字段;CloudEvents 规范也提醒上下文属性可能被中间件检查和记录。
一个事件可同时有三种版本:
- CloudEvents 规范版本,例如
specversion: "1.0"; - 事件类型版本,例如
com.example.order.submitted.v2; dataschema 版本。
它们用途不同,不要用一个 version 字段含糊代替。
兼容演进
通常较安全的改动
- 新增真正可选、且默认语义明确的字段;
- 扩展下游明确允许忽略的 metadata;
- 新增错误码,同时保持未知错误的安全回退;
- 新 handler 同时读取旧检查点,并在内存中显式迁移。
通常是破坏性改动
- 删除或改名必需字段;
- 改变类型、单位、时区或字段含义;
- 把“分”改成“元”却保留字段名;
- 收紧枚举而不处理已存在值;
- 让新代码用不同顺序重放旧事件。
“通常”是工程判断,不是 JSON Schema 的自动兼容保证。发布前应以真实旧夹具做 producer/consumer 合同测试。
长流程版本策略
工作流可能等待数天。发布新代码时有三种策略:
- 旧实例固定旧定义/worker:简单、资源成本高。
- 新 worker 兼容旧历史:需要重放与迁移测试。
- 显式迁移实例:迁移函数需版本化、可审计、可回退,危险默认值应转人工处理。
不要让运行中实例自动“跟随 latest”。定义版本、handler 版本和 schema 版本都应写进检查点;恢复时不兼容就明确失败,不能猜测缺失币种、权限或审批含义。
大结果与敏感数据
检查点只保存恢复所需的小元数据。大文档、模型原始响应和二进制文件放对象存储,状态保存对象 ID、source_revision、内容哈希、大小、类型、敏感级别和访问策略。恢复时再次验证引用存在、版本与哈希匹配,且当前服务身份仍被授权读取;URI、旧时 ACL 判定或模型自述都不能代替执行时授权。若源对象已删除、撤销或换版,应进入显式的 unknown/blocked 或人工路径,不能悄悄读取“最新内容”。
日志字段使用 allowlist:记录 instance_id、步骤、错误码和结果指纹,不记录完整凭据、客户文档、提示和工具参数。LLM 节点还要记录允许发送到哪个供应商/地区的字段清单。
合同测试清单
- 最小合法输入、最大边界和空集合;
- 缺失 required、错误类型、未知枚举和额外字段;
- 当前版本、至少一个真实旧版本和未来未知版本;
- 同一幂等键但载荷不同;
- 同一
operation_id的回执、延迟终态和结果未知,是否走正确的查询/对账路径; - 未认证或已撤销权限的 actor/tenant,是否在读取对象和执行副作用前 fail closed;
- Unicode、超长字符串和嵌套深度限制;
- 敏感字段是否意外进入事件、日志或错误信息;
- 上游输出能否直接通过每个下游输入合同。
练习
为 parse_invoice -> approve_invoice 写两份简化 schema。v1 只有金额,v2 增加 currency:
- 选择 v1 到 v2 是默认币种迁移、拒绝还是人工确认,并说明风险。
- 设计一条“同一事件 ID、不同金额”的冲突测试。
- 列出允许写入日志的字段和必须只保存引用的字段。
- 说明旧审批能否直接用于迁移后金额或币种变化的实例。
自测
- schema 验证与业务验证有什么区别?
- 为什么原始事件应不可变?
specversion、事件类型版本和 payload schema 版本分别解决什么?- 哪些情况下新增可选字段仍可能破坏消费者?
下一步
继续 条件、并行与汇合。
参考资料
- JSON Schema Draft 2020-12(访问于 2026-07-22)
- CloudEvents Specification 1.0.2(访问于 2026-07-22)
- Open Workflow Specification 1.0.3(访问于 2026-07-22)
- OWASP Business Logic Security Cheat Sheet(服务端重算安全值与每次资源授权,访问于 2026-07-22)