Agent Mentor Learn
状态管理与持久化:让长任务经得起中断 · 第 5 / 6 节

第 5 课:回退与分叉:检查点的第二重价值

学习目标:

  • 说清检查点除了灾后恢复,还有两种主动用法——回退到早先的现场重来、分叉出另一条时间轴去试——并理解它们和恢复(resume)同样建立在一份检查点序列上
  • 把检查点从「只留最新一份」改造成按轮次留存的序列,实现 rewindTo(turn),并说清回退拨回的是决策现场,不是已经发生的外部副作用
  • 实现 forkFrom(turn, branchName) 从同一现场复制出独立时间轴,并说清检查点、Git、效果台账三者各自的分工边界

前置要求:完成第 1-4 课,熟悉 checkpoint.json 的字段结构(versiontaskturnstokensUsedmessagespendingToolUse)与原子写、断点恢复怎么处理悬空调用、以及第 4 课的效果台账与幂等键 | 上一课 第 4 课 << | 下一课 第 6 课 >>

检查点不只是保险丝

前三课一路把检查点讲成救灾用的保险——进程崩了,从最近一份检查点把循环接着跑起来。这么用没问题,但如果只在崩溃之后才想起打开它,等于让它大部分时间都闲置着:没崩,是不是就白存了?

不是。一串检查点攒起来,其实是这个任务的一条时间轴——每一轮它在想什么、准备做什么、已经落了哪些账,全都留了痕迹。除了灾后恢复,这条时间轴还能派上另外两种主动用场:回退到早先的某一轮重新来过;从某一轮分叉出去,同时跑另一条路线看结果。一份以 harness 工程为主线的社区路线图,给持久化这个组件的概括正是把这三件事连在一起说的:每一步都要落检查点,为的是能恢复、能回退、能分叉1。这是路线图给出的一个框架性说法,没有规定具体怎么实现,但它点出了一件事:resume 只是检查点用法的三分之一,后面两种才是这一课的正题。

  • 回退(rewind):任务没崩,但走歪了,把决策现场拨回还没走歪的那一轮,重新来。
  • 分叉(fork):还不确定哪条路线更好,从同一个现场复制出两条独立时间轴,分别跑,再挑结果。

这两种都不是「灾后处理」——任务顺顺当当往下跑的时候,都可能用得上。

回退:拨回决策现场,不是拨回外部世界

设想一个要跑 20 多轮工具调用的任务:模型在第 15 轮做了一个错误决策——选错了要改的文件,或者对一条含糊的需求做了错误的假设。接下来的 10 轮,它都在这个错误的基础上继续往下建。本系列第 8 门课讲过,上下文越堆越长、越堆越杂,模型准确召回其中信息的能力会随之下降;何况这段历史里还带着一个错误决策,与其让模型在这段又长又跑偏的上下文里继续挣扎,不如把现场拨回第 14 轮——那个还没做错决策的时间点——从那里重新开始。

要做到这一点,检查点不能再只留「最新一份」。前几课的 saveCheckpoint 每次都覆盖同一份 checkpoint.json,恢复时只能拿到最后一次写入的状态——这对灾后恢复够用,但没法回退,因为第 14 轮那份现场早被第 15 轮盖掉了。要支持回退,检查点得按轮次留存成一个序列,文件名里带上轮次号和存档点:checkpoints/turn-014-A.jsoncheckpoints/turn-014-B.json 这样——每一轮里,模型给出方案、工具还没执行时落一份存档点 A;这一轮的工具结果写回 messages、这一轮真正跑完时再落一份存档点 B。默认「回到第 N 轮」指这一轮跑完后的现场,也就是取该轮里最后落盘的那个存档点:

拿到 rewindTo(14) 返回的现场,接下来的流程和断点恢复一样:用这份 messages 重建历史,从这个 turns 数字继续循环,只是这一次,模型面对的不再是被第 15 轮污染过的上下文,而是那个决策发生之前的干净现场。

但有一件事得主动点破:回退拨回的是决策现场,不是外部世界。如果第 16 轮那个错误决策已经调用了某个高影响工具——比如真的发出了一封邮件——回退到第 14 轮并不会把那封邮件收回来。检查点存的是 messagesturnspendingToolUse 这些你自己定义进快照的状态字段,从来没打算,也做不到去撤销一次已经落地的外部动作。第 4 课的效果台账(effects.json)继续遵守只增不减的规则:回退之后从第 15 轮重新跑,哪怕模型这次选了完全不同的动作,台账里也只会多一条新记录,不会把旧的那条抹掉——被丢弃的十轮里到底发生过什么,账上依然留着痕迹,这正是第 4 课那份幂等视角在回退场景里的延续。

分叉:从同一现场跑出两条时间轴

回退解决的是「这条路走错了,退回去重来」;但有时候问题不是「错没错」,而是「不确定哪条更好」——两种重构方案都说得通,想各跑一遍看效果再挑。这种时候不该覆盖式地二选一,而是从同一个检查点复制出两条独立时间轴,分别跑:

forkFrom(14, "plan-b") 之后,checkpoints-plan-b/ 里有了它自己的检查点序列和一份空白的效果台账,从第 14 轮往后,这条时间轴要往哪走、跑几轮、落多少检查点,都跟主线互不干扰。

分叉出来的两条时间轴各自独立,这件事对高影响工具是个提醒:如果两条时间轴都会调用同一个真正对外的动作——比如都要发同一封邮件——让它们各自无审批地跑下去,就是两条时间轴各发一遍,变成双份副作用。给这类工具接上审批闸,或者分叉期间先切成干跑模式,是分叉之前值得做的准备;这和回退时台账不会跟着回滚是同一个道理:检查点可以复制成两份,但已经落地的外部效果没法跟着复制成「平行世界各一份」。

产品对照:Claude Code 把这套做成了功能

前面这套回退与分叉,Claude Code 已经把它做成了产品级功能——这里只作对照,不是要教的工具。它的检查点机制会在每条用户提示之前自动捕获代码状态2:每条用户提示都会新建一个检查点2,而且检查点跟着会话一起保存,就算你退出会话再回来,依然能用 /rewind2

它的 /rewind 菜单把「恢复什么」拆成了三档:只恢复对话(代码保持当前状态)、只恢复代码(对话保留当前状态)、或者代码和对话一起恢复到那个时间点2——这恰好对应本课「回退拨回的是决策现场」这句话的产品化版本:可以选择只拨回决策现场(对话),也可以连同代码一起拨回。官方文档也列了检查点的几个常见用例,比如探索替代方案、尝试不同实现路径而不丢掉起点,以及从错误中恢复、快速撤销引入 bug 的改动2。要说明的是,这些用例文档是笼统挂在检查点(/rewind)名下讲的,并没有按「回退」「分叉」分开归类;但拿来对照本课「走错了退回去重来」和「不确定就分叉去试」这两种用法,方向是一致的。

分叉这一侧,Claude Code 提供了 /branch 或者 claude --continue --fork-session:在保留原会话完整不动的前提下,另起一条时间轴去试别的路子2

边界与分工:检查点、Git、效果台账各管什么

Claude Code 的文档也主动划了一条边界:它的检查点不追踪 bash 命令改动的文件2,只追踪 Claude 自己那几个文件编辑工具做出的修改2。同理,你自己 harness 里的检查点,覆盖的也只是你显式定义进快照的那几个状态字段——messagesturnstokensUsedpendingToolUse;工具在外部世界造成的改动,无论是写数据库、调用别的服务、还是发一封邮件,统统不在检查点的管辖范围内,那是效果台账的活。

官方文档把这套机制的定位说得很直接:检查点是为快速的会话级恢复而设计的,长期历史和协作,还是要继续用 Git 这样的版本控制2。三者各管一段,摆在一起看更清楚:

机制管什么时间尺度
检查点运行现场——messages、轮次、待执行的工具调用分钟级、会话级
Git代码本身的历史——提交、分支、协作永久、可协作
效果台账已经发生的外部副作用——发过的邮件、写过的记录只增不减,永久保留

留存的代价:按需取舍

按轮次留存整条检查点序列,不是没有成本的——每一轮两份存档点,跑得越久,磁盘上堆的文件就越多。Anthropic 那条「只在复杂度能明确改善结果时才考虑添加」的原则3,放到这里同样适用:如果任务本来就跑不了几轮,也不太需要回退重来,按轮次留存整条序列这份开销,可以考虑省下来——像前几课那样只留最新一份,也够用。反过来,任务动辄几十轮、又常常需要回退或者分叉着试几条方案,序列化留存换来的是明确的收益:出了岔子不必从头再来,试错的成本也压低了。这终归是个按任务规模取舍的判断,不是哪种做法天生更对。

小结

  • 检查点不只是灾后恢复用的保险:一串留存下来的检查点是任务的一条时间轴,除了恢复(resume),还能回退(rewind)、分叉(fork)——一份社区路线图把这三件事作为持久化组件的框架性概括放在一起说1
  • 回退拨回的是决策现场,不是外部世界:已经真实发生的外部动作不会因为回退而撤销,效果台账继续只增不减,这是第 4 课幂等视角在回退场景里的延续
  • 支持回退的前提是检查点按轮次留存成序列(如 turn-014-A/B.json),而不是覆盖式地只留最新一份;rewindTo(turn) 默认取该轮里最后落盘的存档点
  • 分叉是从同一现场复制出独立时间轴,各自带上自己的检查点序列和效果台账;两条时间轴如果都会碰同一个高影响工具,记得接上审批或切成干跑模式,否则就是双份副作用
  • Claude Code 已经把回退与分叉做成产品功能:每条提示自动建检查点、/rewind 可分别恢复对话或代码、/branch--fork-session 用于分叉2;但它自己划了边界——只追踪 Claude 自己文件编辑工具的修改、不追踪 bash 命令改动的文件2,定位是快速的会话级恢复,长期历史与协作仍归 Git2
  • 三者分工不同:检查点管分钟级的运行现场,Git 管永久、可协作的代码历史,效果台账管已经发生的外部副作用;按轮次全量留存检查点有磁盘成本,是否值得取决于任务规模,而不是哪种做法天生更对3

>> 第 6 课:实战:给 harness 装上检查点与恢复

Footnotes

  1. The 2026 Agent Engineering Roadmap — GitHub (codejunkie99/agent-roadmap-2026) — https://github.com/codejunkie99/agent-roadmap-2026 2

  2. Checkpointing — Claude Code Docs — https://code.claude.com/docs/en/checkpointing 2 3 4 5 6 7 8 9 10 11 12

  3. Building Effective AI Agents — Anthropic Engineering — https://www.anthropic.com/engineering/building-effective-agents 2

练习

01

以下四个场景,各自该用回退、恢复、分叉、还是 Git?请逐个作答并说明理由。

Level 1:四个场景,选对工具
  1. 一个跑了 20 多轮的任务里,你发现模型在第 12 轮选错了修改方案,后面的轮次都建立在这个错误方案之上,但进程本身一直好好地在跑,没有崩溃。
  2. 同一个任务跑到第 18 轮,宿主机被重启了,进程整个断掉,什么都没跑完。
  3. 你不确定该把一个模块拆成两个服务还是拆成三个,想让 Agent 各按一种方案跑一遍,再比较结果。
  4. 你想知道三天前这份代码是什么样子、是谁在什么时候改的。
完成标准 · 本地勾选
02

下面这段代码是前几课的旧版本:每次都覆盖同一份 checkpoint.json,只能支持恢复,不能支持回退。请把它改造成本课要求的样子:文件名按 turn-NNN-A.json / turn-NNN-B.json 的规则按轮次留存;提供一个恢复用的 loadLatest(),默认取最新轮次里最后落盘的存档点;再提供一个 rewindTo(turn),取指定轮次的现场。改完之后,写一段验证:连续存 5 轮,调用 rewindTo(3),确认拿回的现场 turns 是 3,而且效果台账 effects.json 的长度没有跟着回滚。

Level 2:把 saveCheckpoint 改造成按轮次序列
完成标准 · 本地勾选