检查点与回滚 本节摘要:持久执行(第 12 节)让崩溃的 Agent 可恢复;propose-then-commit(第 15 节)让已批准动作可审计。本节把两者接起来:当一个已批准动作执行到一半、崩溃、再恢复时,会发生什么?回滚何时跑、对着什么状态跑?答案是:每一次图状态转移(graph-state transition)都持久化;worker 崩溃时它的租约(lease)到期,另一个 worker 从最新检查点接手;Cloudflare Durable Objects 能跨数小时到数周保存状态。真正奏效的组合是四件套——幂等键(防重复执行)+ 前置条件检查(状态仍与批准时一致)+ 动作后验证(副作用真的发生)+ 验证失败即回滚。
本节摘要:持久执行(第 12 节)让崩溃的 Agent 可恢复;propose-then-commit(第 15 节)让已批准动作可审计。本节把两者接起来:当一个已批准动作执行到一半、崩溃、再恢复时,会发生什么?回滚何时跑、对着什么状态跑?答案是:每一次**图状态转移(graph-state transition)都持久化;worker 崩溃时它的租约(lease)**到期,另一个 worker 从最新检查点接手;Cloudflare Durable Objects 能跨数小时到数周保存状态。真正奏效的组合是四件套——幂等键(防重复执行)+ 前置条件检查(状态仍与批准时一致)+ 动作后验证(副作用真的发生)+ 验证失败即回滚。最尖锐的失败模式是「双执行(double-execute)」:提交开始、执行、返回 200、工作流在持久化「已提交」状态前崩溃,恢复时看到「已批准未提交」于是再执行一次,副作用触发两次。缓解是「先标记意图在途、再用幂等键执行、动作后验证成功才标已提交」的状态写入顺序。EU AI Act 第 14 条把这一切落地为操作要求:检查点必须可被审计者查询、回滚必须演练过、审计轨迹必须在部署后存活、验证失败必须告警而非静默记录。本节是工程课:状态的写入顺序决定系统是可靠还是双执行。
对应原课程:Phase 15 · Lesson 16 ·
checkpoints-rollback(原英文phases/15-autonomous-systems/16-checkpoints-rollback/docs/en.md)。前置:第 12 节(持久执行)、第 15 节(propose-then-commit)。
阅读完本节,你应当能够:
持久执行让崩溃的 Agent 可恢复;propose-then-commit 让已批准动作可审计。本节把它们接起来:当一个已批准动作执行到一半、崩溃、再恢复时,会发生什么?回滚何时跑、对着什么状态跑?
真实系统接法不同:
interrupt() 上暂停,而 interrupt() 本身也持久化。Checkpoint 原语;重放加幂等覆盖重试。每种情况里,真正奏效的组合都一样:幂等键(防双执行)+ 前置条件检查(状态仍与批准时一致)+ 动作后验证(副作用真的发生)+ 验证失败即回滚。
⚠️ 核心张力:双执行是这片最常见的产线事故,而它的根因几乎总是状态写入顺序错了——先执行再标记,或先标记完成再执行,都会在「执行与标记之间的崩溃窗口」里翻车。
图状态转移是任何把工作流从一个命名状态移到另一个命名状态的步骤。朴素实现只在特定提交点持久化;产线实现每次转移都持久化。代价(多几次写)相对于可靠性收益(重放能落在任意点、租约恢复精确)很小。
worker 崩溃时,工作流没有丢——租约(「这个 worker 正在执行这次运行」的短期声明)只是到期。另一个 worker 拿起最新检查点并恢复。租约机制是让产线系统在滚动部署中存活而不丢在途工作的关键。
光有幂等不够。设想:一个工作流被批准为「当余额 > 1000 时从 A 转 100 到 B」。工作流提交,执行到一半崩溃,恢复。如果只查幂等键,执行恢复,转账跑一次(正确)。但设想崩溃与恢复之间,A 的余额被另一个工作流降到了 500。幂等检查仍过;前置条件不过。没有前置条件检查,我们就发了一次透支。
每个后果性动作两样都要:
「工具返回 200」不是验证。真正的验证是重新读目标状态并确认副作用真的发生了。模式:
UPDATE ... RETURNING *,断言返回行匹配意图状态。GET。验证失败,工作流处于已知坏状态,回滚介入。
propose-then-commit(第 15 节)里每个后果性动作都带一份回滚计划。类型:
INSERT 后 DELETE、发送后发更正邮件)。空回滚(「我们撤销不了」)必须在提议里命名。没有回滚的动作,在提交时需要更强 HITL(第 15 节质询-应答)。
第 14 条要求高风险系统的「有效人工监督」。操作上,实现者把它读成:
一个在提交到一半崩溃、恢复、完成了副作用却没有验证+回滚路径的工作流,过不了第 14 条的测试。
这片最常见的产线事故:
缓解:在执行前持久化一个「在途(intent in-flight)」意图,用幂等键执行,只在动作后验证成功后才标记「已提交」。如果动作触发了而状态写入失败,你知道要去验证(必要时重触发);如果状态写入成功而动作失败,你验证并通过恢复路径精确触发一次。
原课程 code/main.py 用标准库实现一台带幂等、前置条件、验证、回滚的检查点化工作流。驱动器模拟四个场景:干净运行、崩溃后重试(幂等抓住)、前置条件失败(工作流不发即中止)、验证失败(回滚触发)。下面给出关键骨架。
def commit_safely(proposal, store, execute, read_back, rollback): key = proposal["idempotency_key"] rec = store.get(key) # 已完成 → 直接返回(幂等) if rec and rec["state"] == "committed": return rec # 1. 先标记在途(防崩溃窗口里的双执行) store.put(key, {**proposal, "state": "in_flight"}) # 2. 前置条件:状态仍与批准时一致 if not precondition_holds(proposal): store.put(key, {**proposal, "state": "aborted"}) return None # 余额已变 → 不发, 避免透支 # 3. 用幂等键执行 execute(proposal["action"], idempotency_key=key) # 4. 动作后验证:副作用真的发生了吗? if not read_back(proposal["action"]): rollback(proposal["rollback_plan"]) # 验证失败 → 回滚 + 告警 store.put(key, {**proposal, "state": "rolled_back"}) alert("verify 失败, 已回滚") return None # 5. 验证成功才标记完成 store.put(key, {**proposal, "state": "committed"}) return proposal
设计要点:状态写入顺序是这台机器的灵魂。「在途」标记把崩溃窗口从「执行与完成标记之间」缩到「在途标记写入的瞬间」——即使在那里崩溃,恢复时也能凭幂等键 + 验证精确触发一次。
class Lease: """worker 对一次运行的短期声明;崩溃时到期, 另一 worker 接手。""" def __init__(self, run_id, ttl_seconds=30): self.run_id, self.ttl = run_id, ttl_seconds self.expires_at = now() + ttl_seconds def renew(self, worker): if worker.alive(): # 滚动部署中 worker 被杀 → 不续 self.expires_at = now() + self.ttl def held_by(self, worker): return now() < self.expires_at and worker.alive()
租约到期,最新检查点被另一个 worker 拿起。这是产线在滚动部署中不丢在途工作的关键。
| 框架 | 检查点机制 | 租约/恢复 | 适用场景 |
|---|---|---|---|
| LangGraph | 每次图状态转移 → PostgreSQL | worker 崩溃租约释放, 另一 worker 在最新检查点恢复 | 长程多步工作流, 需 interrupt() 暂停 |
| Cloudflare Durable Objects | 每键状态跨小时到周 | 计算与存储同置, 边缘就近恢复 | 低延迟、长寿命的每键状态 |
| Microsoft Agent Framework | 工作流 API 的 Checkpoint 原语 |
重放 + 幂等覆盖重试 | 企业工作流, 与 Azure 状态服务集成 |
| 裸 stdlib | JSON 文件(教学) | 进程内模拟租约 | 教学演示状态写入顺序 |
关键观察:三家都实现了「每次转移持久化 + 租约恢复 + 幂等」的同一组合。差异在耐用基板(PostgreSQL vs Durable Object vs 托管状态服务)与暴露面(API 形状)。选型看的是基板的可用性与一致性保证,而非概念——概念是共有的。
💡 三家的检查点后端都不是易失的。这是 EU AI Act 第 14 条「审计轨迹抗部署」的工程落地:检查点后端必须能在应用重新部署后存活,否则审计能力在一次部署后就消失。
原课程 outputs/skill-rollback-rehearsal.md 为一条拟议工作流设计回滚演练测试,并审计检查点后端是否符合审计轨迹持久要求。它把「列出每个后果性动作 → 为每个分类回滚类型 → 设计端到端崩溃-恢复-回滚测试 → 审计检查点后端抗部署性」串成可重复清单。
code/main.py 是零依赖状态机,把 JSON 存储换成 PostgreSQL/Redis/Durable Object 即可贴近产线;状态写入顺序、幂等、前置条件、验证、回滚逻辑均无需改动。
跑 code/main.py:验证四个场景。对于「提交到一半崩溃」,确认动作跨重试精确触发一次。
改坏状态写入顺序:把「先标记在途」改成「先执行再标记完成」,重跑崩溃场景,数有多少重复动作触发——直观感受双执行。
为具体产线动作设计回滚(如「往 Slack 频道发帖」):分类为带内、补偿、带外,说明选择理由。
审计一个你熟悉的工作流:列出每个状态转移,每个标上持久性要求(持久化/不持久化),数出你当前没持久化的那些。
演练回滚测试:设计一个端到端测试——跑真实工作流、崩溃它、确认回滚路径触发。测试断言什么?
下一节,我们从「工程层防线」上升到「模型层防线」——Constitutional AI 与规则覆盖(Constitutional AI and Rule Overrides),看如何用一部不可被 Agent 编辑的「宪法」把硬性禁止嵌进模型行为本身。