Harbor 详解:RL 任务的“考场组织者”

Harbor 是一个开源 Python 框架,官方一句话定义是:让任何 agent、用任何模型、在任何沙箱里、并行地去做任何任务(“running any agent with any model on any task in any sandbox in parallel”)。它把评测和 RL rollout 里反复要写的脏活——起沙箱、装 agent、计时、收日志、跑测试、读分数、并发、重试、统计——统一成一套标准。本页从零讲清 Harbor 的概念、任务格式和运行机制,再落到我们两个 repo 是怎么用它的。

两个 repo 固定的版本
harbor==0.24.0(本页所有机制描述以 0.24.0 源码为准)
命令行
harbor,别名 hb / hr(都指向 harbor.cli.main:app)
官方文档
docs.harborframework.com(描述的是最新版,部分行为与 0.24.0 不同,下文会标注)
配套判分包
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_secnullagent 运行超时;null 表示不限
[agent]usernull以哪个用户/UID 运行 agent
[verifier]timeout_sec600评分超时
[verifier]environment_mode推导shared / separate,规则见下一节
[verifier]env{}传给评分脚本的环境变量,支持 ${VAR} 从宿主读取(如 judge 的 API key)
[verifier.environment]同 [environment]null独立 verifier 沙箱的镜像和资源;写了它就隐含 separate
[environment]docker_imagenull预构建镜像;设置后可省略 Dockerfile
[environment]cpus / memory_mb / storage_mb / gpus / gpu_typesnull资源请求;null 表示交给后端决定
[environment]network_mode / allowed_hosts"public"public / no-network / allowlist;agent 和 verifier 阶段还能各自覆盖
[environment]workdirnull默认工作目录,覆盖镜像 WORKDIR
[environment]mcp_servers[]提供给 agent 的 MCP 服务器(stdio / sse / streamable-http)
[environment]build_timeout_sec600构建/启动沙箱的超时

一次 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,用敏感值脱敏输出
    
1

创建与准备

输入
  • TrialConfig:任务路径、agent、environment、verifier、超时倍数
  • Task 目录
处理

Trial.create() 下载/缓存任务,解析 task.toml,校验路径安全;根据有无 steps 选 SingleStepTrial 或 MultiStepTrial。构造时就建好 trial 目录和 lock.json,但还没起沙箱。

输出
  • <trial_dir>/config.json
  • lock.json、trial.log
2

起沙箱、装 agent

输入
  • [environment] 配置
  • agent 类及参数(模型名、版本)
处理

environment.start(),跑 healthcheck,上传 skills;agent.setup()。对内置的“安装型” agent(Codex、Claude Code),这一步会在沙箱里 npm install 对应 CLI。默认安装超时 360 秒。

输出
  • 可用的沙箱 + 已安装的 agent
  • 失败时抛 EnvironmentStartTimeoutError / AgentSetupTimeoutError
3

运行 agent

输入
  • instruction.md 的文本
  • [agent].timeout_sec、网络策略
处理

agent.run(instruction, environment, context),外层 asyncio.wait_for 计时;同时持续同步日志。agent 在沙箱里执行命令、读写文件,直到结束或超时(AgentTimeoutError)。

输出
  • agent/ 下的原生日志
  • AgentContext:token、费用、元数据
4

收日志、转轨迹、收产物

输入
  • 沙箱中的 agent 日志
  • task.toml 的 artifacts 声明
处理

下载 agent 日志;Codex / Claude Code 适配器把各自的 JSONL 转成统一的 ATIF trajectory.json 并回填 token/费用。再按声明把产物从沙箱拷到宿主,冲突时保留第一个并记录。

输出
  • agent/trajectory.json
  • artifacts/ + artifacts/manifest.json
5

评分

输入
  • 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.txt
  • VerifierResult(rewards={...})
6

收尾

输入
  • 内存里的 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 执行、评分四段的起止时间。

超时、重试与钩子

共享 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。

防作弊:artifacts 会被消毒

如果原样拷贝产物,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 传。

capabilities 的原则:只声明你真正能强制执行的

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 怎么工作

agentsetuprun轨迹
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-codebootstrap 或 npm 安装把题面 pipe 给 claude --verbose --output-format=stream-json --print;支持 Anthropic API、自定义 endpoint、OAuth、Bedrockproject JSONL → ATIF,从结果流读真实费用
oracle—上传 solution/ 并运行 solve.sh,用于验证任务“可解”(默认 agent)—
nop—什么也不做,用于测基线和 regrade 占位—
terminus-2无需装外部 CLIHarbor 原生终端 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)是同团队维护的独立包,用来省掉判分样板代码。

它和 Harbor 的真实关系

在 0.24.0 源码里搜 rewardkit,只有两处:一处注释,另一处是结果浏览器会把 verifier/reward-details.json 当作 RewardKit 输出来展示。Harbor 没有 import、没有依赖、也不会自动运行 RewardKit。要用它,得在你自己的 test.sh 里调用。

KUKA 的真实 reward.toml,就是 Rule 80 / VL 20 的来源:

[[reward]]
name = "reward"
aggregation = "weighted-mean"
weights = { native = 0.8, visual = 0.2 }

Job:实验矩阵、并发与统计

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       # 再对所有任务取平均
Harbor 内置 pass@k 的限制

只有当每个 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 当“生命周期框架”,不用它的现成零件
  • 两边都没有调用 harbor run 命令行,都通过 Python API 驱动。
  • 两边都没有用 Docker 后端,都自己写了 BaseEnvironment 子类,用 Linux 独立 UID + Landlock 做隔离。原因:GPU / Blender / 共享 worker 的约束,以及 Bunker worker 内无法嵌套 Docker。
  • 两边都没有用内置的 codex / claude-code agent,都自己写了 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_rlarkevo_task_scaling(KUKA)
用的 Harbor APIJob、JobConfig、TaskConfig(校验)Trial、TrialConfig、AgentConfig、EnvironmentConfig、VerifierConfig
Harbor 粒度每个样本的每个 segment 一个小 Job每个 model × attempt 一个完整 Trial
自定义环境AuthorEnvironment、SolverEnvironment(薄壳,exec 禁用)KukaLandlockEnvironment(完整实现 exec / 上传 / 下载)
自定义 agentAuthorAgent、SolverAgentKukaCliAgent;集成测试用 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.303Codex 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 + LandlockUID + Landlock + PRoot
安装、启动 agent内置 agent(未用)原生 CLI + 中转令牌原生 CLI + 每 trial 网关
轨迹转 ATIF内置 agent 才有原样保存 agent.jsonl 并脱敏保存 cli.jsonl
产物收集与消毒artifacts自己 seal用 Harbor artifacts
独立评分环境separate verifier不用用
读取 rewardreward.json不用用
专业评分规则—作者 verifier + VLM A/B/CBlender probe + 视觉评委 + RewardKit
并发调度Job 队列自己的 40 槽调度器自己限并发 2
avg@k / pass@k仅 0/1 reward无evaluation.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

延伸阅读