跳到主要内容

数据截至 (上游 commit 8b292c9f1b14)

第 2 章 执行面 —— Harness 契约与四种 Runtime

本章讲什么: verifiers 怎么做到「让 Claude Code / Codex 这些真程序来做题」——harness 适配器要遵守的最小契约、能力旗标怎么声明程序的本事、以及 rollout 跑在哪四种隔离级别的盒子里。

1. 为什么不自己写 agent 循环

评测框架最常见的做法是内置一个简化 agent 循环。verifiers 反着来:被评的就该是用户真实在用的那个程序——它的 prompt、它的工具、它的怪癖都在里面,评出来才作数。于是 harness 的职责非常窄(verifiers/v1/harness.py):

  1. setup(runtime):把程序装进 rollout 的盒子(harness.py:95-96);
  2. launch(...):把程序跑到退出,返回 ProgramResult(抽象方法,harness.py:262-284);
  3. resume(..., messages):用户说话了,让程序接着已有的对话答下一段(默认实现:把累积对话当 Messages prompt 重新拉起,harness.py:208-256)。

契约里真正的硬约束只有一个(launch 的 docstring,harness.py:262-284):程序的一切模型调用必须打向拦截服务器的 endpoint(bearer token 是 secret)。至于是不是真起了一个进程,框架不关心——也可以进程内跑循环,只要流量过境,最后返回一个合成的成功 ProgramResult 即可。「拦截是契约,进程不是」。

2. 能力旗标:harness 自报家门

Harness 类上一排 ClassVar[bool] 旗标(harness.py:33-54),框架按旗标决定能对这台程序做什么:

旗标默认含义
APPENDS_SYSTEM_PROMPTFalse能单独发 system 消息;否则把 system_prompt 折进首条 user(resolve_prompt,:59-83)
SUPPORTS_MCPFalse能装 MCP 工具服务器(taskset 导出的 toolset 才挂得上)
SUPPORTS_TOOL_INTERCEPTIONFalse工具流量也能走拦截(改写工具返回)
SUPPORTS_RESUMEFalse默认 resume() 能用累积对话重放续跑
EXECUTES_CODETrue程序会把模型的代码放到本地执行——subprocess 警告和 judge 的沙箱要求都看它
SUPPORTS_SKILLSFalse程序能发现 SKILL.md 技能;setup 里调 install_skills 上传到固定发现目录(:98-118)
NEEDS_CONTAINERTrue必须跑在容器里——只有自家的极简循环(bashnull)豁免

install_skills 有个细到可爱的细节(harness.py:98-118):runtime.write 只搬字节不搬权限位,所以上传完要对可执行文件补一遍 chmod +x(:114-118)。

3. 一段交换 = 若干段「段」

verifiers 把一次人机交换切成段(segment):Harness.run 跑一次 launch 或 resume 到程序让出控制权为止,HarnessSession 是跨段持有状态的手柄(harness.py:287-359)。切段的回报是:「等用户输入」不烧 agent 的时间预算——每段只花自己运行的时间(见第 4 章 rollout 的预算记账)。

错误归因也在这里做(_check_result,harness.py:145-161):程序退出码非零时,先看是盒子死了(SandboxError)还是程序自己崩了(HarnessError);若某轮被 @stop 拒了,退出非零反而是预期内的。

4. 四种 runtime:盒子在哪开

每个 rollout 跑在一个 runtime 里(docs/v1/architecture.md:9-13 + verifiers/v1/runtimes/):

runtime盒子是什么适用
subprocess本地 Python 子进程只用于调试——副作用会互相串门(改到 harness 的全局配置就污染别的 rollout)
docker本机 Docker 容器本地隔离跑
modal远端 Modal 沙箱生产/高并发
primePrime Intellect 沙箱生产、训练,支持 host 级网络策略

框架对「敢把真程序跑在 subprocess 里」有明确态度:只要 harness EXECUTES_CODE=True 且 runtime 是 subprocess,env 初始化时就警告——本地文件和配置会影响评测,subprocess 仅供调试(env.py:123-137)。

runtime 还有一条职责链:start() 起盒子 → prepare_setup() → (harness/工具装好) → prepare_execution([endpoint, *tool_urls]) 落下执行期网络策略,但放过框架自己回调用的路由(rollout.py:209-264)。也就是说:题目声明的 network_allow/network_block(第 1 章)到这一步才真正生效。

5. 内置 harness 族

verifiers/v1/harnesses/ 下自带 14 个适配器,分三类:

  • 厂商 CLI:claude_code、codex、kimi_code、openclaw、hermes_agent、pi、mini_swe_agent、terminus_2——把各家真实编码/终端 agent CLI 装进盒子;
  • 基础循环:bash(带工具的极简循环)、null(无工具纯聊天,唯一 EXECUTES_CODE=False 的)、compactrlm;
  • 特殊通道:browser_use(CDP 驱动浏览器)、pool(池化复用)、node.py

选一个 harness 就是一行配置:--env.agent.harness.id codex 或 TOML [env.agent.harness](docs/v1/evaluation.md:20-23)。工具太多的话几乎所有 harness 都支持 disabled_tools 名单(evaluation.md:50-59)。

6. 自己写一个 harness 的最小样子

官方文档给的最小实现(docs/v1/harnesses.md:16-58)翻成骨架就是:

class MyHarness(Harness[MyHarnessConfig]): # 示意,非源码(节自官方指南)
APPENDS_SYSTEM_PROMPT = True
SUPPORTS_MCP = True

async def setup(self, runtime):
await runtime.run(["sh", "-c", "echo installing..."], {})

async def launch(self, ctx, trace, runtime, endpoint, secret, mcp_urls, data):
_, prompt = self.resolve_prompt(data)
env = {"HARNESS_BASE_URL": endpoint, # 模型出口指向拦截服务器
"HARNESS_API_KEY": secret,
"HARNESS_BASE_MODEL": ctx.model}
return await runtime.run_program(["<HARNESS_BINARY>", str(prompt or "")], env)

要点全在注释里:拿到 prompt、把 endpoint/secret 灌进程序的环境变量、在 runtime 里跑到退出。剩下的(trace 记录、采样改写、工具挂接)都被拦截层接走了——下一章。