数据截至 (上游 commit 8b292c9f1b14)
第 1 章 任务模型 —— TaskData 与 Taskset
本章讲什么: verifiers 里「一道题」到底长什么样——数据和行为为什么劈成两半、打分钩子怎么注册和被覆盖、题库怎么做到「生成器当题库」还能复现采样。
1. 为什么把「题」劈成数据和行为两半
v1 的第一刀切在任务定义上:
TaskData——线上跑的那一半。 一个 frozen pydantic 模型(verifiers/v1/task.py:80-81),只装可序列化的题目数据:prompt、system_prompt、Docker 镜像、工作目录、网络白名单、要跨 runtime 搬运的产物清单、超时和资源需求。它会被序列化后从客户端发往 worker,也会原样记进traces.jsonl的trace.task.data(模块 docstring,task.py:1-8)。Task——行为那一半。 同样是这个类实例(task.py:137),挂着生命周期钩子(setup/finalize/validate)和打分方法(@reward/@metric)。它留在出题端,worker 拿到数据卡后由 taskset 重新构造出来。
好处很直接:线上只流不可变数据,行为(打分代码)不存在序列化/反序列化的安全问题,也不存在「worker 上的版本和出题端不一致」的问题。
2. TaskData:题目卡里有什么
除了 prompt,这张卡还替执行环境把需求说全了(task.py:80-117):
| 字段 | 干什么 | 备注 |
|---|---|---|
prompt / system_prompt | 初始用户 prompt / 系统 prompt | prompt=None 表示由「用户」先开口(task.py:90-93) |
image / workdir | 容器镜像 / 工作目录 | 只对容器类任务有意义(:95-98) |
network_allow / network_block | 该题执行时的出网白/黑名单 | ["*"] 不动 runtime 默认;prime runtime 要 host 级条目且需 vm=true(:100-108) |
artifacts | 要从 runtime 收回的文件路径 | 收集时缺失 = rollout 直接判失败(:110-114) |
timeout / resources | 分阶段超时 / CPU·内存·GPU·磁盘 | TaskTimeout(:65-77)、TaskResources(:50-62) |
还有两个只读但关键的身份字段:hash 是题目卡内容的 SHA-256(task_key,task.py:45-47),key 默认等于 hash,但当数据里带运行期字段(比如自增 idx)时应覆盖成数据集的持久 ID——--resume 靠它认出「这道题跑过了」(:151-159,key 在 taskset 内必须唯一)。
3. Task:行为都在钩子上
Task 的四个可覆盖点(task.py:166-173):setup(trace, runtime)(开局搭 环境)、finalize(trace, runtime)(收尾)、validate(runtime)(这道题当前环境能不能跑)、以及打分 score。
打分不是一个大函数,而是一堆小钩子:
class AdditionTask(vf.Task[AdditionData]): # 示意,非源码(节选自官方示例)
@vf.reward
async def exact_match(self, trace: vf.Trace) -> float:
return float(trace.last_reply == str(self.data.answer))
Task.score 的执行顺序(task.py:175-243):
- 收集
@metric和@reward钩子,外加 config 里插进来的 judge; - 没有 runtime 时自动跳过声明了必需
runtime参数的信号(:180-210)——这让同一份打分代码既能在线(跑完就评)也能离线(只有 trace 文件)用; - 先把所有信号名 seed 进
trace.metrics/trace.rewards(占位,缺测也能看出少了谁),再并发执行,逐个record_reward(key, value, weight)落账(:212-243)。
钩子的合并规则值得单独说(Task.hooks,task.py:258-276):config 里插进来的同名函数会替换装饰器方法,再按「优先级降序、名字升序」排序。也就是说,taskset 作者给默认打分,用户可以在 TOML/CLI 里不改代码就换评分函数——评测的可比性和可定制性各退一步又各进一步。
4. Taskset:只会一件事——出题
Taskset 是个抽象类,唯一的抽象方法是 load() -> Iterable[TaskT](verifiers/v1/taskset.py:50-52)。泛型参数把 task 类型和 config 类型钉死(Taskset[TaskT, ConfigT]),一个 taskset 只出一种 task(模块 docstring,taskset.py:1-13)。
迭代路径和加载路径是分开的(__iter__,taskset.py:54-62):读的时候先套上 config 层的 system_prompt 覆盖(with_system_prompt,task.py:161-164),再应用视图变换。