数据截至 (上游 commit 932e1f2f4c5a)
01 · 任务注册与 Prompt 构建
这一章讲什么: 一条命令行里的
"gsm8k|0"是怎么变成几千条带 few-shot 的 prompt 的。读完你能自己写一个自定义任务文件,也知道 few-shot 到底从哪个 split、按什么策略抽出来的。
1. 它要解决的小问题
评测库的第一道难题是多样性收纳:
- 任务有上千个,每个的数据集结构、题面格式、答案形式都不一样。
- 用户只想跑其中几个,还想现场改参数(几-shot、温度、是否过滤)。
- 社区贡献者要加任务,不该需要改框架代码。
lighteval 的回答:任务 = 一个声明式配置对象 + 一个目录约定。框架扫描目录收集所有配置,用户用一行小语言点名要跑谁。
2. 直觉:任务即配置,样本即 Doc
两个核心抽象先立起来:
任务侧——LightevalTaskConfig(src/lighteval/tasks/lighteval_task.py:48)把一个任务的全部信息装进一个 dataclass:
| 字段 | 作用 |
|---|---|
name / hf_repo / hf_subset | 任务名 + HuggingFace 数据集坐标 |
prompt_function | 一行数据集 → 一个 Doc 的转换函数 |
metrics | 要算哪些指标(决定怎么问模型,见第 3 章) |
evaluation_splits / few_shots_split / few_shots_select | 用哪个 split 评测、few-shot 从哪抽、按什么策略抽 |
generation_size / stop_sequence | 任务级生成参数 |
version | 数据集或 prompt 变了就 +1,用于结果对齐 |
样本侧——Doc(src/lighteval/tasks/requests.py:43)是「一道题」的统一表示:query(题面)、choices(候选答案)、gold_index(正确选项下标),外加运行期注入的 fewshot_samples、generation_size、stop_sequences 等。
直觉一句话:任务配置是「出卷模板」,prompt_function 是「誊写员」——它把数据集的每一行誊成标准答题卡 Doc,后面的批改流程只认答题卡,再也不关心原数据集长什么样。
3. 图示:从任务字符串到 Doc 列表
"gsm8k|0" 之类的任务字符串
│
▼
① Registry.load_all_task_configs 扫 tasks/ 目录,收集所有 TASKS_TABLE
→ {任务名: LightevalTaskConfig} (registry.py:344)
│
▼
② _update_task_configs 解析 "task|few_shot"、展开超集、
→ 覆写 num_fewshots 等参数 覆写 few-shot 数 (registry.py:184)
│
▼
③ LightevalTask.get_docs 下载数据集 → 每行过 prompt_function
→ list[Doc] → 洗牌 → 注入 few-shot 与生成参数
(lighteval_task.py:364)
│
▼
④ FewShotSampler + PromptManager 抽 few-shot 示例 → 拼成最终 prompt 文本
→ 发给模型 (prompt_manager.py)
怎么读这张图:①② 发生在 Pipeline.__init__ 早期(任务先初始化,「定义有问题就尽早炸」,src/lighteval/pipeline.py:140-142);③④ 在真正跑 模型前完成。
4. 原理演示:一个任务的最小骨架
下面这段演示「注册一个任务」需要的全部零件(示意,非源码;真实例子见 §5 的 gsm8k):
# 示意,非源码
from lighteval.tasks.lighteval_task import LightevalTaskConfig
from lighteval.tasks.requests import Doc
def my_prompt(line, task_name): # 誊写员:数据行 → 答题卡
return Doc(
query=f"Question: {line['q']}\nAnswer:",
choices=[f" {line['a']}"], # 候选答案
gold_index=0,
)
my_task = LightevalTaskConfig(
name="my_task",
prompt_function=my_prompt,
hf_repo="my-org/my-dataset", # HF 数据集坐标
hf_subset="default",
evaluation_splits=["test"], # 用 test 评
few_shots_split="train", # few-shot 从 train 抽
metrics=[Metrics.exact_match], # 评分标准
generation_size=64,
)
TASKS_TABLE = [my_task] # 目录约定:框架只认这个变量名
重点看最后那行:TASKS_TABLE 是框架与任务文件之间的唯一约定——Registry._extract_configs 只收集有这个变量的模块(src/lighteval/tasks/registry.py:319-324)。
5. 真实实现
5.1 注册:扫目录,收 TASKS_TABLE
Registry.load_all_task_configs(src/lighteval/tasks/registry.py:344-380)做三件事:
- glob
src/lighteval/tasks/tasks/*.py(本 commit 有 100 个文件),逐个importlib.import_module导入; - 对含
main.py的子目录(如ifeval/、lcb/)单独导入; - 可选加载多语言任务目录和用户自定义任务文件,自定义任务与内置任务撞名会直接报错(
registry.py:372-375)。
收集动作本身极薄——_extract_configs(registry.py:319-324)就是「模 块里有 TASKS_TABLE 就把里面每个 config 按 config.name 登记」。没有任何装饰器、没有元类,纯约定。
一个真实任务定义看 gsm8k(src/lighteval/tasks/tasks/gsm8k.py:67-86):prompt_function=gsm8k_prompt 把每行变成 Doc(query=f"Question: ...\nAnswer:", choices=[f" {answer}"], gold_index=0)(gsm8k.py:58-64),文件末尾 TASKS_TABLE = [gsm8k](gsm8k.py:88-90)。注意该文件还带了 solver/scorer/sample_fields 三个字段——那是给 inspect-ai 新入口用的双轨定义,见第 4 章。
5.2 点名:任务字符串小语言
_update_task_configs(src/lighteval/tasks/registry.py:184-245)解析三种写法:
| 写法 | 含义 |
|---|---|
gsm8k | 0-shot(默认) |
| `gsm8k | 5` |
task@k=v | 参数化指标,如给 pass@k 传 k/n(解析在 registry.py:208-213,用 ast.literal_eval 转类型) |
超集展开靠冒号命名约定:mmlu 会自动展开成 mmlu:abstract_algebra、mmlu:college_biology 等所有子任务——_task_superset_dict 把所有任务名按 : 前缀 groupby,子任务多于 1 个的前缀才算超集(registry.py:258-273),_expand_task_definition 命中超集就返回全部子任务(registry.py:275-291)。
解析后 config 会被 copy.deepcopy 再覆写 num_fewshots,full_name 变成 "gsm8k|0" 形态(registry.py:228-230)——deepcopy 是为了同一份注册配置能被多次点名、互不污染。
5.3 折样本:数据集行 → Doc
LightevalTask.get_docs(src/lighteval/tasks/lighteval_task.py:364-407)是任务侧的主循环:
eval_docs()把 evaluation split 每行过prompt_function(_get_docs_from_split,lighteval_task.py:284-323);formatter 返回None的行被丢弃(任务借此过滤样本)。- 用固定种子 42 洗牌(
lighteval_task.py:388-390)——所有 run 的样本顺序一致,这是可比性的第一层保障。 - 给每个 Doc 注入:few-shot 示例(
FewShotSampler.sample_fewshot_examples)、任务的生成参数、num_samples(lighteval_task.py:394-405)。
few-shot 从哪个 split 抽?get_first_possible_fewshot_splits(lighteval_task.py:259-282)按 train → dev → valid → default 的顺序挑第一个不在评测 split 里的;实在没有就用评测数据自己当 few-shot 池,并打一条 warning(lighteval_task.py:281)。
5.4 抽 few-shot:三种策略
FewShotSelection(src/lighteval/tasks/prompt_manager.py:188-193)定义了五种选择方式,归到三种排序策略:
| 策略 | 行为 |
|---|---|
sequential | 取池子前 N 个;按 num_fewshot × seed 轮转池子以换一批(prompt_manager.py:261-267) |
random | 种子非 0 时整体洗牌(prompt_manager.py:269-276) |
balanced(默认) | 按类别标签轮转抽取,保证各类答案均衡出现(prompt_manager.py:278-326) |
balanced 是默认且最值得看的:_init_fewshot_sampling_balanced 先按 fewshot_sorting_class(没有就用 gold 答案)把池子分桶,标签按桶大小降序排(同数随机打散),然后用 itertools.cycle 轮转各桶、每桶随机弹一个(prompt_manager.py:289-324)。
抽完还有一次「防泄题」:sample_fewshot_examples 会把恰好等于当前被测样本的 few-shot 剔除再截到 N 个(prompt_manager.py:230-231)——所以池子里其实多抽了一个(prompt_manager.py:308-310 注释说明)。
5.5 拼 prompt:chat template 与纯文本双路
PromptManager.prepare_prompt(src/lighteval/tasks/prompt_manager.py:48-57)按模型是否用 chat template 分流:
chat 路(_prepare_chat_template,prompt_manager.py:97-139):
- system:模型的
system_prompt(若有); - 每个 few-shot 样本 = 一对
user(题面)/assistant(get_golds()[0],即标准答案); - 最后一条 user 是当前题;
add_generation_prompt=True收尾,等模型接着写。
纯文本路(_prepare_plain_text,prompt_manager.py:141-165):system prompt + instruction + 每条 few-shot 的「题面 + 答案」+ 当前题,用 \n\n 拼接。
两路共用一个防重复小机关:_extract_query(prompt_manager.py:167-178)检查题面是否已以 instruction 开头——是就不再重复前置。chat 路里 instruction 只拼进第一个 few-shot 或主题面一次(prompt_manager.py:113-125),避免任务说明在 prompt 里出现 N 次。
6. 坑
suite|task|few_shot三段写法已废弃。 还兼容,但 suite 段被直接忽略并打 deprecation warning(registry.py:198-201);新写法是task|few_shot。- few-shot 可能来自评测集本身。 任务没配
few_shots_split且没有 train/dev/valid 可用时,few-shot 池就是评测数据(lighteval_task.py:281有 warning;fewshot_docs在lighteval_task.py:333-350)。对分数敏感时要留意这条路径。 - few-shot 数量会被「防泄题」吃掉一个。 若被测样本恰好进了 few-shot 池,剔除后实际 few-shot 数可能少于你点的 N(
prompt_manager.py:230-231)。 num_fewshot_seeds参数在当前主循环里没落线。 CLI 各入口都收这个参数,但Pipeline.evaluate只把它记进日志(src/lighteval/pipeline.py:272-276);多种子跑 few-shot 的FewShotSampler.get_fewshot_seeds(prompt_manager.py:328-335)在本 commit 的src/里没有调用方——看似是遗留管线,不要指望它生效。- 自定义任务撞名内置任务会直接拒绝加载(
registry.py:372-375),改名即可。 - 数据集下载可多进程但实现很薄。
LightevalTask.load_datasets用multiprocess.Pool并行(lighteval_task.py:418-439),网络抖动时失败即整体失败,没有重试逻辑(看不出有重试)。
7. 代码地图(本章)
| 主题 | 文件路径 | 符号名 |
|---|---|---|
| 任务注册总入口 | src/lighteval/tasks/registry.py | Registry.load_all_task_configs、_extract_configs |
| 任务字符串解析 | src/lighteval/tasks/registry.py | _update_task_configs、_task_superset_dict、_expand_task_definition |
| 任务配置与运行时 | src/lighteval/tasks/lighteval_task.py | LightevalTaskConfig、LightevalTask.get_docs、_get_docs_from_split |
| 统一样本 | src/lighteval/tasks/requests.py | Doc、SamplingMethod |
| few-shot 策略 | src/lighteval/tasks/prompt_manager.py | FewShotSampler、_init_fewshot_sampling_balanced、FewShotSelection |
| prompt 拼装 | src/lighteval/tasks/prompt_manager.py | PromptManager.prepare_prompt、_prepare_chat_template、_extract_query |
| 真实任务示例 | src/lighteval/tasks/tasks/gsm8k.py | gsm8k_prompt、TASKS_TABLE |
| 自定义任务模板 | examples/custom_tasks_templates/ | custom_yourbench_task.py 等三个模板 |