数据截至 (上游 commit 8b292c9f1b14)
第 5 章 编排与评测 —— Env、预制多 Agent 环 境与 eval CLI
本章讲什么: 单个 rollout 往上那层——多个 agent 怎么被编进一局;四个预制 env 各自解决什么评测形态;以及跑评测、看输出、断点续跑的完整操作面。
1. Env:一局的导演
Env(verifiers/v1/env.py:73)的约定就一句话:run(task, agents) 用普通命令式 Python 编排这局的 agent 互动(:151-158)。它背后是三条设计决定:
- agent 由 config 声明:多 agent env 在自己的
EnvConfig子类上把每个角色声明成一个AgentConfig字段——字段名就是 agent 名(:105-112); - 每局一拨新 agent:
_episode_agents为每局新造值对象,骑在 env 级共享资源(拦截、共享工具)上,并发度由--env.max-concurrent-agents的信号量管(:189-237); - agent 失败是数据,env 抛异常才是局失败:
run()里某个 agent 跑砸,只是它 trace 上的一条记录;run()自己抛出来,这一局才记errors(:151-158 注释)。
一局的生命周期(run_episode,env.py:239-294):setup(agents) → run(task, agents) → finalize(task, episode);每步都有 EnvError 边界和超时;跑完 episode.ok = all(t.ok for t in episode.traces)(:293)。
finalize 是总评判席(:160-167):单 trace 的打分在第 4 章已经各自跑完,这里是跨 trace 的评判——比如「solver 和对照组谁先解出」。注释特意警告:raise 会让整局(可重试单元)失败,所以严格校验,绝不记猜测分。
complete() 决定 --resume 保留什么(:169-173):默认 episode.ok;若你的 run() 容许某个参与者失败(比如对照组允许挂),要覆盖它,否则续跑会把已接受的 rollout 重做一遍。
2. 四个预制 Env
verifiers/v1/envs/ 下五个实现,对应四种常见评测形态:
| Env | 形态 | 关键点 |
|---|---|---|
SingleAgentEnv | 默认:一个 agent 解一题 | 不配 env 时就是它 |
UserSimEnv | 模拟用户与 assistant 逐轮对话 | 用户也是个 agent(envs/user_sim/env.py:51) |
BestOfNEnv | 同题跑 n 次独立尝试 | 记 best 和 pass_at_n 指标(envs/best_of_n/env.py:26-44),拒绝采样/pass@k 用 |
AgenticJudgeEnv 家族 | solver + 裁判 agent 顺序互动 | 裁判可复用 solver 的 runtime(Shared…)或另开新盒子(Isolated…) |
BestOfNEnv.finalize 的写法是教科书示例:等 n 个兄弟都跑完,给最高分的标 best,给达到 --env.threshold 的全局标记 pass_at_n(envs/best_of_n/env.py:36-44)——平局时每个兄弟都算 best 的退化处理也写在注释里。
3. Episode:一局的官方记录
Episode(verifiers/v1/episode.py:22)是多 agent 一局的完整档案:traces(按完成顺序)、ok、errors(跨重试的全部错误,旧到新)。几个顺手的聚合:usage(跨 trace 求和,:42-45)、by_agent(按角色分组,:68-73)、to_record(落盘 JSON,剔除原始张量,:75-81)。
单 agent 的场景也有退化形态:Episode.of(trace) 把一条 trace 包成它自己的一局(:83-86)。
消费方的细节:WireEpisode / WireTaskData 是给「没有装本环境包」的读取方用的宽松解析器——未知任务字段留在 task.model_extra,agent 配置宽松解析(episode.py:89-91、task.py:127-130)。这让分析工具不用安装每个任务集的包就能读 traces.jsonl。
4. 跑评测:eval CLI 全操作面
入口是 uv run eval <taskset-id>(docs/v1/evaluation.md:3-8)。配置三层叠加:TOML 文件 → CLI 点号参数覆盖(evaluation.md:30):
model = "nvidia/NVIDIA-Nemotron-3-Ultra-550B-A55B"
[sampling]
temperature = 1.0
[env.taskset]
id = "primeintellect/terminal-bench-2"
[env.agent.harness]
id = "codex"
[env.agent.runtime]
type = "docker"
常用旋钮(evaluation.md:34-44):num_tasks(无限 taskset 必填)、num_rollouts(每题几局)、shuffle(固定种子,可复现)、verbose。
输出结构(evaluation.md:32):outputs/<env>--<model>--<harness>/<uuid>/ 下有本次用的 config.toml、全部 episode 的 traces.jsonl、以及 eval.log。
断点续跑(evaluation.md:46-48):--resume <output-dir> 原样重载上次的 config.toml(不接受别的参数),好 rollout 保留、错误或缺失的重跑,追加写回同一个 traces.jsonl。配第 1 章的 Task.key,大规模评测跑到一半断了也不用心疼。
5. 框架层的重试与观测
单局之上还有两层保障:
- 整局重试:
run_slot按--env.retries对整局重试(env.py:302-340),重试的历史并入errors但不污染ok的判定(:291-293 注释); - 过程可观测:
RunSlot让进行中的每一局可被外部观察——traces收当前尝试(重试会重开列表),episode/done落幕时落定(env.py:50-67)。
并发模型一句话说清:episode 级并发由 runner 的信号量管,agent 级并发由 --env.max-concurrent-agents 管(默认 1)——run() 里 asyncio.gather 写出并发,不代表真同时跑(:151-158 注释)。这对评测复现很关键:代码里的「逻辑并发」和运行时的「资源并发」被显式分开了。