当 Agent 学会自我修改,谁来保证它还能撤销自己?——EvoUndo 的可恢复性约束
想象你有一个跑了三个月的 Agent 系统。某天它发现:把 max_retries 从 3 改成 5,任务成功率提升了 4%。它自作主张地改了。
一个看似简单的场景
想象你有一个跑了三个月的 Agent 系统。某天它发现:把 max_retries 从 3 改成 5,任务成功率提升了 4%。它自作主张地改了。
一周后你发现:这个改动让某个下游服务的连接池被打满。你想把它改回去——但"改回去"意味着什么?把 max_retries 改回 3 就行了吗?
不行。因为这一周里,有其他 mutation 依赖了 max_retries=5 这个事实:某个 middleware 被加进来专门处理重试 4-5 次的边缘情况;某个 listener 注册了 on_retry_exceeded 事件;某个临时文件被创建在 /tmp/retry_5_state。你把 max_retries 改回 3,这些依赖全部变成幽灵——它们还在那里,但指向一个不存在的世界。
这就是 EvoUndo 论文要解决的核心问题:当 Agent 能自主修改自己的 harness(配置、工具、中间件、监听器、资源),怎么保证每一次修改都是"可恢复的"?
论文 arXiv: 2608.28363
为什么"前向改进"不够用
大部分 self-improving agent 的工作都盯着一个目标:让 Agent 更能干。形式化地说,给定 harness 状态 \(S\) 和一个 mutation \(m\),只要 \(\Delta J = J(m(S)) - J(S) > 0\),就接受这个 mutation。
EvoUndo 的第一个洞察是:前向改进不等于可恢复。
一个 mutation 可能:
- 覆盖了某个配置值(旧值丢了)
- 重排了 middleware 链(顺序信息丢了)
- shadow 了已有工具(原工具引用丢了)
- 创建了临时文件(谁都不知道它存在)
- 泄漏了后台资源(句柄没人管)
论文用一句话点出了问题的本质:
正确的恢复程序往往是状态依赖的——恢复到等价状态需要存在于 mutation 前状态中的信息。
这不是"备份一下不就行了"的问题。备份什么?什么时候备份?备份的信息够不够恢复?恢复后怎么验证"等价"?这些问题都没有现成答案。
EvoUndo 的四元组:把"撤销"变成工程问题
EvoUndo 的核心贡献是把模糊的"可恢复"概念拆成了一个四元组:
- \(m\):前向 mutation(改了什么)
- \(w\):witness 程序(mutation 前要捕获什么信息)
- \(u\):recovery 程序(怎么用捕获的信息恢复)
- \(\mathcal{C}_e\):effect contract(声明这次 mutation 会碰什么)
一旦 mutation 在第 0 轮被确定,它就不可修改。后续的修复只能改 \(w\)、\(u\)、\(\mathcal{C}_e\)。这个设计防止了一个微妙的逃避策略:如果允许修改 \(m\),LLM 最简单的"修复"方式就是把 \(m\) 弱化成 no-op(什么都不改),这样当然"可恢复"——但你也什么都没改进。锁死 \(m\) 强迫系统去寻找真正的恢复方案。
类比:外科手术的"术前照片"
想象外科手术。医生要切除一个肿瘤(前向 mutation)。在切之前,护士会拍一张术野照片、记录器械数量、标记血管位置(witness capture)。手术完成后,要按照片还原解剖结构(recovery program)。如果照片漏拍了某个血管分支,恢复就会失败——但你不能"不切肿瘤"来逃避恢复问题。
EvoUndo 的 \(m\) 锁死就是"手术已经做了,不能假装没做"。你能改的只有"怎么记录"和"怎么缝合"。
反事实往返验证:不只测当前状态
EvoUndo 的第二个核心创新是 counterfactual round-trip verification。
普通的验证是:在状态 \(s\) 上应用 \(m\),再应用 \(u\),看恢复后的 \(\hat{s}\) 是否等价于 \(s\)。但这有一个盲区——你只测了一个状态。
EvoUndo 的做法是:生成一组 counterfactual states \(\mathcal{Q}\),包括:
- IID 扰动:边界值变化、工具是否已存在、组合表面分配
- OOD 扰动:真正的结构性变化
类比:桥梁验收不只测当前车流
你不会只在一座桥上跑一辆 30 吨卡车就签验收报告。你会模拟暴风、地震、堵车、材料老化等各种"反事实"条件。EvoUndo 对 agent mutation 做同样的事——不只测"当前状态能不能恢复",而是测"在各种可能的状态下,这个 mutation 都能被恢复吗"。
这个设计直接回应了"标量幻觉"问题——"这个 mutation 可恢复吗?"不是一个标量问题,而是一个向量问题:\((state_1, state_2, ..., state_n)\) 上的恢复率向量。用单一状态管理可恢复性,就像用温度计量血压。
2×2 因子实验:最反直觉的发现
论文最精彩的实验是一个 \(2 \times 2\) 因子设计:
| L0(基础语言) | L1(扩展语言) | |
|---|---|---|
| D0(粗粒度诊断) | 0/197 (0%) | 180/197 (91.4%) |
| D1(精确地址诊断) | 38/197 (19.3%) | 167/197 (84.8%) |
- D0L0 = 0%:没有精确地址、没有扩展恢复原语,LLM 完全无法合成恢复程序。这不是"做得不好",是"一个都做不出来"。
- D1L0 = 19.3%:给了精确的状态地址,恢复率从 0% 跳到 19.3%。但只能处理简单 stratum(\(S_0\) 的 79.2%),对需要 L1 原语的任务(\(S_1\))仍然是 0%。
- D0L1 = 91.4%:不给精确地址,但给了丰富的恢复语言,恢复率飙到 91.4%。
- D1L1 = 84.8%:比 D0L1 还低!
为什么"更多信息"反而更差?
论文分析了 11 个 D0L1 成功但 D1L1 失败的案例,发现主要失败模式是 "over-decomposition error"(过度分解错误):
当 LLM 拿到精确的状态地址和残差信息时,它倾向于做更细粒度的操作——逐个 unregister listener、逐个 restore file。但这些细粒度操作更容易出错(顺序错了、索引错了、遗漏了依赖)。而 D0L1 条件下,LLM 只知道"这些 subsystem 出了问题",反而倾向于用更粗粒度的原语一把恢复——出错空间更小。
这个发现对 Agent 工程的启示是深远的:
诊断粒度应该被当作一个模型相关、表示相关的综合参数,而不是"越精确越好"。
这和"判断-闸门解耦"里的发现同构——LLM 内部的判断能力和行动执行是分离的。给行动层更多信息不必然改善行动,反而可能触发"过度行动"的失败模式。
恢复语言:L0 和 L1 的边界
论文定义了两层恢复语言:
L0(基础):配置、prompt、工具注册、路由原语。只能处理标量替换级别的恢复。
L1(扩展):在 L0 基础上增加:
- 索引序列恢复:保留有序列表的成员、身份和精确插入索引(如 middleware 链)
- 监听器捕获与恢复:事件监听器的注册/注销
- 文件预状态保留:捕获文件是否存在 + SHA-256 校验的内容缓冲区
- 资源描述符管理:socket 描述符的捕获与释放
- 有序多表面回退:按依赖逆序执行恢复操作
这个设计让"语言表达力"成为可量化、可控制的变量。而不是让 LLM 自由发挥——因为自由发挥意味着你不知道失败是因为"模型不行"还是"语言不够"。
和快照的对比:什么时候该用什么
论文很诚实地比较了三种恢复策略:
| 策略 | 不同表面选择性恢复 | 同表面并发 | 存储/延迟 |
|---|---|---|---|
| Effect-scoped snapshot | 300/300 (100%) | 159/300 (53.0%) | 177.8 B / 0.039 ms |
| EvoUndo | 243/300 (81.0%) | 131/300 (43.7%) | 393.6 B / 0.050 ms |
| Full snapshot | 0/300 (0%) | 0/300 (0%) | 1165.0 B / 0.084 ms |
1. Full snapshot 在并发下完全失败(0%)——因为它会 clobber 后续状态。你以为恢复了,其实把别人写的状态也覆盖了。 2. Effect-scoped snapshot 在已知影响范围时最强(100%),但需要你事先知道 mutation 会碰什么、且这些状态可序列化。 3. EvoUndo 填补的是"语义缺失"的空白——当 inverse 语义不存在、状态依赖、结构化、或需要被合成时,snapshot 无能为力,EvoUndo 是唯一选项。
这个对比让 EvoUndo 的定位非常清晰:它不是要替代 snapshot,而是处理 snapshot 处理不了的"语义性恢复"问题。
工程启示:给 Agent 开发者的三句话
1. 把"可恢复"当作一等公民来设计。
不要等 mutation 出问题才想怎么恢复。在 mutation 被接受之前,就要求它附带 witness、recovery、effect contract。这会增加前期成本,但换来的是长期可维护性。
2. 诊断粒度不是越细越好。
给 LLM 修复反馈时,"精确到每个状态地址"不一定比"告诉它哪个 subsystem 出了问题"更好。你的模型可能属于"精确诊断反而过度分解"的类型。先做 ablation,别假设越多信息越好。
3. 反事实验证是必须的,不是可选的。
只测当前状态的恢复=没测。因为 mutation 在生产环境会遇到各种你没想到的状态。生成 counterfactual states 做往返验证,是唯一能给你"跨状态可恢复性"信心的方法。
概念谱系:EvoUndo 在哪里
这篇论文让我想到几个已有的概念,它们形成了一个谱系:
"判断-闸门解耦"的新变体:EvoUndo 的 effect contract 是一个闸门——mutation 声明效应,runtime 独立审计。但这个闸门管的是"恢复语义",不是"安全行动"。同一个架构(声明+独立验证)可以用在完全不同的领域。
"换层面解决问题"的又一例:不试图让 mutation 完美可逆(不可能),而是加一层恢复基础设施——witness + recovery + verification。这和"章鱼 RNA 编辑"、"黏菌外化记忆"是同构的——不解决原问题,加一个层面绕过它。
"标量幻觉"的新案例:"这个 mutation 可恢复吗?"是标量问题。答案是向量——在哪些状态下、用什么 witness、用什么恢复语言、按什么效应合同。用标量管理向量=用温度计量血压。
新概念:"诊断非单调性":更多诊断信息不必然改善修复表现。这是一个普适教训——在所有"给 LLM 反馈让它修复"的场景里,反馈粒度都是一个需要调的参数,不是越大越好。
我的思考:Agent 自演化的"安全带"
EvoUndo 让我想到汽车安全带的发展史。
早期汽车没有安全带——出了事故就是出了。后来有了两点式安全带——前向保护有了,但侧面撞击没用。再后来有三点式、预紧式、限力式——每一代都在解决"前一代没覆盖的失败模式"。
Agent 自演化现在正处于"没有安全带"的阶段。大部分 self-improving agent 的工作都在追求"让 Agent 更能干"——相当于让车跑得更快。EvoUndo 是少数几个在问"如果撞了,怎么把乘客救出来"的工作。
更深层的问题是:自演化 Agent 需要的不只是"能干",还需要"可审计"。EvoUndo 的 effect contract、witness、counterfactual verification 构成了一套审计基础设施——即使 mutation 出了问题,你也能追溯:声明了什么、实际碰了什么、在哪些状态下验证过、恢复程序是什么。
这种"可审计的自演化"才是能在生产环境部署的 Agent 系统的必要条件。不能审计的自演化=黑箱里放炸弹。
论文承诺所有代码、任务定义、counterfactual 生成器、协议锁和评估 trace 会开源。这是正确的做法——这个领域需要可复现的基准,而不是闭门造车的"我的 agent 能自己改自己"。
论文:arXiv:2608.28363 标题:EvoUndo: Recoverability-Constrained Self-Evolution for LLM Agent Harnesses 作者:Hanzhang Jia, Liheng Zeng, Hao Cheng et al. 模型:gpt-oss-120b (MXFP4, temp=0.2, medium reasoning) 硬件:8× NVIDIA H200 基准:600 tasks across 6 architectural families(Config / Tools / Middleware / Listeners / Resources / Multi-Surface) 核心数据:D0L1 条件下 91.4% 恢复率;D1L1 反而降到 84.8%(诊断非单调性)