数据截至 (上游 commit 5359534c6f00)
第 6 章 · 奖励、harness 与数据采集
本章讲训练侧的三层:环境内怎么算奖励(Rubric / Transform)、怎么把环境驱动成一条轨迹(harness)、怎么把轨迹存成数据集(collect)。设计依据是
rfcs/004-rubrics.md和rfcs/005-agentic-harnesses.md。
6.1 一条铁律:奖励只能诞生在环境里
这是 OpenEnv 反复强调的不变量。理由不难懂:奖励是领域知识。什么叫「这段代码写得对」「这步棋走得好」,只有环境自己知道。如果让训练框架在外面拍脑袋加分,同一个环境换个框架就换个分数,实验根本没法比。
所以这一章的三层结构可以这么理解:
┌────────────────────────────────────────────────┐
│ 环境内部 │
│ Rubric / Transform ← 奖励在这里产生 │
└───────────────────┬────────────────────────────┘
│ Observation.reward
▼
┌────────────────────────────────────────────────┐
│ harness 层 │
│ 驱动 rollout + _resolve_env_reward 交叉校验 │
└───────────────────┬────────────────────────────┘
│ HarnessRolloutResult
▼
┌────────────────────────────────────────────────┐
│ collect 层 │
│ 落盘成 JSONL 数据集,可推到 HF Hub │
└────────────────────────── ──────────────────────┘
6.2 环境内算分:两种工具,不同定位
Transform | Rubric | |
|---|---|---|
| 输入 | 只有 observation | action + observation |
| 定位 | 观察后处理管道 | 评分器 |
| 借鉴自 | TorchRL transform | PyTorch nn.Module |
| 组合方式 | CompositeTransform 串联 | 属性赋值自动注册子评分器 |
| 位置 | interfaces.py:115 | rubrics/base.py:17 |
简单场景用 Transform 就够(比如 envs/coding_env/server/transforms.py 的 CodeSafetyTransform 扫危险模式打惩罚)。复杂场景才上 Rubric。
6.3 Rubric:把评分器写成 nn.Module
直觉
如果你写过 PyTorch,Rubric 会非常眼熟:
| PyTorch | Rubric |
|---|---|
实现 forward() | 实现 forward(action, observation) -> float |
| 子模块属性赋值即注册 | 子 rubric 属性赋值即注册 |
named_modules() | named_rubrics() |
register_forward_hook | register_forward_hook |
state_dict() | state_dict() |
自动注册怎么做到的
重写 __setattr__(src/openenv/core/rubrics/base.py:50-54):
def __setattr__(self, name: str, value: Any) -> None:
if isinstance(value, Rubric):
self._rubric_children[name] = value
object.__setattr__(self, name, value)
__init__ 里用 object.__setattr__ 初始化那几个内部字段,避免自己触发自己(base.py:44-48)。
组合示意
# 示意,非源码
class CodeRubric(Rubric):
def __init__(self):
super().__init__()
self.syntax = SyntaxRubric() # 赋值即注册为子评分器
self.tests = TestPassRubric()
def forward(self, action, observation):
return 0.3 * self.syntax(action, observation) + 0.7 * self.tests(action, observation)
rubric = CodeRubric()
reward = rubric(action, obs)
for name, r in rubric.named_rubrics():
print(name, r.last_score) # "syntax 1.0" / "tests 0.5" —— 可逐项审查
last_score 由 _finish_forward() 自动记录(base.py:95-103),这是可观测性的关键:训练时能看到每个子项各贡献了多少。Environment 的文档字符串里就演示了这个用法(interfaces.py:164-167)。
同步/异步双路
__call__(base.py:56-76)先用 inspect.iscoroutinefunction(self.forward) 判断,再走不同分支。注意注释里的一处讲究:同步路径的前置钩子必须在 forward() 之前调,而异步路径因为 forward() 返回的只是协程(还没执行),钩子可以在 _call_async 里补调。
现成的 rubric
| 类别 | 类 | 文件 |
|---|---|---|
| 容器 | Sequential、Gate、WeightedSum、RubricList、RubricDict | rubrics/containers.py |
| 轨迹 | TrajectoryRubric、ExponentialDiscountingTrajectoryRubric | rubrics/trajectory.py |
| 模型裁判 | LLMJudge | rubrics/llm_judge.py |