Checkpoint 与回滚
每一次图状态转移都持久化。当一个 worker 崩溃时,它的租约(lease)过期,另一个 worker 在最新的 checkpoint 处接手。Cloudflare Durable Objects 跨几小时或几周持有状态。先提议后提交(第 15 课)为每个动作定义一份回滚计划。动作后核验闭合这个循环。EU AI Act 第 14 条让高风险系统的有效人类监督成为强制——实践中这意味着 checkpoint 必须可查询、回滚必须演练过,而审计轨迹必须在一次部署中存活。尖锐的失败模式:没有幂等键和前置条件检查,一次瞬时失败后的重试可能把一个已批准的动作执行两次。动作后核验正是抓住它的东西。
类型: Learn 语言: Python(标准库,checkpoint 与回滚状态机) 前置要求: 阶段 15 · 12(持久化执行),阶段 15 · 15(先提议后提交) 预计时间: ~60 分钟
问题背景
持久化执行(第 12 课)让一个崩溃的 agent 可恢复。先提议后提交(第 15 课)让一个已批准的动作可审计。本课把它们接起来:当一个已批准的动作部分执行、崩溃、然后恢复时会发生什么?回滚在什么时候跑、针对什么状态跑?
真实系统接法各异:
- LangGraph 把每一次图状态转移 checkpoint 到 PostgreSQL。worker 崩溃时,租约释放,另一个 worker 在最新的 checkpoint 处恢复。工作流在
interrupt()处暂停,这本身就会持久化。 - Cloudflare Durable Objects 跨几小时或几周持有按键的状态。把计算跟已批准动作的存储放在一起。
- Microsoft Agent Framework 在工作流 API 里暴露
Checkpoint原语;重放加幂等性覆盖重试。
每种情况下,真正管用的组合是:幂等键(防止重复执行)+ 前置条件检查(状态仍是我们当初批准时所针对的)+ 动作后核验(副作用确实发生了)+ 核验失败时回滚。
核心概念
每一次转移都持久化
图状态转移是任何把工作流从一个命名状态移到另一个的步骤。朴素的实现只在特定的提交点持久化;生产实现持久化每一次转移。代价(几次额外写入)相对于可靠度收益(重放能落在任何地方,租约恢复很精确)来说很小。
租约恢复
当一个 worker 崩溃时,工作流不会丢;租约(一个短命的、表明这个 worker 正在执行这次运行的声明)只是过期了。另一个 worker 拿起最新的 checkpoint 并恢复。正是这套租约机制,让生产系统能在滚动部署中存活而不丢失飞行中的工作。
幂等性加前置条件
光有幂等性不够。考虑:一个工作流被批准"当余额 > $1000 时从 A 转 $100 给 B"。工作流被提交,在执行中途崩溃,然后恢复。如果只检查幂等键,执行恢复,转账跑一次(正确)。但考虑在崩溃和恢复之间,A 的余额通过另一个工作流降到了 $500。幂等检查仍然通过;前置条件不通过。没有前置条件检查,我们就交付了一次透支。
每个有后果的动作两者都需要:
- 幂等键:防止重复执行。
- 前置条件检查:确认状态仍跟当初批准的一致。
动作后核验
"工具返回了 200"不是核验。真正的核验重新读取目标状态,确认副作用确实发生了。模式:
- 数据库更新:
UPDATE ... RETURNING *,然后断言返回的行跟意图状态相符。 - 邮件发送:提交后在已发送文件夹里检查消息 ID。
- 文件写入:把文件读回来并求哈希。
- API 调用:对目标资源做一次后续
GET。
如果核验失败,工作流就处在一个已知的坏状态。回滚启动。
回滚计划
先提议后提交(第 15 课)里每个有后果的动作都带一份回滚计划。类型:
- 带内(in-band)回滚:直接反转副作用(
INSERT后DELETE,发送后发送一封更正邮件)。 - 补偿事务:一个抵消原动作的新动作(标准 SAGA 模式)。
- 带外(out-of-band)回滚:告警一个人、暂停工作流,把坏状态留给调查。
空操作回滚("我们没法撤销这个")必须在提议里点名。没有回滚的动作在提交时需要更强的 HITL(第 15 课的挑战-应答)。
EU AI Act 第 14 条的运营解读
第 14 条要求高风险系统有"有效的人类监督"。用运营的话说,实现者把它读成:
- checkpoint 可被审计员查询。
- 回滚演练过(至少端到端测过一次)。
- 审计轨迹在一次部署中存活(checkpoint 后端不是易逝的)。
- 失败的核验被告警,而不是被悄悄记进日志。
一个在提交中途崩溃、恢复、然后在没有核验 + 回滚路径的情况下完成副作用的工作流,扛不过第 14 条的测试。
尖锐的失败模式:重复执行
这个领域里最常见的生产事故:
- 动作被批准,幂等键 k。
- 提交开始、执行、返回 200。
- 工作流在持久化"已提交"状态之前崩溃。
- 工作流恢复;看到"已批准但未提交";重新执行。
- 副作用触发两次。
缓解:在执行前持久化一个"飞行中"意图,带一个幂等键执行,然后只在动作后核验成功后才标记"已提交"。如果动作触发了而状态写入失败了,你就知道要去核验并(必要时)重新触发。如果状态写入成功了而动作失败了,你就通过恢复路径核验并恰好触发一次。
checkpoint-replay实际使用
code/main.py 实现一个带幂等性、前置条件、核验和回滚的 checkpoint 化工作流。驱动程序模拟四个场景:干净运行、崩溃后重试(幂等性抓住)、前置条件失败(工作流不触发就中止)、核验失败(回滚触发)。
拿去用
outputs/skill-rollback-rehearsal.md 为一个提议的工作流设计一个回滚演练测试,并审计 checkpoint 后端的审计轨迹持久性。
练习
运行
code/main.py。验证这四个场景。对于"提交期间崩溃"的情况,确认动作跨重试恰好触发一次。把"先标记完成,再去做"的模式改成状态写入在动作之后触发。重跑崩溃场景。测量触发了多少个重复动作。
为一个特定的生产动作(比如"发到一个 Slack 频道")设计一份回滚计划。归类为带内、补偿还是带外。论证这个选择。
拿一个你熟悉的工作流。指出每一次状态转移。给每一次标上持久性要求(持久化 / 不持久化)。数一数你当前没在持久化的那些。
演练回滚测试:设计一个端到端测试,跑一个真实工作流、让它崩溃,并确认回滚路径触发。这个测试断言什么?
关键术语
| 术语 | 大家嘴上怎么说 | 实际指什么 |
|---|---|---|
| Checkpoint | "存档点" | 每一次图状态转移都持久化到一个持久存储 |
| Lease(租约) | "worker 声明" | 一个 worker 正在执行某次运行的短命声明;崩溃时过期 |
| Precondition(前置条件) | "状态门" | 断言状态仍跟已批准动作一致 |
| Post-action verify(动作后核验) | "重读检查" | 确认副作用确实在目标系统里发生了 |
| In-band rollback(带内回滚) | "直接撤销" | 用逆操作反转副作用 |
| Compensating transaction(补偿事务) | "SAGA 撤销" | 一个抵消原动作的新动作 |
| Mark-as-done-first(先标记完成) | "状态写入次序" | 在从提交返回之前持久化已提交状态 |
| Article 14(第 14 条) | "EU AI Act 人类监督" | 运营上:可查询的 checkpoint、演练过的回滚、可审计的轨迹 |
延伸阅读
- Microsoft Agent Framework — Checkpointing and HITL —— checkpoint 原语与租约恢复。
- Cloudflare Agents — Human in the loop —— 把 Durable Objects 作为状态基底。
- EU AI Act — Article 14: Human oversight —— 监管基线。
- Anthropic — Measuring agent autonomy in practice —— 长程工作流的可靠度框架。
- Anthropic — Claude Code Agent SDK: agent loop —— Claude Code Routines 的工作流形态。