「编辑并重新生成」是一个你的工具兑现不了的回滚承诺,而检查点恢复的是你的文件系统,不是这个世界。
Claude Code 的检查点只捕获经由它自己那套文件编辑工具所做的改动,所以一条 git push、一封已发出的邮件、一行经 shell 命令写入的记录,都活过了那次看起来把它们抹掉的回退——而亲眼看见对话回滚的用户,相信的是另一回事。这个控件依然值得做;要改的是它所做出的那个声明。请在回退界面里写明你能恢复的边界,把分叉点放在不可逆步骤之前而不是之后,并把「回退对话」和「回退世界」当成两个独立控件——因为它们本来就是。
步骤 1这个交互来自聊天,在那儿它是诚实的;模型一拿到工具,它就不诚实了。
编辑一条消息、重新生成、把线程分叉:在一个纯文本助手里,这些就是它们看起来的那个样子。除了 token 什么都没发生,所以从更早的某一点重新推导,是一次完整而真实的撤销。用户的心智模型——这段对话是一份我可以修订的文档——与实现精确吻合。
给同一个循环配上一条工具腰带,心智模型活了下来,底下的实现却没有。如今这份记录,记的是「发生在这份记录之外的那些系统身上」的事。回退它,编辑的是那份记录。由此而来的 bug 报告有一个经典形状:一位用户回退到「把摘要发给客户」之前,改了自己的指令、重跑一遍,于是客户收件箱里有两封邮件,而对话记录里有一封——而他把这件事描述为「产品发出了一封它被告知不要发的邮件」,从他所站的位置看,这个描述完全正确。
这不是在论证应该拿掉这个控件。回退恰恰是智能体体验里价值最高的交互之一,因为它让你能在别处普遍降低摩擦——撤销与可逆性把这个论证完整地做过一遍。这里要说的是:这个控件在做出一个声明,这个声明目前没有任何限定,而声明与实现之间的那道缝,就是你的事故所在。
步骤 2你的检查点边界就是你的工具面——而用户会把它当成整个世界。
具体的版本值得说出来,因为领先的实现对此是写明了的,而人们照样错过。Claude Code 在每一条用户提示处创建一个检查点,跨会话保留,三十天后自动清理,并允许你恢复代码、对话,或两者。它追踪的是经由它自己那套文件编辑工具所做的改动。以别的方式做出的改动——一条写文件的 shell 命令、任何离开了这台机器的动作——都在检查点之外,而且没有子提示级粒度,所以回退的单位是「你的一轮」,不是「智能体的一个动作」。
这里每一条都是合理的工程决定。合在一起,它们划出了一条对实现者清晰、对用户不可见的边界,而用户看到「恢复检查点」,读到的是「把东西放回去」。在设计这块界面之前,先按这四项把你自己的边界明确写下来:
- 哪些效果被捕获。通常是:经由你自己那些工具实现的写入,因为那些是你能包起来的。通常不是:任何经由通用逃生口(比如一个 shell)执行的东西——这是每一个带逃生口的实现里都有的那个洞。
- 粒度多大。按用户轮次是常见选择,而且比用户预期的粗——一轮里可以包含智能体的十五个动作,恢复这一轮会把这十五个全部丢掉。
- 保留多久。一个保留窗口会恰好在最被需要的时候让回退不可用,也就是某人在一周后才注意到问题的时候。
- 恢复什么,且可以分开恢复。代码、对话、两者——见 STEP 4;这一项经常被塌缩成一个按钮,而它不该被塌缩。
逃生口才是真正的那个洞。一个能跑 shell 命令的智能体,能产生任何你的工具包装层看不见的效果,而你的智能体越能干,它真正的工作就有越多比例走那条路径。如果你在同一个产品里既发检查点又发 shell,诚实的界面就该说出来——「可恢复 Claude 编辑过的文件;不会撤销 Claude 运行过的命令」只是一行字,放得下,而且它把一场未来的事故换成了一条已知限制。
步骤 3把工具分进三个桶,因为其中只有一个是真能回退的。
工程上的问题不是「我们能不能给状态拍快照」,而是「每个工具容许哪一种撤回」,而答案恰好有三个。在注册时给你工具目录里的每一个工具打上一个标签;回退界面读的就是这个标签。
- 可回退(rewindable)。你手里有旧值,可以单方面恢复:一次文件写入、一份草稿、一条你自己拥有的应用内记录。事前快照,事后恢复,完事。只有在这个桶里,「撤销」这个词才是字面成立的。
- 可补偿(compensatable)。你无法恢复先前状态,但可以发出第二个动作去抵消第一个:退款、删掉你创建的那行、发一条更正、关掉你开的那张工单。系统永远回不到更早的那个状态——账上留着一笔退款——但用户可见的结果被恢复了。补偿需要原调用返回一个句柄,这意味着它得在工具里被设计进去,而不是在回退时临时加上。
- 不可逆(irreversible)。你发不出任何能让它没发生过的东西:一条已发出的消息、一篇已发布的帖子、一笔已清算的付款、一个没有备份的已删对象、一条已经被人读到的通知。这里唯一有效的控件是发生之前的那个——一道闸门,见审批与确认体验。
设计上的后果直接从这套分类里掉出来:把分叉点放在第一个不可逆动作之前,而不是放在这一轮的末尾。一次读了六份资料、起草一封回信、然后把它发出去的运行,应当把回退目标设在「已起草、未发送」那一刻,因为那是回退真正能恢复的最后一个状态。改按轮次边界设检查点,就意味着你唯一能给出的那个提议横跨了「发送」这一步,于是那个提议有一半是假的。
这也给了不可逆那一桶一个便宜而诚实的退路——在你不想加闸门时:一个暂留窗口。把不可逆动作排队三十秒,带一个可见的取消,在这个窗口内它是免费可回退的,因为它还没发生。
步骤 4回退对话和回退世界是两种操作,而用户想要分开用。
把它们塌缩成一个按钮,是这里最常见的设计错误,而这个拆分并不是给高级用户的锦上添花——四种组合里的每一种,每周都有人真的提出来。
- 两者都退。「整个方向都错了。」这是默认项,也是多数产品唯一发出去的那个。
- 只退世界,保留对话。「它写的代码很糟,但把我们带到这儿的那段讨论是好的。」丢掉产物,保留推理,让用户从一个智能体仍然拥有的上下文里重新掌舵。这是最常被想要、也最常缺席的那一项。
- 只退对话,保留世界。「留着它建好的东西,忘掉我们为它吵过的架。」想要它,通常是因为上下文窗口里塞满了一条死胡同,而那条死胡同如今正在主动误导模型——一次披着回退外衣的压缩请求。
- 都不退,只分叉。「从这里再试一种做法,但别丢掉第一种。」见 STEP 5,也是陷阱最锋利的那一个。
「保留世界、回退对话」有一个值得点名的失效,因为它正是本页开头那个失效的镜像:智能体如今是在一个「含有它毫无记忆做过的改动」的文件系统上作业。它会重新创建一个已经存在的文件、重新跑一次已经执行过的迁移,或者推翻自己先前的某次编辑。如果你提供只退对话这一档,事后要给智能体重新接地——一次对当前状态的重新读取胜过任何量的对话记录,而这与中断与交接在「人类在运行中的智能体底下改了东西」时所要求的,是同一项调和义务。
步骤 5分叉不是一次干净的重摇骰子;它是在一个另一条分支已经改过的世界上重试。
把一段对话分叉,会从一个共享前缀上给出两条并列线程,并强烈暗示它们是互相独立的实验。它们不是。这两条分支恰恰共享一样东西——它们中任何一条碰过的每一个外部系统——而那样东西根本没有任何分叉语义。
由此生出三种具体失效,而三种在用户眼里都读作「智能体很飘」,而不是「这块界面撒了谎」:
- 重复的效果。分支 B 重跑了分支 A 已经跑过的「建一张 Jira 工单」。两张工单,其中一张没人认领。解法是一个从逻辑动作而不是从分支推导出来的幂等键——与幂等与重试同一套纪律,只是应用在一个交互控件而不是一次网络重试上。
- 前缀里的陈旧读取。两条分支都继承了分叉之前采集的工具结果。如果此后分支 A 改动了分支 B 正在读的那个东西,分支 B 就是在一份如今已经错了的快照上推理,而它的上下文里没有任何东西说出这一点。给工具结果打时间戳,并在重新进入时失效,而不是按年龄失效。
- 交错执行。没有任何东西阻止用户同时跑两条分支,于是两个目标相同、凭据相同的智能体正在没有协调的情况下写同一批系统——一个没人设计过的双智能体系统,带着协同失效所意味的一切。要么把分支执行串行化,要么让第二条分支的冲突写入大声失败。
摆到用户面前的诚实说法是:分叉分的是对话,不是世界。如果你的产品无法在工具层强制这一点,次优选项是让一条被分叉出来的运行先把它继承到的一切重新读一遍,而不是信任它们。
步骤 6回退界面的职责,是展示它不会撤销什么。
上面的一切汇聚到一块界面上,而它的内容恰好与这类界面通常展示的东西相反。一个列出「将被恢复什么」的回退确认,是一份安慰。一个列出「将不会被恢复什么」的回退确认,是一份决策辅助——它是用户自己推不出来的唯一部分,也是唯一能拦下事故的那部分。
上面放三样东西,按这个顺序:
- 范围之外清单,放在最前,不可错过。「这不会撤销:1 封发给 dana@… 的邮件、1 次推到
main的提交。」它机械地从「被丢弃的那一段里的不可逆与未补偿工具调用」推导出来——而这之所以可能,正是因为 STEP 3 里打了标签。 - 把可提供的补偿做成勾选项。对可补偿那一桶,既不要默默撤回,也不要默默跳过:把它们列出来、撤回动作默认勾上,并允许用户保留其中某一项。「关掉它开的那张工单」是一个选择,而正在回退的那个人,才是有资格做这个选择的人。
- 恢复范围,放最后。代码、对话、两者——给一个默认值,而不是在用户还不知道利害关系时先抛一个问题。
给这块界面埋一个指标:用户在读完范围之外清单后退出的比例。比例高不是界面问题,是你的闸门放错了位置——人们正在回退这一刻才发现某个不可逆动作已经发生了,这意味着他们本该在它发生之前就被问过。把这个信号喂回到审批闸门该放在哪里,而不是喂回到弹窗的文案上。
按这个顺序分三步发,而第一步才是要紧的那一步。其一:给每个工具打上可回退/可补偿/不可逆的标签,并把范围之外清单放进回退弹窗——一周的工作量,把你最糟的那个失效模式换成一条可见的披露。其二:把恢复拆成代码/对话/两者,并把检查点边界从轮次边界挪到「第一个不可逆动作之前」。其三:加上补偿和幂等键,让可补偿那一桶真的可撤回、让分叉不再重复产生效果。团队通常先去做第三步,因为那是有意思的工程;而第三步的缺席是用户会原谅的,一次悄悄留下了一封已发邮件的回退则不会。至于那件不可逆的事已经发生、而没有人来得及回退时该怎么办,见修补智能体的副作用。