Agent 断点恢复要验证产物完全一致
文章分析补丁已写入磁盘、步骤完成状态尚未持久化时发生重启,导致恢复后重复应用补丁的故障。提出将恢复执行与连续执行的产物指纹一致设为不变量,而非只检查测试是否通过。
AI 深度解读
Agent 改完代码,测试全绿,重启后却把同一段修改写了两遍。原文讨论的就是这种恢复故障:文件已经落盘,执行器却没记住任务完成,于是从旧检查点重放,悄悄改变了最终产物。
作者提出的验收标准很直接:相同事件顺序下,中断恢复与连续执行应该得到相同的产物指纹,否则就明确拒绝继续。这个思路值得带回自己的 AI 编程流水线,但原文模拟器也有一处与结论不一致的地方,需要一起看清。
为什么测试通过,恢复结果仍然可能是错的
原文先给出一个 8 步任务:第 6 步调用模型、拿到工具意图、应用补丁,并把补丁刷入工作区磁盘。随后宿主机重启,执行器还没来得及记录第 6 步完成,只能从第 3 步的持久检查点恢复。
第 4 到第 8 步重新执行,第 6 步的修改再次落地。同一段内容出现在两个位置,构建和测试却可能照样通过。测试只覆盖了部分行为,并不一定检查修改是否重复。
所以作者要求比较产物指纹(fingerprint),用哈希表示最终内容,再检查恢复前后的结果是否一致。
恢复正确性的标准,是最终产物一致;测试通过不能替代这项检查。
这里有几个前提。计算节点可能在任意指令边界消失;共享模型端点的回答不确定,重新调用不能保证得到相同决策;模拟中的外部效果只有产物写入。如果步骤还会推送 commit、发送 webhook 或发表工单评论,就需要另行处理这些效果的提交边界。
作者还把恢复视为正常执行路径,要求放进 CI。它不是等服务器真的挂掉之后,再靠日志临时修补的应急功能。
补丁和执行回执,必须一起提交
问题的核心是两份状态:产物日志记录「实际写了什么」,执行回执记录「执行器认为哪些步骤已经应用」。
如果它们分属不同的持久化域,也就是可能各自保存或丢失的存储范围,重启就可能只留下其中一份。补丁留下、回执丢失,会重复应用;回执留下、补丁丢失,则可能跳过尚未真正保存的修改。
作者用 Python 标准库构造了一个 8 步模拟器,步骤 ID 为 s0 到 s7,补丁内容为 +line-0 到 +line-7,并设置 CHECKPOINT_EVERY = 3。
下面这段代码展示了去重与崩溃后的状态恢复,解决的是「执行器凭什么判断一个步骤已经做过」:
def apply(self, step):
if step["id"] in self.receipts: # 幂等检查
return
self.log.append(step["hunk"])
self.receipts.add(step["id"])
self.cursor += 1
def crash(self):
b = Box(self.log_durable, self.receipts_durable)
b.log = list(self.log) if self.log_durable else []
b.receipts = set(self.receipts) if self.receipts_durable else set()
b.cursor = (self.cursor if self.receipts_durable
else (self.cursor // CHECKPOINT_EVERY) * CHECKPOINT_EVERY)
return b
要盯住 self.log.append(...) 和 self.receipts.add(...):模拟器把 apply() 当作原子操作,真实系统里的这两次写入却可能被崩溃隔开。仅仅给两份数据都接上持久存储,并不能证明它们会一起成功。
作者要求的是:补丁与回执在同一个持久化域中,一次提交成功,或者都不提交。检查点只能减少重放工作量,不能承担这项正确性保证。
回执也不该只是一个完成标记。作者建议把步骤决策保存进去,使恢复能够重放已记录的决定,避免重新询问模型。重新规划会产生新的执行分支,无法保证延续原来的任务。
故障注入要覆盖哪些位置
原文的指纹函数很短,用 SHA-256 对按顺序拼接的补丁内容计算哈希:
def fingerprint(box):
return hashlib.sha256("\n".join(box.log).encode()).hexdigest()[:10]
要注意最后的 [:10]:这是演示用的截断指纹,比较对象也只是 box.log。接入真实代码仓库时,需要先定义产物范围,否则遗漏文件根本不会进入比较。
模拟器把连续运行 Box(True, True) 的结果记为 REF,再枚举 log_durable、receipts_durable 的四种组合。每种组合都在 for k in range(1, len(STEPS)) 指定的位置中断,恢复后输出 SAME 或 DIVERGED。
但作者要求的真实测试,比这段模拟器更细:
- 在每个步骤边界崩溃,检查最终指纹是否一致。
- 在产物写入与回执写入之间崩溃,暴露两次提交之间的空隙。
- 对同一个步骤 ID 调用两次
apply(),确认只产生一份补丁。 - 从落后于持久日志的旧游标恢复,要求正确完成或明确报错。
- 检查恢复期间的模型调用,确认已有回执的步骤不会重新请求模型。
这些检查分别覆盖中断、重复投递、状态落后和决策漂移。只模拟一次进程重启,无法替代它们。
我的判断
我认同作者把「恢复等价性」设为不变量:无论在哪里中断,都应该维持的系统性质。它比「服务能重新启动」「任务最终显示完成」更接近用户真正关心的结果,也给 CI 提供了可判断的失败条件。
不过,原文关于「完全易失可能更安全」的论证,需要打一个问号。
从头重放确实可能比部分状态残留更容易推理,前提是输入和决策可复现,且没有已经提交到外部的副作用。但按给出的代码推导,Box(False, False) 并不会始终从头开始:第 3 步后崩溃,日志与回执清空,cursor 却按 CHECKPOINT_EVERY = 3 保留为 3,恢复只运行剩下 5 步,结果仍然是 DIVERGED。这是静态推导,原文没有给出修正后的实测结果。
因此,作者的「全部丢弃再重放」策略,与这份模拟器的游标逻辑并不一致。真实模型又是不确定的,即使游标归零,重新调用模型也未必能复现原产物。
实现层面,把产物和回执放入同一事务,是满足要求的一条路径;使用对象存储或 git remote,把产物移出计算节点,则只是作者提出的存储调整。迁移位置本身不会自动带来原子提交。
原文没有提供真实流水线的对照数据、恢复耗时或模型调用成本,工程收益仍属于经验判断。文章也披露参与了 MonkeyCode 的产品推广;免费模型、免费服务器和写作时的 10M token 额度属于运营方提供的信息,不能据此推导持久性保证或 SLA。硬延迟 SLO、受监管数据、状态必须留在机器上的要求,也都超出了作者建议方案的适用范围。
能拿来做什么
- 给会修改前端或 Node 仓库的 Agent 定义产物指纹范围,并用同一组已记录决策分别执行连续运行和中断恢复,比较最终内容。
- 为工具步骤分配稳定 ID,保存可重放的决策,同时检查补丁与回执能否一起提交;仅有去重 ID 仍不足以关闭崩溃窗口。
- 在 CI 中加入步骤边界中断、重复意图和旧检查点恢复,并单独验证已有回执的步骤不会再次调用模型。
- 为检查点与产物日志不一致的情况设置明确报错路径;如果任务还会推送 commit 或发送 webhook,再为这些外部效果设计各自的恢复处理。