第 16 章

第 16 章:RewardKit 入门——声明式打分

第 16 章:RewardKit 入门——声明式打分

写 verifier 是任务创建中最枯燥、最容易出错的环节:各家基准的 verifier 往往是难以阅读和复用的脚本,导致样板代码在任务间被反复复制,埋下隐性 bug。RewardKit 把常见打分模式打包成可复用组件:目录结构即 verifier、内置 20+ 常用 criteria、一行代码声明打分、TOML 声明 LLM judge。本章覆盖程序化 criteria、reward.toml 聚合、多 reward 子目录与隔离机制,judge 评分留到第 17 章展开。

16.1 设计原则

RewardKit 针对传统 verifier 的四大痛点,提出四条设计原则:

原则 含义
Simplicity verifier 由目录结构定义,一眼可读;常见 criteria 一行搞定;自定义逻辑是装饰器函数;judge 是可复用的 TOML 文件
Reuse & sharing criteria 就是目录里的普通文件,可跨任务共享、用 git 做版本管理
Zero boilerplate 内置 20+ criteria 覆盖文件、命令、JSON、CSV、HTTP、图片与轨迹;LLM / agent judge 原生支持
Isolation criteria 可在隔离的文件系统快照中运行,互不干扰,也不会弄脏 agent 工作区

RewardKit 是独立的 Python 包,与 Harbor 搭配最强大,但单独使用也完全可行。

16.2 安装与目录约定

uv tool install harbor-rewardkit
# 需要 criteria 或 judge 读取图片、PDF/DOCX/PPTX/XLSX 时安装全部 extras
uv tool install harbor-rewardkit[all]

在 Harbor 任务中,Harbor 会把任务的 tests/ 目录复制到沙箱的 /tests 并执行 test.sh。你把 criteria 文件与 test.sh 放在一起:

tests/
├── files.py       # 程序化 criteria(Python)
├── quality.toml   # judge criteria(TOML)
└── test.sh        # 入口脚本

test.sh 的标准写法只有一行核心调用:

#!/bin/bash
uvx --from 'harbor-rewardkit==0.2.*' rewardkit /tests

RewardKit 会自动发现 /tests 里的 criteria,对位于 /app 的 agent 工作区打分,并把结果写到 /logs/verifier/reward.json:

{ "reward": 0.75 }

所有默认路径都与 Harbor 的约定对齐。想让自己的编码 agent 帮你设计 verifier,可以安装官方 rewardkit skill:npx skills add harbor-framework/harbor --skill rewardkit。

16.3 内置 criteria:一行打分

在任何 tests 目录下的 Python 文件里直接调用:

import rewardkit as rk

rk.file_exists("output.txt")
rk.file_contains("output.txt", "hello")
rk.command_succeeds("python main.py")

所有 criteria 都接受可选的 weight(默认 1.0)和 isolated(默认 false)参数。内置 20+ criteria 按类别如下:

类别 Criterion 参数 说明
文件 file_exists path 文件存在
文件 file_not_exists path 文件不存在
文件 file_contains path, text 文件包含子串
文件 file_contains_regex path, pattern 内容匹配正则
文件 file_matches path, expected 内容与期望文本相等(去空白)
文件 files_equal path1, path2 两文件内容相同
文件 diff_ratio path, expected 内容相似度(0.0–1.0)
命令 command_succeeds cmd, cwd?, timeout? 命令以 0 退出
命令 command_output_contains cmd, text, cwd?, timeout? stdout 包含文本
命令 command_output_matches cmd, expected, cwd?, timeout? stdout 相等(去空白)
命令 command_output_matches_regex cmd, pattern, cwd?, timeout? stdout 匹配正则
数据 json_key_equals path, key, expected JSON 顶层键等于期望值
数据 json_path_equals path, json_path, expected 点分隔路径取值相等
数据 csv_cell_equals path, row, col, expected CSV 单元格相等
数据 xlsx_cell_equals path, cell, expected, sheet? Excel 单元格相等
数据 sqlite_query_equals db_path, query, expected SQL 查询结果相等
HTTP http_status_equals url, status?, timeout? 响应状态码符合(默认 200)
HTTP http_response_contains url, text, timeout? 响应体包含文本
图片 image_similarity path1, path2 像素级相似度(0.0–1.0)
图片 image_size_equals path, width, height 图片尺寸正确
轨迹 trajectory_tool_used tool_name, min_count?, path? agent 至少用过某工具 N 次
轨迹 trajectory_tool_not_used tool_name, path? agent 未用过某工具
轨迹 trajectory_turn_count max_turns, path? 超出轮次预算则线性衰减到 0

命令类默认 timeout 30 秒,cwd 相对于工作区;HTTP 类默认 timeout 10 秒;轨迹类默认读取 /logs/agent/trajectory.json。两类 extras 需额外安装:图片类要 uv tool install harbor-rewardkit[image],xlsx_cell_equals 要 harbor-rewardkit[documents]。csv_cell_equals 的行号规则:col 为整数时第 0 行是表头;col 为字符串(列名)时第 0 行是表头后的第一条数据。

16.4 自定义 criteria:@criterion 装饰器

任务专属逻辑用 @criterion 装饰的函数表达。第一个参数永远是 workspace: Path,返回 bool、float 或带 score 的 dict:

from pathlib import Path
from rewardkit import criterion

@criterion
def has_valid_output(workspace: Path) -> bool:
    output = (workspace / "output.txt").read_text()
    return len(output.splitlines()) >= 10

除 workspace 外没有其他参数的 criterion 会被自动调用。带参数的 criterion 需要通过 rk 显式调用,description 里可用模板占位符:

import rewardkit as rk
from rewardkit import criterion

@criterion(description="output has at least {n} lines")
def has_n_lines(workspace: Path, n: int) -> bool:
    output = (workspace / "output.txt").read_text()
    return len(output.splitlines()) >= n

rk.has_n_lines(10, weight=2.0)
rk.has_n_lines(50, weight=1.0)

返回 dict 时,reasoning、confidence、model 是可选键,会写入 reward-details.json:

@criterion
def report_is_relevant(workspace: Path) -> dict:
    probability = classify((workspace / "report.md").read_text())
    return {
        "score": probability >= 0.5,
        "reasoning": "classified as on-topic",
        "confidence": max(probability, 1 - probability),
        "model": "relevance-classifier-v1",
    }

16.5 分数聚合:reward.toml

每个 Python 文件注册的 criteria 汇成一个分数,每个 judge TOML 也各产出一个分数,名字取文件名去后缀(files.py → files)。只含 import 或共享定义、不注册任何 criteria 的文件会被忽略。当 verifier 文件直接位于 tests/ 下时,各分数等权合成 reward。要改变两级聚合行为,在 criteria 旁放 reward.toml:

# 要求 files.py 注册的每个 criterion 全部通过
[scoring.files]
aggregation = "all-pass"

# files.py 的权重是 quality.toml 的两倍
[[reward]]
name = "reward"
aggregation = "weighted-mean"
weights = { files = 2.0, quality = 1.0 }

[scoring.<name>] 控制单个文件内部 criteria 的合成;命名的 [[reward]] 表控制目录内各分数的合成——没有它时 RewardKit 以加权平均生成 reward,judge 的权重来自其 [judge] 里的 weight。可用的聚合模式:

模式 语义
weighted-mean 加权平均(默认行为)
weighted-sum sum(value × weight),不归一化;唯一允许负权重的模式,结果可能超出 [0, 1]
all-pass 全部通过才得 1
any-pass 任一通过即得 1
threshold 聚合值达到阈值即通过,阈值同表设置,默认 0.5
required-pass 所有非 optional 项通过才得 1(对程序化 criteria 等价于 all-pass)

16.6 多 reward 子目录与嵌套

想在 correctness、structure、quality 等维度分别出分时,把 criteria 组织成子目录,每个子目录成为一个独立 reward:

tests/
├── test.sh
├── correctness/
│   ├── files.py
│   └── behavior.py
├── structure/
│   └── files.py
└── quality/
    └── judge.toml

输出变成:

{ "correctness": 0.75, "structure": 1.0, "quality": 0.6 }

维度内部各文件等权合成;放一个维度级 reward.toml 可以改权重(用文件名词干引用,省略 name,目录名即分数名)。子目录还可以嵌套:correctness/behavior/ 先合成内部 behavior 分数,父级用目录名引用它,且 behavior/ 内部的权重不会泄漏到父级。

要额外产出一个跨维度的总分,在 tests/ 根放带名字的 [[reward]] 表:

[[reward]]
name = "reward"
aggregation = "weighted-mean"
weights = { correctness = 2.0, structure = 1.0, quality = 1.0 }

输出将同时包含各维度分数与 reward。注意:多 reward 任务在没有根聚合时没有隐式 reward,而 Harbor 读取 reward 键作为任务主分。跨维度复用自定义 criteria 的办法:在 tests 根定义时加 shared=True,其他维度即可 rk.word_count_correct(weight=3.0) 这样调用。

16.7 隔离与反注入

有些 criteria 运行的命令会修改工作区。为避免 criteria 互相影响,RewardKit 支持隔离运行:以 overlayfs 把工作区挂成只读,该 criterion 结束后丢弃其所有改动。程序化 criteria 传 isolated=True;agent judge 在 [judge] 里设 isolated = true。

rk.command_succeeds("python main.py", isolated=True)

agent judge 与工作区共享文件系统,因此任务触发多次 agent 运行(多个 agent judge、多次采样、individual 模式多个 criteria)时,RewardKit 会直接报错,除非每个 agent judge 都设 isolated = true。

在 Harbor 任务中启用隔离需要额外的挂载权限——正确做法是给独立的 verifier 环境授权,绝不给 agent 环境:

  1. verifier 镜像安装 fuse-overlayfs;
  2. compose 文件里加 cap_add: [SYS_ADMIN]、devices: [/dev/fuse]、security_opt: [apparmor:unconfined];
  3. task.toml 设 artifacts = ["/app/main.py"] 与 [verifier] environment_mode = "separate"。

该方案在 Docker 环境下可用,其他沙箱对挂载的支持各异。此外,针对"agent 往被评文件里写诱导性指令"的提示注入攻击,judge TOML 提供 guard 选项(flag 仅上报、penalize 将被标记的提交记 0 分),详见第 17 章。

16.8 输出、对比与 CLI

RewardKit 并排写出两个文件:reward.json(分数)与 reward-details.json(每个 criterion 的得分、judge 理由、错误与警告)。harbor view 会在 Verifier Logs → Rewards 下以可折叠树渲染细节。传入多个测试目录可以并排对比 verifier 设计,输出形如 v1/correctness 的命名空间键:

rewardkit /tests/v1 /tests/v2

# 完整 CLI(所有 flags 可选)
rewardkit  \
  --workspace /app \
  --output /logs/verifier/reward.json \
  --max-concurrent-programmatic 8 \
  --max-concurrent-llm 8 \
  --max-concurrent-agent 2

Python API 同样简洁:rk.run("/tests", workspace="/app") 运行并取分,rk.discover("/tests", workspace="/app") 只探查会发现哪些 reward。

本章小结

  • 传统 verifier 的痛点是难读、难扩展、难复用;RewardKit 用"目录结构即 verifier + 可复用组件"解决。
  • Harbor 把 tests/ 复制到 /tests 并执行 test.sh,核心调用是 uvx --from 'harbor-rewardkit==0.2.*' rewardkit /tests。
  • 内置 20+ criteria 覆盖文件、命令、数据格式、HTTP、图片、轨迹六类,全部支持 weight 与 isolated。
  • @criterion 函数首参为 workspace: Path,无额外参数时自动调用,带参数时通过 rk.xxx() 调用。
  • 聚合分两级:[scoring.<file>] 管文件内部,[[reward]] 管目录内合成;模式有 weighted-mean/weighted-sum/all-pass/any-pass/threshold/required-pass。
  • 子目录即多 reward;嵌套子目录先内后外逐层合成;跨维度复用靠 shared=True。
  • 隔离用 overlayfs 只读挂载实现;Harbor 任务中须给独立 verifier 环境授权,不能给 agent 环境。
  • reward.json 与 reward-details.json 并排输出,多测试目录可并排对比 verifier。

延伸阅读