这篇《Returning to Development States: When Full Sandbox Snapshots Help Coding Agents》把编码 agent 的失败恢复重新当搜索问题摆了一遍。结论先给:主流的从头重试是在浪费计算,diff 重建只补一半,而且基本上不是最关键的那半;完整沙箱快照恢复在跨服务长周期任务里有效得多,因为它救的不是代码,是代码之外的状态。
论文底细:Daniel Thi Graviet 和 Tanmay Shah,2026 年 10 月 9 号提交到 arXiv,编号 arXiv:2610.13841 v1,7 页 6 幅图,预印本,未经同行评审。主分类 cs.LG,交叉 cs.SE,ACM 标的是 I.2.6 和 I.2.2。我把它当方法提案看,不当定论看。实验在 Daytona 沙箱上跑,核心是 Go-Explore 风格搜索——先把搜索过程中遇到的有趣状态存下来,再跳回那个点继续探。思路不新,新的是这个组合被用到了编码 agent 的沙箱恢复上。
我把失败恢复拉成三种方案放在同一把尺子下,六个维度:仓库代码状态、运行时状态、跨服务覆盖、恢复成本、分支搜索友好度、实现复杂度。评分 1-5,5 最优。
| 恢复策略 | 仓库代码状态 | 运行时状态 | 跨服务覆盖 | 恢复成本(越低越好) | 分支搜索友好度 | 实现复杂度(越低越好) |
|---|---|---|---|---|---|---|
| 从头重试 | 1 | 1 | 1 | 1 | 1 | 1 |
| diff 重建 | 4 | 2 | 2 | 2 | 2 | 2 |
| 完整沙箱快照 | 5 | 5 | 5 | 4 | 5 | 3 |
几个分数得解释。diff 重建的仓库代码状态给 4,不是它重建不出来,是它依赖 diff 的完整性和顺序,diff 只要漏一条或者顺序错了,重建出来的代码就和当时对不上。运行时状态给 2 是往高了给,它最多靠 Dockerfile 或配置把静态环境再搭出来,但数据库改过的 schema、服务里累积的数据、队列里卡住的消息全都得丢。完整沙箱快照这三项给 5 没有悬念,它冻住的是整个沙箱——代码、db、服务、运行时全在。恢复成本给 4 是实打实的,存储快照要钱,恢复也要时间。这张表里没有赢家,只有 trade-off。
这篇论文真正值钱的是它把“仓库之外的状态”这条线画出来了。作者原话我不照搬,用我的话讲:更高的求解率主要取决于仓库之外那部分进展——数据库、各类服务、运行时状态。这些不在 diff 里,但决定 agent 失败后还能不能继续往前走。长周期任务里,代码写对只是第一步,把整个系统盘活才是任务本身。从头重试等于把盘活系统的过程全部抹掉,只留最后一次代码 diff。完整快照有效的原因很直白:它恢复的不只是代码,是那个快照时刻的系统状态。
最容易想反的地方在这。多数人以为 agent 失败是代码没写对,于是只盯着 diff。实际上长周期任务里的失败很多发生在代码之外:数据库 schema 改了一半、服务 A 已经跑了数据、服务 B 还没跟上、队列里卡了几条消息。这些状态一旦丢掉,光有代码 diff 也复现不出当时上下文。说具体点:agent 在改一个用户注册流程,改了 API、跑了数据库 migration、还往 Redis 队列里塞了验证消息。失败后,diff 能把 API 改动和 migration 重建出来,但 Redis 里那些消息回不来,数据库里测试数据的状态也不对。agent 重跑时看到的是“代码对了但队列空着、数据不齐”的错位状态,于是又在一堆半成品状态上反复踩坑。这就是 diff 重建最烦人的地方——它看起来恢复了,其实没有。
三种恢复策略长什么样,我用一段示意代码把核心差异摆出来,不是论文原始实现,能跑通的部分我贴出来,跑不通的地方我标注释。
# 策略一:从头重试
state = initial_state()
for attempt in range(max_attempts):
result = run_agent(state, task)
if result.solved:
return result
state = initial_state() # 所有机器状态全丢,包括 runtime
# 策略二:diff 重建
base = checkout_repo(base_commit)
for diff in saved_diffs:
apply_patch(base, diff)
state = base # 代码回来了,db、服务、队列里的状态一个没回来
# 策略三:完整沙箱快照
snapshot = take_sandbox_snapshot() # 冻住 db、服务、队列、缓存
for branch in interesting_states(snapshot):
state = restore_sandbox(branch)
result = run_agent(state, task, branch=True)
if result.solved:
return result
只看这段,完整沙箱快照多出来的那步不是魔法,是 take_sandbox_snapshot 和 restore_sandbox 这两个操作把运行时状态显式当成了搜索空间的一部分。从头重试把这个空间压成一个点;diff 重建把它压成一条线,只有代码路径;完整快照还保留着一个面,状态路径,所以能分支。论文把失败恢复当搜索问题,自然推到这里。Go-Explore 那套“记住有趣状态、从那里重新分支”在沙箱上特别顺,因为快照天然就是状态存档点。
下面这张示意图把三种策略在跨服务长周期任务上的差异抽象成指标对照。全部为示意数据,非真实测试结果——论文没有公开这么细的 benchmark,我不会硬编数字。读的时候看相对位置,别看绝对值。
| 恢复策略 | 求解率(示意) | 恢复后分支求解率(示意) | 恢复耗时 | 存储成本 |
|---|---|---|---|---|
| 从头重试 | 低 | 无分支 | 低 | 无 |
| diff 重建 | 中低 | 低 | 中 | 低 |
| 完整沙箱快照 | 高 | 高 | 中高 | 高 |
这张表唯一想说的是:完整沙箱快照的收益不在恢复耗时上,在求解率和分支求解率上。恢复耗时它反而是亏的,因为要拷贝状态、要起服务。只盯着恢复耗时,会得出快照不划算的结论——那是把账算错了。从头重试虽然便宜,但失败一次就丢掉全部上下文,长周期任务里反复重试的成本涨得比存储快照的固定开销快得多。我在上一篇横评里反复强调过:脱离任务形态谈恢复策略,是耍流氓对比。便宜是这件事最贵的地方。
四类典型场景,分发如下:
| 场景 | 首选策略 | 理由 |
|---|---|---|
| 单服务、纯代码改动的短任务 | 从头重试或 diff 重建 | 没有跨服务状态,快照开销不划算 |
| 跨服务、含数据库/缓存/队列状态的长任务 | 完整沙箱快照 | 仓库之外的状态才是求解率关键 |
| 失败前已有部分进展、且进展在代码之外 | 完整沙箱快照 + 分支搜索 | 从中间状态分支优于从头重启 |
| 快速原型、低失败成本的试错 | 从头重试 | 恢复成本最低,失败代价小 |
补一句公正补丁:没出现在推荐里不代表差,是在这几个典型场景下不匹配。diff 重建在单服务纯代码任务里不差,恢复快、存储省;从头重试在任务短、失败成本低时是合理默认。完整沙箱快照不是银弹——它只是在状态保真度这个维度上补上了关键一环,拿它跑单服务 CRUD 是杀鸡用牛刀。论文作者自己也这么说:当长周期任务跨越多个服务、且这些服务的状态无法仅凭单个 diff 重建时,完整快照搜索才真正划算。
这篇论文的结论我整体认,但几个地方保留。第一,实验只在 Daytona 这一个沙箱实现上跑过,Daytona 的原生快照能力是前提。换成自建 Docker 快照、或没有原生快照的沙箱,恢复成本可能完全翻倍,结论未必站得住。第二,论文没有把容器镜像恢复这个中间方案拉进来对照——它比 diff 强、比完整快照弱、成本低很多,很多团队就在这个灰色地带里凑合。这个缺口补上,结论会细得多。第三,预印本,7 页 6 幅图,没人审过。这几个保留不影响我认它大方向,但它的边界还没画清楚。等下一版数据多点我会重跑判断。盯着呢。
我的推荐很偏:长周期多服务任务,完整沙箱快照恢复当主线,其他是补充。还在拿从头重试当默认的人,先算笔账——你丢的不是时间,是 agent 已经跑出来的系统状态。那才是它真正完成工作的一半。
