Checkpoint 存储的首要目标不是节省空间,而是 不破坏执行历史的因果关系。只追加记录能够保留每次推进的父节点和顺序,但由此产生的中间节点约束、并发冲突和存储增长必须显式处理。
一张表就能表达链
最小模型可以由会话、Checkpoint 标识、父节点和状态载荷组成:
session_idcheckpoint_idparent_checkpoint_idsequencepayloadcreated_at每次推进执行 INSERT,旧节点不做原地更新。恢复读取会话末端节点,审计沿 parent_checkpoint_id 回溯,分叉则从任一历史节点创建新的后继。
只追加的价值不是实现简单,而是任何状态都保留“从哪里来”的证据。若直接覆盖当前状态,系统只能看到结果,无法判断它经历过哪些工具调用、重试和分支。
中间节点不能按普通历史删除
链上的中间节点同时承担父子关系和恢复语义。删除一个仍被后继引用的节点,会让后续状态失去来源;只保留首尾节点,则无法判断中间副作用是否已经发生。
可行的清理方式不是随意删除,而是显式压缩:
- 选择一个仍然有效的边界节点;
- 把恢复所需状态汇总为新的基线;
- 让后续链从该基线继续;
- 在确认审计与回放需求允许后,再清理此前记录。
压缩改变了可追溯粒度,因此必须是产品策略,而不能只是数据库定时任务。
并发会把正常分叉变成故障
有意从历史节点创建两个后继,是分支功能;两个执行者同时推进同一会话末端,则通常是并发错误。两者在表结构上都表现为“一个父节点有多个子节点”,必须借助运行语义区分。
我的默认做法是为会话维护单一写入所有权,并在插入时校验预期父节点:
读取末端 A执行一步仅当末端仍为 A 时插入 B如果另一个执行者已经插入了新末端,当前写入失败并重新读取,而不是静默制造两条都自称为主线的链。需要真正分支时,则使用新的分支标识或子会话标识。
我的选择:先用完整快照换正确性
每个节点保存完整状态最容易恢复,但累计存储会随着会话增长迅速放大;增量记录更节省空间,却让恢复、迁移和损坏修复更复杂。
在状态规模尚可控时,我会先使用完整快照和只追加表,把恢复正确性、并发约束和版本迁移做清楚。只有监控确认存储或写入成为瓶颈后,才引入基线加增量,并保留周期性完整节点控制恢复链长度。
这项选择的代价是空间和序列化成本。状态结构升级时还需要明确版本字段和迁移逻辑;语言原生序列化虽然方便,却可能带来跨版本兼容和不可信反序列化风险。存储格式必须服务于长期恢复,而不是只服务于当前进程。
下一篇转向输入边界:上下文工程为什么首先是一项注意力分配问题。