<!-- title: 工具副作用回滚：补偿事务Saga实测 -->

## 概述

把 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 的 LIFO 补偿栈：Redis 栈 + 锁 + 超时

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 门控并写入审计轨迹。

```go
// 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 检查点：显式状态机驯服副作用

LangGraph 的做法是不把多步任务建模成"一个又长又黑的 prompt+tools 序列"，而是拆成显式命名阶段，套一个状态机：`PENDING → RUNNING → COMPENSATING → FAILED | COMPLETED`。**每一个有副作用的步骤前后都写一次持久检查点**（LangGraph Platform checkpointer），前置检查点捕获状态快照、拟定动作 payload、以及该步注册的补偿 handler；后置检查点记录外部系统响应并确认阶段完成。

```python
# 每个副作用步骤前后的检查点 + 补偿注册
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 天然回滚

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）：记录不可恢复状态、发一条补偿性通知、打标等人工处理。

```go
// 不可逆动作的损害遏制：不假装 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)>` 格式派生一个；下游系统按这个键去重，重复派发直接幂等返回。

```go
// 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>` 后，双倍退款事故归零。这条"前向幂等键与补偿幂等键隔离"是工程评审必查项，也是训练营"幂等设计"单元的核心考点。

## 补偿事务架构图

```mermaid
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 的"撤销"才算从愿望落地成工程。

## 参考来源

1. [The Agent Undo Button Is a Saga, Not a Stack](https://tianpan.co/blog/2026-04-27-agent-undo-saga-multi-tool-rollback) — 十二次工具调用撤销的 3/2/7 可逆性分级与残余副作用 UX
2. [Runtime Rollback Pattern for Agent Tasks (2026)](https://www.agentnative.dev/patterns/runtime-rollback-pattern-for-agent-tasks) — LangGraph Deep Agents / Claude Code 2.1 子代理补偿债、状态机、检查点与损害遏制
3. [AI Agent Rollback & Compensation Patterns (2026)](https://cordum.io/blog/ai-agent-rollback-compensation) — Cordum 的 Redis LIFO 栈、per-workflow 锁、FAILED_FATAL 触发与补偿幂等键派生（engine.go / saga.go）
4. [Microsoft agent-governance-toolkit: Saga Orchestration](https://microsoft.github.io/agent-governance-toolkit/tutorials/11-saga-orchestration/) — 多步 Agent 事务的 Saga 编排子系统与补偿交易治理面
5. [The Saga Pattern: Managing Long-Running Multi-Step Agent Transactions](https://callsphere.ai/blog/saga-pattern-managing-long-running-multi-step-agent-transactions) — 长时运行多步 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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
