1017 字
5 分钟
Agent 工程(三):Checkpoint 是执行历史,不是状态备份

Checkpoint 的核心价值不是“保存一份当前状态”,而是记录 执行如何到达当前状态。只有保留这条因果链,系统才能知道应该从末端续跑,还是从中间节点重放,以及哪些外部副作用可能再次发生。

存储可以回到旧版本,现实世界却不会同步回退。邮件已经发出、工单已经创建、文件已经覆盖时,恢复一份旧消息数组并不能撤销这些结果。这是 Checkpoint 设计的起点。

Checkpoint 记录的是可恢复边界#

一次 Agent 运行可能经历多轮模型调用和工具执行。只在整轮结束时保存,进程崩溃后就会重做整轮;每次迭代完成后保存,则最多丢失当前一步,但写入成本更高。

保存内容也不能只有 messages

状态恢复时解决的问题
消息历史模型当时看到了什么
当前执行位置下一步应该从哪里开始
待完成工具调用是否还欠模型一个工具结果
累积业务状态待办、计数器和已处理对象是否丢失
运行绑定工具集、工作区和版本是否仍与当时一致

恢复的目标是还原“当时能够继续执行的完整条件”,而不只是还原说过的话。

安全恢复要求历史结构完整#

工具调用与工具结果必须配对。如果系统在工具执行中被打断并直接落盘,历史末尾可能只剩一个悬空调用;恢复后将这份历史重新提交给模型,协议本身就可能无效。

因此可控打断不应等同于立即终止:

  1. 请求停止后先尝试运行到工具结果已经记录的安全边界;
  2. 无法等待时,为未完成调用写入明确的失败结果;
  3. 把待完成调用作为显式状态保存,而不是只放在调用栈局部变量中。

这只能保证历史结构完整,不能撤销工具已经产生的副作用。崩溃与打断的差别在于:打断还有机会完成配对;两者对外部世界造成的变化都需要额外处理。

链结构区分续跑与重放#

每个 Checkpoint 指向前一个节点,形成一条可追溯链。链不仅支持查看历史,也明确区分两种操作:

  • 续跑:从链末端继续,已经完成的步骤不再执行,适合崩溃恢复;
  • 重放:从中间节点重新执行,其后的步骤可能再次产生副作用。

重放一封已经发送过的邮件会再发一封,即使内部状态恢复得完全正确。缓解方法包括:

  • 在原链旁创建分支,不覆盖已有记录;
  • 区分纯计算步骤与有副作用步骤;
  • 让工具端使用幂等键识别重复请求;
  • 在重放不可逆动作前请求人工确认。

所以“恢复”和“重新生成”不应该共用一条默认执行路径:前者可以自动续跑,后者必须显式承担重复副作用的风险。

我的选择:优先保证边界正确#

我会在每次模型响应与对应工具结果完整写入后创建 Checkpoint,并让恢复逻辑只从完整节点续跑。对于高频、低价值步骤,可以在确认恢复窗口允许后降低落盘频率,但不能跨过不可逆工具调用而不留记录。

这套方案的代价是写放大和状态迁移成本。只有当任务本身无外部副作用、失败后整轮重试可以接受时,我才会退化为轮次级快照。存储层如何保留这条链,下一篇继续说明:只追加记录为什么不能随意删除中间节点