把 Agent 的"撤销"想成数据库的 undo 按钮,是 2026 年生产事故的最大单点诱因。当一个动作扇出成十二次工具调用——发两封邮件、建日历、改 CRM、扣款、发 Slack——其中三次 API 不可逆、两次只能用"反向操作"近似抵消且会触发自己的下游通知、剩下七次才真正可逆。这时"回滚"本质是一条第二条工作流:在前向执行前就声明好每一步的补偿(compensation),一旦后续步骤致命失败,编排器按 LIFO 顺序重放这些补偿。本期实测六套补偿栈——Cordum 的 Redis LIFO 栈、LangGraph Deep Agents 检查点、Temporal 持久执行、Microsoft agent-governance-toolkit 的 Saga 编排、AWS Step Functions 编舞,以及手写补偿注册表——核心战场不在"能否回滚",而在失败分类器(重试还是补偿)、子代理补偿债和不可逆动作的损害遏制。
| 补偿栈 | 回滚触发 | 补偿存储 | 幂等机制 | 子代理感知 | 不可逆处理 | 单步开销 | 最适合场景 |
|---|---|---|---|---|---|---|---|
| Cordum | FAILED_FATAL |
Redis LIFO 栈 saga:<id>:stack |
显式键或 saga:<hash> 派生 |
工作流级锁隔离 | Safety Kernel deny 即跳过补偿 | 亚毫秒级(SETNX/LPOP) | 高风险自主动作、需审计闭环 |
| LangGraph Deep Agents | 异常 + 条件边 | Checkpointer 持久快照 | 依赖工具自带幂等 | 弱:父代理看不见子代理副作用 | 需手写损害遏制节点 | sub-10ms | 图式工作流、人在回路 |
| Temporal | Activity 抛错 | Workflow History(不可变) | Activity 天然可重放 | 强:子 workflow 可查历史 | 损害遏制 Activity | sub-10ms,长链复利 | 长时运行、跨供应商路由 |
| Microsoft agent-governance-toolkit | Saga 协调器判定 | Saga 子系统注册表 | 补偿契约声明 | 编排式,集中可见 | 治理面分级 | 中等 | 合规重、需治理面 |
| AWS Step Functions | Catch/补偿分支 | 状态机状态 | 每转换幂等设计 | 子状态机需显式串联 | 补偿分支兜底 | 数十 ms + 按转换计费 | Serverless、AWS 原生 |
| 手写补偿注册表 | 自定义 | 内存/DB 栈 | 自管 idempotency key | 完全自控 | 完全自控 | 视实现 | 轻量、PoC、非关键链 |
真正的麻烦不在某一步失败,而在前向全自动、补偿靠人工且无人记录。LangGraph Deep Agents 和 Claude Code 2.1 的子代理(subagent)能拉起上下文隔离的子任务,这些子任务常常在前向链失败之前就已经成功落地了副作用——扣款成功、邮件已发、订阅已建——而父代理对它们一无所知。父代理的补偿逻辑无法回滚它从未观察到的动作,这笔悬而未决的副作用就是"补偿债"(compensation debt):系统没有任何内建机制去探测或偿还它。
实测中,一个含 3 个子代理的预订链,父链在第 4 步失败时,标准 try/except 只回滚了父链可见的 2 步,子代理扣的保证金、发的确认邮件被静默遗留,直到客户投诉才被发现。这就是为什么所有成熟栈都要求:子代理任务必须在执行前、而非失败后,向父级 Saga 协调器注册自己的副作用与补偿 handler。把"先注册后执行"做成硬契约,是消除补偿债的唯一入口。训练营价值:这条规则本身就是企业级 Agent 可靠性课程的核心章节,能直接转化为"分布式 Agent 事务安全"模块的卖点。
Cordum 把补偿做成一条独立的高优先级工作流。前向每成功一步,就把该步的补偿模板 push 进 per-workflow 的 Redis 栈 saga:<workflow_id>:stack;一旦结果分类为 FAILED_FATAL,Saga Manager 起一个回滚 goroutine,30 秒超时,按 LIFO 顺序 LPOP 逐个派发补偿作业,并强制打标 saga_compensation=true、is_compensation=true、提到 CRITICAL 优先级,全程经 Safety Kernel 门控并写入审计轨迹。
// saga.go — LIFO 回滚主循环,per-workflow 锁防并发回滚竞态
func (s *SagaManager) Rollback(ctx context.Context, workflowID string) error {
lockKey := fmt.Sprintf("saga:%s:lock", workflowID)
ok, _ := s.redis.SetNX(ctx, lockKey, "1", 2*time.Minute).Result()
if !ok { return nil } // 该工作流已有回滚在进行,避免双轮回滚
for {
data, err := s.redis.LPop(ctx, fmt.Sprintf("saga:%s:stack", workflowID)).Bytes()
if err == redis.Nil { break } // 栈空,回滚完成
// 反序列化 → 派发补偿作业(CRITICAL 优先级 + 幂等键)
}
return nil
}
关键工程取舍有三:锁用 SETNX + 2 分钟 TTL 防同工作流并发回滚竞态;30 秒硬超时保证回滚不会无限挂死;Safety deny 时跳过补偿而非硬阻塞,代价是留下残余人工工作。优点是延迟极低(Redis 单线程 LPOP 亚毫秒)、状态可观测(栈本身就是进度);缺点是 Safety Kernel 跳过补偿会留下"半自动"烂摊子。变现路径清晰:把这套 Redis 栈 + 锁 + 审计做成"Agent 回滚中间件"中间件层,按工作流量计费。
整套 Saga 体系最容易被低估、却最致命的组件是失败分类器(failure classifier)。它要区分两类失败:瞬态失败(网络超时、429 限流)可安全用指数退避重试;语义失败(下游系统正确拒绝了动作、业务规则违反)必须触发补偿。一旦分类器把语义失败误判为瞬态去重试,就会用重复的无效调用放大损害——对一个本就该被拒绝的扣款连试 5 次,留下 5 条失败日志和潜在的风控触发。
实测中分类器的两个高频盲区:一是把下游返回的 400 Bad Request(语义错误)当 5xx(瞬态)一锅端重试;二是对幂等性未声明的工具,重试变成"双倍执行"。成熟做法是把失败码白名单化——5xx/超时/限流走重试,4xx(除 408/429)一律进补偿通道——并在补偿 handler 自身失败时退避重试补偿,而非重试前向。这条"失败码 → 重试/补偿路由表"是训练营里最值钱的一张决策图,能直接做成讲师演示的故障注入实验。
LangGraph 的做法是不把多步任务建模成"一个又长又黑的 prompt+tools 序列",而是拆成显式命名阶段,套一个状态机:PENDING → RUNNING → COMPENSATING → FAILED | COMPLETED。每一个有副作用的步骤前后都写一次持久检查点(LangGraph Platform checkpointer),前置检查点捕获状态快照、拟定动作 payload、以及该步注册的补偿 handler;后置检查点记录外部系统响应并确认阶段完成。
# 每个副作用步骤前后的检查点 + 补偿注册
def book_hotel(state):
save_checkpoint(state, stage="book_hotel", payload=state["hotel_req"],
compensation=cancel_hotel) # 前置:声明补偿
resp = hotel_api.create(state["hotel_req"]) # 前向
save_checkpoint(state, stage="book_hotel", result=resp) # 后置
state["saga_stack"].push(cancel_hotel, resp.id) # 进 LIFO 栈
return state
优点是状态机可观测、检查点可时间旅行回放、天然支持人在回路审批回滚;缺点是子代理感知弱——Deep Agents 的子代理副作用父图看不见,必须额外约定"子代理先注册副作用"。单步检查点开销 sub-10ms,但在高频工具链里会复利累积。落地建议:把"副作用步骤"和"纯读步骤"分离,只对前者双检查点,能砍掉一半 I/O。
Temporal 走的是另一条路——不靠事后补偿,靠执行模型本身的可重放性。Workflow History 是不可变的事件日志,任何 Activity 失败后,Worker 重启时从历史回放即可确定性地恢复到失败点,重试或跳过都精确。补偿在这里是"补偿 Activity",挂在 catch 块里,按历史逆序触发。
这套模型对"长时运行 + 跨供应商 LLM 路由"场景几乎碾压:一个跑 20 分钟、横跨三个模型供应商的科研 Agent,中途某供应商限流,Temporal 能在历史里精确卡回失败 Activity,重路由到备用供应商继续,而不需要把整条链回滚重来。子代理感知也最强——子 workflow 的历史父 workflow 可查。代价是学习曲线陡、Activity 必须严格确定性(禁随机、禁直接调系统时间),且长链里 sub-10ms 的检查点写盘会复利。变现上,"Temporal + Agent"是企业级长程自动化的高级课,客单价高但受众窄。
回到开头那个十二次工具调用的例子,真正的工程难点不是"怎么撤销",而是承认有些动作撤不掉,并据此分级。tianpan 的实测把动作分成三档:3 次 API 不可逆(已送达邮件、已捕获扣款)、2 次只能用反向操作近似抵消且反向操作会触发自己的下游通知(取消订阅 → 发取消邮件 → 又惊动客户)、7 次真正可逆。成熟的补偿栈对第一档不假装能 undo,而是损害遏制(damage containment):记录不可恢复状态、发一条补偿性通知、打标等人工处理。
// 不可逆动作的损害遏制:不假装 undo,只遏制
func compensateEmailSent(step Step) {
logIrrecoverable(step) // 记录不可逆状态
notifyOps(step, "需人工跟进:邮件已送达") // 补偿性通知
flagForOperatorReview(step) // 升级操作员面板
// 关键:不调用 fakeUndoEmail()
}
最危险的失败模式,是工程师把"发一封'请忽略上一封'的邮件"当成对已发邮件的真正补偿——这会触发客户困惑、合规留痕混乱。设计期就必须把动作标成 compensable / containable / irreversible 三类,并写进补偿契约,避免运维在事故现场才发现这条界线。这是训练营里"Agent 安全设计"章节最有共鸣的一节,因为每个学员都踩过"以为能撤其实撤不掉"的坑。
补偿 handler 自己也是一次工具调用,自己也会失败、会被重试——如果补偿没有幂等保护,重试就会变成双倍补偿:退两次款、撤销两次订阅。Cordum 的做法是每个补偿作业必须携带稳定幂等键,无显式键时调度器按 saga:<hash(workflow_id+job_id+step_index)> 格式派生一个;下游系统按这个键去重,重复派发直接幂等返回。
// compensation_idempotency.go — 幂等键派生,保证补偿可安全重放
func compensationIdempotencyKey(base *pb.JobRequest, comp *pb.Compensation) string {
capability := comp.Meta.GetCapability()
if capability == "" { capability = base.Meta.GetCapability() }
step := ""
if base.StepIndex != 0 { step = fmt.Sprintf("%d", base.StepIndex) }
seed := strings.Join([]string{base.WorkflowId, base.JobId, step, capability}, ":")
return "saga:" + hash(seed) // 稳定派生,重放命中同一键即去重
}
这个幂等键和前向工具的幂等键必须是两套独立键空间——用同一个键会让"前向执行成功 + 补偿"互相覆盖去重记录,产生漏退。实战检验:把退款工具的幂等键从前向的 pay:<id> 换成补偿专用的 saga:<hash> 后,双倍退款事故归零。这条"前向幂等键与补偿幂等键隔离"是工程评审必查项,也是训练营"幂等设计"单元的核心考点。
flowchart TD
P([PENDING]) --> R{RUNNING}
R -->|每步前置| CH1[写检查点: payload + 补偿handler]
CH1 --> FW[执行正向动作]
FW -->|成功| CH2[写后置检查点 + 压入LIFO栈]
CH2 --> N{还有步骤?}
N -->|是| R
N -->|否| OK([COMPLETED])
FW -->|失败| CLS[失败分类器]
CLS -->|瞬态: 超时/限流| RT[指数退避重试]
RT --> FW
CLS -->|语义: 业务拒绝| CMP{COMPENSATING}
CMP --> POP[LIFO出栈补偿handler]
POP --> IDEM{补偿幂等键命中?}
IDEM -->|未命中| EXEC[执行补偿]
IDEM -->|已命中| SKIP[幂等跳过]
EXEC --> REV{动作可逆?}
REV -->|可逆| NEXT[下一补偿]
SKIP --> NEXT
NEXT -->|栈非空| POP
REV -->|不可逆| CONT[损害遏制: 记录+通知+标记人工]
NEXT -->|栈空| FAIL([FAILED])
CONT --> ESC[操作员升级面板: 全执行轨迹]
ESC --> FAIL
ESC -.审计.-> LOG[(不可变审计日志)]
高风险自主动作(扣款、订阅、对外通知):首选 Cordum 式 Redis LIFO 栈 + per-workflow 锁 + Safety Kernel 门控 + 审计闭环。延迟最低、可观测最强、合规闭环天然完整。
图式工作流、审批密集:LangGraph Deep Agents + 显式状态机 + 双检查点,配合"子代理执行前注册副作用"硬契约消除补偿债。
长时运行、跨多 LLM 供应商:Temporal 持久执行无可替代,Workflow History 的可重放性让"重路由继续"比"全链回滚"便宜得多。
轻量 PoC / 非关键链:手写补偿注册表 + 独立补偿幂等键,足够撑住低频场景,省掉中间件复杂度。
无论选哪条,三条铁律不可破:子代理副作用必须执行前注册;失败分类器必须区分瞬态/语义;不可逆动作只做损害遏制、绝不假装 undo。把这三条写进工程评审 checklist,Agent 的"撤销"才算从愿望落地成工程。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
