Skip to main content
一批评估共用同一套环境准备(同一个工具链、同一个仓库 checkout)时,每条 Attempt 都新建 Sandbox 意味着这套准备要付 N 次。在 Experiment 里声明 sandboxReuse: true, 多条 Attempt 依次共用同一个 Sandbox:Sandbox 创建和公共准备每个 Sandbox 只付一次, 每道题之间 NiceEval 自动把工作目录 git reset 回公共准备完成时的状态。
本页讲 Experiment 级的 sandboxReuse。只有几组兼容的评估需要各自的复用边界时,使用 评估组:同组串行复用,不同组继续并行。选中评估组的 Experiment 不能再声明 sandboxReuse: true
题间 reset 不是整台 Sandbox 归零。NiceEval 只按分类账恢复 workdir/opt$HOME/tmp 等 workdir 外状态、全局安装、包缓存和后台进程会保留。大型持久 build/cache 由作者负责选择容量上限、阈值告警、清理或轮换策略。无法安全继续时还要有退休 Sandbox 的策略。
先说清代价,再说怎么开:
  • 复用运行的结果照常进缓存。 pair 具备稳定 carry identity 时,终态结果指纹匹配就直接 沿用,不创建 Sandbox。Sandbox Plugin 的 attachment owner、name、instance key、behavior revision、声明 identity、顺序与 setup/teardown 形状都属于 carry identity;任一项变化都会 让 slot 变为 fresh。Callback 函数体仍是 opaque;行为变化时需同步修改声明 identity 或使用 --rerun all。只有未沿用的 Attempt 才会在本次共用 Sandbox 中执行。
  • 中断不等于外部状态回滚。 只在跨 Attempt 状态能回到最后一个终态提交边界时,续跑才是 同一条实验轨迹。否则应换干净 cohort 从头重建。
  • 工作目录之外的状态会留下。 $HOME/tmp、全局安装、后台进程都活过题间重置。 评估的准备代码必须能接受这一点(下面「把准备代码写对」)。
  • --keep-sandboxlocalSandbox() 互斥。
适合的场景:本地冒烟一批评估、同一道题重复跑 N 次看稳定性、验证接线——要的是快, 同时保留结果沿用与 --rerun 的选择。

开启:Experiment 三件套

timeoutMslifetimeMs 是两个时钟,量的是两个对象:前者限一条 Attempt 跑多久, 后者限一个 Sandbox 活多久。想让 Sandbox 活得久一点,调 lifetimeMs, 不要调大 timeoutMs——那会同时放宽对卡死 Agent 的保护。lifetimeMs 的上限 由 Provider 账号档位决定(例如 e2b 免费档是 1 小时),超了会在创建时报 Provider 的原话。

多开终端时 Sandbox 不共享

Sandbox 复用只发生在一次 Invocation 里。两个终端同时跑一个实验时,两边有各自的 Run 和 Sandbox 池。NiceEval 不把运行中的 Sandbox handle 交给另一个进程。同一个 Record root 同时只允许一项操作;第二个终端指向同一个 root 会立即得到 record-root-busy,不会和第一个终端并行写入。 确实需要并行时,给两个终端不同的 Record root:
两个 root 各自形成完整结果,NiceEval 不自动合并它们。 如果 Sandbox 只保留自己的临时状态,两边可以同时跑不同评估用例。两边都会在 Sandbox setup() 恢复同一 checkpoint,并在 teardown() 回存时,再给实验声明稳定的非密 key:
NiceEval 会在创建 Sandbox 前独占这个 key,到 Sandbox teardown、Provider finalizer 和实验 teardown 后才释放。等待方不创建 Sandbox。租约释放后,等待方继续自己的既定计划;它不读取或沿用另一 Record 的结果。 key 会进入结果的配置身份。换 key 表示换了状态 cohort,旧结果不会混入。这个租约只按 sharedState.key 保护外部 checkpoint,不替代 Record root 的独占规则。不同机器或工作副本需要外部分布式互斥。

生命周期:谁跑几次

每条 Attempt 结束后,NiceEval 对 workdir 执行 git reset --hard + git clean 回到重置点,再开始下一条。这个 reset 只管 workdir/opt$HOME/tmp、 全局安装、包缓存和后台进程不会因此消失。它们要么由评估用例的 teardown 自己收, 要么就是作者明确留给下一条用的持久状态。 大型持久 build/cache 不能只依赖「一直增长」。作者应明确容量上限和达到上限前的阈值。 正常大小和命中情况用 facts 记录,达到风险阈值才用 diagnostic 告警,并提供清理、 轮换或退休 Sandbox 的策略。

把准备代码写对:按「随什么变化」分层

  • 所有实验都要的重依赖(Agent CLI、语言运行时)→ 烘进 Provider 的 image / template / snapshot,不进任何 setup
  • 整批评估共用的准备(装工具链、clone 共同仓库、预热构建缓存)→ Sandbox 级 .setup()。它每个 Sandbox 只跑一次,产物成为题间重置点的一部分,每道题白拿。
  • 只有这道题要的素材(它自己的仓库、数据、依赖)→ 评估用例自己的 setuptest(t)。每题重置后重放,所以必须是重放一遍还对的代码。每题各自 clone 时, 临时 clone 目录整个实验统一用一个名字、加进 diff.ignore,clone 前先 rm -rf .git <临时目录> 清掉上一题的残留——.git 会活过题间重置。
  • 起了后台进程、占了端口 → 评估用例的 teardown 自己收,题间重置不杀进程。

幂等是硬要求:一反一正

Sandbox 级和评估用例级的准备代码都可能面对「上一次留下的半截状态」。两种写法:
判据一句话:准备代码描述目标状态,不描述「要不要做」。探测-跳过式的守卫 把半截状态当成完成态,恰好只在复用时炸,本地单跑永远发现不了。

Agent 原生 Plugin:安装由 Adapter 收敛

codexAgent({ plugins: [...] })claudeCodeAgent({ plugins: [...] })sandboxReuse 可以同时声明。Agent 原生 Plugin 装在 $HOME 里,$HOME 活过题间重置, 但这不用你处理:每条 Attempt 开始前,Adapter 把 Plugin 安装收敛到你声明的配置—— 上一条 Attempt 留下的同名 marketplace 注册和插件,被替换成按声明 sourceref 的全新安装。插件自带脚本改写 marketplace 注册(比如换成托管源)也被同一条规则吸收。 这里说的是 Agent factory 里的原生 Plugin。评估用例、实验和评估组顶层的 plugins 字段 承载 NiceEval 条件,两者的分工见用 Plugin 复用完整评估条件 两件事仍归你:
  • postSetup 脚本必须幂等。它每条 Attempt 都在残留的 $HOME 上重跑, 往全局配置登记 hook 的脚本,重跑一遍要收敛到同一份配置。开复用前先拿两三道题 压一条 Sandbox 泳道跑一轮——第二题起才是真正的判据。
  • 插件数据放哪。插件安装目录每条 Attempt 都被重装覆盖。插件运行时要留到下一题的数据, 只能写在安装目录之外。留下的数据会不会污染下一题,是你的实验设计要回答的问题。
收敛不等于省钱:插件安装每条 Attempt 照付,复用省的是 Sandbox 创建和 Sandbox 级 setup。marketplace 拉取太慢时用 sparse 只拉插件路径,或把插件烘进 template 并从 plugins 里拿掉声明——代价是安装 manifest 里就没有插件与解析版本的记录了。

一批里混了不同环境

选中的评估用例用 environment 解析到不同预制产物时,不需要拆命令。Runner 按解析后的 环境 profile 分组:同一个 Sandbox 只承接同组 Attempt,每组建立自己的题间重置点, 组之间不共享 Sandbox 也不共享 Sandbox 级 setup 的产物。实验的 maxConcurrency 约束的是所有组加起来的同时执行数。

典型节奏

第 3 步必须换实验:--keep-sandboxsandboxReuse 互斥,而且复用的 Sandbox 被整批共享, 留下来对任何一道题都不忠实。「串起来挂、单独跑过」出现时同样这么办——不要在共用批次里 排查这种问题。流程见保留 Sandbox 现场排查问题

做不到?用并发,别复用

准备代码改不成幂等、或者评估依赖全新的 $HOME(记忆类被测对象、有状态服务), 不要声明 sandboxReuse。提速走并发就够了:
复用比纯并发多省的只有「每个 Sandbox 一次的创建 + 公共准备」这一段。这段时间 远小于单条 Attempt 的执行时间时,并发已经拿到几乎全部墙钟收益,还不用背复用的 状态语义。先并发,量出公共准备确实是大头,再考虑复用。

相关阅读