跳到主要内容

数据截至 (上游 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_samplesgeneration_sizestop_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)做三件事:

  1. glob src/lighteval/tasks/tasks/*.py(本 commit 有 100 个文件),逐个 importlib.import_module 导入;
  2. 对含 main.py 的子目录(如 ifeval/lcb/)单独导入;
  3. 可选加载多语言任务目录和用户自定义任务文件,自定义任务与内置任务撞名会直接报错(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)解析三种写法:

写法含义
gsm8k0-shot(默认)
`gsm8k5`
task@k=v参数化指标,如给 pass@k 传 k/n(解析在 registry.py:208-213,用 ast.literal_eval 转类型)

超集展开靠冒号命名约定:mmlu 会自动展开成 mmlu:abstract_algebrammlu: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)是任务侧的主循环:

  1. eval_docs() 把 evaluation split 每行过 prompt_function(_get_docs_from_split,lighteval_task.py:284-323);formatter 返回 None 的行被丢弃(任务借此过滤样本)。
  2. 用固定种子 42 洗牌(lighteval_task.py:388-390)——所有 run 的样本顺序一致,这是可比性的第一层保障。
  3. 给每个 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_docslighteval_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_datasetsmultiprocess.Pool 并行(lighteval_task.py:418-439),网络抖动时失败即整体失败,没有重试逻辑(看不出有重试)。

7. 代码地图(本章)

主题文件路径符号名
任务注册总入口src/lighteval/tasks/registry.pyRegistry.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.pyLightevalTaskConfigLightevalTask.get_docs_get_docs_from_split
统一样本src/lighteval/tasks/requests.pyDocSamplingMethod
few-shot 策略src/lighteval/tasks/prompt_manager.pyFewShotSampler_init_fewshot_sampling_balancedFewShotSelection
prompt 拼装src/lighteval/tasks/prompt_manager.pyPromptManager.prepare_prompt_prepare_chat_template_extract_query
真实任务示例src/lighteval/tasks/tasks/gsm8k.pygsm8k_promptTASKS_TABLE
自定义任务模板examples/custom_tasks_templates/custom_yourbench_task.py 等三个模板