DAG 能保证顺序,但不能天然保证正确撤销。只要流程里有外部副作用,比如发邮件、创建工单、扣费、改状态,就需要补偿设计。
真正难点不在“写一个 undo 函数”,而在于你是否能证明补偿发生在正确时机、作用在正确对象、且不会二次伤害系统。
先把动作分级,再谈补偿
| 动作类型 | 示例 | 处理策略 |
|---|---|---|
| 可逆动作 | 更新草稿状态 | 自动补偿 |
| 条件可逆 | 扣减额度 | 先读状态再补偿 |
| 不可逆动作 | 对外发送邮件 | 人工兜底 + 证据包 |
不先分级,会把不可逆动作也放进自动重试,风险极高。
Saga 在 agent 里的最小状态机
建议至少定义以下状态:
EXECUTED -> COMPENSATING -> COMPENSATED | COMPENSATION_FAILED
不要只记录布尔值。布尔值无法表达“正在补偿”与“补偿失败待人工”的区别。
补偿记录的最小字段
- 原动作 ID(actionId)
- 目标对象 ID(targetId)
- 补偿动作 ID(compensationId)
- 补偿函数版本(compensationVersion)
- 执行时间、执行人/执行体
- 残留风险说明
这些字段必须进入 ledger,否则事故复盘会陷入“到底补偿没”的口水战。
设计原则:补偿不是反向重放
补偿动作应显式声明,而不是“把原步骤反着跑”。例如:
- 原动作:创建工单
- 补偿动作:关闭工单并标记原因
两者语义不同,不能靠自动推导。
失败案例:重复补偿引发二次事故
某团队在超时恢复时重复触发补偿,把已经回滚成功的订单再次撤销。根因是补偿链路没有幂等键,且重试策略把补偿当普通节点重跑。
修复动作:
- 所有补偿请求强制携带 compensationId。
- 补偿执行前读取目标状态并做“已补偿”判断。
- 补偿失败进入人工队列,不再自动无限重试。
如何与审批流协同
高风险补偿建议引入审批节点:
| 场景 | 是否审批 |
|---|---|
| 内部状态回滚 | 否 |
| 对外通知撤回 | 是 |
| 资金/额度回滚 | 是 |
审批并不是增加负担,而是避免补偿链路扩大事故面。
关键指标
- 补偿成功率
- 补偿幂等命中率
- 补偿平均完成时长
- 补偿后残留风险率
补偿成功率高但残留风险率高,说明你的补偿只是“技术成功”,不是“业务成功”。
补偿策略如何写进节点契约
不要把补偿逻辑藏在业务代码里。建议直接写入节点契约:
{
"nodeId": "publish_response.v3",
"sideEffects": [
{
"effectType": "external_message",
"target": "crm.contact_thread",
"idempotencyKey": "runId + nodeId + targetId",
"reversible": false,
"compensationPolicy": "manual_followup_with_audit_note"
}
]
}
这段契约明确告诉 runtime:该节点不是简单可逆动作。失败后不能自动“撤回消息”,而应创建人工跟进任务,并附带审计说明。
补偿优先级不是按失败时间排
当多个补偿任务同时出现时,优先级应按业务风险排序:
| 优先级 | 类型 | 原因 |
|---|---|---|
| P0 | 资金、权限、对外承诺 | 影响不可控且扩散快 |
| P1 | 外部状态写入 | 影响客户和协作系统 |
| P2 | 内部状态回滚 | 可控性较高 |
| P3 | 临时 artifact 清理 | 可延后处理 |
这能避免团队把时间花在“清理漂亮”,却放任高风险副作用继续扩散。
Checklist
- 每个副作用动作都有补偿声明
- 补偿状态机可观测而非布尔值
- 补偿流程支持幂等执行
- 不可逆动作有人工兜底和审批策略
- 补偿结果进入 run ledger 与复盘模板
延伸阅读:


