Harbor 详解:RL 任务的“考场组织者”
Harbor 是一个开源 Python 框架,官方一句话定义是:让任何 agent、用任何模型、在任何沙箱里、并行地去做任何任务(“running any agent with any model on any task in any sandbox in parallel”)。它把评测和 RL rollout 里反复要写的脏活——起沙箱、装 agent、计时、收日志、跑测试、读分数、并发、重试、统计——统一成一套标准。本页从零讲清 Harbor 的概念、任务格式和运行机制,再落到我们两个 repo 是怎么用它的。
harbor==0.24.0(本页所有机制描述以 0.24.0 源码为准)harbor,别名 hb / hr(都指向 harbor.cli.main:app)harbor-rewardkit(独立 Python 包,Harbor 核心并不依赖它)官方文档跟的是最新版。下面几处 0.24.0 的实际行为和文档不一样,读文档时要注意:
- 独立 verifier 默认值:文档说“v0.25 起
harbor tasks init默认独立 verifier”。0.24.0 中,task.toml不写[verifier]的 mode 和 environment 时是共享模式。 - 错误分类:文档中的
error_policy.py、reward_valid、failure_cause在 0.24.0 源码里不存在。0.24.0 只有异常类型 + 重试配置。 - task.toml schema 版本:0.24.0 默认
"1.4",文档示例写"1.3";旧字段version会被自动迁移成schema_version。 - 源码里的两处笔误:
TrialPaths的注释写results.json,实际文件是result.json;任务模板注释写rewards.json,verifier 实际只读reward.json或reward.txt。
为什么需要 Harbor
假设你要评测“Codex + gpt-6-astra 能不能在 Blender 里做好一个机械臂动画”,而且要跑 100 道题、每题 3 次。没有框架时,每个团队都要自己写一遍:
起场地
为每次尝试准备一个干净、隔离、资源受限的沙箱(Docker、云沙箱、虚拟机……)。
请考生
在沙箱里安装 agent CLI,配好模型、API 地址、密钥,把题面喂给它,计时。
收卷
收集 agent 的产物和完整对话/操作日志(轨迹),整理成统一格式。
阅卷
运行评分脚本,读出分数;评分环境最好和考生环境隔离,防作弊。
再加上并发控制、失败重试、超时、结果汇总、pass@k 统计、结果浏览器……Harbor 把这些都做成了标准件,还提供 harbor task migrate 把 Terminal-Bench 格式的任务迁移过来。
把 Harbor 想成考务系统:Task 是一份试卷(题面 + 考场要求 + 阅卷脚本),Environment 是考场,Agent 是考生,Verifier 是阅卷老师,Trial 是“一个考生考一份卷”的完整过程,Job 是一整场考试(很多考生 × 很多试卷 × 每人考几次)。Harbor 自己不出题、不判对错,只保证每场考试按同样的规矩组织、留下同样格式的记录。
六个核心对象
Task 任务
一个目录:instruction.md(题面)+ task.toml(配置)+ environment/(沙箱定义)+ tests/(评分脚本)+ 可选 solution/(参考解)。任务不依赖 Harbor 本身,任何支持该格式的框架都能跑。
Environment 沙箱
能启动、执行命令、上传/下载文件、施加资源和网络限制的隔离环境。接口是 BaseEnvironment,内置 31 种后端(Docker、Podman 和各类云沙箱),也可以自己实现。
Agent 考生
把题面变成在沙箱里的一系列动作,并产出日志、轨迹、token 和费用。接口是 BaseAgent,内置 40 多种(Codex、Claude Code、Gemini CLI、OpenHands……)。
Verifier 阅卷
运行任务的 tests/test.sh,读取它写在 /logs/verifier/ 下的 reward 文件。可以和 agent 共用沙箱,也可以开一个全新的独立沙箱。
Trial 一次尝试
“一个 agent 解一道题一次”。把前四者串成一个带超时、产物收集、事件钩子和结果记录的状态机,最后写 result.json。
Job 一次实验
把“任务/数据集 × agent × 尝试次数”展开成许多 Trial,控制并发与重试,汇总成平均分和 pass@k。
另外两个常见名词:Dataset 是一组任务(通常对应一个基准,如 Terminal-Bench),可以发布到 Harbor Hub;Trajectory 轨迹是 agent 完整的对话和动作记录,标准格式叫 ATIF。
flowchart LR JC["JobConfig
tasks × agents × n_attempts"] --> J["Job"] J -->|"展开"| T["Trial"] TK["Task 目录
instruction · task.toml
environment/ · tests/"] --> T T -->|"1. start"| E["Agent Environment
沙箱"] T -->|"2. setup + run"| A["Agent"] A <-->|"exec / 读写文件"| E E -->|"3. 日志 · 轨迹 · artifacts"| T T -->|"4a. shared:同一沙箱"| V["Verifier
/tests/test.sh"] T -->|"4b. separate:上传 artifacts"| VE["Verifier Environment
全新沙箱"] VE --> V V -->|"reward.json / reward.txt"| R["TrialResult
result.json"] R --> J J -->|"Mean · pass@k"| JR["JobResult"]
Task:一道题长什么样
目录结构
my-task/
├── instruction.md # 题面:给 agent 看的自然语言需求
├── task.toml # 配置:超时、资源、网络、verifier 模式、artifacts…
├── environment/ # 沙箱定义:Dockerfile / docker-compose.yaml / 其他后端认识的格式
│ └── Dockerfile
├── solution/ # 可选:参考解,内置的 oracle agent 会运行 solve.sh
│ └── solve.sh
└── tests/ # 评分:入口固定为 test.sh(Windows 为 test.bat)
├── test.sh
└── ... # 任意辅助文件;可放 tests/Dockerfile 作为独立 verifier 镜像
还支持多步任务:把题拆进 steps/step-1/、steps/step-2/……每步有自己的题面、测试和参考解,可以设 min_reward 不达标就停止,trial 总分按 mean 或 final 汇总。我们的两个 repo 都没用到多步。
reward 契约:Harbor 只认这两个文件
这是 Harbor 和评分脚本之间唯一的接口:tests/test.sh 不管内部做什么,最后必须在沙箱里写出其中一个文件。
| 文件 | 格式 | 解析结果 |
|---|---|---|
/logs/verifier/reward.json | 扁平 JSON 对象,每个值是有限的数字 | 多维 reward,例如 {"reward": 0.97, "native": 1.0};Harbor 把 reward 键当主分 |
/logs/verifier/reward.txt | 一个有限浮点数 | 转成 {"reward": 值} |
两者都存在时优先读 reward.json;文件缺失、为空、无法解析、出现 NaN/Infinity 都会抛专门的异常。最简单的 test.sh:
#!/bin/bash
set -euo pipefail
if pytest /tests/test_outputs.py; then
echo 1 > /logs/verifier/reward.txt
else
echo 0 > /logs/verifier/reward.txt
fi
task.toml 关键字段
完整字段很多,下表只列和我们相关的。原则是:能写进 environment/(如 Dockerfile)的就不写进 task.toml,只有缺乏约定或容易踩坑的才放在这里。
| 节 | 字段 | 默认 | 含义 |
|---|---|---|---|
| 根 | schema_version | "1.4" | 格式版本;旧写法 version 自动迁移 |
| 根 | artifacts | [] | 要从沙箱收集的路径;可写 {source, destination, exclude};/logs/artifacts 总会被自动收集 |
[task] | name 等 | — | 发布到 registry 用的包信息(org/name) |
[metadata] | 任意 | {} | 作者自定义元数据,Harbor 不解释 |
[agent] | timeout_sec | null | agent 运行超时;null 表示不限 |
[agent] | user | null | 以哪个用户/UID 运行 agent |
[verifier] | timeout_sec | 600 | 评分超时 |
[verifier] | environment_mode | 推导 | shared / separate,规则见下一节 |
[verifier] | env | {} | 传给评分脚本的环境变量,支持 ${VAR} 从宿主读取(如 judge 的 API key) |
[verifier.environment] | 同 [environment] | null | 独立 verifier 沙箱的镜像和资源;写了它就隐含 separate |
[environment] | docker_image | null | 预构建镜像;设置后可省略 Dockerfile |
[environment] | cpus / memory_mb / storage_mb / gpus / gpu_types | null | 资源请求;null 表示交给后端决定 |
[environment] | network_mode / allowed_hosts | "public" | public / no-network / allowlist;agent 和 verifier 阶段还能各自覆盖 |
[environment] | workdir | null | 默认工作目录,覆盖镜像 WORKDIR |
[environment] | mcp_servers | [] | 提供给 agent 的 MCP 服务器(stdio / sse / streamable-http) |
[environment] | build_timeout_sec | 600 | 构建/启动沙箱的超时 |
一次 Trial 的完整生命周期
这是 Harbor 最核心的部分,代码在 harbor/trial/trial.py(约 1900 行)。Trial.run() 的骨架非常简单:
self._init_result()
await self._emit(TrialEvent.START)
try:
await self._prepare() # 起沙箱、健康检查、安装 agent
if not self.config.install_only:
await self._run() # 跑 agent → 收产物 → 跑 verifier
except Exception as exc:
self._record_exception(exc)
await self._close_bridge()
await self._recover_outputs() # 出错也尽量把日志和产物捞回来
finally:
await self._finalize() # 停沙箱、写 result.json、脱敏
return self.result
sequenceDiagram
participant T as Trial
participant E as Agent 沙箱
participant A as Agent
participant VE as Verifier 沙箱
T->>T: 写 config.json,建 TrialResult
T->>E: start(force_build),健康检查
T->>A: setup(E):安装 CLI,写配置
T->>A: run(instruction, E, context),asyncio.wait_for 计时
A->>E: exec 命令 / 读写文件(多轮)
T->>E: 下载 agent 日志,转换成 ATIF trajectory.json
T->>E: 收集 artifacts,写 manifest.json
alt separate 模式
T->>E: stop
T->>VE: start,上传 artifacts 到原路径,上传 tests/
T->>VE: 执行 /tests/test.sh
VE-->>T: /logs/verifier/reward.json
T->>VE: stop
else shared 模式
T->>E: 上传 tests/ 到 /tests,执行 test.sh
E-->>T: /logs/verifier/reward.json
T->>E: stop
end
T->>T: 写 result.json,用敏感值脱敏输出
创建与准备
TrialConfig:任务路径、agent、environment、verifier、超时倍数- Task 目录
Trial.create() 下载/缓存任务,解析 task.toml,校验路径安全;根据有无 steps 选 SingleStepTrial 或 MultiStepTrial。构造时就建好 trial 目录和 lock.json,但还没起沙箱。
<trial_dir>/config.jsonlock.json、trial.log
起沙箱、装 agent
[environment]配置- agent 类及参数(模型名、版本)
environment.start(),跑 healthcheck,上传 skills;agent.setup()。对内置的“安装型” agent(Codex、Claude Code),这一步会在沙箱里 npm install 对应 CLI。默认安装超时 360 秒。
- 可用的沙箱 + 已安装的 agent
- 失败时抛
EnvironmentStartTimeoutError/AgentSetupTimeoutError
运行 agent
instruction.md的文本[agent].timeout_sec、网络策略
agent.run(instruction, environment, context),外层 asyncio.wait_for 计时;同时持续同步日志。agent 在沙箱里执行命令、读写文件,直到结束或超时(AgentTimeoutError)。
agent/下的原生日志AgentContext:token、费用、元数据
收日志、转轨迹、收产物
- 沙箱中的 agent 日志
task.toml的artifacts声明
下载 agent 日志;Codex / Claude Code 适配器把各自的 JSONL 转成统一的 ATIF trajectory.json 并回填 token/费用。再按声明把产物从沙箱拷到宿主,冲突时保留第一个并记录。
agent/trajectory.jsonartifacts/+artifacts/manifest.json
评分
tests/- shared:agent 沙箱本身;separate:收集到的 artifacts
上传 tests/ 到 /tests(独立 verifier 镜像自带测试时跳过),合并 verifier 环境变量,执行 /tests/test.sh,stdout 写到 /logs/verifier/test-stdout.txt,下载 /logs/verifier 并解析 reward。
verifier/reward.json或reward.txtVerifierResult(rewards={...})
收尾
- 内存里的
TrialResult
受保护地停止沙箱,记录各阶段耗时,写 result.json,触发 END 钩子;最后扫描 trial 目录,把敏感环境变量的真实值替换成 [REDACTED]。
result.json- 出错时
exception.txt
Trial 输出目录
<trial_dir>/
├── agent/ # agent 原生日志 + trajectory.json(ATIF)
├── verifier/
│ ├── test-stdout.txt
│ └── reward.json | reward.txt
├── artifacts/
│ ├── manifest.json # 收集了什么、从哪来、是否成功;regrade 依赖它
│ └── ...
├── config.json # 完整 TrialConfig,可用于复现
├── lock.json
├── result.json # TrialResult
├── exception.txt # 仅出错时
└── trial.log
TrialResult 主要字段:任务名与校验和、完整 config、agent_info(名称/版本/模型)、agent_result(token、费用)、verifier_result(rewards)、exception_info(类型、消息、traceback),以及环境准备、agent 安装、agent 执行、评分四段的起止时间。
超时、重试与钩子
- 四段超时:沙箱构建
build_timeout_sec、agent 安装(默认 360s)、agent 运行[agent].timeout_sec、评分[verifier].timeout_sec。实际值 =min(基础值, 上限) × 倍数,倍数可以全局或分阶段设置。 - 重试(0.24.0):
TrialQueue每次重试都新建一个 Trial 并删除失败的 trial 目录,按min(min_wait × multiplier^n, max_wait)指数退避。默认max_retries=0;超时、reward 解析失败、额度用尽、安全拒绝、认证失败、模型不存在默认不重试。 - 钩子:
START、ENVIRONMENT_START、AGENT_START、AGENT_END、VERIFICATION_START、END、CANCEL,可用于接观测平台。
共享 verifier vs 独立 verifier
这是 Harbor 最重要的安全设计,也是我们 KUKA 案例用到的关键能力。
shared 共享模式
评分脚本在 agent 刚用过的沙箱里运行,直接读 agent 改过的文件。简单,但 agent 理论上能看到或篡改 /tests、提前写好 reward 文件、留后台进程。
separate 独立模式
收完产物后新开一个全新的沙箱,只把声明过的 artifacts 拷进去(放回它们在 agent 沙箱里的原路径),再上传或内置 tests。agent 的其他文件改动不会被继承。
模式怎么判定(0.24.0)
environment_mode | [verifier.environment] | 结果 |
|---|---|---|
| 不写 | 不写 | shared(0.24.0 的默认) |
| 不写 | 写了 | separate(用 verifier 专用环境) |
"shared" | 不写 | shared |
"shared" | 写了 | 校验报错 |
"separate" | 不写 | separate(复制一份顶层 [environment]) |
"separate" | 写了 | separate(用 verifier 专用环境) |
独立 verifier 的镜像按优先级选:[verifier.environment].docker_image → tests/Dockerfile → 顶层 docker_image → environment/Dockerfile。前两种视为“测试已打进镜像”,不再上传 tests。
如果原样拷贝产物,agent 可以作弊:交一个 setuid 程序,评分时以 root 身份运行并改写 reward;或者把 /app/output.png 做成指向 /tests/solution.png 的软链接,让评分脚本拿参考答案和它自己比。Harbor 把每个 artifact 打包成 tar,再用 Python 的 data 过滤器解包:清除 setuid/setgid 位、跳过特殊文件和指向外部的软链接、规范权限。即便如此,artifact 内容仍应视为不可信输入。
重评 regrade
只改了评分逻辑、不想重跑 agent 时:harbor trial regrade <trial> -p <新任务目录>。Harbor 复制原 trial 的 agent/ 和 artifacts/ 到新 trial,用空操作 agent 占位,只起一个独立 verifier 跑新测试,原 trial 不被修改。前提是新 verifier 必须是 separate 模式,且它需要的每个输入都在原 trial 的 manifest.json 里有记录。KUKA 从 0.4.3 到 0.4.4 的“统一重评”就是这个思路(具体由 repo 脚本实现)。
Environment:沙箱抽象
任何后端只要实现 BaseEnvironment 的这几个方法,就能接入 Harbor:
class MySandbox(BaseEnvironment):
@staticmethod
def type() -> str: return "my-sandbox"
@property
def capabilities(self) -> EnvironmentCapabilities: return EnvironmentCapabilities()
def _validate_definition(self) -> None: ... # 校验任务的 environment/ 是否符合要求
async def start(self, force_build: bool) -> None: ...
async def stop(self, delete: bool) -> None: ...
async def exec(self, command, cwd=None, env=None, timeout_sec=None, user=None) -> ExecResult: ...
async def upload_file(self, source_path, target_path) -> None: ...
async def upload_dir(self, source_dir, target_dir) -> None: ...
async def download_file(self, source_path, target_path) -> None: ...
async def download_dir(self, source_dir, target_dir) -> None: ...
ExecResult 只有 stdout、stderr、return_code 三个字段。自定义后端不用改 Harbor 注册表,配置里写 import_path = "my_module:MySandbox"(命令行 -e my_module:MySandbox)即可,构造参数用 kwargs 传。
EnvironmentCapabilities 包括 gpus、tpus、windows、mounted、docker_compose、disable_internet、network_allowlist、dynamic_network_policy 等。任务要求某项能力而后端没声明时,Harbor 在启动前直接拒绝,不会静默降级。所以后端不能为了“能跑”而虚报能力。
内置后端
0.24.0 注册了 31 种:docker、podman、apple-container、singularity、kata,以及云沙箱 daytona、e2b、modal、runloop、ec2、gke、ack、openshift、vercel、beam、blaxel、tensorlake、skypilot、hf-sandbox 等。云沙箱的 SDK 按需安装:pip install 'harbor[modal]'。
Docker 后端用 Docker Compose 统一处理单 Dockerfile、预构建镜像和多容器任务;镜像名由任务内容哈希生成。网络隔离:静态 no-network 就是 network_mode: none;需要按阶段切换或 allowlist 时,起一个带 NET_ADMIN 的出口控制 sidecar,任务容器共享它的网络命名空间,Harbor 等 sidecar 就绪后才放 agent 进来。
Agent:考生适配器
自定义 agent 实现四个方法:name()、version()、setup(environment)、run(instruction, environment, context),并用 AgentCapabilities 如实声明是否支持 ATIF、恢复、MCP、skills 等。同样用 import_path 或 --agent module:Class 接入。
内置的“安装型” agent 怎么工作
| agent | setup | run | 轨迹 |
|---|---|---|---|
codex | 沙箱内装 node/npm,npm install @openai/codex[@版本] | 临时 CODEX_HOME,写 config.toml 和 auth;执行 codex exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check --json;密钥和地址通过 OPENAI_API_KEY / OPENAI_BASE_URL | 原生 rollout JSONL → ATIF |
claude-code | bootstrap 或 npm 安装 | 把题面 pipe 给 claude --verbose --output-format=stream-json --print;支持 Anthropic API、自定义 endpoint、OAuth、Bedrock | project JSONL → ATIF,从结果流读真实费用 |
oracle | — | 上传 solution/ 并运行 solve.sh,用于验证任务“可解”(默认 agent) | — |
nop | — | 什么也不做,用于测基线和 regrade 占位 | — |
terminus-2 | 无需装外部 CLI | Harbor 原生终端 agent:tmux + LLM 循环 | 每轮写 ATIF |
其他内置 agent 还有 gemini-cli、cursor-cli、aider、goose、openhands、swe-agent、mini-swe-agent、opencode、qwen-coder、kimi-cli、devin 等,共 40 多个;用 harbor agent list 查看。
Trajectory:ATIF 统一轨迹格式
每种 agent 的原生日志格式都不同。ATIF(Agent Trajectory Interchange Format)把它们统一成一种 JSON,供结果浏览器展示、训练数据整理、或作为下一次会话的初始上下文。
{
"schema_version": "ATIF-v1.7",
"session_id": "session-123",
"agent": { "name": "codex", "version": "0.163.0", "model_name": "openai/gpt-5.6-sol" },
"steps": [
{ "step_id": 1, "source": "user", "message": "Create hello.txt." },
{ "step_id": 2, "source": "agent", "message": "I'll create it.",
"tool_calls": [{ "tool_call_id": "call-1", "function_name": "write_file",
"arguments": { "path": "hello.txt", "content": "Hello" } }],
"observation": { "results": [{ "source_call_id": "call-1", "content": "File created" }] },
"metrics": { "prompt_tokens": 1200, "completion_tokens": 80, "cost_usd": 0.004 } }
],
"final_metrics": { "total_steps": 2 }
}
规则:step_id 从 1 连续;source 是 system / user / agent;只有 agent 步可以带模型名、推理内容、工具调用和 metrics;observation 必须引用同一步的 tool call ID。0.24.0 接受 v1.0 到 v1.8。
RewardKit:配套的判分工具包
Harbor 只规定“test.sh 最后写一个 reward 文件”,不管你怎么算分。RewardKit(harbor-rewardkit)是同团队维护的独立包,用来省掉判分样板代码。
在 0.24.0 源码里搜 rewardkit,只有两处:一处注释,另一处是结果浏览器会把 verifier/reward-details.json 当作 RewardKit 输出来展示。Harbor 没有 import、没有依赖、也不会自动运行 RewardKit。要用它,得在你自己的 test.sh 里调用。
- 程序化判据:在
tests/下写 Python 文件,用@criterion装饰函数(参数第一个是workspace: Path,返回 bool / float / 带score的 dict),或调用 20 多个内置判据(rk.file_exists、rk.command_succeeds…)。 - Judge 判据:用 TOML 配置 LLM 或 agent 作为评委,支持 binary / likert 打分。
- 合分:每个 Python 文件或 judge TOML 产出一个分数;用
reward.toml指定聚合方式:weighted-mean、weighted-sum、all-pass、any-pass、threshold、required-pass。子目录可以产出多维 reward。 - 隔离:会修改工作区的判据可设
isolated=True,用 overlayfs 只读挂载并丢弃改动;Worker以全新无账号 UID 运行提交的代码,读不到/tests、写不了/logs/verifier。 - 输出:
reward.json(分数)+reward-details.json(逐条判据分数、评委理由、错误)。
KUKA 的真实 reward.toml,就是 Rule 80 / VL 20 的来源:
[[reward]]
name = "reward"
aggregation = "weighted-mean"
weights = { native = 0.8, visual = 0.2 }
Job:实验矩阵、并发与统计
- 展开:trial 总数 = 任务数 × agent 数 ×
n_attempts(默认 1)。 - 并发:
n_concurrent_trials(默认 4)是全局上限,信号量包住整个 trial;每个 agent 还可以设n_concurrent,只在 agent 运行阶段占用。 - 结果:
JobResult按agent__model__dataset分组统计完成/出错/重试数、metric、pass@k、token 与费用,并列出每种 reward 值和每种异常对应的 trial。 - 续跑:
harbor job resume从已有 job 目录继续未完成的 trial。
metric 与 pass@k 的实际算法
默认 metric 是 Mean:没有结果的 trial 记 0,多个 reward 键分别平均,缺键记 0。另有 max / min / sum 和自定义 uv 脚本。pass@k 用的是标准无偏估计(每题 n 次尝试、c 次成功):
if n - c < k:
return 1.0
product = 1.0
for i in range(k):
product *= (n - c - i) / (n - i)
return 1.0 - product # 再对所有任务取平均
只有当每个 trial 的 reward 恰好一个键、且值严格为 0 或 1 时才计算;输出的 k 只取 2、4、8… 和 5、10、15… 中不超过每题最少尝试次数的值。所以默认 n_attempts=1 时没有任何 pass@k 输出,pass@3 也不在内置列表里。像 KUKA 这种连续 reward(0.97、0.99)也不满足 0/1 条件。这就是 arkevo_task_scaling 自己用 evaluation.py 算 avg@3 / pass@3 的原因:它的 success 是按“无关键失败、总分 ≥ 0.9、每项 VL ≥ 0.75”独立判定的。
CLI 速查
| 命令 | 用途 |
|---|---|
harbor run(= harbor job start) | 启动 Job:harbor run -t <task> -a codex -m openai/gpt-5.6-sol -e docker,或 --config config.json |
harbor trial start | 单独跑一个 trial |
harbor task init | 生成任务脚手架(默认带 pytest 测试);--steps N 生成多步任务 |
harbor task start-env | 手动起一个任务沙箱调试,可上传 solution/tests、安装 agent |
harbor job resume | 续跑未完成的 trial |
harbor job regrade / harbor trial regrade | 只重跑 verifier |
harbor trial handoff | 把已完成 trial 的 agent 会话交给本地 CLI 继续 |
harbor view | 本地 Web 结果浏览器:轨迹、产物、verifier 输出 |
harbor check / harbor analyze | 按 rubric 检查任务质量 / 用 agent 分析轨迹 |
harbor agent list | 列出内置 agent |
harbor task schema / harbor job schema | 打印 TaskConfig / JobConfig 的 JSON Schema |
harbor publish / download / upload | 和 Harbor Hub 交换任务、数据集、结果 |
我们的两个 repo 怎么用 Harbor
- 两边都没有调用
harbor run命令行,都通过 Python API 驱动。 - 两边都没有用 Docker 后端,都自己写了
BaseEnvironment子类,用 Linux 独立 UID + Landlock 做隔离。原因:GPU / Blender / 共享 worker 的约束,以及 Bunker worker 内无法嵌套 Docker。 - 两边都没有用内置的
codex/claude-codeagent,都自己写了BaseAgent子类,直接启动版本固定的原生 CLI,并把真实 API 密钥留在控制器里,agent 只拿本地中转的临时令牌。
区别在于“把多少事交给 Harbor”。可以这样比喻:Harbor 是统一的“导演”,在 blender_rl 里导演只管每一幕的开场和收场,在 KUKA 里导演负责完整的一场演出。
arkevo_blender_rl:Harbor 只管“一段会话”
flowchart TD W["solver/worker.py segment()"] -->|"临时小任务 + JobConfig"| J["harbor Job
1 任务 · 1 次 · 并发 1
verifier.disable = True"] J --> SE["SolverEnvironment
start 只检查 workspace 存在
exec 直接报错"] J --> SA["SolverAgent.run()"] SA -->|"confine.py:独立 UID + Landlock"| CLI["codex exec / claude
resume 同一 session"] CLI <-->|"MCP"| B["Blender 5.2.1
单卡 GPU · Xvfb"] SA -->|"逐行写"| LOG["Harbor logs_dir/agent.jsonl"] SA -->|"每秒检查"| CLK["budget.Clock
累计 4h 活跃时间"] J --> RES["harbor-result-NNNN.json"] LOG --> SEAL["seal.py:脱敏轨迹 + 冻结作品
→ submission_ready"] SEAL --> SC["仓库自己的异步打分器
Rule 70 + VLM 30"]
每个求解样本可能被拆成多个“segment”(进程被杀、API 中断后续跑)。每个 segment 都是一个独立的小 Harbor Job:
cfg = JobConfig.model_validate({'job_name': f'{job.name}-s{number:04d}',
'jobs_dir': str(job / 'harbor'), 'n_concurrent_trials': 1, 'n_attempts': 1,
'environment': {'import_path': 'arkevo_blender_rl.authoring.harbor_adapter:AuthorEnvironment'},
'agents': [{'import_path': 'arkevo_blender_rl.authoring.harbor_adapter:AuthorAgent',
'model_name': config['model']}],
'verifier': {'disable': True}, 'tasks': [{'path': str(author_task)}]})
(上面是造题侧;求解侧同理,换成 SolverEnvironment / SolverAgent。)环境类是一个有意的“薄壳”:
class SolverEnvironment(BaseEnvironment):
@property
def capabilities(self): return EnvironmentCapabilities(gpus=True)
@staticmethod
def type(): return 'arkevo-native-solver'
async def start(self,force_build=False):
if not self.workspace.is_dir(): raise FileNotFoundError(self.workspace)
async def exec(self,*args,**kwargs): raise RuntimeError('Use the isolated native solver runner')
async def upload_file(self,*args,**kwargs): raise RuntimeError('Inputs staged by controller')
这里 Harbor 真正提供的是:trial 目录和日志目录(self.logs_dir)、一致的 config/result 记录,以及造题发布前用 harbor.models.task.config.TaskConfig 校验题包里的 task.toml。题包里的 task.toml 很小:
version = "1.0" # 0.24.0 自动迁移为 schema_version
[agent]
timeout_sec = 43200
[verifier]
timeout_sec = 3600
[environment]
cpus = 4
memory_mb = 8192
arkevo_task_scaling(KUKA):完整的 Harbor trial + 独立 verifier
flowchart TD R["run_harbor_kuka.py one()"] -->|"TrialConfig"| T["harbor Trial
每个 model × attempt 一个"] T --> SE["KukaLandlockEnvironment
solver 根文件系统模板
独立 UID · 两层 Landlock · PRoot"] T --> KA["KukaCliAgent
codex 0.155.1 / claude 2.1.278
无会话恢复"] KA <-->|"Bash / Python / Blender CLI"| SE SE -->|"artifacts: /workspace/output → submission"| T T --> VE["KukaLandlockEnvironment
verifier 根文件系统模板(全新)"] VE --> TS["/tests/test.sh
→ verify_kuka_submission.py"] TS --> P["Blender 4.0.2 原生 probe
+ 固定 7 张画面"] TS --> VL["视觉评委
claude-opus-5-5"] P --> RK["RewardKit
native 0.8 + visual 0.2"] VL --> RK RK -->|"封顶 + success 判定"| RW["/logs/verifier/reward.json"] RW --> T T --> REC["records.json → evaluation.py
avg@3 · pass@3"]
config = TrialConfig(task=TaskConfig(path=args.task.resolve()),
trial_name=f'{model}-{attempt:02}', trials_dir=args.output/'trials',
agent=agent, verifier=VerifierConfig(disable=args.phase == 'smoke'),
environment=EnvironmentConfig(
import_path='harbor_kuka_runtime:KukaLandlockEnvironment', delete=False))
trial = await Trial.create(config)
result = await trial.run()
KUKA 的 task.toml 明确用了独立 verifier 和 artifacts 交接:
schema_version = "1.3"
artifacts = [{ source = "/workspace/output", destination = "submission" }]
[task]
name = "arkevo/kuka-two-station"
[agent]
timeout_sec = 3610
user = "agent"
[environment]
docker_image = "kuka-solver-v043" # 不是 Docker 镜像!是自定义后端选根文件系统模板的标签
workdir = "/workspace"
cpus = 4
memory_mb = 16384
[verifier]
environment_mode = "separate"
timeout_sec = 2400
user = "agent"
[verifier.environment]
docker_image = "kuka-verifier-v043"
workdir = "/workspace"
KUKA 的 docker_image 字段没有被任何 Docker 使用。自定义后端 KukaLandlockEnvironment._validate_definition() 只接受 kuka-solver-v043 和 kuka-verifier-v043 两个值,按它选择一份预制的根文件系统模板(用 cp -al 硬链接出每次 trial 的独立根目录)。借用这个字段是因为 Harbor 的 separate verifier 逻辑要靠它区分两种环境。
并排对比
| 维度 | arkevo_blender_rl | arkevo_task_scaling(KUKA) |
|---|---|---|
| 用的 Harbor API | Job、JobConfig、TaskConfig(校验) | Trial、TrialConfig、AgentConfig、EnvironmentConfig、VerifierConfig |
| Harbor 粒度 | 每个样本的每个 segment 一个小 Job | 每个 model × attempt 一个完整 Trial |
| 自定义环境 | AuthorEnvironment、SolverEnvironment(薄壳,exec 禁用) | KukaLandlockEnvironment(完整实现 exec / 上传 / 下载) |
| 自定义 agent | AuthorAgent、SolverAgent | KukaCliAgent;集成测试用 ReferenceReplayAgent |
| 隔离 | 独立 UID + Landlock + CPU 绑核;只放行本地中转端口和 MCP 端口 | 独立 UID + 两层 Landlock + PRoot + 进程文件边界 + CPU 绑核 |
| GPU / 显示 / MCP | 单卡 GPU、每槽 Xvfb、Blender MCP | 无 GPU、后台 Blender、无 MCP |
| CLI 版本 | Codex 0.163.0、Claude Code 2.1.303 | Codex 0.155.1、Claude Code 2.1.278 |
| 会话恢复 | 跨 segment 快照并 resume 同一原生会话;找不到原会话就拒绝 | 不恢复:--ephemeral / --no-session-persistence |
| 预算 | 累计活跃 4 小时(budget.Clock);Harbor 墙钟超时 24 小时只是兜底 | 3600 秒;Harbor 超时 3610 秒 |
| Harbor verifier | 关闭 | 开启,separate 模式 |
| 谁打分 | 仓库自己的异步打分器(Rule 70 + VLM 30) | Harbor 调 tests/test.sh,内部用 RewardKit(0.8 / 0.2) |
| 产物交接 | 仓库自己冻结 workspace;Harbor 日志作为轨迹来源 | Harbor artifacts:/workspace/output → submission → verifier |
| 指标 | 无聚合 | 仓库 evaluation.py 算 avg@3 / pass@3 |
职责划分:哪些是 Harbor 做的,哪些是仓库做的
| 职责 | Harbor 0.24.0 提供 | blender_rl 自己做 | KUKA 自己做 |
|---|---|---|---|
| trial 生命周期、目录、config/result 记录 | 是 | — | — |
| 超时控制 | 墙钟超时 | 活跃时间预算 | — |
| 沙箱隔离 | 接口 | UID + Landlock | UID + Landlock + PRoot |
| 安装、启动 agent | 内置 agent(未用) | 原生 CLI + 中转令牌 | 原生 CLI + 每 trial 网关 |
| 轨迹转 ATIF | 内置 agent 才有 | 原样保存 agent.jsonl 并脱敏 | 保存 cli.jsonl |
| 产物收集与消毒 | artifacts | 自己 seal | 用 Harbor artifacts |
| 独立评分环境 | separate verifier | 不用 | 用 |
| 读取 reward | reward.json | 不用 | 用 |
| 专业评分规则 | — | 作者 verifier + VLM A/B/C | Blender probe + 视觉评委 + RewardKit |
| 并发调度 | Job 队列 | 自己的 40 槽调度器 | 自己限并发 2 |
| avg@k / pass@k | 仅 0/1 reward | 无 | evaluation.py |
踩过的坑与边界
- Bunker worker 上跑不了 Docker 后端:没有 Docker daemon/socket、cgroup v2 只读、不能创建用户命名空间,所以 KUKA 只能自己写 Landlock/UID/PRoot 后端。通用 Harbor–Bunker 适配器和 Linux + Docker 后端都还没验证。
- 内存保护误杀(KUKA 0.4.0):自研后端把共享容器的文件缓存也算作内存压力,提前杀掉只用了 4–4.5 GiB 的 trial。0.4.1 改为按 UID 统计进程 RSS(上限 16 GiB)并扣掉可回收缓存,并发从 6 降到 2。这类监控是“发现就杀”,不是 cgroup 硬预留,所以没有向 Harbor 声明内存能力。
- Blender 在 PRoot 下偶发启动卡住:换新 UID 重试即可成功,根因未定位;后来只在可信初始化完成前允许重试,不能重放 solver 的工作。
- 两层超时要分清:blender_rl 的公平预算是累计活跃时间,Harbor 的
timeout_sec只是墙钟兜底。解读“预算用尽”时要看仓库的budget.json,不是 Harbor 的超时异常。 - blender_rl 的题包还没被 Harbor 独立跑过评分:两个组件里 Harbor verifier 都是关闭的,题包里的
tests/test.sh能否在标准 Harbor 后端上直接运行,仍需单独验证。 templates/harbor/故意不能运行:它的test.sh直接exit 2。真实的 KUKA 题包由package_kuka_harbor.py生成。
术语表
| 术语 | 含义 |
|---|---|
| Task / Dataset | 一道题 / 一组题 |
| Trial / Job | 一个 agent 解一道题一次 / 一组 trial 构成的实验 |
| Environment / Sandbox | 隔离的运行环境,实现 BaseEnvironment |
| Agent | 解题程序,实现 BaseAgent;“安装型” agent 会在沙箱里装 CLI |
| Verifier | 运行 tests/test.sh 并读取 reward 的组件 |
| shared / separate verifier | 评分在 agent 沙箱里进行 / 在全新沙箱里进行 |
| Artifacts | 声明要从沙箱收集、并交给独立 verifier 的文件 |
| ATIF | 统一的 agent 轨迹 JSON 格式,文件名通常是 trajectory.json |
| Oracle / Nop agent | 运行参考解的 agent / 什么都不做的 agent |
| Regrade | 不重跑 agent,只用新 verifier 对已有产物重新评分 |
| RewardKit | 独立的判分工具包,在 test.sh 里调用,产出 reward.json + reward-details.json |
import_path | 以 module:Class 形式接入自定义环境、agent 或 verifier |