如何实现 Agent Checkpoint?
长任务一定会中断。
浏览器刷新。进程崩溃。网络断开。模型请求失败。用户临时离开。工具超时。机器重启。
这些都不稀奇。
真正危险的是:Agent 中断后从头再来。
从头再来听起来只是浪费时间。实际更麻烦:
- 已经写过的文件又写一次。
- 已经发出的请求又发一次。
- 用户已经拒绝的权限又问一次。
- 上次测试失败的证据丢了。
- 用户在中断后手动改了文件,Agent 恢复时覆盖掉。
所以 Agent Runtime 需要 checkpoint。
但 checkpoint 不是保存聊天记录。
Checkpoint 保存的是一致性。
聊天记录不是 checkpoint
很多系统第一次做 resume,会保存 message history。
这只能解决一小部分问题。
聊天记录能告诉模型'刚才说了什么',但不能告诉 runtime:
哪些工具已经开始执行?
哪些工具已经成功?
哪些工具状态不确定?
哪些文件被 Agent 改过?
用户批准了哪个高风险动作?
哪个工具不能重放?
当前 trace 写到哪里?
预算还剩多少?
没有这些信息,恢复就只能靠模型猜。
让模型猜恢复状态,是很糟糕的设计。
一个 checkpoint 至少包含什么
一个可用 checkpoint 至少要有:
type Checkpoint = {
id: string;
runId: string;
stepId: string;
createdAt: string;
messagesRef: string;
runState: RunStateSnapshot;
toolLedger: ToolLedgerEntry[];
fileSnapshot: FileSnapshot;
approvalsRef: string;
budget: BudgetState;
traceRef: string;
};
这几个字段不是为了好看。
runState 说明任务到哪里了。
toolLedger 说明工具执行到哪里了。
fileSnapshot 说明工作区发生了什么变化。
approvalsRef 说明用户批准或拒绝过什么。
budget 说明还能不能继续。
traceRef 说明后续复盘能从哪里接上。
Checkpoint 的价值,是让这些状态彼此对得上。
保存时机比格式更重要
很多人纠结 checkpoint 格式:JSON、SQLite、git commit、event log。
格式当然重要,但更容易出事的是保存时机。
建议至少在这些点保存:
run 开始
每个 step 请求模型前
工具计划生成后
高风险工具审批后
工具执行开始时
工具执行完成后
文件写入后
run 结束
最容易漏的是'工具执行开始时'。
如果只在工具完成后保存,进程在等待外部 API 响应时崩溃,checkpoint 里就像什么都没发生。恢复时 Agent 可能再次调用同一个工具。
对读文件来说问题不大。
对发邮件、创建 issue、扣款、提交订单来说,这就是事故。
Tool Ledger 是 checkpoint 的账本
工具账本应该在工具执行前后都写:
type ToolLedgerEntry = {
callId: string;
stepId: string;
toolName: string;
inputHash: string;
status:
| "planned"
| "approved"
| "started"
| "succeeded"
| "failed"
| "uncertain";
idempotency: "idempotent" | "conditional" | "non_idempotent";
replayPolicy: "auto" | "ask" | "never";
sideEffects: string[];
startedAt?: string;
finishedAt?: string;
resultRef?: string;
};
uncertain 很关键。
工具请求已经发出,但响应没回来。这个状态不能写成普通失败,也不能写成没发生。
恢复时看到 uncertain:
- 幂等工具可以重放。
- 条件幂等工具要检查状态。
- 非幂等工具禁止自动重放。
没有 ledger,checkpoint 就只是半截聊天记录。
文件系统要单独保存
对 coding agent 来说,文件系统是一等状态。
最轻量的方案是 patch-based checkpoint:
type FilePatchEntry = {
path: string;
beforeHash: string;
afterHash: string;
patchRef: string;
toolCallId: string;
createdAt: string;
};
写文件前记录 beforeHash。写文件后记录 afterHash 和 diff。
恢复时:
当前 hash = afterHash:说明 patch 已应用,不要重复写。
当前 hash = beforeHash:可以重新应用 patch。
当前 hash 两者都不是:用户或其他工具改过,进入冲突处理。
这个判断比'直接覆盖'安全得多。
保护用户改动是底线。
Git 可以帮忙,但别污染用户历史
代码项目里,git 很适合做 checkpoint 辅助。
可以读取 HEAD、dirty 状态、保存 patch、生成临时 worktree。
但不要一上来就自动 commit 到用户仓库。
更稳的做法:
检测是否是 git 仓库
读取当前 HEAD 和 dirty 状态
checkpoint 保存 patch 文件和 metadata
不自动创建用户可见 commit
用户要求提交时再 commit
回滚前检查当前 dirty changes
aider 这类 coding agent 有 /undo,本质上也是在处理'如何撤销上一次 AI 变更'。Git 自带 revert/restore/reset/stash 等能力,但 Agent Runtime 不能粗暴调用 destructive 命令。它要知道哪些 diff 是 Agent 写的,哪些是用户原本就有的。
不要用'回滚 Agent 改动'误伤用户正在写的代码。
Resume 流程要保守
恢复流程建议这样:
1. 找到最新完整 checkpoint。
2. 校验 checkpoint 文件完整性。
3. 校验 workspace 状态。
4. 恢复 message history 和 run state。
5. 检查最后一个 tool call 是否完成。
6. 禁止自动重放不可重复工具。
7. 给模型注入恢复说明。
8. 从下一个安全 step 继续。
恢复说明不要写成'继续之前的任务'。
应该具体:
系统从 checkpoint 恢复。
Checkpoint: ckpt_004
上一次已完成 step_8。
工具 write_file(call_13) 已成功写入 src/runtime.ts。
工具 run_command(call_15) 执行 npm test,失败,错误摘要如下……
不要重复执行 write_file(call_13)。
请根据已有观察规划下一步。
模型需要知道自己不是重新开始。
它是在继续一个有证据的 run。
Rollback 不是 undo 按钮
用户说'回滚',可能有三种意思:
回滚文件
恢复会话状态
补偿外部副作用
这三件事不能混。
type RecoveryAction =
| { type: "rollback_files"; checkpointId: string }
| { type: "restore_session"; checkpointId: string }
| { type: "compensate_external_action"; ledgerEntryId: string };
文件 patch 可以回滚。
会话状态可以恢复。
外部副作用通常只能补偿。创建了 issue,可以关闭;发出的邮件不能撤回;数据库 migration 可能需要 down migration。
不要把所有东西都叫 undo。
模糊的词,会让用户以为系统能做它其实做不到的事。
本书 demo 的最小实现
本书配套 demo 里有一个 FileCheckpointStore。
它保存两类东西:
run-state-latest.json
snapshots/
run-state-latest.json 用来恢复 Runtime 状态。
snapshots/ 保存写文件前的副本,rollback 时根据 tool ledger 反向恢复。
代码方向大概是:
class FileCheckpointStore {
async save(state: RunState, label: string): Promise<string> {
// 写 run-state-latest.json
// 写 checkpoints/<step>-<label>.json
}
async loadLatest(): Promise<RunState> {
// 读取 run-state-latest.json
}
async captureBeforeWrite(path: string): Promise<SnapshotRecord> {
// 写入前复制旧文件到 snapshots/
}
async rollback(state: RunState): Promise<string[]> {
// 根据 toolLedger 反向恢复写入副作用
}
}
这不是完整生产方案。
但它很适合作为第一版:先让 run 能保存、能恢复、能撤销 Agent 写入。
和 LangGraph、Temporal 的关系
LangGraph 文档里有 persistence 和 checkpointer,核心是让 graph state 能跨步骤持久化,支持恢复和 human-in-the-loop。
Temporal 讲 durable execution,会把 workflow 的事件历史持久化,让长时间运行的流程在 worker 崩溃后继续。
这些系统都在解决类似问题:长任务不能只靠内存。
Agent Runtime 可以借鉴它们,但 Agent 多了几个麻烦点:
- 模型输出不稳定。
- 工具可能有副作用。
- 文件系统可能被用户手动修改。
- 上下文压缩会影响后续决策。
- 需要把 checkpoint 信息写回给模型。
所以 Agent checkpoint 不能只保存状态机位置。它还要保存工具证据、上下文证据和文件证据。
第一版实现清单
如果你已经有了前面文章里的 loop、maxSteps、budget、retry、idempotency,可以这样加 checkpoint:
1. 为每个 run 创建 runDir。
2. 保存 run-state-latest.json。
3. 每个 step 后保存 checkpoints/<step>.json。
4. 工具执行前写 ledger started。
5. 工具执行后写 ledger succeeded/failed/uncertain。
6. 写文件前保存 snapshot 或 beforeHash。
7. 写文件后保存 afterHash 和 patchRef。
8. resume 时加载最新 checkpoint。
9. resume 时禁止重放 non_idempotent 工具。
10. rollback_files 只撤销 Agent 自己的 patch。
先别一上来做分布式存储。
先把一致性语义写清楚。
最后
Checkpoint 不是'保存一下当前聊天'。
它保存的是:
这个 Agent run 执行到这里,
哪些事情已经发生,
哪些事情可能发生,
哪些事情不能重复发生,
从哪里可以安全继续。
这句话听起来啰嗦,但就是 checkpoint 的价值。
Agent 一旦能写文件、执行命令、调用外部系统,就必须有这层。
否则中断后的'重新来一次',迟早会变成事故。
参考资料
- LangGraph Docs: Persistence
- LangGraph Docs: Durable execution
- Temporal Docs: Durable execution
- Git Docs: git restore
- aider Docs: Undoing changes


