sandboxReuse: true,多条 Attempt 会依次共用同一个 Sandbox。每个 Sandbox 只创建一次,题与题之间 NiceEval 会把工作目录恢复到起始状态。每条 Attempt 仍然真实执行,也仍然有自己的 .before() / .after() 准备和收尾。没变的 Docker 准备步骤会直接复用,不重新执行。
本页讲 Experiment 级的
sandboxReuse。只有几组兼容的评估需要各自的复用边界时,使用
评估组:同组串行复用,不同组继续并行。选中评估组的
Experiment 不能再声明 sandboxReuse: true。- 沿用上次结果照常生效。 一道题的配置和上次相同、上次已有
passed或failed时,直接沿用旧结果,不为它创建 Sandbox。只有这次真正要跑的 Attempt 才会进入共用的 Sandbox。 - NiceEval 看不到回调函数体的变化。 Sandbox Plugin 的名字、实例、
behaviorRevision、声明的identity和准备步骤变了,旧结果会失效、重新跑。只改了回调里的代码时,同步修改behaviorRevision或identity,或用--rerun all。 - 中断不会回滚外部状态。 只有跨 Attempt 共享的状态能恢复到最后一个完成的 Attempt 之后,续跑才和一口气跑完等价。做不到时,换一份干净的状态从头跑。
- 工作目录之外的状态会留下。
$HOME、/tmp、全局安装、后台进程都会留到下一道题。准备代码必须能接受这一点,写法见下面「把准备代码写对」。 - 不能和
--keep-sandbox一起用。
--rerun。
开启:在 Experiment 里写三个字段
timeoutMs 和 lifetimeMs 是两个时钟:前者限一个 Attempt 跑多久,后者限一个 Sandbox 活多久。想让 Sandbox 活得久一点,调 lifetimeMs。不要调大 timeoutMs,那会同时放宽对卡死 Agent 的保护。
lifetimeMs 的上限由 Provider 账号档位决定,例如 E2B 免费档是 1 小时。超过上限时,创建 Sandbox 会报出 Provider 的原始错误。
运行时用 pnpm exec niceeval exp <experiment>。跑完后用 pnpm exec niceeval show 在终端查看结果,或用 pnpm exec niceeval view 在浏览器里查看。
多开终端时 Sandbox 不共享
Sandbox 只在一次niceeval exp 命令内部复用。两个终端同时跑同一个实验时,各有各的 Run 和 Sandbox,NiceEval 不会把一个进程里正在运行的 Sandbox 交给另一个进程。两边都可以把结果写进同一个项目的 .niceeval/record.sqlite,不需要为此拆开结果文件。
要复制或归档 .niceeval/record.sqlite,等命令成功结束后再做。运行中的 SQLite 文件不能当作可搬运的快照。
如果每个 Sandbox 只保留自己的临时状态,两边可以同时跑不同的评估用例。动态 .before() 回调要恢复、回存同一份外部 checkpoint 时,给 Experiment 声明一个稳定、不含秘密的 key:
setup 和创建 Sandbox 之前取得这个 key 的租约,等 Sandbox 清理、Provider 收尾和 Experiment teardown 全部完成后才释放。另一个终端里用同一个 key 的运行会排队等待,不会提前创建 Sandbox。拿到租约后,它照自己的计划继续跑,不读取或沿用对方的结果。
key 也是结果配置的一部分。换一个 key 就是换一份状态,旧结果不会混进来。租约只保护外部 checkpoint,和结果文件无关;不用 sharedState 的运行照样可以并发写入结果。不同机器或不同 working copy 之间仍需要外部分布式锁。
生命周期:谁跑几次
每个 Attempt 结束后,NiceEval 对
workdir 执行 git reset --hard 和 git clean,恢复到起始状态,再开始下一个。这一步只管 workdir,/opt、$HOME、/tmp、全局安装、包缓存和后台进程都不会消失。它们要么由评估用例的 teardown 自己清理,要么是你有意留给下一道题用的状态。
大型的持久构建产物或缓存不能放任增长。给它定一个容量上限和提前告警的阈值:正常的大小和命中情况用 facts 记录,接近风险阈值时用 diagnostic 告警,同时准备好清理、轮换或换掉这台 Sandbox 的办法。
把准备代码写对:按「随什么变化」分层
- 所有实验都要的重依赖(Agent CLI、语言运行时):烘进 Provider 的 image / template / snapshot,或写成最低频的
.before()准备步骤。 - 整批评估共用的准备(装工具链、clone 公共仓库、预热构建缓存):写成低频的
.before()准备步骤。在 Docker 上,没变的准备步骤下次运行可以直接沿用。这些步骤仍然每个 Attempt 都要经过一次,你不能要求它在整台 Sandbox 上只跑一次。 - 只有这道题要的素材(它自己的仓库、数据、依赖):放进评估用例自己的
setup或test(t)。每道题重置后都会再跑一遍,所以必须是重复执行也正确的代码。 - 起了后台进程、占了端口:由评估用例的
teardown自己清理,重置workdir不会杀进程。
.niceeval-task-clone,并把它加进 diff.ignore。clone 前只运行 rm -rf .niceeval-task-clone,清掉上一道题的残留。绝不要删除 workdir 根目录的 .git:题间重置依赖它,而且它会一直留着。
幂等是硬要求:一反一正
Sandbox 级和评估用例级的准备代码,都可能遇到上一次留下的半截状态。对比两种写法:Agent 原生 Plugin:安装由 Adapter 处理
codexAgent({ plugins: [...] })、claudeCodeAgent({ plugins: [...] }) 可以和 sandboxReuse 一起用。Agent 原生 Plugin 装在 $HOME 里,会留到下一道题,但这不用你处理:每个 Attempt 开始前,Adapter 都按你声明的 source 和 ref 重新安装。上一道题留下的同名 marketplace 注册和插件会被替换掉。插件自带脚本改写了 marketplace 注册(比如换成托管源)时,也按同样的方式恢复。
这里说的是 Agent factory 里的原生 Plugin。评估用例、实验和评估组顶层的 plugins 字段是 NiceEval 的 Plugin,两者的分工见用 Plugin 复用完整评估条件。
两件事仍归你:
postSetup脚本必须可以重复执行。它每个 Attempt 都会在留下来的$HOME上再跑一遍。往全局配置登记 hook 的脚本,跑多少遍都要得到同一份配置。开复用前,先拿两三道题在一条 Sandbox 泳道上跑一轮,从第二道题开始才能看出问题。- 插件数据放在哪。插件安装目录每个 Attempt 都会被重装覆盖。要留到下一道题的数据,只能写在安装目录之外。留下的数据会不会影响下一道题,是你的实验设计要回答的问题。
.before() 准备步骤在 Docker 上还能沿用。marketplace 拉取太慢时,用 sparse 只拉插件路径,或者把插件烘进 template,并从 plugins 里去掉声明。代价是安装清单里不再记录插件和它解析到的版本。
一批里混了不同的 Sandbox 配置
选中的评估用例各自声明了不同的template 时,不需要拆成几条命令。NiceEval 逐个检查评估用例和 Experiment 的组合。Sandbox 配置不同的 Attempt 不会共用一台 Sandbox,也不会共用正在运行的 Sandbox。实验的 maxConcurrency 限制所有选中的题同时运行的总数。相同的准备步骤仍可以各自沿用 Docker 上的准备结果。
典型节奏
--keep-sandbox 不能和 sandboxReuse 一起用,而且共用的 Sandbox 被整批题用过,留下来的现场对任何一道题都不准确。遇到“一起跑会失败、单独跑能过”时也这么办,不要在共用 Sandbox 的批次里排查。完整流程见保留 Sandbox 现场排查问题。
做不到幂等时,用并发,不要复用
准备代码改不成可以重复执行的样子,或者评估依赖全新的$HOME(例如被测对象自带记忆、有状态服务)时,不要声明 sandboxReuse。提速靠并发就够了:
相关阅读
- 评估组——显式列出组内成员,同组共用 Sandbox,组间照常并行。
- 重跑与沿用——复用 Sandbox 时怎样沿用上次结果,以及何时使用
--rerun。 - 并发与执行顺序——不复用时怎样用并发得到同样的提速。
- Sandbox Provider 配置——各 Provider 的 template / image / snapshot 怎么做。