Skip to main content
Experiment 是可签入的运行配置:同一批评估用例对着哪个 adapter、哪个模型、开哪些 flags、跑几次、预算多少,都写在 experiments/ 里。CLI 的位置参数只负责筛评估用例,不负责临时改 agent 或运行配置。

最小实验

agent 放的是已经配置好的 agent 实例。被测系统的 URL、鉴权、协议细节通常传给 adapter 工厂。运行器不会单独保存一个 agentConfig 字段。

评估不同的 System Prompt 对于 Agent 的影响

使用 flag 机制,配置不同的 Flag,设置两个实验,对比不同 Prompt 的区别。两格之间只差一个变量(这里是 flags.promptVariant),其余字段逐字相同——差别越少,分数差能归因到的东西越明确:
两格写成同一个 flags(或模型对比里两格钉同一个 model,包括两格都写成同一个环境变量默认值)是最常见的失误:跑得完、报告也出得来,但两列数字比的是同一套配置,看不出任何东西。 model 会作为 ctx.model 传给 Adapter。如果你的 Agent 支持模型选择,自己构建请求 flags 会作为 ctx.flags 传给 Adapter,也会作为 t.flags 出现在评估用例里。 语义就是产品 A/B 测试里的 feature flag,应该编写 Adapter 把 Flag 发给你的 Agent,Agent 根据 Flag 切换不同的 System Prompt 或者行为。

编写一组实验

一个实验文件是一格配置。要比较多个模型、agent 或 flag 取值,就写多个文件。目录只负责 id:
报告直接比较当前 Scope 中的这些 experiments,不需要额外分组字段。 只想验证某一个配置时,把位置参数写成该配置的完整 id:
用于逐配置排查——确认某一格改动是否达标,不用先跑完整组,也不用把其它配置文件挪出目录。

常用字段

记下这轮连的是哪个地址,又不让它作废缓存

flags 写的是实验条件,它整袋参与缓存判定:值一变,已经跑完的结果就要重跑。所以隧道地址不要写进 flags——隧道每次重启换一个 URL,写进去就等于每次重启都作废全部已完成结果。 这个地址是 setup 跑起来才拿到的,用 ctx.fact() 把它记进结果:
换了 URL 再跑,已完成的照常复用,只跑还缺的:
记录一样不少:niceeval showfacts: 行会列出这条 Attempt 连的地址,报告也能按它分组。复用进来的结果带的是它当初那一轮的地址,不会被这一轮的新 URL 顶替。 服务端版本号则相反:写成 flags: { memoryVersion: "0.10.39" },换了版本行为可能真的不一样,那种变化就该让结果重跑。判据是谁写下这个值——你在实验里声明的是条件,写 flags。跑起来才知道的是观测,用 ctx.fact()。完整 fact document 不能超过 65,536 UTF-8 bytes;大内容要用专用 Attempt channel 与 blob。Adapter 和评估用例都不需要读、只给报告分组用的标注写 labels

启动 Experiment 共享服务

有些资源是「一个实验一份、所有 Attempt 共用」的:一条到内网记忆服务的隧道、一个实验专用的 mock server、一个 license 租约。这类资源写进一对实验级 Hook setup / teardown:整场至多各跑一次。setup 在这个实验第一个要派发的 Attempt 前执行。teardown 在全部 Attempt 收尾后执行(运行被中断也执行),当且仅当 setup 的时点已经走到才触发——setup 抛错同样要走到 teardown,收尾代码要对可能未赋值的变量做防御。上一次的结果全部被复用、这个实验一个 Attempt 都不需要真正运行时,setupteardown 都不会执行:
setup 在跑的时候,终端的 ACTIVE 区会显示一行 experiment setup · <实验 id>ctx.progress(...) 的消息就更新在这一行末尾。等它的 Attempt 计入排队数,这不是卡住。在 CI 或 agent 输出里,setup / teardown 的开始和结束各追加一行。 setup 抛错时,这个实验的每条 Attempt 都记为 errored(错误码 experiment-setup-failed)、逐条进报告。同一批的其它实验照常跑——环境起不来不该伪装成绿,也不该连坐别人。 teardown 里资源释放是必达底线:用 try/finally 包住,不管前面的观测代码是否出错都要执行。观测类动作(health 检查、指标上报)只是 best-effort——给它自己的短超时、失败不要拦住释放,并在 ctx.signal.aborted 时直接跳过。中断路径上,一次可能挂起的观测不该挡在「拆隧道、退租约」前面:
setup / teardown 只管「在你机器上、一个实验一份」的服务。要在跑 agent 前按实验在Sandbox 里准备环境(装二进制、预热、跨 attempt 载入和回存状态),挂在 sandbox 字段的 spec 上:
固定的 Agent CLI、系统包和大模型缓存应先做进 image/template/snapshot。.setup() 不应在每个 Attempt 重建同一套环境。从官方 Docker 镜像、E2B 模板或 Vercel runtime 派生预制环境的步骤见 Sandbox Provider · 从官方基线继续构建以提速 Hook 的执行时机、多 Hook 顺序和失败语义见 Sandbox provider · 生命周期

与 Sandbox Hook 协作

实验级 Hook 起宿主机侧服务,Sandbox Hook 每个 Sandbox 把坐标写进去、收尾时回存状态——两层在同一个文件里靠模块变量衔接,时序由 runner 保证:实验级 setup 早于本实验任何 Sandbox Hook,Sandbox Hook 读到的变量一定已经赋好值:
一份实验文件从上往下读就是完整的运行说明:整场一次的宿主机资源在实验级 Hook 对里。每 Sandbox 的写入与回存在 sandbox 链式 Hook 里,读实验级产物。agent 怎么连自己、评估用例的Fixture各在 agent 定义与 EvalDef 里,不进实验文件。

多个实验共享同一套生命周期代码

对比组里常常是几个实验对着同一类基础设施——同一个记忆产品,claude 与 codex 各一格对照,起停机制完全一样。把起停写成一个工厂函数,返回共享同一闭包的整套件:实验级 Hook 对、给 agent / MCP 工厂读坐标的 getter、把坐标写进 Sandbox 的 sandbox Hook。每个实验文件各自调用一次工厂,同一套代码、各自的实例与坐标:
实验文件里换 agent 只换 agent 那几行,生命周期四行接完:
两条纪律保证多个实验并发跑同一套代码不互相踩踏:
  • 工厂在 import 期只创建闭包,不做 I/O、不读配置——实验文件在 niceeval exp 的发现阶段就会被 import,import 抛错会连累同批无关实验。所有硬失败留给 setup
  • 运行时坐标活在工厂闭包里,不放模块级单例——同批并行的两个实验各持一份,互不覆写。坐标在 setup 之后才存在。
服务起多份太贵、必须让同批实验共享一份实例时,用「首进启动、末出关停」的引用计数代替按实验各建一份:
计数能保持平衡,靠的是成对触发规则本身:teardown 当且仅当同层 setup 时点走到过才执行、setup 抛错也照样配对触发 teardownrefs 不会泄漏。 边界在生命周期:同批共享的服务活不过这次 run。要跨 run 存在的服务(先起好、连续跑多次 niceeval exp)仍归外部编排(docker compose 之类)起停,URL 经环境变量传入。

让不同评估用例使用不同预制环境

一批真实任务可能需要不同版本的运行时和依赖。把具体的 template-bearing factory 直接放在每条 Eval 上,让任务与执行环境保持为同一份声明:
没有 profile registry,也没有按 source kind 登记的 materializer 表。每个 factory 同时拥有支持声明与实现,physical planning 会在创建任何 Sandbox 前校验所有选中 Eval。重复 template 用普通 TypeScript 辅助函数 共享。同一个 Experiment 仍能覆盖全部 Eval,link planning 会逐条把 Eval layer 与 Experiment layer 配对。 跨配置比较的设计建议见实验矩阵。Adapter 对 ctx.modelctx.flags 的用法见 Adapter