Skip to main content
每个 Attempt 都从同一个起点开始。系统包、Agent CLI、编译好的二进制、大模型缓存这类内容稳定、体积大,每次都一样,与其每个 Attempt 重装一遍,不如在跑评估之前做进预制环境,让 Attempt 直接从装好的环境启动。 三种 Provider 引用预制环境的写法一致:
NiceEval 只读取构建结果的 ID,不提供跨 Provider 的构建命令。Docker 镜像、Vercel 快照和 E2B template 的凭据、构建方式、发布和过期规则各不相同,构建脚本用 Provider 自己的官方工具维护,最后把 ID 或名字写进 Experiment。

不预制也能跑

预制只是为了更快。Adapter 和 Sandbox 各管一边:Sandbox 配置决定用哪个 Provider、从哪个预制环境启动;Adapter 检查需要的 CLI 在不在。Claude Code 和 Codex 缺少 CLI 时会在运行时安装:安装包在评估网络之外准备好,再通过文件接口送进 Sandbox。
  • Codex 送进 Sandbox 的是自带运行时的原生安装包,镜像里没有 Node 也能装。
  • Claude Code 要求镜像里有 npm。
  • 默认的 Bub 0.4.0 除了 OTel 插件,还固定了 any-llm-sdk==1.17.0 和 openai==2.31.0。Bub 版本、这两个依赖、OTel 插件或 Python 插件列表任何一项变了,都会重新安装,不能只凭 command -v bub 判断已经装好。用旧安装标记做的预制环境会重装一次。你显式选了其它 Bub 版本时,它依赖的其它包由你自己负责。

从官方基线继续构建

三个内置 Provider 都可以从官方基线继续构建,不用从空白环境装 Agent。预制环境只在依赖变化时构建一次,每次构建换一个带版本号的名字。不要把构建放进每个 Attempt 都会执行的动态回调。

E2B:从官方与公共模板派生

E2B 官方提供 Claude Code 的 claude template 和 Codex 的 codex template。NiceEval 提供一个 E2B 专用的薄封装,让你从这两个官方起点继续用 E2B 原生 API 构建。E2B 没有官方的 Bub template,所以 Bub 分支和 bubAgent() 使用同一套固定依赖和 OTel 插件。 NiceEval 还发布了三份任何 E2B Team 都能引用的公共模板。完整的名字和版本由 NiceEval 维护,直接用常量:
这些模板都实际启动验证过:Claude Code 2.1.207、Codex 0.144.1、Bub 0.4.0。模板版本号跟着里面的 Agent 走,形如 0.144.1-r1,即 Agent 版本加 NiceEval 的构建修订号。三个 Agent 各自独立发版,和 NiceEval 库的版本无关。所以项目里直接用常量,不要复制这些字符串,也不用自己跟踪版本。派生模板需要记下基础模板时,同样用常量的值。 这批公共模板里,Claude Code 模板沿用 E2B 官方的 /usr npm prefix,普通用户不能直接写全局模块。运行时要再装 pnpm、yarn 等全局 Node 工具时,显式装到 /usr/local,两个模板都已经把它加进 PATH:
不要改用 sudo npm install -g,模板里已有的 root 全局包可能产生文件冲突。NiceEval 会在之后发布的模板里统一这个默认 prefix;在发布说明明确新模板已经处理之前,继续保留 --prefix /usr/local。 从官方 Codex template 派生自己的模板:
scripts/build-e2b-template.ts
然后在 Experiment 里只引用构建结果:
也可以直接从 NiceEval 公共模板继续派生,只为项目自己的依赖付构建时间:
e2bCodingAgentTemplate("claude-code" | "codex" | "bub") 返回 E2B 原生的 TemplateBuilder,可以继续用 .aptInstall()、.runCmd()、.copy() 等 E2B 能力。构建出的 alias 把官方起点和项目依赖固定在同一个可复现的结果里。依赖变了就重新构建,换一个带版本号的 alias。 Bub Adapter 配了 pythonPlugins 时,构建模板时要把同一组包传给 factory。这样运行时才会识别出插件已经预装:

Docker:使用 NiceEval 维护的镜像,或从官方 node 基础镜像派生

要直接运行 NiceEval 内置的 claude-code、codex 或 bub Adapter,可以用对应的公开镜像:niceeval/claude-code、niceeval/codex 或 niceeval/bub。 每个镜像只装自己的 Agent CLI,同时提供 linux/amd64 和 linux/arm64 版本。tag 和对应的 E2B 公共模板同号,版本号是镜像里 Agent 的版本。稳定的 CI 用具名常量或 digest,不要用会变的 latest:
这些是 NiceEval 维护的公开镜像,不是 Docker 的 library/* 官方镜像。Agent 版本或构建方式变化时才发布新 tag,和 NiceEval 库的发版节奏无关。 只需要一个 Agent,或者还要加项目自己的依赖时,写 Dockerfile 从 Docker 官方的 node:24-slim 派生:
Dockerfile
然后在 Experiment 里只引用构建结果:
Docker Sandbox 用镜像自己声明的用户运行命令。上面的 Dockerfile 没写 USER,所以默认以 root 运行,/usr/local/bin 已经在 root 的 PATH 上,npm install -g 装的命令可以直接用。 需要非 root 用户时(比如 Claude Code 在 root 下会拒绝 --dangerously-skip-permissions),在 Dockerfile 里加一行 USER node,node:24-slim 自带这个用户。也可以用 dockerSandbox({ source: { type: "image", image }, user: "node" }) 覆盖。 Agent 装到别处时(比如 ~/.local/bin),记得把那个目录加进 PATH。dockerSandbox 必须写明 image,稳定的 CI 引用不会变的 tag。

Vercel:从官方 runtime 拍快照

Vercel 没有 E2B 那样的 template 仓库,也不用 Dockerfile。Vercel 的快照是从一台运行中的 microVM 拍下来的:用 Vercel SDK 从官方 runtime(node24)启动一台 Sandbox,装好 Agent CLI,调用 .snapshot() 拿到 snap_...,再交给 vercelSandbox({ snapshotId }):
scripts/build-vercel-snapshot.ts
然后在 Experiment 里引用打印出的 ID:
Vercel 快照不能像 E2B 那样公开发布。快照 ID 受创建它的 Team/Project 权限控制,同一个项目的成员可以复用,其他人要在自己的 Vercel Project 里重新拍一份。NiceEval 项目自己验证过、永不过期的快照是 snap_7sIjfs71xfmVly0WEUTGhTBoMGeL,但它不能跨账号使用。

运行时 checkpoint

createCheckpoint() / restoreCheckpoint() 解决的是另一件事:把 Sandbox 里指定的 Linux 路径打包成 Buffer,再恢复到另一台已经创建好的 Sandbox:
它适合运行时的缓存,不会产生可发布的 image、template 或快照,也不管共享、版本和过期。打包或恢复失败会直接抛错。 要让这些目录在 Attempt 之间延续,在动态 .before() 里调用 restoreCheckpoint()。恢复成功后,用 context.onCleanup() 登记 createCheckpoint(),这样回存会在 Sandbox 销毁之前完成。 sandboxReuse: true 只在一次 niceeval exp 命令内部保留同一台 Sandbox。几个终端同时运行、都要读写同一份 checkpoint 时,在 Experiment 顶层声明 sharedState: { key }。NiceEval 在 Experiment setup 和创建 Sandbox 之前取得这个 key 的租约,等 checkpoint 回存、Sandbox 销毁和 Experiment teardown 都完成后才释放。它只保证同一时刻只有一方在用,不负责保存 checkpoint、出错回滚或跨机器协调。