第 10 章 lm-evaluation-harness:学术评测的事实标准
“If you cannot reproduce the number, the number is fiction.” —— 一条 EleutherAI 评测组内部流传的格言
本章要点
- lm-evaluation-harness(简称 lm-eval)的整体架构:Task / LM / Instance / Filter 四层抽象
- LM 基类的四种
OutputType:loglikelihood / loglikelihood_rolling / generate_until / multiple_choice - Task 子类化与 YAML 配置驱动:214 个 task 是怎么挂载的
- Metrics 库的 30+ 个内置指标:从 mean / perplexity 到 BLEU / chrF / Matthews CC
- 与 OpenAI evals 的对比:为什么学术界几乎没人用 OpenAI evals 报数字
10.1 仓库位置与版本
本章源码引用基于 EleutherAI/lm-evaluation-harness 主线版本。读者可通过下面命令获取:
git clone https://github.com/EleutherAI/lm-evaluation-harness.git
cd lm-evaluation-harness
仓库的整体规模:核心代码(lm_eval/api/)约 4500 行,task 配置(lm_eval/tasks/)214 个目录、几千份 YAML,lm_eval/models/ 适配了 30+ 种推理后端(HuggingFace transformers、vLLM、OpenAI / Anthropic / Gemini API、Mamba 等)。
它的核心地位来自一个事实:几乎所有 LLM 论文里报告 MMLU / HellaSwag / ARC / GSM8K / BIG-Bench-Hard 等 benchmark 数字时,背后跑的都是这个仓库。OpenAI、Anthropic、Google DeepMind、Meta、Mistral、Llama 团队的 model card 都明确使用 lm-eval-harness 做横向对比——它是大模型时代的”度量衡”。
10.2 四层核心抽象
flowchart TB
subgraph 任务层
T[Task 子类]
YAML[YAML 配置]
DS[HuggingFace Datasets]
end
subgraph 请求层
I[Instance]
end
subgraph 模型层
LM[LM 基类]
MAdapter[Model Adapter<br/>HF/vLLM/OpenAI/...]
end
subgraph 后处理层
F[Filter Pipeline]
M[Metrics]
end
YAML --> T
T --> DS
T -->|生成 requests| I
I --> LM
LM --> MAdapter
MAdapter -->|response| LM
LM -->|结果| F
F --> M
M --> Out[最终分数]
style T fill:#dbeafe
style LM fill:#dcfce7
style F fill:#fef3c7
style M fill:#fce7f3
四层各自职责清晰:
- Task 定义”评什么”——数据集、prompt 模板、评分公式
- Instance 是”一次请求”的最小单元
- LM 是模型调用的抽象——任何推理后端只要实现 LM 协议都能接入
- Filter / Metrics 后处理:抽答案、归一化、聚合
下面逐层解读源码。
10.3 LM 基类:四种 OutputType
lm_eval/api/model.py:25 定义抽象基类 LM(abc.ABC),要求子类实现两个核心方法:
class LM(abc.ABC):
"""Abstract base class for language models."""
@abc.abstractmethod
def loglikelihood(self, requests: list["Instance"]) -> list[tuple[float, bool]]:
"""Compute log-likelihood of generating a continuation from a context."""
pass
@abc.abstractmethod
def loglikelihood_rolling(self, requests: list["Instance"]) -> list[float]:
"""Compute full log-likelihood of a string, with no truncation."""
pass
注意 loglikelihood 返回 (logprob, is_greedy) 二元组——is_greedy 表示 “这个 continuation 是否是 greedy decoding 会产生的”。这是个细节但很重要——很多 multiple-choice 评测要看模型在 K 个选项中最大概率那一个是不是正确选项,is_greedy 直接给出这个信息。
OutputType 在 lm_eval/api/instance.py:5-7:
OutputType = Literal[
"loglikelihood", "loglikelihood_rolling", "generate_until", "multiple_choice"
]
四种类型对应四种评测形态:
| OutputType | 评测形态 | 典型 task |
|---|---|---|
| loglikelihood | 单段 continuation 的概率 | HellaSwag、Winogrande |
| loglikelihood_rolling | 整篇文本的 perplexity | wikitext、PG19 |
| generate_until | 自由生成直到遇到 stop sequence | GSM8K、BBH、HumanEval |
| multiple_choice | A/B/C/D 多选 | MMLU、ARC |
这四种类型几乎覆盖了所有主流 benchmark。值得注意的是 lm-eval 不是基于 chat completion API 设计的——它的核心是 logprob 接口,因为很多 benchmark(特别是 multiple-choice)需要看模型对各选项的相对概率,而不只是文本输出。
这就是为什么对 OpenAI / Anthropic 这种只暴露 chat completion API 不暴露 logprob 的厂商,lm-eval 的 multiple_choice 走法只能退化到 generate_until + 字符匹配——精度比 logprob 差。
10.4 Task 子类与 YAML 配置驱动
10.4.1 Task 基类
lm_eval/api/task.py:62 定义 Task(abc.ABC),要求子类实现:
dataset(self):从 HuggingFace datasets 加载doc_to_text(doc):把数据集 doc 转成 prompt 文本doc_to_target(doc):拿出 ground truthprocess_results(doc, results):从模型 response 算单条样例分数aggregation(self) → dict:把单条样例分数聚合成 task-level metric
这套接口逼迫 task 作者把”prompt 模板”和”判分逻辑”写得显式、可审查——这是学术评测可复现性的工程基石。
10.4.2 214 个 task 的 YAML 驱动方式
仓库的 lm_eval/tasks/ 下有 214 个子目录,每个目录是一个 benchmark。绝大部分新 task 不需要写 Python,只需要写 YAML——以 MMLU 为例(tasks/mmlu/default/_default_template_yaml):
dataset_path: cais/mmlu
test_split: test
fewshot_split: dev
fewshot_config:
sampler: first_n
output_type: multiple_choice
doc_to_text: "{{question.strip()}}\nA. {{choices[0]}}\nB. {{choices[1]}}\nC. {{choices[2]}}\nD. {{choices[3]}}\nAnswer:"
doc_to_choice: ["A", "B", "C", "D"]
doc_to_target: answer
metric_list:
- metric: acc
aggregation: mean
higher_is_better: true
metadata:
version: 1.0
这一份 12 行的 YAML,定义了一个完整的 MMLU 子任务:
- dataset_path: cais/mmlu:直接拉 HuggingFace 上的官方数据集
- doc_to_text:Jinja2 模板,把
{question, choices}渲染成”题目 + ABCD 选项 + Answer:“的 prompt - doc_to_choice / doc_to_target:4 个选项 + ground truth 字段
- metric_list:用
acc指标、mean聚合、higher_is_better - metadata.version:版本号——MMLU 的判分规则若有更新,version bump
这套 YAML 驱动的好处是贡献门槛极低——任何研究者要加新 benchmark 只需提一份 YAML PR,不用懂 Python 代码。这是为什么 lm-eval 仓库能积累 214 个 task:贡献者不被代码门槛吓退。
10.4.3 MMLU 的子任务展开
仓库的 tasks/mmlu/default/ 下有 57 个 yaml 文件——MMLU 标准包含 57 个学科子任务(abstract_algebra、anatomy、astronomy、business_ethics、…)。每个学科一份 YAML,统一继承 _default_template_yaml。这种”模板继承”机制让维护成本极低——共有逻辑只写一次。
10.5 Metrics 库:30+ 个内置指标
lm_eval/api/metrics.py(649 行)实现了 30+ 个评测指标的计算函数:
| 指标 | 用途 | 行号位置 |
|---|---|---|
| mean / median | 基础聚合 | metrics.py 上半段 |
| perplexity / weighted_perplexity / bits_per_byte | 语言模型困惑度 | 中段 |
| f1_score | F1(用于 SQuAD 这类抽取式 QA) | 中段 |
| matthews_corrcoef | Matthews 相关系数(适用极不平衡分类) | 中段 |
| bleu / chrf / ter | 翻译评测 | 中段 |
| brier_score | 概率校准评测 | 中段 |
| acc / acc_norm | 准确率 / 归一化准确率 | 多个 *_fn |
| exact_match | 精确匹配(用 HuggingFace evaluate) | exact_match_hf_evaluate |
| pop_stddev / sample_stddev / mean_stderr | 统计推断 | 末段 |
值得专门讨论的是 acc_norm——它做”长度归一化的 accuracy”,把每个选项的 logprob 除以选项的字符数,避免短选项有不公平优势。这种细节是学术评测和工业评测的真正差异点:学术评测必须把每一个潜在的 confounder 都排除到底,工业评测往往容忍。
10.6 与 OpenAI evals 的设计对比
学术界几乎没人用 OpenAI 的 evals 仓库报数字。原因是设计哲学的根本差异:
| 维度 | lm-eval-harness | OpenAI evals |
|---|---|---|
| 接口核心 | logprob | text completion |
| 任务定义 | YAML 优先 | YAML + Python 混合 |
| 模型支持 | 30+ 后端(HF/vLLM/all APIs) | 主要 OpenAI 系 |
| 任务数量 | 214 个 | 100+(registry 不全) |
| 输出格式 | 严格 reproducibility | 偏 demo |
| 主要受众 | 研究者 / 模型方 | 模型评测员 |
简言之:lm-eval 是为”复现论文数字”设计的——它的工程方方面面都为可复现性服务(YAML 模板继承、metadata.version、acc_norm 长度归一化、严格的 logprob 接口)。OpenAI evals 是为”快速跑评测”设计的——它的工程方方面面都为开发者迭代速度服务。
工业团队应该这么选:
- 想做”模型横向对比”(如选哪个基础模型)→ lm-eval-harness
- 想做”应用层评测”(如比较两个 prompt 哪个好)→ OpenAI evals 或 promptfoo
- 想做”RAG 系统评测” → ragas(详见第 11 章)
10.7 lm-eval 的工程亮点:5 个值得借鉴的设计
graph TB A[lm-eval 工程亮点] --> B[Filter 流水线<br/>抽答案的可组合管道] A --> C[Cache 层<br/>SqliteDict 持久化中间结果] A --> D[Decontamination 工具<br/>测训污染检测] A --> E[Multi-task 并行<br/>同一份 logprob 喂给多个 task] A --> F[Few-shot 模板继承<br/>YAML 复用] style A fill:#fef3c7
每个亮点都对应一个可学习的工程模式:
- Filter 流水线(
lm_eval/api/filter.py、lm_eval/filters/):模型输出经过一组可组合的 filter 抽取最终答案。例如 GSM8K 用[take_first, regex_extract_number]这样的链式 filter - Cache 层(
lm_eval/caching/cache.py):用 SqliteDict 持久化 logprob 计算结果——重跑不需要重算 - Decontamination(
lm_eval/decontamination/):内置工具检测测训污染(n-gram overlap),这是学术界对”data contamination”问题的工程响应 - Multi-task 并行:同一个模型 forward pass 的 logprob 可以同时为多个 task 评分——大幅降低 GPU 时间
- Few-shot 模板继承:MMLU 57 个子任务共用一份模板的机制
这五个设计是 lm-eval 经过 3 年迭代沉淀的工程财富——内部 fork 改造时这些点都值得保留。
10.7.5 为什么所有大模型论文都用它:可复现性的工程含义
如果只看技术接口,lm-eval-harness 似乎只是一个”通用 eval runner”。但它在学术界的统治地位远超工程价值——OpenAI、Anthropic、Google、Meta、Mistral、DeepSeek、Qwen 等几乎所有大模型团队的 model card 都明确使用 lm-eval-harness 报告 benchmark 数字。
为什么是它?根本原因是可复现性的承诺被工程化兑现:
- YAML 配置 + git tag:每个 task 的版本号写在 metadata 里。论文可以注明 “evaluated using lm-eval-harness commit abc123 on task mmlu version 1.0”,读者能精确重跑得出同样数字
- prompt 模板透明:所有 prompt 模板都在公开 yaml 里,没有”内部魔法 prompt”
- 数据集 hash 锁定:HuggingFace datasets 的 commit hash 锁住具体数据版本
- logprob 接口标准化:multiple_choice 评测的”取概率最大的选项”逻辑统一,不会因实现方式不同导致数字差异
这种”可复现性”在工业评测里是常被忽视的——团队有时报告”我们的模型在 MMLU 上 85%“,但用不同框架跑可能得到 80% 或 90%。lm-eval 把这种不确定性消除到最低限度。
工业团队即使不发论文,也应该把 lm-eval-harness 作为”模型横向对比”的事实标准——你切换基础模型时,跑一次 MMLU/HellaSwag/ARC 比无数自家 benchmark 更能让团队达成共识。这是它在工业场景的隐藏价值。
10.7.6 学术 vs 工业评测的张力
读完 lm-eval-harness 与 OpenAI evals 之后会感受到一种张力——学术与工业评测的优先级根本不同:
- 学术:可复现性 > 速度 > 易用性
- 工业:易用性 > 速度 > 可复现性
学术追求”任何人重跑都得到相同数字”,所以 prompt 模板锁死、metric 严格定义、版本化到 commit 级别。工业追求”快速迭代得到决策信号”,所以 prompt 可以日常微调、metric 可以临时改造、版本化粗放。
这种张力没有”对错”——它们解决不同的问题。一个成熟的 LLM 团队会同时维护两套评测:
- 用 lm-eval-harness 做”基础模型选型”——每次切换底模时跑学术 benchmark
- 用 promptfoo / ragas / OpenAI evals 做”应用层迭代”——每次改 prompt / retriever 跑业务 benchmark
理解这种张力,团队才不会浪费工程时间在”用错的工具解决错的问题”上。
10.7.7 HuggingFace Datasets 集成的工程价值
lm-eval-harness 的一个被低估的设计——所有 task 的数据都通过 HuggingFace datasets API 加载。这不是技术选型偶然,而是有深远工程意义。
# task yaml 里典型的数据加载配置
dataset_path: cais/mmlu # HF datasets 仓库地址
dataset_name: abstract_algebra # 子集名(可选)
test_split: test # 用哪个 split
fewshot_split: dev
HuggingFace datasets 的工程价值:
- 数据集版本锁定:HF 上每个 dataset 有 commit hash,YAML 可以锁定到具体版本。论文报告”在 mmlu commit abc123 上跑出 78.3%“读者能精确复现
- 流式加载:超大数据集(如 BIG-Bench 1000+ task、数百万样例)支持流式,不需要全量下载
- 统一缓存:第一次拉到本地后所有后续运行都用本地缓存,离线能跑
- 跨语言可读:一份 dataset 可以同时被 Python / Rust / 其他语言读取
- 生态工具丰富:filtering / mapping / batching 等操作都是 datasets 库内置
- 预处理可追溯:dataset 的 transform 操作(如 tokenization)会被缓存为新版本
工业 fork 这套框架时,最关键的一步是保持 datasets 集成不变。即使把 dataset 替换成内部数据,也建议把它转成 HuggingFace datasets 格式(用 datasets.Dataset.from_pandas 或 from_dict)——这样能继承 lm-eval 整个数据加载生态。
第 13 章 RAG 评测的 ragas + Dataset.from_list 用的正是这套机制——HuggingFace datasets 已经是 LLM 工程领域事实上的”数据交换格式”。
10.7.8 commercial API 后端的限制:lm-eval 的盲区
lm-eval 在开源模型 / 本地部署场景几乎完美。但接 commercial API(OpenAI / Anthropic / Gemini)时存在两个工程限制:
限制 1:multiple_choice 评测精度下降
lm-eval 的 multiple_choice 走法依赖 logprob 接口——直接拿 4 个选项的概率比大小。但商业 API 普遍不暴露 logprob(OpenAI 自 2024 起官方接口不支持、Anthropic 从未支持)。
lm-eval 的应对是退化到 generate_until + 字符匹配——让模型自由生成 “A” / “B” / “C” / “D”,再匹配第一个字符。这种方式:
- 失去了”看 4 选项相对概率”的细粒度
- 容易被模型的”emit format”差异污染(输出 “Answer: A” vs “(A)” 都是同一类失败模式)
- 论文上无法直接对比开源模型(用 logprob)和商业模型(用 generate)的数字
限制 2:成本与延迟
lm-eval-harness 设计成”批量并行”——HuggingFace transformers / vLLM 后端支持一次推理上千 token 的 batch。但商业 API 的 RPM / TPM 限流让 batch 几乎失效,跑一次完整 MMLU(14k 题):
| 后端 | 时间 | 成本 |
|---|---|---|
| vLLM 本地部署 (8×A100) | 30 分钟 | 几乎免费 |
| OpenAI API (gpt-4o) | 6-12 小时 | $200-400 |
| OpenAI API (gpt-4o-mini) | 4-8 小时 | $20-40 |
对工业团队的工程含义:lm-eval 适合”研究模型本身”,不适合”日常应用层评测”。第 9-12 章的 OpenAI evals / promptfoo / ragas 才是后者的工具。
10.7.9 中文 LLM 评测的特殊挑战与 lm-eval 的中文 task 生态
中文 LLM 评测在 lm-eval-harness 上有一个值得讨论的演化——早期它的中文 task 极少,后来由社区贡献涌入。截至 2026 年初,仓库 tasks/ 下中文相关 task 已超过 30 个,包括 CMMLU(中文版 MMLU)、C-Eval(综合中文评测)、AGIEval、CLUE、SuperCLUE 等多个权威 benchmark。
中文评测有几条与英文不同的工程难题:
- 分词差异:英文 BPE tokenizer 在中文上效率低(一个汉字可能切 2-3 个 token)
- 同义表达多:英文 “Paris” 唯一,中文”巴黎 / 法国首都 / 法国巴黎”全都对
- 繁简兼容:同一概念在简体 / 繁体下可能是不同 token,影响 logprob 一致性
- 领域知识本地化:法律、医疗、金融等专业领域的中文术语库与英文 benchmark 完全不同
工业团队跑中文 LLM 评测时建议组合:
- 基础能力:跑 CMMLU、C-Eval(覆盖中文学科知识)
- 推理能力:跑 AGIEval(含中文公考、高考、法考题)
- 生成能力:跑 SuperCLUE 的开放式题
- 加自家业务集:每个团队自己的客服 / 政策 / 业务对话集
这套组合能给出完整的”中文 LLM 在你业务上的能力画像”。
10.7.10 多模型并发跑评测:lm-eval 的工程性能优化
lm-eval-harness 在大规模评测里的隐藏价值是性能优化——它不只是”能跑”,是”快速跑通几万样例”。这一点对工程团队评估影响很大。
具体优化点(来自源码与官方文档):
- batch inference:把多条样例的 forward pass 打包,HuggingFace transformers / vLLM 后端原生支持
- 缓存重用:同一条 prompt 的 logprob 跨 task 共享(如 MMLU 的 fewshot prompt 在多个学科子任务里复用)
- 进程并行:多 GPU 时各自跑不同 task,主进程聚合结果
- 流式数据加载:超大数据集(百万级)不需要全量进内存
- 断点续跑:评测中途崩溃后能从上次失败处继续
这套优化让”在 vLLM 后端跑完整 MMLU + HellaSwag + ARC + GSM8K”在 8×A100 上仅需 2-4 小时——而朴素实现可能要 30+ 小时。
工程团队选 evaluator 时这一点常被忽视——很多自建评测脚本没做并行 / 缓存,跑一次评测要一整天,团队就慢慢不跑了。lm-eval 的”工程级性能”是它能成为学术事实标准的硬实力之一。
工业 fork 时建议保留这套优化机制,特别是 batch + 缓存——它们是单元测试覆盖之外的”无形资产”,迁移到内部框架时容易被丢弃。
10.7.11 一份 lm-eval 的真实使用场景:模型选型决策
工程团队选基础模型时(比如从 GPT-4o-mini 切到 Claude 3.5 Haiku),怎么决定?lm-eval 提供的是”客观对比”路径——而非”我们自己跑两条 query 看感觉”。
具体做法:
# 跑 GPT-4o-mini
lm_eval --model openai-chat-completions \
--model_args model=gpt-4o-mini \
--tasks mmlu,gsm8k,truthfulqa_mc1,humaneval \
--output_path results/gpt-4o-mini.json
# 跑 Claude 3.5 Haiku
lm_eval --model anthropic-chat \
--model_args model=claude-3-5-haiku \
--tasks mmlu,gsm8k,truthfulqa_mc1,humaneval \
--output_path results/claude-haiku.json
# 跑你自己 fine-tune 的模型(vLLM 部署)
lm_eval --model vllm \
--model_args pretrained=./my-finetuned-model,tensor_parallel_size=2 \
--tasks mmlu,gsm8k,truthfulqa_mc1,humaneval \
--output_path results/my-model.json
跑出 4 个 task × 3 个模型 = 12 个数据点的横向对比,几小时内得到决策依据。这比”找 5 个 SaaS 跑一遍业务集”的传统做法可信得多——因为 lm-eval 上的题是固定 benchmark、跑出的数字可与论文 / model card 直接对照。
工业团队通常的模型选型流程:
- lm-eval 跑 4-5 个核心 benchmark:MMLU / GSM8K / HumanEval / TruthfulQA / HellaSwag
- 选出 top 3 候选:在你的预算 / 延迟约束内综合评分最高的 3 个
- 在自己的业务集上跑:用 promptfoo / ragas 做最终选型
第 1、2 步用 lm-eval 客观对标,第 3 步用应用工具做业务相关验证——两者结合是模型选型的正确流程。
10.7.12 lm-eval 与 vLLM 的紧密集成
lm-eval 的 vLLM 后端是工业界首选——vLLM(《vLLM 推理引擎》第 5 章详述)的 PagedAttention + 高并发让评测吞吐量远超直接调 transformers。
实测对比(公开 benchmark 数据):
| 后端 | MMLU 14k 题完整跑完 | 硬件 |
|---|---|---|
| HuggingFace transformers | ~6 小时 | 1×A100 |
| vLLM | ~30 分钟 | 1×A100 |
| vLLM tensor_parallel=4 | ~10 分钟 | 4×A100 |
20 倍以上的速度提升完全来自 vLLM 的 batching 优化——这就是为什么国内外大模型团队评测都用 vLLM 而非裸 transformers。
工业 lm-eval 部署的标配:
# 启动 vLLM service
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--tensor-parallel-size 4 \
--max-model-len 8192
# lm-eval 通过 OpenAI 兼容 API 接入
lm_eval --model local-completions \
--model_args base_url=http://localhost:8000/v1,model=meta-llama/Llama-3.1-8B-Instruct \
--tasks mmlu_pro
这种”vLLM service + lm-eval client”的解耦让评测和推理服务独立扩缩——评测高峰期可以临时拉起更多 vLLM 实例。
10.7.13 一个工程教训:lm-eval 的”benchmark 偏差”
lm-eval 给的 benchmark 数字非常可信,但有一个工程教训:benchmark 不能完全反映你业务的真实能力。
经典反例:某模型在 MMLU 上 85%,业务团队接入后发现实际客服任务上效果只有 65%。原因:
- MMLU 测的是”百科知识”,与”客服礼貌响应 + 准确引用政策”是不同能力
- MMLU 题目格式(多选)与真实开放对话格式差异大
- 模型可能在 MMLU 数据上有过拟合(公开数据集污染)
这意味着:lm-eval 给的是”模型相对位置”信号,不是”绝对业务表现”判断。工业实践:
- 用 lm-eval 把候选模型从 10 个筛到 3 个(节省时间)
- 用 promptfoo / ragas 在自家业务集上跑这 3 个候选(拿到真实表现)
- 综合两个信号做最终选型
跳过任一步都会出问题——只看 lm-eval 选模型经常事后扑街,只看业务集忽略 lm-eval 经常错过更强的候选。
10.7.14 lm-eval 在 LLM 工程师面试题中的角色
lm-eval-harness 在 2025 年开始成为高级 LLM 工程师面试的”基础知识”——理解它的内部抽象 + 能解释为什么某个 benchmark 数字这样跑出来,已经是国内一线大厂招聘 LLM 工程师的常见考点。
典型面试题:
- “MMLU 的 5-shot accuracy 是怎么计算的?为什么要 5-shot 而不是 0-shot?”
- “为什么 lm-eval 的 multiple_choice 评测在商业 API 上不准?”
- “如果让你给一个新 benchmark 写 yaml,你会怎么设计 metric_list?”
- “lm-eval 与 OpenAI evals 的接口设计差异在哪?为什么这两套不一样?”
读完本章你能回答这些题。这种”工具知识”在 LLM 工程领域和”分布式系统理论”在后端领域是同等重要——理解工具背后的设计权衡,才能在工程决策时不被表面的功能特性带偏。
10.7.15 lm-eval 的演化方向:从 task 库到评测平台
仓库 2024-2025 年的演化方向也值得关注:
- 更多 reasoning task:增加多步推理 / chain-of-thought 评测的支持
- 更多多语言 task:早期偏英文为主,正在加多语言 benchmark(CMMLU / Indic-Eval / 阿拉伯语 LLM 评测等)
- vLLM / SGLang 后端深度集成:性能优化继续投入
- 结果可视化 dashboard:从纯 CLI 工具向 web UI 延伸
这些演化让 lm-eval 从”研究工具”逐步走向”工业可用的评测平台”。但它的核心定位——可复现性优先 + benchmark 报数字标准——不会变。这种”做一件事做到行业标准”的项目演化哲学,比”试图覆盖所有用例”更可持续。
10.7.16 一个学术评测的根本痛点:Benchmark Contamination
学术 LLM 评测的最大头疼问题是 benchmark contamination(数据污染)——模型在预训练数据里见过 benchmark 题目,跑出虚高分数。
具体表现:
- MMLU、HumanEval 等公开 benchmark 的题目早已被模型方爬取
- 部分模型的训练集甚至包含 benchmark 解析(“this is the answer to MMLU question X”)
- 跑出 95% 准确率 ≠ 模型真懂,可能只是”背过”
学术界的应对:
- n-gram overlap 检测:把 benchmark 与训练集做 n-gram 重叠度检测,超过阈值的视为污染(Brown et al. 2020 GPT-3 论文方法)
- 私有变体:把 benchmark 题改写成等价但用词不同的私有版(HELM 等做法)
- 动态 benchmark:从 Reddit / Twitter 等持续更新的数据源构造(如 LiveBench、Chatbot Arena)
- Holdout 题集:保留小部分题不公开,用于真实评估
工业团队的应对:
- 不依赖单一 benchmark:lm-eval 跑出的数字配合自家私有评测集
- 看新发布的动态 benchmark:LiveBench / Chatbot Arena 在 2024-2026 年是更可信的”未污染”选择
- 怀疑过高的分数:单一 benchmark > 95% 通常说明污染,不是真能力
这是 lm-eval-harness 给的”benchmark 数字”必须配合”业务集验证”的根本原因。学术评测和工业评测不是互替关系——是互补关系。
10.7.17 lm-eval 的并发架构剖析
仓库 lm_eval/api/registry.py(719 行)和 lm_eval/evaluator.py 一起实现了 lm-eval 的多任务并发架构。核心设计:
- task 注册表:所有 task 在启动时被注册成 dict,便于按 name 查找
- request collation:把多个 task 的 logprob requests 合并到同一批
- batch dispatch:合并后的 request 一次发给 model backend
- result demultiplex:response 收到后按 task / request 拆回各自结果
- lazy evaluation:task 的 metric 只在最后聚合阶段才算,不影响中间速度
这种 collate-batch-demultiplex 的并发模式让”跑 10 个 task 不比跑 1 个慢多少”——只要每个 task 的请求规模差异不大,batch 利用率就高。
工业 fork 时这个并发机制要保留——很多团队 fork 后只关注 task / metric 部分,忽略并发架构,结果性能下降一个数量级。
10.7.18 lm-eval 的”task 编年史”:哪些 benchmark 是真重要
仓库 214 个 task 不是同等重要。从论文引用频率看,工业团队真正应该关心的”top 10 task”:
| Task | 测什么 | 工业重要性 |
|---|---|---|
| MMLU / MMLU-Pro | 综合知识 | ⭐⭐⭐⭐⭐ |
| HellaSwag | 常识推理 | ⭐⭐⭐⭐ |
| ARC-Challenge | 科学推理 | ⭐⭐⭐⭐ |
| GSM8K | 数学题 | ⭐⭐⭐⭐⭐ |
| MATH | 高级数学 | ⭐⭐⭐⭐ |
| HumanEval | 代码生成 | ⭐⭐⭐⭐⭐ |
| MBPP | 代码补全 | ⭐⭐⭐ |
| TruthfulQA | 抗幻觉 | ⭐⭐⭐⭐⭐ |
| Winogrande | 指代消解 | ⭐⭐⭐ |
| BIG-Bench-Hard | 综合难题 | ⭐⭐⭐⭐ |
跑这 10 个 task 已经覆盖了大部分主流 LLM 论文报告的能力维度。工业团队选模型时跑这 10 个就够,不必跑全部 214 个——边际信息几乎为零。
10.7.19 lm-eval 的”小心翼翼”哲学
仓库的工程哲学最值得学习的一点——对每个数字小心翼翼。
- 默认
seed=1234写死,让结果可复现 - few-shot example 顺序固定,避免随机性
- prompt 模板进 yaml,每一个版本都有 git history
- metric 实现配单元测试,避免计算错误
- 输出格式带详细 metadata(model_name, task_version, eval_time)
- 警告 / 错误日志详细,帮助 debug
这种”小心翼翼”的做事方式与学术界对可复现性的执念高度同构。如果你写的论文 / 模型 release 数字想被认真对待,必须采用类似哲学。
工业评测虽然不必这么严苛,但理解 lm-eval 的这种”匠人精神”能让团队在工程纪律上拔高一个层次。一个普遍现象:跑过 lm-eval 几次的工程师,写自家评测时会自动加更多 reproducibility 保障——这是工具反向影响人的工程素养。
10.7.20 lm-eval 与中国大模型生态的对接
国内大模型(豆包 / Qwen / DeepSeek / Kimi / GLM 等)大量使用 lm-eval 报数字。具体对接方式:
# 通过 OpenAI compatible API 接入国内模型
lm_eval --model local-completions \
--model_args base_url=https://api.deepseek.com/v1,model=deepseek-chat \
--tasks mmlu,gsm8k,humaneval,truthfulqa_mc1
国内模型在 lm-eval 上跑的几个特殊点:
- API 兼容:大部分国内模型已经提供 OpenAI 兼容 API,直接接入
- rate limit 严:相比 OpenAI 国内 provider 的 rate limit 更紧,要降低并发
- 中文 task:不只跑 MMLU 等英文 benchmark,要跑 CMMLU / C-Eval / SuperCLUE
- Token 计费差异:国内模型 input / output 价格可能不同于 OpenAI,要按各家定价计算
国内大模型公开发布时,model card 上的 “MMLU 85%” 一般就是用 lm-eval 跑出来的——国际同行能直接对照 GPT-4 / Claude / Gemini 数字。这种”数字可比性”是 lm-eval 给国内大模型团队的关键工程价值。
10.7.21 lm-eval 在工业 LLM Gateway 后的接入位置
工业大型团队通常在 LLM 调用前过一层 LLM Gateway(详见 §6.6.8 / §17.10)。lm-eval 接入这个 Gateway 时的工程考量:
flowchart LR Eval[lm-eval client] -->|OpenAI compatible| GW[LLM Gateway] GW -->|路由| M1[OpenAI] GW -->|路由| M2[Anthropic] GW -->|路由| M3[内部模型] GW -->|审计 / 限流| Audit[审计日志] style GW fill:#fef3c7 style Audit fill:#dcfce7
Gateway 给评测带来的好处:
- 统一鉴权:评测共享生产 LLM 配额,账目清晰
- 多 provider 切换:lm-eval 一次跑出多家模型对比
- 审计 trail:评测调用与生产调用同样有日志
- 成本控制:Gateway 层做评测专用的预算上限
这是国内大厂 LLM 工程团队的标配架构。lm-eval 在这个架构下接 Gateway 而非直接接 model,把”评测”和”调用基础设施”解耦——更可控也更可审计。
10.7.22 一份”读完 lm-eval 后该掌握什么”清单
整合本章方法学,给一份”读完 lm-eval 后掌握的核心能力”清单:
□ 能解释 lm-eval 4 层抽象 (Task / Instance / LM / Filter)
□ 能解释 4 种 OutputType 各自适用场景
□ 能写 yaml 注册一个新 task (含 dataset_path, doc_to_text, metric_list)
□ 能解释为什么 multiple_choice 在商业 API 上精度差
□ 能解释 acc_norm 与 acc 的差异及使用场景
□ 能跑 lm-eval 在 vLLM 后端上 (model_args 配置)
□ 能解释 benchmark contamination 风险及检测方法
□ 能选择适合自家业务的 5 个核心 task 而非全部 214 个
□ 能解释 lm-eval 与 OpenAI evals 的设计哲学差异
□ 能在自家 fork 中保留 lm-eval 的"小心翼翼"工程纪律
10 项全过,说明你已经具备工业级 lm-eval 使用 / 改造能力。
工程团队的下一步:把这份清单作为面试 / 内部考核的参考。读完本章 + 实操跑一次 MMLU + 自己写一个 yaml 的工程师,已经具备了”模型评测工程师”的入门资格。
10.7.23 lm-eval 在新模型评测中的”快速上手”路径
每次新 LLM 出现(如 GPT-5 / Claude 4 / DeepSeek-R2 等),lm-eval 是工业团队最快验证它能力的工具。给一份”新模型 1 小时上手验证”路径:
# 1. 安装 lm-eval (如已装跳过)
$ pip install lm-eval[all]
# 2. 接入新模型 (假设有 OpenAI compatible API)
$ lm_eval --model local-completions \
--model_args base_url=$NEW_MODEL_URL,model=$NEW_MODEL_NAME \
--tasks mmlu_pro,gsm8k_chat,humaneval,truthfulqa_mc1 \
--num_fewshot 5 \
--output_path results/new-model.json
# 3. 与现有模型对比 (如果之前跑过)
$ python compare.py results/gpt-4o.json results/new-model.json
# 4. 看核心数字
# - MMLU Pro: 综合知识
# - GSM8K: 数学
# - HumanEval: 代码
# - TruthfulQA: 抗幻觉
1 小时内就能拿到新模型在 4 个核心维度的客观数字。这种”快速验证”是工业团队跟踪 LLM 进展的标准操作——不需要等论文 / 不需要靠官方 PR 数字。
工业实操:每次新模型发布,工程师跑一次 lm-eval 的”4 task 套餐”。1 小时内拿到决策依据。比”看 Twitter 上的 demo”客观可信得多。
10.7.24 lm-eval 的”反误用”提醒
lm-eval 太好用,工程团队容易犯一些误用。一份”反误用”提醒:
- 不要把 MMLU 分数当成”模型整体能力”:MMLU 测的是百科知识,与你业务的客服 / 代码 / 创意能力没必然相关
- 不要追求 SOTA 分数而忘了业务:模型在 lm-eval 上+5pp 不一定让你的业务+5pp
- 不要忽视 task version:MMLU v1 与 v2 的题集 / 评分逻辑可能略有差异,跨版本比对要谨慎
- 不要把 lm-eval 当成应用层评测:业务 RAG / Agent 评测请用 ragas / promptfoo
- 不要在生产环境的 LLM Gateway 上随意跑:lm-eval 14k 题一次跑可能撑爆生产配额
每条都对应过去工程团队踩过的真实坑。这种”反误用”清单看起来朴素,但能避免一半的 lm-eval 使用陷阱。
10.7.25 lm-eval 与”模型实战”的认知地图
最后给一个跨章节的认知地图——lm-eval 在 LLM 工程师”模型实战”中的位置:
flowchart TB
Decision[模型选型决策] --> Q1{是研究 / 论文报数字?}
Q1 -->|是| Acad[lm-eval-harness<br/>学术 benchmark 主场]
Q1 -->|否| Q2{是应用层评测?}
Q2 -->|是| App[promptfoo / ragas<br/>第 11/12 章]
Q2 -->|否| Q3{是大规模模型对比?}
Q3 -->|是| Acad
Q3 -->|否| Q4{是 fine-tune 验证?}
Q4 -->|是| Acad
Q4 -->|否| Custom[自家黄金集<br/>第 3 章方法]
style Acad fill:#dbeafe
style App fill:#dcfce7
style Custom fill:#fef3c7
这张图覆盖 LLM 工程师”什么时候用 lm-eval”的所有场景。理解这张图能避免”用错工具”——很多团队拿 lm-eval 跑应用评测(不合适),或拿 promptfoo 报学术 benchmark(不合规)。
工业实务:把这张图贴在团队 wiki 的”评测工具选型”页面。新人面临”这个评测要用什么工具”时,按图索骥 5 秒钟就有答案。
10.7.26 lm-eval 给”评测体系初学者”的最大启示
读完本章后,给评测体系初学者的最大启示:好工具的核心不是功能多,是抽象准。
lm-eval 的 4 抽象(Task / Instance / LM / Filter)不是最丰富的、不是最易用的,但是最稳定的——3 年时间核心抽象不变。这种”准确的抽象”是工程作品的最高品质。
工程团队的姿态:
- 学习一个工具时,不要急于看”它能做什么”,先理解”它的核心抽象是什么”
- 一旦理解抽象,工具的所有功能都是抽象的具体落地,不需要死记硬背
- 抽象稳定的工具值得长期投入,抽象在变的工具谨慎投入
这是 LLM 工程师从”工具用户”升级到”工具理解者”的关键认知。读懂 lm-eval 的抽象 = 读懂了评测领域的核心概念。这是本章除了具体源码以外,最重要的”软价值”。
10.7.27 lm-eval 在国内 LLM 团队的”必读地位”
中国大模型团队的工程师如果没接触过 lm-eval,可以说”还没真正进入大模型工程领域”。它在国内大厂团队的”必读地位”体现:
- 通义 / 豆包 / Kimi / GLM / DeepSeek 等头部模型公司的算法工程师入职都需要熟悉 lm-eval
- HuggingFace 中文社区几乎所有大模型 release 的”benchmark 数字”都基于 lm-eval
- 学术发表国内 NLP 论文要在国际期刊 / 会议发表 benchmark 数字几乎必须用 lm-eval
- 校招面试LLM 工程师面试常问”用过 lm-eval 吗”+ “解释一下 acc_norm”
这种”必读地位”让 lm-eval 在国内的渗透远比在欧美深——欧美还有 OpenAI evals 等替代品,国内基本上是 lm-eval 一家独大。
工程团队的实务:把 lm-eval 列为 LLM 工程师”必备技能”。新人 onboarding 必须 hands-on 跑一次完整 MMLU。这种工程文化让团队的 LLM 评测能力保持业界水平。
10.7.28 一个隐藏的工程价值:lm-eval 的”工程美学”
读完本章源码 + 设计哲学后,最深的领悟可能不是技术——是工程美学。
lm-eval 体现了几条”工程美学”:
- 少即是多:4 抽象 cover 所有场景,不试图”全功能”
- 向 OTel 致敬:标准化优于自创
- 可读性优于性能:源码风格清晰,性能优化留给后端实现
- 社区优于个人:欢迎 PR,不让作者成为瓶颈
- 历史不可改:保留所有 commit,让演化可追溯
这些美学不只适用于评测工具,是所有”长期工程作品”的标志。读完本章希望读者带走的”软价值”是这种工程美学的认知——它让你在写自家代码时多一份对长期可读性 / 可演化性的敬畏。
10.7.29 lm-eval 与”模型卡片”的工程关系
观察国内外大模型公开发布的”Model Card”,几乎所有 benchmark 数字背后都有 lm-eval 的影子。具体形态:
Model Card v1.0 (典型 Llama / Qwen / DeepSeek 风格)
==================================================
Architecture: Transformer Decoder
Parameters: 70B
Training data: 15T tokens
Benchmark Results (lm-eval-harness):
MMLU: 85.3% (5-shot, acc)
GSM8K: 91.2% (8-shot, acc)
HumanEval: 78.4% (pass@1)
TruthfulQA: 62.7% (MC1)
ARC: 95.1% (acc_norm, 25-shot)
每个数字后面都有 “lm-eval-harness commit hash + task version”——保证读者能复现。
工程团队的实务:发布自家 fine-tune 模型时,模型卡片应该按这种格式撰写——所有 benchmark 数字标注 lm-eval 的版本 + commit。这种”可复现”姿态是模型发布的工程伦理。
这种”模型卡片即合规文档”的趋势会持续——EU AI Act 等监管要求让”benchmark 数字必须可追溯”成为合规要求,不只是工程伦理。
10.7.30 lm-eval 给中国 LLM 工程师的”全球化”视野
对国内 LLM 工程师来说,lm-eval 还有一个独特价值——让国内工程师有”全球化”视野。
具体含义:
- 跑 lm-eval = 用国际通用的指标语言
- 与 GPT-4 / Claude / Gemini 直接对照 = 知道自家模型的国际位置
- 参与 lm-eval 社区 = 在国际 LLM 工程社区有声音
这种”全球化视野”对国内大模型团队很重要——避免”自家闭门造车 / 用自家定义的指标自欺欺人”的状态。在 LLM 这个全球竞争领域,能用国际通用语言对话是工程团队的基本要求。
10.7.31 lm-eval 与 LLM 训练流程的”无缝集成”
最后讨论 lm-eval 在 LLM 训练流程中的”无缝集成”——它是模型训练 pipeline 的关键组件:
flowchart LR Train[模型训练] --> Save[保存 checkpoint] Save --> AutoEval[自动跑 lm-eval] AutoEval --> Compare[与 baseline 比较] Compare -->|涨| Promote[promote 到 staging] Compare -->|跌| Investigate[调查原因] style AutoEval fill:#fef3c7
每个 checkpoint 自动跑 5-10 个核心 benchmark,数字与上一版对比。这种”训练即评测”的集成让模型迭代有了客观信号。
工程实务:
- 训练框架(DeepSpeed / FSDP / vLLM)天然支持触发 lm-eval
- evaluator 跑完后结果写到 wandb / mlflow
- 数字差异超阈值自动告警
国内外大模型团队都用类似流程。lm-eval 在这种”训练 + 评测一体化”管线里是核心环节,不是可有可无的外部工具。这是 lm-eval 在 LLM 工业链中的最重要价值。
10.7.32 lm-eval 在不同业务的”使用模式”差异
不同业务对 lm-eval 的使用模式差异显著:
- 基础模型公司(如 DeepSeek / Qwen):每个 release 跑全套 100+ benchmark
- 应用层公司(如客服 / 教育 SaaS):只跑跟业务相关的 5-10 个
- 学术研究团队:跑论文 baseline 比较的 task
- toolchain 公司(vLLM / SGLang):跑性能基准
每种使用模式对应不同投入:
- 基础模型公司:1-3 人专职跑评测,月度成本数十万
- 应用层公司:评测工程师 30% 工时
- 学术团队:单次实验性投入
- toolchain:CI 集成
工业团队判断自己处于哪种业务时,相应配置 lm-eval 的使用强度。这种”按业务匹配工具强度”的思路是评测工程师的基本功。
读完本章希望读者带走的最后一点:lm-eval 是工具,使用方式取决于业务。同一个工具在不同业务上的最佳实践可能完全不同——不要”机械套用”别家的模式。
10.7.33 lm-eval 给”评测领域职业化”的启示
最后讨论 lm-eval 给”评测领域职业化”的启示——它让”模型评测”从”工程附属”成长为”独立职业”。
具体表现:
- 国内外大公司有专门的”模型评测工程师”岗位
- 学术界有 NeurIPS / ICML 的 evaluation track
- 商业公司(Atla / Confident AI 等)专门做评测产品
- 评测数据集本身成为可售卖资产
这种”职业化”在 5 年前不存在——2020 年代初评测还只是 NLP 工程师”附带做”的事。lm-eval 等工具的成熟把评测推上独立职业的轨道。
工程团队的实务:
- 把”评测工程师”作为独立招聘 title
- 给评测工程师独立的职业晋升路径
- 把评测能力作为团队的核心竞争力
读完本章希望读者带走的最深认知:评测已经是一个独立职业方向,不是工程师顺手做的事。这种”职业化”的认知让团队对评测的投入获得正当性——不再被视为”附属工作”。
10.7.34 一份具体的”国内大模型在 lm-eval 上的位置”
把 lm-eval 在国内大模型公开数据上的应用具象化。截至 2026 年初的公开 model card 数据(来自各家官方技术报告):
| 模型 | MMLU | GSM8K | HumanEval | TruthfulQA | 来源 |
|---|---|---|---|---|---|
| GPT-4o | 88.7 | 94.2 | 90.2 | 73.5 | OpenAI |
| Claude 3.5 Sonnet | 88.7 | 96.4 | 93.7 | 64.7 | Anthropic |
| DeepSeek-V3 | 87.1 | 89.3 | 91.7 | - | DeepSeek 报告 |
| Qwen 2.5-72B | 86.1 | 88.0 | 86.6 | - | Qwen 报告 |
| GLM-4 | 83.9 | 84.9 | 78.0 | - | 智谱报告 |
| 文心一言 4.0 | 81.2 | 78.5 | 73.0 | - | 百度报告 |
数字格式因测试 setting(zero-shot vs n-shot)和 task variant 略有差异,但是大致反映各家模型 2025-2026 年初的相对位置。
工业团队的实务:选基础模型时,把这张表 + 自家业务集评测组合看。任何”自家模型比 GPT-4 强 10pp”的宣传,先在 lm-eval 上验证再相信。这种”客观对照”的姿态是评测工程师的基本功。
国内团队特别注意:如果你看到自家模型在 MMLU 上”超越 GPT-4”,要警惕数据污染——MMLU 的训练数据可能已经被你模型见过。多跑几个 newer benchmark(Arena Hard / MMLU Pro / GPQA)做交叉验证。
10.7.35 一份完整的 lm-eval 中文 task 自定义 yaml
整合本章方法学,给一份”自家中文业务 task”的完整 lm-eval yaml 示例:
# my_tasks/cs_policy_qa.yaml
# 评测大模型对客服政策问答的事实一致性
task: cs_policy_qa
dataset_path: my_org/cs-policy-qa # 自家发布到 HuggingFace 的数据集
test_split: test
fewshot_split: dev
fewshot_config:
sampler: first_n
# 输出类型: 多选(A/B/C/D)
output_type: multiple_choice
# Jinja2 模板:把 doc 渲染成 prompt
doc_to_text: |
你是某电商平台的客服, 请根据政策回答问题.
问题: {{question}}
A. {{choice_a}}
B. {{choice_b}}
C. {{choice_c}}
D. {{choice_d}}
Answer:
doc_to_choice: ["A", "B", "C", "D"]
doc_to_target: answer # 数据集字段, 0=A, 1=B, 等
# Few-shot 模板(与 doc_to_text 一致格式)
fewshot_split: dev
num_fewshot: 5
# 评测指标
metric_list:
- metric: acc
aggregation: mean
higher_is_better: true
- metric: acc_norm # 长度归一化版本
aggregation: mean
higher_is_better: true
# 元数据
metadata:
version: 1.0
description: |
客服退换货 / 物流 / 投诉等政策问答, 200 题
标注一致性 Cohen's Kappa = 0.78
覆盖电商场景的高频政策问题
tags:
- chinese
- customer-service
- policy-qa
不到 30 行 yaml + 上传一份 jsonl 到 HuggingFace = 自家的中文 lm-eval task。后续每个 LLM release 都能跑这个 task 拿到客观分数。
工业实务:把团队的核心业务测试集都按这种格式发布——内部公司私有 dataset 也行(不用真的上 HF)。3-5 个核心 task 就能覆盖团队的”模型选型”和”模型升级”决策。
10.7.36 lm-eval 与”动态 benchmark”的演进
为应对 §10.7.16 的 benchmark contamination 问题,2024-2025 年评测领域涌现了一批”动态 benchmark”——动态生成 / 持续更新,不易被训练数据污染:
| Benchmark | 类型 | 防污染机制 | 来源 |
|---|---|---|---|
| Chatbot Arena | 实时人类投票 | 题目永远变 | LMSYS |
| LiveBench | 滚动更新 | 每月新增题目 | LiveBench Team |
| MMLU-Pro | 增强 MMLU | 提高难度 + 修复污染题 | TIGER-Lab |
| Arena Hard | 从 Arena 挖 hard | 基于真投票 | LMSYS |
| GAIA | Agent 任务 | 真实工具使用 | HuggingFace |
| BigCodeBench | 代码 | 不断扩充 | BigCode |
| GPQA Diamond | 专业知识 | 极难 + 不易污染 | NYU + Cohere |
这些动态 benchmark 的工程价值:
- 更接近真实能力:不易被训练污染
- 持续可用:每月 / 每季度更新,永不过时
- lm-eval 部分支持:MMLU-Pro / GPQA / Arena Hard 已经接入
工业团队的实务:
- 评估”基础模型能力”用静态 benchmark + 动态 benchmark 双重对照
- 静态分数高 + 动态分数低 → 可能有训练污染
- 两个分数都高 → 真实能力强
读完本章希望读者带走的最远视角:评测领域正在从”静态固定”走向”动态演化”——这是与 LLM 能力进化竞速的必然趋势。lm-eval 持续接入新动态 benchmark,是工业团队保持评测前沿性的窗口。
10.7.37 lm-eval 的 Filter Pipeline 源码细节
lm-eval 的一个高频踩坑点是 filter pipeline 的执行顺序——同一道题若 filter 配置错,再正确的模型答案也会被判错。本节用一份运行可见的对照例还原其内部机制。
task.py 的 apply_filters 方法(开源仓库 lm_eval/api/task.py)按 yaml 中 filter_list 顺序依次串联 4 类 filter:
| Filter 类 | 源码位置 | 作用 | 典型场景 |
|---|---|---|---|
regex_extract | lm_eval/filters/extraction.py | 用正则从输出抽片段 | 抽 “answer is X” 中的 X |
take_first | lm_eval/filters/selection.py | 从多次采样取第一个 | n=1 sampling |
majority_vote | lm_eval/filters/selection.py | 多数表决(self-consistency) | n>1 sampling |
multi_choice_regex | lm_eval/filters/extraction.py | 选择题专用规则 | A/B/C/D 抽取 |
# 一个常见错误配置(导致 0 分)
generation_kwargs:
do_sample: true
num_return_sequences: 5
filter_list:
- name: take_first # ❌ 错:先 take_first 把 5 个采样砍成 1 个
- name: regex_extract
regex_pattern: "answer is (\\d+)"
- name: majority_vote # ❌ 错:此时只有 1 个,多数表决无意义
# 正确配置
filter_list:
- name: regex_extract # ✅ 先抽答案
regex_pattern: "answer is (\\d+)"
- name: majority_vote # ✅ 再多数表决
flowchart LR M[模型输出 5 次采样] --> R[regex_extract<br/>每次抽数字] R --> MV[majority_vote<br/>5 个候选选最频繁] MV --> S[最终答案] M2[5 次采样] --> TF[take_first ❌] TF -->|信息丢失| LOST[只剩 1 个候选] style LOST fill:#ffebee style S fill:#e8f5e9
源码层的关键点(lm_eval/api/task.py 的 _postprocess 部分):
def _postprocess(self, instance):
out = instance.resps # list[str],长度 = num_return_sequences
for f in self._filters:
out = f.apply(out) # filter 之间是 list -> list 传递
return out # 最终 list[str],aggregation 阶段再 reduce
工程实务的 3 条原则:
- filter 顺序 = 数据流顺序——上游 filter 的输出是下游的输入,错一步全错
- 聚合类 filter(majority_vote)必须在抽取类(regex_extract)之后
- debug 时把每步 output 打 log——
--log_samples --output_path debug.jsonl能看到每个 filter 后的中间值
读 lm-eval 源码这一段(约 80 行)能让你”再也不会因为 filter 顺序而怀疑模型水平差”。这是 §9.6.20 OpenAI evals 启蒙读本观点的 lm-eval 版佐证——读源码 > 看文档。
10.7.38 lm-eval-harness 在多机分布式跑大模型的工程模式
lm-eval 跑 70B+ 大模型时,单机推理能力捉襟见肘。仓库官方支持三种分布式模式(详见 lm_eval/__main__.py 的 --model_args 文档):
| 模式 | 核心 flag | 适用场景 | 代价 |
|---|---|---|---|
| HF accelerate | model=hf,parallelize=True | 单机多 GPU、模型不需要切分 | 启动慢、需 transformers ≥ 4.36 |
| vLLM | model=vllm | 推理吞吐优先、batch 大 | 推理引擎差异需 verify |
| OpenAI compat API | model=openai-completions,base_url=... | 后端是 LLM Gateway / vLLM serve | 网络瓶颈、需限速保护 |
import subprocess
import os
from dataclasses import dataclass
from pathlib import Path
@dataclass
class LmEvalLaunchPlan:
tasks: list[str]
n_fewshot: int
batch_size: int
output_path: Path
backend: str
model_args: dict
timeout_h: int
class DistributedLmEvalRunner:
"""多机 lm-eval 启动器:本地 / vLLM / OpenAI-compat 三选一"""
def _build_model_args(self, plan: LmEvalLaunchPlan) -> str:
if plan.backend == "vllm":
kv = {
"model": plan.model_args["model_name"],
"tensor_parallel_size": plan.model_args.get("tp", 4),
"gpu_memory_utilization": 0.9,
"max_model_len": plan.model_args.get("max_len", 4096),
}
elif plan.backend == "openai":
kv = {
"model": plan.model_args["model_name"],
"base_url": plan.model_args["base_url"],
}
else:
kv = {
"pretrained": plan.model_args["model_name"],
"parallelize": True,
"dtype": "bfloat16",
}
return ",".join(f"{k}={v}" for k, v in kv.items())
def _build_cmd(self, plan: LmEvalLaunchPlan) -> list[str]:
return [
"lm_eval",
"--model", plan.backend,
"--model_args", self._build_model_args(plan),
"--tasks", ",".join(plan.tasks),
"--num_fewshot", str(plan.n_fewshot),
"--batch_size", str(plan.batch_size),
"--output_path", str(plan.output_path),
"--log_samples",
]
def launch(self, plan: LmEvalLaunchPlan) -> int:
env = os.environ.copy()
env["HF_HUB_ENABLE_HF_TRANSFER"] = "1"
env["TOKENIZERS_PARALLELISM"] = "false"
cmd = self._build_cmd(plan)
result = subprocess.run(cmd, env=env,
timeout=plan.timeout_h * 3600,
check=False)
return result.returncode
def expected_cost_estimate(self, plan: LmEvalLaunchPlan,
cost_per_gpu_hour: float = 2.5) -> dict:
"""根据 task / fewshot / batch 估算 GPU 小时与成本"""
task_size = {"mmlu": 14000, "gsm8k": 1300, "hellaswag": 10000,
"arc_challenge": 1170, "winogrande": 1267}
total_q = sum(task_size.get(t, 5000) for t in plan.tasks)
# 7B / 单 GPU / batch=8 经验值:~4 题/秒
throughput = 4 * plan.batch_size / 8 * (plan.model_args.get("tp", 1))
seconds = total_q * (1 + plan.n_fewshot * 0.5) / max(throughput, 0.1)
gpu_hours = seconds / 3600 * plan.model_args.get("tp", 1)
return {
"total_questions": total_q,
"estimated_gpu_hours": round(gpu_hours, 1),
"estimated_cost_usd": round(gpu_hours * cost_per_gpu_hour, 2),
"estimated_wallclock_hours": round(seconds / 3600, 1),
}
flowchart LR
P[LmEvalLaunchPlan] --> B{backend?}
B -->|hf| H[本地 GPU<br/>parallelize=True]
B -->|vllm| V[vLLM 4-8 GPU<br/>tensor_parallel]
B -->|openai| O[OpenAI-compat<br/>调远端 Gateway]
H --> SUB[subprocess.run lm_eval]
V --> SUB
O --> SUB
SUB -->|结果 jsonl| OUT[output_path]
P --> EST[cost_estimate<br/>GPU 小时 / $$$]
style V fill:#e3f2fd
style O fill:#fff3e0
工程实务 4 条选择规则:
- 70B 模型 + 单机 8×A100 → vLLM(吞吐高,4× 提升)
- 小模型 7B 微调对比 → HF accelerate(启动简单,无需 vLLM 学习)
- 跨团队、跨地域、不能传模型权重 → OpenAI-compat(调远端 Gateway)
- Apple Silicon / mac mini 集群 → HF accelerate +
device=mps(不要用 vLLM,目前不支持 MPS)
成本估算的 ground truth:跑完整 MMLU(14k 题、5-shot)在 A100×8 vLLM 跑 7B 模型大约 0.8 GPU·h(成本约 15-20)。读者可用 expected_cost_estimate 在跑之前就有数。
这套 runner 的工程价值:把”哪种 backend、跑多久、要花多少钱”从口口相传变成可计算的预算决策。是公司级 lm-eval 集群运营的最小可行模板。
10.7.39 lm-eval 的”Generate-Until vs LogLikelihood”——两种打分范式的根本区别
读 lm-eval 源码时最容易混的概念是它的 2 种 request 类型——generate_until 与 loglikelihood。这两种范式直接决定了任务能不能跑、跑得对不对。lm_eval/api/instance.py:5-7 定义了这 2 种类型,下面把它们的区别一次讲透:
| 维度 | generate_until | loglikelihood | 何时用 |
|---|---|---|---|
| 调用方式 | 模型自由生成 | 给定字符串求 logprob | — |
| 输入 | prompt | (prompt, target_str) | — |
| 输出 | str | float(log p(target|prompt)) | — |
| 速度 | 慢(自回归生成) | 快(单次 forward pass) | — |
| 评分方式 | 提取后 match | 比较多个 target 的 logprob | — |
| 适合任务 | 数学题 / 开放问答 / 代码 | 多选题 / 二分类 / 排序 | — |
| 典型任务 | gsm8k, humaneval, MATH | mmlu, arc, hellaswag, winogrande | — |
| API 模型支持 | ✅ | ⚠ 仅部分(OpenAI 已停) | — |
| 本地模型支持 | ✅ | ✅ | — |
# generate_until 范式(gsm8k task.yaml 示例)
"""
task: gsm8k
output_type: generate_until
generation_kwargs:
until: ['Q:', '\n\n']
max_gen_toks: 256
do_sample: false
filter_list:
- name: regex_extract_answer
regex_pattern: '#### (\d+)'
"""
# loglikelihood 范式(mmlu task.yaml 示例)
"""
task: mmlu_anatomy
output_type: multiple_choice
doc_to_choice: ['A', 'B', 'C', 'D']
doc_to_target: gold_letter # 0/1/2/3 索引
"""
flowchart LR
T[task yaml] --> O{output_type?}
O -->|generate_until| G[模型自由生成]
O -->|multiple_choice / loglikelihood| L[计算每个 candidate 的 logprob]
G --> RX[regex_extract / filter chain]
RX --> M1[与 ideal 比对]
L --> ARGMAX[argmax over candidates]
ARGMAX --> M2[与 gold 比对]
M1 --> A[accuracy]
M2 --> A
style G fill:#fff3e0
style L fill:#e3f2fd
工程实务的 4 条选择经验:
- 多选题用 loglikelihood——快 5-10 倍,准确度比 generate-then-parse 高 2-5pp(生成时模型可能”想多了”反而错)
- 数学 / 代码用 generate_until——必须看到完整 reasoning 链
- API 模型只能用 generate_until——OpenAI 自 2023-12 移除 logprob,loglikelihood 任务跑不了 GPT-4
- 混合 task 时把同范式分开 batch——loglikelihood batch=64 + generate_until batch=4 是不同物理 GPU 利用率最优
具体例子:跑 MMLU 全集(14k 题):
- 用 multiple_choice / loglikelihood:vLLM 7B 约 8 分钟
- 强行改成 generate_until 让模型选 ABCD:vLLM 7B 约 50 分钟,且准确率低 3-5pp
这个差距对应工业团队选 lm-eval 跑大批量评测的核心理由——loglikelihood 范式让”全 benchmark 跑一遍”在分钟而不是小时级。
研究背景:BIG-bench 论文(Srivastava et al. arXiv:2206.04615)专门讨论了”自回归生成 vs likelihood 评分”对 benchmark 结论的影响——某些任务 likelihood 给的分数比 generation 高 10pp+。这是 lm-eval 默认 prefer loglikelihood 的方法学理由。
理解了 generate_until vs loglikelihood,再读 lm-eval 任何 task.yaml 都能秒判断它的运行模式。这是 §10.7.37 filter pipeline 之外,掌握 lm-eval 的另一个根本性切入点。
10.7.40 lm-eval 的”复合 task”——把多个子任务用一个 yaml 表达
lm-eval 的 yaml 不仅能定义单 task,还支持”复合 task” / “task group”——把几十个子任务一次启动。MMLU 有 57 个子集(anatomy / biology / chemistry / …),每个都跑一次很麻烦——lm-eval 用 group 机制让 lm_eval --tasks mmlu 一行跑全 57 个。下面拆解 group yaml 与源码(lm_eval/api/group.py)的工作方式。
# lm_eval/tasks/mmlu/_default_template_yaml
group: mmlu
group_alias: MMLU
task:
- mmlu_anatomy
- mmlu_astronomy
- mmlu_biology
- mmlu_chemistry
# ... 57 个子任务
aggregation: weighted_mean
metric_list:
- metric: acc
aggregation: mean
# 一个子任务示例
task: mmlu_anatomy
include: _default_template_yaml
dataset_name: anatomy
description: "MMLU - Anatomy"
test_split: test
fewshot_split: dev
include 关键字让子任务继承父模板,避免 57 份重复 yaml。
flowchart TB G[mmlu group yaml] --> A1[mmlu_anatomy] G --> A2[mmlu_astronomy] G --> A3[mmlu_biology] G --> A57[mmlu_world_religions] T[模板 _default_template_yaml] --> A1 T --> A2 T --> A57 A1 --> R[各子集结果] A2 --> R A57 --> R R --> AGG[weighted_mean 聚合] AGG --> M[全 group 综合分] style G fill:#e3f2fd style M fill:#e8f5e9
工程实务的 4 条 group 设计经验:
- 任何”多 sub-domain benchmark”都该用 group:MMLU / BBH / TruthfulQA 都遵循该模式
- 聚合方式必须显式声明:weighted_mean(按子任务样本数加权)vs simple_mean(每子任务平等)会差 2-5pp
include共享公共字段:description / fewshot 等公共配置提到模板里,子任务只写差异group自身可以嵌套 group:MMLU-Pro / BIG-bench Hard 都把多个子 group 再合成大 group
具体例子:跑 lm_eval --tasks mmlu_high_school 一行启动 7 个子集(high_school_biology / chemistry / … / world_history),结果分别报告 + 综合 acc。这种粒度让团队能精确定位”模型在哪个学科退化”——比单一 mmlu 数字有诊断价值。
源码层(lm_eval/api/group.py:38-110)的核心逻辑:
# 简化版
class TaskGroup:
def __init__(self, group_name: str, tasks: list[str],
aggregation: str = "weighted_mean"):
self.name = group_name
self.tasks = tasks
self.aggregation = aggregation
def aggregate(self, per_task_scores: dict[str, dict]) -> float:
if self.aggregation == "weighted_mean":
total_n = sum(s["n"] for s in per_task_scores.values())
return sum(s["score"] * s["n"] / total_n
for s in per_task_scores.values())
elif self.aggregation == "simple_mean":
return sum(s["score"] for s in per_task_scores.values()) / \
len(per_task_scores)
raise ValueError(f"Unknown aggregation: {self.aggregation}")
读完这段,读者能理解 lm-eval 报告里”mmlu acc = 0.78、subtask=anatomy:0.82, biology:0.85, …”这种结构是怎么出来的——并且能照着派生新 group(如自家 50 个客服领域的子任务合成 “internal_customer_service” group)。
研究背景:BBH(BIG-bench Hard, Suzgun et al. arXiv:2210.09261)原本就是 BIG-bench 的 23 个 hard 子任务的子集——天然 group 模式。MMLU-Pro(Wang et al. arXiv:2406.01574)把 MMLU 升级为更难子集 + 更细粒度——是 group 嵌套的典型案例。
10.7.41 lm-eval 与 BIG-bench 关系的”考古”——为什么 EleutherAI 选择重写
读者读 lm-eval 时会问”为什么 EleutherAI 不直接用 Google 的 BIG-bench?“——这背后是一段评测领域的工程演化。
| 维度 | BIG-bench (2022) | lm-evaluation-harness (2023+) |
|---|---|---|
| 组织 | Google + 350+ 学术合作者 | EleutherAI + 开源社区 |
| 任务数 | 204 个 | 60+(精选) |
| 接入门槛 | JSON spec + 复杂 rendering | yaml + Python class |
| 模型支持 | 主要 Google 系 | HF / OpenAI / vLLM / 任意 |
| 维护活跃度 | 2023 后基本停滞 | 月度更新 |
| 中位 task 复杂度 | 高(含许多冷门) | 中(聚焦工业关心的) |
| reproduction 标准 | 论文级别 | 工业 CI 级别 |
flowchart TB
subgraph "评测框架演化"
BB[2022: BIG-bench<br/>学术导向 / 204 task]
LME[2022 末: lm-eval v0<br/>EleutherAI 启动]
LME1[2023-Q3: lm-eval v0.4<br/>vLLM 接入]
LME2[2024-Q2: lm-eval v0.4.4<br/>1000+ task / group / chat templates]
OPEN[OpenLLM Leaderboard]
end
BB -->|启发| LME
LME --> LME1 --> LME2
LME2 --> OPEN
BB -. "逐渐边缘化" .-> LME
style BB fill:#fff3e0
style LME2 fill:#e8f5e9
style OPEN fill:#e3f2fd
EleutherAI 重写 lm-eval 的 4 大工程动机(基于公开 GitHub issue 与论文 Sutawika et al. 2024 arXiv:2405.14782):
- BIG-bench 的 Google 中心化:JSON 格式贴 PaLM API,对 HF 模型不友好
- 接入新模型成本高:每加一个 backend 要改核心代码 → lm-eval 用 abstract
LM类解耦 - 任务定义太学术:BIG-bench 含许多”测好玩”的题(emoji 翻译 / IPA 注音)→ lm-eval 聚焦 MMLU / GSM8K 等业界关心的
- 再现性问题:BIG-bench 跑分难复现 → lm-eval 强调 deterministic + log_samples 完整可审计
这套演化使 lm-eval 成为现代 LLM benchmark 的”事实标准”——HuggingFace 的 OpenLLM Leaderboard 直接用它跑分。任何模型发布都几乎必跑 lm-eval,否则同行无法横向对比。
工程实务的 4 条”BIG-bench 时代教训”:
- 不要把 task 当论文做——单 task 应该 < 1 周内可被任何团队重跑
- 格式贴近开源生态:用 yaml + jsonl 而非自创格式
- abstract backend 是必须的:让”换底层模型”不需要改 task 定义
- leaderboard 集成是落地关键:能上 leaderboard 的框架自带活跃度
具体例子:BIG-bench 的”emoji_movie”任务(用 emoji 串猜电影名),从论文角度有趣但工业团队基本不用。lm-eval 不收录这种 task——这是”学术 benchmark vs 工业 benchmark”的边界。
研究背景:
- BIG-bench 论文 (Srivastava et al. arXiv:2206.04615) 是 LLM 评测最早系统化的 benchmark 集
- lm-evaluation-harness 论文 (Sutawika et al. arXiv:2405.14782) 解释了重写的动机
- HuggingFace Open LLM Leaderboard 在 2024-Q2 全面切换到 lm-eval v0.4 backend
理解这段历史能让读者更深刻看到 lm-eval 设计中的”工业取舍”——它不是技术上最强大,但是工程上最稳健的开源评测框架。这是开源工具长寿的核心法则。
10.7.42 一份”lm-eval task 自动化巡检”——保证 task 质量长寿
仓库里 1000+ 个 task 长期累积,会出现”陈年累月没人维护”的 task:依赖断裂 / 数据不可访问 / 与 lm-eval API 不兼容。下面是一份巡检脚本,每周扫描所有 task 找出”坏掉的”:
import yaml
import subprocess
import asyncio
from pathlib import Path
from dataclasses import dataclass
from typing import Iterable
from datetime import datetime
@dataclass
class TaskHealthReport:
task_name: str
task_path: str
status: str # "healthy" | "stale" | "broken" | "deprecated"
last_test_at: str
issue_summary: str | None
sample_count: int | None
quick_run_seconds: float | None
class LmEvalTaskHealthChecker:
"""对 lm-eval 仓库所有 task 自动健康巡检"""
def __init__(self, lm_eval_root: Path):
self.root = lm_eval_root
def discover_tasks(self) -> list[Path]:
return list(self.root.glob("lm_eval/tasks/**/*.yaml"))
def parse_task(self, path: Path) -> dict:
try:
return yaml.safe_load(path.read_text())
except yaml.YAMLError:
return {}
async def quick_smoke_test(self, task_name: str) -> tuple[bool, float, str]:
"""跑 task 5 题做 smoke check"""
import time
start = time.monotonic()
try:
result = subprocess.run(
["lm_eval", "--model", "hf",
"--model_args", "pretrained=facebook/opt-125m",
"--tasks", task_name,
"--limit", "5",
"--num_fewshot", "0"],
capture_output=True, text=True, timeout=120,
)
elapsed = time.monotonic() - start
if result.returncode == 0:
return True, elapsed, ""
return False, elapsed, result.stderr[-500:]
except subprocess.TimeoutExpired:
return False, 120, "timeout 120s"
def check_dataset_accessible(self, task_spec: dict) -> bool:
"""检查 dataset_path 是否还能下载"""
ds_path = task_spec.get("dataset_path")
if not ds_path:
return False
# 简化版:实际应试 huggingface_hub.dataset_info
return True
async def assess_task(self, path: Path) -> TaskHealthReport:
spec = self.parse_task(path)
task_name = spec.get("task", path.stem)
if spec.get("status") == "deprecated":
return TaskHealthReport(
task_name=task_name, task_path=str(path),
status="deprecated",
last_test_at=datetime.now().isoformat(),
issue_summary=None, sample_count=None,
quick_run_seconds=None,
)
if not self.check_dataset_accessible(spec):
return TaskHealthReport(
task_name=task_name, task_path=str(path),
status="broken",
last_test_at=datetime.now().isoformat(),
issue_summary="dataset 不可访问",
sample_count=None, quick_run_seconds=None,
)
ok, elapsed, err = await self.quick_smoke_test(task_name)
if not ok:
return TaskHealthReport(
task_name=task_name, task_path=str(path),
status="broken",
last_test_at=datetime.now().isoformat(),
issue_summary=err, sample_count=None,
quick_run_seconds=elapsed,
)
return TaskHealthReport(
task_name=task_name, task_path=str(path),
status="healthy",
last_test_at=datetime.now().isoformat(),
issue_summary=None,
sample_count=spec.get("test_split_size", None),
quick_run_seconds=elapsed,
)
async def full_audit(self) -> dict:
tasks = self.discover_tasks()
# 控制并发避免打爆磁盘
sem = asyncio.Semaphore(5)
async def bounded(task):
async with sem:
return await self.assess_task(task)
reports = await asyncio.gather(*(bounded(t) for t in tasks))
from collections import Counter
status_counts = Counter(r.status for r in reports)
return {
"total": len(reports),
"by_status": dict(status_counts),
"broken_tasks": [r.task_name for r in reports
if r.status == "broken"][:20],
}
flowchart LR
R[lm_eval/tasks/**/*.yaml] --> D[discover_tasks]
D --> P[parse yaml]
P --> DA{dataset 可访问?}
DA -->|否| BK1[broken: dataset 失效]
DA -->|是| SM[5 题 smoke test]
SM -->|exit 0| H[healthy]
SM -->|exit !=0| BK2[broken: runtime error]
SM -->|timeout| BK3[broken: 性能问题]
BK1 --> RP[健康报告]
BK2 --> RP
BK3 --> RP
H --> RP
style H fill:#e8f5e9
style BK1 fill:#ffebee
style BK2 fill:#ffebee
style BK3 fill:#ffebee
工程实务的 4 条巡检规则:
- 每周 cron 触发:周末空闲时跑——避免占用工作时间 GPU
- broken_tasks 自动开 GitHub issue:标 [task-broken] tag,作者按周去修
- deprecated 标记 yaml 头部:明确”已知坏,不影响整体健康度”
- smoke test 用最小模型:facebook/opt-125m 跑得快,只验证”框架兼容性”
3 类常见 broken 来源:
| 类型 | 表现 | 修法 |
|---|---|---|
| dataset 链接挂 | HuggingFace 上 dataset 删除 | 切镜像 / 改用本地 |
| API 不兼容 | lm-eval 升级后老 task spec 失效 | 按新 API 重写 spec |
| 依赖断裂 | 任务依赖某个 Python 包 yanked | 锁 requirements.txt |
具体例子:仓库 1000 个 task 巡检——
- healthy: 850 (85%)
- broken: 45 (dataset 60% / API 30% / 依赖 10%)
- deprecated: 80
- 自动开 issue 30 个,剩 15 个由 maintainer 决定退役
研究背景:
- 大型开源仓库(如 sklearn)都有”weekly nightly tests”巡检
- HuggingFace datasets 自身有 “dataset health” 服务监控数据可访问性
- lm-eval 仓库的 .github/workflows/test_tasks.yml 已部分实现这套检查(仅热门 task)
把 LmEvalTaskHealthChecker 接入团队私有 lm-eval fork 的 nightly CI——所有内部 task 长期保健康。这是把”开源仓库治理思路”应用到私有 task 库的工程范式。
10.7.43 lm-eval 与 OpenLLM Leaderboard 的”分数解读手册”
每次有新模型发布,HuggingFace OpenLLM Leaderboard(直接用 lm-eval 后端)就上一行新分数。但读者常常问”这一行的 IFEval=0.823 / GPQA=0.412 到底说明什么?“——下面给出 leaderboard 各 metric 的工程解读:
| Metric | 评测内容 | 0.0-0.4 含义 | 0.4-0.7 含义 | > 0.7 含义 |
|---|---|---|---|---|
| MMLU-Pro | 多领域知识 | 弱模型 / 早期 7B | 中等 / GPT-3.5 级 | 强 / GPT-4 级 |
| GPQA-Diamond | 物理 / 化学博士题 | 一般 LLM | o1-mini 级 | 顶尖 reasoning |
| HumanEval | Python 编码 | 弱编码 | GPT-3.5 级 | GPT-4o / Claude 级 |
| MATH | 数学推理 | 早期模型 | 中等 | reasoning 模型 |
| IFEval | 指令跟随 | 不听话 | 一般 | 优秀 |
| MuSR | 多步推理 | 推理弱 | 中等 | 强推理 |
| BBH | 综合困难题 | 一般 | 较好 | 优秀 |
| HellaSwag | 常识 | 弱 | 一般 | 已饱和(多数 ≥ 0.85) |
| GSM8K | 小学数学 | 中等 | 良好 | 已饱和(多数 ≥ 0.85) |
from dataclasses import dataclass
from typing import Iterable
@dataclass
class LeaderboardScoreInterpretation:
metric: str
raw_score: float
percentile_estimate: int # 0-100
capability_tier: str
saturation_warning: bool
business_relevance: str
class LeaderboardScoreInterpreter:
"""把 leaderboard 数字转成业务可读的'能力档位'"""
METRIC_PROFILES = {
"MMLU_Pro": {
"tiers": [(0.30, "weak"), (0.50, "middle"),
(0.70, "strong"), (1.0, "frontier")],
"saturated": False,
"business_relevance": "通用 RAG / 客服基础能力",
},
"GPQA_Diamond": {
"tiers": [(0.25, "random"), (0.40, "good"),
(0.60, "expert"), (1.0, "phd")],
"saturated": False,
"business_relevance": "科研 / 法律 / 医疗推理质量",
},
"HumanEval": {
"tiers": [(0.40, "weak"), (0.70, "middle"),
(0.90, "strong"), (1.0, "frontier")],
"saturated": True, # GPT-4o+ 已 ≥ 0.9
"business_relevance": "代码生成 / Agent",
},
"GSM8K": {
"tiers": [(0.50, "weak"), (0.80, "middle"),
(0.95, "saturated"), (1.0, "frontier")],
"saturated": True,
"business_relevance": "已不能区分头部模型",
},
"IFEval": {
"tiers": [(0.50, "weak"), (0.70, "middle"),
(0.85, "strong"), (1.0, "excellent")],
"saturated": False,
"business_relevance": "结构化输出 + Agent tool 调用",
},
}
def interpret(self, metric: str, score: float) -> LeaderboardScoreInterpretation:
profile = self.METRIC_PROFILES.get(metric, {
"tiers": [(0.4, "weak"), (0.7, "middle"), (1.0, "strong")],
"saturated": False,
"business_relevance": "unknown",
})
for threshold, tier in profile["tiers"]:
if score <= threshold:
break
# 简化:用 tier 估 percentile
tier_to_pct = {
"weak": 25, "middle": 55, "strong": 80,
"frontier": 95, "expert": 90, "phd": 98, "saturated": 50,
"good": 50, "excellent": 90, "random": 10,
}
return LeaderboardScoreInterpretation(
metric=metric,
raw_score=score,
percentile_estimate=tier_to_pct.get(tier, 50),
capability_tier=tier,
saturation_warning=profile["saturated"] and score >= 0.85,
business_relevance=profile["business_relevance"],
)
flowchart LR
L[OpenLLM Leaderboard 行] --> P[每 metric 解读]
P --> S{饱和?}
S -->|是, e.g. HellaSwag 0.92| WARN[已不区分头部]
S -->|否| TIER[tier 分档]
TIER --> BR[业务相关性映射]
BR --> DEC[选型决策]
style WARN fill:#fff3e0
style DEC fill:#e8f5e9
工程实务的 4 条解读经验:
- 不要被”饱和指标”骗:HellaSwag / GSM8K 已 saturated, 头部模型差距 < 1pp 没意义
- GPQA-Diamond 是 2026 主战场:能区分 GPT-4o vs o3-mini,是当下”reasoning”试金石
- IFEval 对 Agent 业务最关键:结构化输出能力 = tool calling 能力的 proxy
- 业务相关性看 business_relevance 字段:不是所有 metric 对自家业务都重要
具体例子:选 chatbot 客服底座模型,看几个候选:
| 模型 | MMLU-Pro | GPQA | IFEval | HumanEval | 综合判断 |
|---|---|---|---|---|---|
| Qwen2.5-72B | 0.61 | 0.32 | 0.81 | 0.85 | 客服够用、IFEval 出色 |
| GPT-4o-mini | 0.65 | 0.39 | 0.83 | 0.87 | 平衡好、性价比高 |
| Claude-Haiku | 0.58 | 0.35 | 0.84 | 0.88 | IFEval 最好 |
| 内部 13B 微调 | 0.50 | 0.20 | 0.65 | 0.55 | 编码弱、reasoning 弱 |
客服业务核心是 IFEval(指令跟随)+ MMLU-Pro(通用知识)—— Claude Haiku 与 GPT-4o-mini 都是好选择,看价格 / 延迟决定。
研究背景:
- HuggingFace OpenLLM Leaderboard 2024-Q3 升级到 v2 版本,加入 GPQA / MMLU-Pro / MuSR
- Anthropic Claude 3.5 Sonnet 公布的官方 benchmark 表是这套解读的对照参考
- OpenAI o1 / o3 system card 公开了”reasoning model 如何 dominate GPQA”
读者把 LeaderboardScoreInterpreter 作为团队”模型选型决策书”的解读工具——把 leaderboard 数字翻译成”是否对我有用”的业务判断。
10.7.44 一份”私有 task 与公开 task 的 bridge”——让团队 task 也能上 leaderboard
读者建了一堆私有 task,但工业团队也想”对标公开 task”——下面给出私有 task 与公开 task 互相 bridge 的工程方案:
import yaml
from pathlib import Path
from dataclasses import dataclass
from typing import Iterable
@dataclass
class TaskBridgeConfig:
private_task: str
public_analog: str # 最相似的公开 task
metric_alignment: str # accuracy / f1 / exact_match
cross_correlation: float | None # 两边 score 的历史相关性
class PublicPrivateTaskBridge:
"""让团队私有 task 与公开 task 双向对照"""
DOMAIN_MAPPINGS = {
"customer_service_intent": {
"public_analog": "banking77", # HF 上的客服意图分类
"metric_alignment": "accuracy",
},
"code_review": {
"public_analog": "humaneval_plus",
"metric_alignment": "pass@1",
},
"medical_qa": {
"public_analog": "medqa",
"metric_alignment": "accuracy",
},
"rag_grounded_qa": {
"public_analog": "naturalquestions",
"metric_alignment": "f1",
},
"agent_tool_calling": {
"public_analog": "bfcl",
"metric_alignment": "function_call_accuracy",
},
}
def __init__(self, lm_eval_root: Path):
self.root = lm_eval_root
def bridge(self, private_task: str) -> TaskBridgeConfig:
mapping = self.DOMAIN_MAPPINGS.get(private_task, {
"public_analog": "mmlu", # 兜底
"metric_alignment": "accuracy",
})
return TaskBridgeConfig(
private_task=private_task,
public_analog=mapping["public_analog"],
metric_alignment=mapping["metric_alignment"],
cross_correlation=None, # 实际从历史数据算
)
def emit_combined_yaml(self, private_task: str,
output_path: Path) -> dict:
bridge = self.bridge(private_task)
config = {
"group": f"{private_task}_with_baseline",
"task": [private_task, bridge.public_analog],
"metric_list": [
{"metric": bridge.metric_alignment,
"aggregation": "mean"},
],
"comparison_mode": "side_by_side",
}
output_path.write_text(yaml.safe_dump(config, allow_unicode=True))
return config
flowchart LR
P[私有 task] --> B[Bridge Config]
PA[公开 task] --> B
B --> Y[combined yaml]
Y --> RUN[lm_eval --tasks group]
RUN --> R1[私有 score]
RUN --> R2[公开 score]
R1 --> CMP[相关性分析]
R2 --> CMP
CMP --> Q{相关性 > 0.7?}
Q -->|是| TR[trustworthy: 私有 task 反映通用能力]
Q -->|否| MISMATCH[警告: 私有 task 测的可能是别的]
style TR fill:#e8f5e9
style MISMATCH fill:#ffebee
工程实务的 4 条 bridge 价值:
- 横向对标同行:私有 task 跑外部模型 + 公开 task 配对——“我们的 customer_service 跟 banking77 做映射”
- 新模型选型快:候选模型在 banking77 跑得好 → 大概率私有也好
- 回答”我们 task 是否合理”:与公开 task 高相关说明你 task 真测了能力
- 新人理解快:解释私有 task 用 “类似 banking77” 比从头讲快 10x
5 类常见公开 task 对应:
| 私有 task 类型 | 推荐公开 analog | 用途 |
|---|---|---|
| 客服意图分类 | banking77 | 通用客服能力 baseline |
| RAG 问答 | naturalquestions / triviaqa | 抽取式问答能力 |
| 代码 | humaneval / mbpp | 编码能力 |
| 推理 | gsm8k / math / gpqa | reasoning 能力 |
| 安全 | jbb_jailbreak / strongreject | 安全能力 |
具体例子:某团队的 customer_service_intent 与 banking77 做 bridge:
- 同跑 5 个候选模型
- 私有分数(依次):0.84 / 0.79 / 0.72 / 0.81 / 0.87
- banking77 分数:0.78 / 0.71 / 0.65 / 0.75 / 0.80
- Spearman 相关性:0.93 → 私有 task 与 banking77 高度相关
- 结论:选 GPT-4o-mini(私有 0.87)合理——可信任公开 leaderboard 顺序
研究背景:
- “Calibration of model selection benchmarks” (Sutawika 2024) 系统讨论 bridge 概念
- HuggingFace 的 “Open LLM Leaderboard custom task” 鼓励私有 task 接入
- 中国大模型团队多用”通用 benchmark + 中文版私有 task”双轨
读者把 PublicPrivateTaskBridge 加到团队 lm-eval 配置——任何私有 task 必带 1 个公开 analog 一起跑。这是评测体系”自我验证 + 同行对照”的工程化武器。
10.7.45 lm-eval 在团队”模型卡片自动生成”中的位置
每个新模型上线必出”Model Card”——下面给出 lm-eval 自动产出 model card 的工程方案:
import json
import subprocess
from pathlib import Path
from datetime import datetime
from dataclasses import dataclass
from typing import Iterable
@dataclass
class ModelCardSection:
section_name: str
content: str
class ModelCardAutoGenerator:
"""从 lm-eval 输出自动生成 Model Card"""
BENCHMARKS_TO_INCLUDE = [
"mmlu", "mmlu_pro", "gsm8k", "humaneval",
"ifeval", "gpqa_diamond", "bbh",
]
def __init__(self, model_id: str, model_meta: dict):
self.model_id = model_id
self.meta = model_meta
def run_evals(self, output_dir: Path) -> dict:
"""跑全套公开 benchmark"""
results = {}
for bench in self.BENCHMARKS_TO_INCLUDE:
output_file = output_dir / f"{bench}.json"
subprocess.run([
"lm_eval", "--model_args", f"pretrained={self.model_id}",
"--tasks", bench,
"--output_path", str(output_file),
"--limit", "500",
], check=False)
if output_file.exists():
results[bench] = json.loads(output_file.read_text())
return results
def generate_card(self, eval_results: dict) -> str:
"""合成 model card markdown"""
sections = []
# Header
sections.append(f"# Model Card: {self.model_id}\n")
sections.append(f"**Generated**: {datetime.now().isoformat()}\n")
sections.append(f"**Created by**: {self.meta.get('author', 'unknown')}\n")
sections.append(f"**License**: {self.meta.get('license', 'unknown')}\n\n")
# Intended use
sections.append("## Intended Use\n")
sections.append(self.meta.get("intended_use", "TBD") + "\n\n")
# Benchmark scores
sections.append("## Benchmark Performance\n")
sections.append("| Benchmark | Score | Tier |\n|---|---|---|\n")
for bench, result in eval_results.items():
score = result.get("results", {}).get(bench, {}).get("acc", 0)
tier = self._tier(score)
sections.append(f"| {bench} | {score:.3f} | {tier} |\n")
# Limitations
sections.append("\n## Known Limitations\n")
for lim in self.meta.get("limitations", []):
sections.append(f"- {lim}\n")
# Safety
sections.append("\n## Safety Evaluation\n")
sections.append(self.meta.get("safety_summary",
"见独立 §16 评测") + "\n")
# Reproducibility
sections.append("\n## How to Reproduce\n")
sections.append("```bash\n")
sections.append(f"lm_eval --model_args pretrained={self.model_id} \\\n")
sections.append(f" --tasks {','.join(self.BENCHMARKS_TO_INCLUDE)} \\\n")
sections.append(f" --output_path ./results.json\n")
sections.append("```\n")
return "".join(sections)
def _tier(self, score: float) -> str:
if score >= 0.85:
return "frontier"
if score >= 0.70:
return "strong"
if score >= 0.50:
return "middle"
return "weak"
flowchart LR M[新模型] --> R[run lm-eval 7 benchmark] R --> S[7 个 score] S --> T[tier 分类] T --> CARD[Model Card markdown] M --> META[人工填: license / intended_use / limitations] META --> CARD CARD --> PUB[发布 HF / GitHub / 内部 wiki] style CARD fill:#e8f5e9 style PUB fill:#e3f2fd
工程实务的 4 条 model card 设计经验:
- 必含至少 5 个 benchmark:单个 benchmark 没说服力
- 必有 reproducibility 段:让外部能验证
- 限制必坦诚:写 “weak in Chinese 古文” 比”全能”更有信任
- 每个新版本必更新:v2 ≠ v1,必重跑
具体例子:某团队内部模型 v2.1 自动 model card 片段:
# Model Card: company/internal-llm-v2.1
## Benchmark Performance
| Benchmark | Score | Tier |
|---|---|---|
| mmlu | 0.62 | middle |
| mmlu_pro | 0.41 | weak |
| gsm8k | 0.83 | strong |
| humaneval | 0.71 | strong |
| ifeval | 0.78 | strong |
| gpqa_diamond | 0.21 | weak |
| bbh | 0.55 | middle |
## Known Limitations
- Chinese 古文理解能力弱
- 数学题超过中学难度准确率显著下降
- Python 之外的编程语言能力较弱
3 类常见 model card 错误:
| 错误 | 现象 | 修法 |
|---|---|---|
| 只写好的 benchmark | 显得 cherry-picking | 必含 weak 维度 |
| 不写 limitations | 用户惊讶 | 主动声明已知短板 |
| 不更新 | v3 model 用 v1 card | 强制每版重跑 |
研究背景:
- “Model Cards for Model Reporting” (Mitchell et al. 2019, arXiv:1810.03993) 是 model card 的奠基论文
- HuggingFace Model Hub 强制要求 README + model card
- Anthropic / OpenAI 的 system card 是 model card 的扩展版
读者把 ModelCardAutoGenerator 接入 CI——任何新模型 release 自动出 card。这是 LLM 工程化的”必备产物”——评测分数最终要”包装”成可消费的文档。
10.7.46 lm-eval 在 fine-tuning loop 的”训练 → 评测 → 反馈”闭环
模型 fine-tuning 时的 lm-eval 用法和”评测某个固定模型”完全不同——它是 training loop 里的一环。下面给出工程化集成:
import asyncio
import subprocess
import json
from pathlib import Path
from dataclasses import dataclass
from typing import Iterable
@dataclass
class CheckpointEvalResult:
checkpoint_step: int
benchmark_scores: dict[str, float]
overall_score: float
is_best_so_far: bool
delta_from_baseline: float
class FineTuningLoopEvalIntegrator:
"""fine-tuning 训练循环中嵌入 lm-eval"""
KEY_BENCHMARKS = ["mmlu", "ifeval", "gsm8k"]
EVAL_EVERY_STEPS = 500 # 每 500 步评测一次
LIMIT_PER_TASK = 200 # 每 task 抽 200 题(速度优先)
def __init__(self, model_save_dir: Path, baseline_score: float):
self.save_dir = model_save_dir
self.baseline = baseline_score
self.best_score = baseline_score
self.history = []
def should_eval(self, step: int) -> bool:
return step > 0 and step % self.EVAL_EVERY_STEPS == 0
async def eval_checkpoint(self, step: int,
checkpoint_path: Path
) -> CheckpointEvalResult:
scores = {}
for bench in self.KEY_BENCHMARKS:
output_file = self.save_dir / f"step_{step}_{bench}.json"
subprocess.run([
"lm_eval", "--model_args",
f"pretrained={checkpoint_path}",
"--tasks", bench,
"--limit", str(self.LIMIT_PER_TASK),
"--output_path", str(output_file),
], check=False)
if output_file.exists():
data = json.loads(output_file.read_text())
scores[bench] = data["results"][bench].get("acc", 0)
overall = sum(scores.values()) / max(len(scores), 1)
is_best = overall > self.best_score
delta = overall - self.baseline
if is_best:
self.best_score = overall
result = CheckpointEvalResult(
checkpoint_step=step,
benchmark_scores=scores,
overall_score=overall,
is_best_so_far=is_best,
delta_from_baseline=delta,
)
self.history.append(result)
return result
def early_stop_signal(self, patience: int = 3) -> bool:
"""连续 N 个 checkpoint 没改善 → early stop"""
if len(self.history) < patience:
return False
recent = self.history[-patience:]
return all(not r.is_best_so_far for r in recent)
flowchart LR
T[fine-tuning train step] --> S{每 500 步?}
S -->|否| T
S -->|是| E[lm-eval 3 benchmark]
E --> SC[overall score]
SC --> CMP{> best?}
CMP -->|是| SAVE[保存为 best checkpoint]
CMP -->|否| HIST[加入 history]
HIST --> ES{连续 3 次没改善?}
ES -->|是| STOP[early stop]
ES -->|否| T
SAVE --> T
style SAVE fill:#e8f5e9
style STOP fill:#fff3e0
工程实务的 4 类训练评测设计:
| 维度 | 训练时评测 | 上线评测 |
|---|---|---|
| 评测集 | 公开 benchmark 抽样 | 完整 benchmark + 私有集 |
| 频率 | 每 500-1000 步 | release 时一次 |
| 速度 | 必须快(≤ 5 min) | 可慢(30 min) |
| 决策 | early stop / 选 checkpoint | 上线 / 不上线 |
具体例子:fine-tune 12 小时训练 360 steps:
| step | mmlu | ifeval | gsm8k | overall | best? |
|---|---|---|---|---|---|
| 500 | 0.55 | 0.62 | 0.40 | 0.52 | baseline 0.50 → ✅ |
| 1000 | 0.60 | 0.68 | 0.50 | 0.59 | ✅ |
| 1500 | 0.62 | 0.71 | 0.55 | 0.63 | ✅ |
| 2000 | 0.61 | 0.70 | 0.54 | 0.62 | 否 |
| 2500 | 0.60 | 0.69 | 0.53 | 0.61 | 否 |
| 3000 | 0.61 | 0.70 | 0.52 | 0.61 | 否 → early stop |
best checkpoint = step 1500,避免 over-training 到 step 3000+。
3 类常见 fine-tune eval 错误:
| 错误 | 现象 | 修法 |
|---|---|---|
| 评测频率过低 | 漏掉 best checkpoint | ≤ 1000 步一次 |
| limit 太小 | 50 题统计噪声大 | ≥ 200 题 |
| 不 early stop | 训练完发现 overfit | patience=3 |
研究背景:
- HuggingFace Transformers Trainer 支持 evaluation callback
- lm-eval —limit 让训练循环 eval 可控时长
- DeepSpeed / vLLM 配合 lm-eval 是标准 fine-tune 评测栈
读者把 FineTuningLoopEvalIntegrator 接入团队 fine-tune 流程——避免”训练完才发现 overfit”。这是 §10 章在 LLM 训练侧的工程化应用。
10.7.47 lm-eval 与”模型市场购买决策”——给采购看的评测报告模板
很多公司从供应商买 LLM 时需要评测对比——下面给出”采购决策评测报告”工程化模板:
import json
from dataclasses import dataclass, field, asdict
from typing import Iterable
@dataclass
class VendorModelEvalReport:
vendor: str
model_name: str
benchmarks: dict[str, float]
weighted_score: float
cost_per_1m_tokens_in: float
cost_per_1m_tokens_out: float
annual_cost_estimate_usd: float
p99_latency_ms: int
license_compliance: str
recommended: bool
rationale: str
class VendorModelComparisonReport:
"""供应商模型比较的工程化评测报告"""
BUSINESS_WEIGHTS = {
"mmlu_pro": 0.20,
"ifeval": 0.25, # 客服业务最重要
"humaneval": 0.10,
"gpqa_diamond": 0.05,
"gsm8k": 0.10,
"safety": 0.20, # 必须高
"latency": 0.10,
}
def evaluate(self, vendor: str, model_name: str,
benchmark_scores: dict, pricing: dict,
monthly_query_volume: int) -> VendorModelEvalReport:
# 综合加权
weighted = sum(
benchmark_scores.get(b, 0) * w
for b, w in self.BUSINESS_WEIGHTS.items()
)
# 年成本估算
avg_in_tokens = 500
avg_out_tokens = 200
annual_cost = (
monthly_query_volume * 12 *
(avg_in_tokens / 1_000_000 * pricing.get("in", 0) +
avg_out_tokens / 1_000_000 * pricing.get("out", 0))
)
# 推荐判定(综合 score / 成本 / 合规)
license_ok = pricing.get("license", "Unknown") in (
"Commercial-Use", "Apache-2.0", "MIT", "Custom-Approved"
)
score_ok = weighted >= 0.65
cost_ok = annual_cost < 50_000_000 # 上限可调
recommended = score_ok and cost_ok and license_ok
if recommended:
rationale = f"通过:score={weighted:.2f}, "\
f"cost=${annual_cost:.0f}, license=OK"
else:
issues = []
if not score_ok: issues.append("score 不达标")
if not cost_ok: issues.append("成本超预算")
if not license_ok: issues.append("license 不合规")
rationale = "拒绝:" + " + ".join(issues)
return VendorModelEvalReport(
vendor=vendor,
model_name=model_name,
benchmarks=benchmark_scores,
weighted_score=round(weighted, 3),
cost_per_1m_tokens_in=pricing.get("in", 0),
cost_per_1m_tokens_out=pricing.get("out", 0),
annual_cost_estimate_usd=round(annual_cost, 0),
p99_latency_ms=pricing.get("p99_latency_ms", 0),
license_compliance="OK" if license_ok else "REVIEW_NEEDED",
recommended=recommended,
rationale=rationale,
)
flowchart LR C[3 候选模型] --> E[Comparison Report] E --> A[Anthropic Claude] E --> O[OpenAI GPT-4o] E --> Q[阿里 Qwen-Max] A --> S1[加权 score] O --> S2[加权 score] Q --> S3[加权 score] S1 --> D[决策矩阵] S2 --> D S3 --> D D --> WIN[推荐 + rationale] D --> REJ[淘汰 + 原因] style WIN fill:#e8f5e9 style REJ fill:#ffebee
工程实务的 4 类采购评测维度:
| 维度 | 权重 | 说明 |
|---|---|---|
| 业务能力 (MMLU/IFEval) | 45% | 核心评测 |
| 安全 | 20% | 红线 |
| 成本 | 20% | 预算约束 |
| 延迟 | 10% | 用户体验 |
| 合规 / license | 5% | 合规底线 |
具体例子:客服 chatbot 选 LLM 报告:
| 候选 | weighted score | 年成本 | license | 推荐 |
|---|---|---|---|---|
| GPT-4o | 0.78 | $48k | OpenAI ToS | ✅ |
| Claude-Sonnet | 0.81 | $52k | Anthropic ToS | ✅ |
| Qwen-Max | 0.71 | $32k | 阿里商用 | ✅(首选 - 最便宜) |
| Gemini-1.5 | 0.76 | $40k | Google ToS | ✅ |
| 内部 13B | 0.55 | $8k | 自有 | ❌ score 不达标 |
最终决策:Qwen-Max(性价比最高 + 合规无问题)。
3 类常见采购评测错误:
| 错误 | 现象 | 修法 |
|---|---|---|
| 只看 benchmark 分 | 选了贵但能力没区别的 | 必加成本权重 |
| 不算业务权重 | MMLU 重要 vs IFEval 重要混淆 | 业务方定权重 |
| 不验 license | 商用拿不到 license | 必查 ToS |
研究背景:
- ML 模型采购的 RFP(Request for Proposal)模板是这套思路的工业范本
- HuggingFace Open LLM Leaderboard 2024-Q3 加 “compliance” 字段
- 中国信通院《大模型评测白皮书》给采购方法学指南
读者用 VendorModelComparisonReport 处理任何 LLM 采购决策——从此采购不再凭”听说 GPT-4 牛”——而是基于业务权重的科学决策。
10.7.48 lm-eval 的”长尾任务覆盖率”——learderboard 高分但生产差的诊断框架
行业常见现象:模型在 MMLU / GSM8K 等头部 benchmark 拿 90+,但接到具体业务后表现”一般”。原因不是模型差,而是”benchmark 与业务任务分布不匹配”——头部 benchmark 集中在 5-10 类任务,而业务可能涉及 50+ 类长尾任务。这个 10.7.48 给读者一份”长尾任务覆盖率”诊断框架。
graph LR
A[模型 leaderboard 高分] --> B{部署到业务后}
B -->|实际表现差| C[根因诊断]
C --> D[业务任务分布分析]
C --> E[lm-eval 任务覆盖分析]
D & E --> F[覆盖率矩阵]
F --> G[发现 gap]
G --> H[补充 lm-eval 长尾 task]
H --> I[重测得到真实分数]
I --> J[决策修正]
lm-eval 头部 / 长尾任务分布对照:
| 任务类型 | lm-eval 内 task 数 | 业务实际占比 | 偏差 | 诊断 |
|---|---|---|---|---|
| 通用知识 (MMLU 系) | 60+ | 5% | 严重过覆盖 | leaderboard 偏向通用 |
| 数学推理 (GSM8K, MATH) | 8 | 3% | 过覆盖 | 学术热点 |
| 代码 (HumanEval, MBPP) | 12 | 8% | 适度 | 行业刚需 |
| 中文 (C-Eval, CMMLU) | 15 | 30% | 严重不足 | 中文业务必扩 |
| 多轮对话 | 3 | 25% | 严重不足 | 实际 chat 主流场景 |
| 工具调用 / Agent | 2 | 15% | 严重不足 | RAG / Agent 场景 |
| 安全 / 合规 | 5 | 10% | 不足 | 监管要求 |
| 业务垂直域 | 0 | 4% | 完全无覆盖 | 必须自建 task |
配套实现:长尾任务覆盖率诊断器:
from dataclasses import dataclass, field
@dataclass
class TaskTypeCoverage:
name: str
lm_eval_task_count: int
business_traffic_share: float # 0.0~1.0
def coverage_index(self) -> float:
"""归一化的覆盖偏差:>1 过覆盖,<1 不足"""
denom = max(self.business_traffic_share, 0.01)
return (self.lm_eval_task_count / 100) / denom
def gap_severity(self) -> str:
idx = self.coverage_index()
if idx > 3: return "严重过覆盖(投入浪费)"
if idx > 1.5: return "过覆盖"
if idx >= 0.5: return "适度"
if idx >= 0.2: return "不足(可能漏诊)"
return "严重不足(leaderboard 与业务严重脱节)"
@dataclass
class LongTailDiagnosis:
coverages: list[TaskTypeCoverage]
def by_severity(self) -> dict[str, list[str]]:
groups = {"严重不足": [], "不足": [], "适度": [],
"过覆盖": [], "严重过覆盖": []}
for c in self.coverages:
sev = c.gap_severity()
for key in groups:
if key in sev:
groups[key].append(c.name)
break
return groups
def recommendation(self) -> list[str]:
groups = self.by_severity()
recs = []
for name in groups.get("严重不足", []):
recs.append(f"高优:自建 lm-eval task 补充 {name}(业务占比高但完全无覆盖)")
for name in groups.get("不足", []):
recs.append(f"中优:扩展 {name} task 覆盖")
for name in groups.get("严重过覆盖", []):
recs.append(f"低优:可减少 {name} 评测频次,节省算力")
return recs
def overall_score(self) -> float:
"""加权平均偏差,越接近 1 越对齐"""
total_weight = sum(c.business_traffic_share for c in self.coverages)
weighted = sum(
min(c.coverage_index(), 3.0) * c.business_traffic_share
for c in self.coverages
)
return weighted / max(total_weight, 0.01)
举例:某中文客服公司诊断 → overall_score = 0.4(远小于理想 1.0),4 类严重不足(中文 / 多轮 / Agent / 业务域)。recommendation 给出 4 条高优行动。两个季度后扩充 30 个 lm-eval task,overall_score 提升到 0.85,从此 leaderboard 分数与业务实际表现 Spearman 相关从 0.3 升到 0.78。
配套行业研究背景:
- “Benchmark validity gap” 来自 BIG-bench paper 2022 第 3 节
- “Long-tail evaluation” 来自 Stanford HELM 2023 提出 16 种场景全覆盖
- “中文大模型评测的本土化挑战” 来自上海 AI 实验室 OpenCompass 白皮书 2024
- 中国信通院《大模型业务对齐能力评估方法》2025
读者把 LongTailDiagnosis 接入年度选型 review——5 分钟看清”模型 leaderboard 分数”与”业务实际能力”的 gap,把评测投入花在长尾刀刃上。这是 lm-eval 在企业落地的”防虚假繁荣”工程工具。
10.7.49 lm-eval 与”开源模型 vs 闭源模型对照”——技术决策中的 4 维平衡矩阵
企业 LLM 选型最反复出现的辩论:用 GPT-4 / Claude(闭源 API)还是用 Llama / Qwen / DeepSeek(开源自托管)?lm-eval 的角色不只是”打个分”——而是给出”在你的场景下,开源是否真的足够”的工程化决策依据。这个 10.7.49 给读者一份 4 维平衡矩阵 + lm-eval 跑分自动决策器,避免”凭情怀选开源”或”凭便利选闭源”两类常见误判。
graph LR
A[选型决策] --> B[4 维评估]
B --> C[1. 任务能力<br/>lm-eval 跑分]
B --> D[2. 数据合规<br/>必须自托管?]
B --> E[3. 成本结构<br/>API vs 自托管]
B --> F[4. 工程能力<br/>能否运维?]
C & D & E & F --> G[决策矩阵]
G --> H[开源自托管<br/>合适场景]
G --> I[闭源 API<br/>合适场景]
G --> J[混合方案<br/>主备策略]
4 维平衡矩阵 + 推荐路径:
| 维度 | 开源(Llama/Qwen) | 闭源(GPT-4/Claude) | 决策权重 |
|---|---|---|---|
| lm-eval 任务覆盖能力 | MMLU 75-80(中端) | MMLU 86-89(顶端) | 30% |
| 数据合规 / 隐私 | ✓ 完全本地 | ✗ 需 BAA / DPA | 25%(金融 / 医疗高) |
| 单次请求成本 | $0.001-0.005 | $0.01-0.05 | 20% |
| 运维工程能力 | 高(GPU 集群 / 推理优化) | 极低(API 调用) | 25% |
配套实现:开源 vs 闭源决策器:
from dataclasses import dataclass, field
from typing import Literal
DeploymentMode = Literal["open_self_hosted", "closed_api", "hybrid"]
@dataclass
class ModelCandidate:
name: str
is_open_source: bool
lm_eval_avg_score: float # 0.0~1.0
self_hostable: bool
cost_per_1k_request_usd: float
requires_dedicated_ops_fte: float # 需要专职 ops 多少人力
@dataclass
class SelectionCriteria:
data_compliance_hard: bool # 数据必须不出公司
monthly_request_volume: int
available_ops_fte: float # 公司能投入的运维 FTE
quality_threshold: float = 0.75 # lm-eval 最低分
@dataclass
class OpenVsClosedDecider:
candidates: list[ModelCandidate]
criteria: SelectionCriteria
def filter_eligible(self) -> list[ModelCandidate]:
eligible = []
for c in self.candidates:
if self.criteria.data_compliance_hard and not c.self_hostable:
continue
if c.lm_eval_avg_score < self.criteria.quality_threshold:
continue
if c.requires_dedicated_ops_fte > self.criteria.available_ops_fte:
continue
eligible.append(c)
return eligible
def score_candidate(self, c: ModelCandidate) -> dict:
# 4 维加权打分
quality_score = c.lm_eval_avg_score
compliance_score = 1.0 if (not self.criteria.data_compliance_hard or c.self_hostable) else 0.0
monthly_cost = c.cost_per_1k_request_usd * (self.criteria.monthly_request_volume / 1000)
cost_score = max(0, 1 - monthly_cost / 50_000) # $50K/月作为基准
ops_score = max(0, 1 - c.requires_dedicated_ops_fte / max(self.criteria.available_ops_fte, 0.1))
weighted = (quality_score * 0.30 + compliance_score * 0.25
+ cost_score * 0.20 + ops_score * 0.25)
return {
"name": c.name,
"weighted_score": weighted,
"quality": quality_score,
"compliance": compliance_score,
"monthly_cost_usd": monthly_cost,
"ops_fte": c.requires_dedicated_ops_fte,
}
def recommend(self) -> dict:
eligible = self.filter_eligible()
if not eligible:
return {"verdict": "no_match", "reason": "无候选满足硬性约束"}
scored = sorted(
(self.score_candidate(c) for c in eligible),
key=lambda x: x["weighted_score"], reverse=True
)
top = scored[0]
# 混合策略:top 1 (开源) + top 1 (闭源) 都进 short-list 时建议混合
opens = [s for s, c in zip(scored, eligible) if c.is_open_source]
closeds = [s for s, c in zip(scored, eligible) if not c.is_open_source]
if opens and closeds and abs(opens[0]["weighted_score"] - closeds[0]["weighted_score"]) < 0.1:
return {"verdict": "hybrid",
"primary": opens[0]["name"],
"fallback": closeds[0]["name"],
"reason": "开源 / 闭源得分相近,建议主备双模"}
return {"verdict": "single",
"winner": top["name"],
"all_scores": scored}
举例:某金融客服系统选型:
- 候选 = [GPT-4-Turbo, Claude Sonnet, Llama-3.1 70B, Qwen2.5 72B, DeepSeek V2]
- criteria:data_compliance_hard=True / 月 5M 请求 / 2 FTE 运维
- filter_eligible → GPT-4 / Claude 直接被淘汰(数据出公司)
- score → Qwen2.5(合规 ✓ + 运维 1.5 FTE)胜 Llama-3.1(运维 2.5 FTE 超预算)
- recommend → “single” winner = Qwen2.5
非合规场景比如内部知识助手,则可能 verdict = “hybrid”(Qwen 主 + GPT-4 fallback)。
配套行业研究背景:
- “Open vs Closed LLM” 决策模型 来自 a16z “State of Open Source AI” 2024
- 自托管 vs API 成本拐点 来自 Anyscale “When Self-hosting Pays Off” 2024
- “Buy vs Build for ML” 来自 Andreessen Horowitz “AI infrastructure stack” 2024
- 中国《大模型选型与采购指南》对开源 vs 闭源决策有专项指导
读者把 OpenVsClosedDecider 接入年度选型决策——5 分钟基于 lm-eval 分数 + 4 维加权得到明确推荐,避免”开源情怀派”和”闭源便利派”的无效辩论。这是 lm-eval 在企业战略决策中”工程化”的最高形态。
10.7.50 lm-eval 的”评测可复现性 + 公开报告 reproducibility 报告卡”——为什么大家跑不出论文里的数
lm-eval 是论文 benchmark 报告的事实标准,但论文里的数和你跑出来的数往往差 1-3pp。如果团队不能解释这个 gap,对内对外都会陷入”是不是我们用错了 lm-eval”的怀疑。这个 10.7.50 给读者一份可复现性 checklist + reproducibility 报告卡,让团队对每次报告负责,避免被外部质疑。
graph LR
A[论文报 MMLU 86.4] --> B{你跑出 84.1}
B --> C[认知冲突]
C --> D[复现性诊断]
D --> E[1. 模型版本]
D --> F[2. lm-eval 版本]
D --> G[3. prompt 模板]
D --> H[4. few-shot 数 K]
D --> I[5. decoding 参数]
D --> J[6. 截断 / 系统 prompt]
E & F & G & H & I & J --> K[gap 归因]
K --> L[reproducibility 卡]
L --> M[公开报告附带卡]
M --> N[同行可复现 / 可质疑]
6 大复现性维度 × 典型 gap × 排查方式:
| 维度 | 典型 gap | 必填字段 | 排查方式 |
|---|---|---|---|
| 模型版本 | 1-3pp | exact model id + revision | huggingface revision hash |
| lm-eval 版本 | 0.5-2pp | git commit | tag / commit |
| prompt 模板 | 0.5-1pp | task yaml hash | lm_eval --show_config |
| few-shot K | 1-5pp | k value | --num_fewshot |
| decoding 参数 | 0.3-1pp | temperature, top_p | —gen_kwargs |
| 截断 / 系统 prompt | 0.5-2pp | max_seq_len, system | 配置文件 |
配套实现:lm-eval 复现性报告卡生成器:
import hashlib
import json
import subprocess
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
@dataclass
class ReproducibilityCard:
task_name: str
model_id: str
model_revision: str # huggingface revision hash 或 API version
lm_eval_commit: str
task_yaml_hash: str
num_fewshot: int
temperature: float
top_p: float
max_seq_len: int
system_prompt_used: bool
score: float
n_samples: int
bootstrap_std: float | None = None
timestamp: datetime = field(default_factory=datetime.now)
def card_id(self) -> str:
canonical = f"{self.task_name}|{self.model_id}|{self.model_revision}|{self.lm_eval_commit}|{self.task_yaml_hash}|{self.num_fewshot}|{self.temperature}"
return hashlib.sha256(canonical.encode()).hexdigest()[:16]
def render_markdown(self) -> str:
return f"""
## Reproducibility Card
| 字段 | 值 |
|------|------|
| task | `{self.task_name}` |
| model | `{self.model_id}@{self.model_revision[:8]}` |
| lm_eval commit | `{self.lm_eval_commit[:8]}` |
| task yaml sha256 | `{self.task_yaml_hash}` |
| num_fewshot | {self.num_fewshot} |
| temperature | {self.temperature} |
| top_p | {self.top_p} |
| max_seq_len | {self.max_seq_len} |
| system_prompt | {'yes' if self.system_prompt_used else 'no'} |
| samples | {self.n_samples} |
| **score** | **{self.score:.4f}**{f' ± {self.bootstrap_std:.4f}' if self.bootstrap_std else ''} |
| card_id | `{self.card_id()}` |
| run_at | {self.timestamp.isoformat()} |
任何人凭这张卡可以精确复现这个分数。
""".strip()
@dataclass
class ReproducibilityComparator:
@staticmethod
def diagnose_gap(card_a: ReproducibilityCard, card_b: ReproducibilityCard) -> dict:
diffs = []
if card_a.model_revision != card_b.model_revision:
diffs.append(("model_revision", card_a.model_revision[:8], card_b.model_revision[:8]))
if card_a.lm_eval_commit != card_b.lm_eval_commit:
diffs.append(("lm_eval_commit", card_a.lm_eval_commit[:8], card_b.lm_eval_commit[:8]))
if card_a.task_yaml_hash != card_b.task_yaml_hash:
diffs.append(("task_yaml_hash", card_a.task_yaml_hash, card_b.task_yaml_hash))
if card_a.num_fewshot != card_b.num_fewshot:
diffs.append(("num_fewshot", card_a.num_fewshot, card_b.num_fewshot))
if card_a.temperature != card_b.temperature:
diffs.append(("temperature", card_a.temperature, card_b.temperature))
if card_a.system_prompt_used != card_b.system_prompt_used:
diffs.append(("system_prompt", card_a.system_prompt_used, card_b.system_prompt_used))
return {
"score_a": card_a.score,
"score_b": card_b.score,
"delta_pp": (card_a.score - card_b.score) * 100,
"differing_fields": diffs,
"verdict": "fully_reproducible" if not diffs else "configuration_drift",
}
@dataclass
class ReproducibilityRequirementChecker:
"""检查报告是否携带了完整的复现性卡"""
REQUIRED_FIELDS = ["model_revision", "lm_eval_commit", "task_yaml_hash",
"num_fewshot", "temperature", "n_samples"]
def check(self, card_dict: dict) -> dict:
missing = [f for f in self.REQUIRED_FIELDS if not card_dict.get(f)]
return {
"complete": len(missing) == 0,
"missing_fields": missing,
"verdict": "ok_to_publish" if not missing else "must_complete_before_publish",
}
举例:某团队报告 MMLU 86.4,外部用户跑出 84.1:
- diagnose_gap → differing_fields: num_fewshot 5 vs 0(论文用 5-shot,外部用 0-shot,差 2.3pp)
- 修正复现 → 84.1 → 86.5(与论文对齐)
- 团队对外公布 reproducibility card → 自此外部 zero-cost 复现,团队声誉建立
- 半年内被引用 12 篇论文,对外报告全部带 reproducibility card → 行业内”信任级”评测团队
配套行业研究背景:
- “ML reproducibility crisis” 来自 NeurIPS 2019 Reproducibility Challenge
- “Model card” 来自 Mitchell et al. FAT 2019
- “Eval card” 来自 EleutherAI lm-eval 文档 2024
- 中国《人工智能可复现性报告规范》对评测复现性卡有规范
读者把 ReproducibilityCard 嵌入团队公开评测报告 / 内部 review template——5 分钟生成 + 5 分钟外部复现,把”评测分数像玄学”升级为”可审计 / 可复现 / 可质疑”。这是 lm-eval 在工业 / 学术报告中的核心可信度工具。
10.7.51 lm-eval 与”评测训练污染检测”——模型是真懂还是背过 benchmark
行业最隐秘的”虚假高分”来源:模型在训练时见过 MMLU / GSM8K 等 benchmark 的题目(公开数据中混入),评测看似 86 分实则是”背题”。lm-eval 跑出来的高分不能直接采信,必须配套训练污染检测。这个 10.7.51 给读者一份「污染检测 + 报告卡」工程方案,让评测分数自带可信度评级。
graph LR
A[lm-eval 评测分数] --> B{污染检测}
B --> C[1. n-gram 重叠]
B --> D[2. 完美匹配率<br/>异常高?]
B --> E[3. 训练数据公开声明<br/>是否覆盖该 benchmark]
B --> F[4. canary 字符串嵌入]
C & D & E & F --> G[污染评分]
G --> H{评分阈值}
H -->|< 0.05| I[clean<br/>分数可信]
H -->|0.05-0.20| J[suspect<br/>需独立测试]
H -->|> 0.20| K[contaminated<br/>分数不可信]
K --> L[报告必须标记 disclaimer]
J --> M[补充 hold-out 测试]
4 类污染检测信号 × 阈值 × 处置:
| 检测信号 | 度量 | warn 阈值 | block 阈值 | 实现 |
|---|---|---|---|---|
| n-gram 重叠 | benchmark 13-gram 在训练数据中比例 | > 0.05 | > 0.20 | 字符串扫描 |
| 完美匹配率异常 | 模型答案 100% 一字不差比例 | > 0.30 | > 0.50 | 比对 ideal |
| 训练数据声明 | 是否公开声明含某 benchmark | 不含 | 含 | 公开声明扫描 |
| canary 字符串 | 故意植入的”咒语”是否被记住 | 部分匹配 | 完全匹配 | canary test |
配套实现:训练污染检测器 + 评测可信度评级:
import re
from dataclasses import dataclass, field
from typing import Literal
ContaminationStatus = Literal["clean", "suspect", "contaminated"]
@dataclass
class ContaminationScore:
benchmark_name: str
ngram_overlap_pct: float
perfect_match_rate: float
training_data_declares_benchmark: bool
canary_match: bool
overall_score: float
status: ContaminationStatus
@dataclass
class ContaminationDetector:
ngram_n: int = 13
overlap_warn: float = 0.05
overlap_block: float = 0.20
perfect_warn: float = 0.30
perfect_block: float = 0.50
def ngram_overlap(self, benchmark_text: str, training_corpus_sample: str) -> float:
"""简化:N-gram 集合交集 / benchmark 总 N-gram 数"""
def ngrams(text: str, n: int) -> set[str]:
tokens = text.split()
return set(" ".join(tokens[i:i+n]) for i in range(len(tokens) - n + 1))
b_ng = ngrams(benchmark_text, self.ngram_n)
t_ng = ngrams(training_corpus_sample, self.ngram_n)
if not b_ng: return 0.0
return len(b_ng & t_ng) / len(b_ng)
def perfect_match_rate(self, model_outputs: list[str], ideals: list[str]) -> float:
if not ideals: return 0.0
matches = sum(1 for m, i in zip(model_outputs, ideals)
if m.strip() == i.strip())
return matches / len(ideals)
def detect(self, benchmark_name: str,
benchmark_text: str,
training_corpus_sample: str,
model_outputs: list[str],
ideals: list[str],
training_data_declares: bool = False,
canary_string: str | None = None,
model_response_to_canary: str | None = None) -> ContaminationScore:
overlap = self.ngram_overlap(benchmark_text, training_corpus_sample)
perfect = self.perfect_match_rate(model_outputs, ideals)
canary_hit = bool(canary_string and model_response_to_canary
and canary_string in model_response_to_canary)
# 综合评分
score = (
min(overlap / 0.20, 1.0) * 0.40 +
min(perfect / 0.50, 1.0) * 0.30 +
(1.0 if training_data_declares else 0.0) * 0.20 +
(1.0 if canary_hit else 0.0) * 0.10
)
if score < 0.05:
status: ContaminationStatus = "clean"
elif score < 0.20:
status = "suspect"
else:
status = "contaminated"
return ContaminationScore(
benchmark_name=benchmark_name,
ngram_overlap_pct=overlap,
perfect_match_rate=perfect,
training_data_declares_benchmark=training_data_declares,
canary_match=canary_hit,
overall_score=round(score, 3),
status=status,
)
def report_disclaimer(self, c: ContaminationScore) -> str:
if c.status == "clean":
return f"[clean] {c.benchmark_name} 分数可直接采信"
if c.status == "suspect":
return (f"[suspect] {c.benchmark_name} 污染分 {c.overall_score};"
f"建议补充 hold-out 测试验证")
return (f"[contaminated] {c.benchmark_name} 污染分 {c.overall_score};"
f"分数不可作为模型能力证明,必须用 fresh benchmark 重测")
举例:某团队评测一款国产开源模型 MMLU 跑 88 分(高于 GPT-4 的 86):
- detect → ngram_overlap 0.15 / perfect_match 0.40 / 训练数据声明含 MMLU = True
- score = 0.15/0.20×0.4 + 0.40/0.50×0.3 + 1.0×0.2 + 0×0.1 = 0.74 → contaminated
- report_disclaimer:「[contaminated] MMLU 分数不可作为模型能力证明」
- 团队改用 fresh benchmark(自建 100 题 hold-out) → 模型实际分 73 分(远低于”88 分”假象)
- 选型决策从「这模型超 GPT-4」修正为「实际中端水平,慎选」
避免依赖虚假高分做出错误的模型采购决策。
配套行业研究背景:
- “Benchmark contamination” 来自 Sainz et al. arXiv:2310.18018
- “Canary string detection” 来自 BIG-bench paper 2022 第 5.4 节
- “Held-out test sets” 来自 ML 经典实验设计
- 中国《大模型评测公正性指南》对污染检测有规范
读者把 ContaminationDetector 接入模型选型流程——5 分钟出”clean / suspect / contaminated”评级 + disclaimer,把 lm-eval 分数从「裸数字」升级为「带可信度评级的工程化数据」。这是 lm-eval 在「行业 leaderboard 越来越多被污染」时代的核心防御工具。
10.8 跨书关联
- 本书第 4 章 §4.7 讨论的统计推断,对应
metrics.py的pop_stddev / sample_stddev / mean_stderr - 本书第 9 章 OpenAI evals 的
bootstrap_std,与本章mean_stderr是同一个统计意图的两种实现 - 本书第 11 章 ragas:会展示 RAG 评测专用框架与”通用 benchmark 评测”的设计差异
- **《vLLM 推理引擎》**第 17 章讨论的吞吐优化,正是 lm-eval 调用 vLLM 后端的目标——快速跑完 MMLU 的 14k 题
- 《Claude Code 工程化》:lm-eval 不直接适用,但其 YAML 配置思想可借鉴
10.9 本章小结
- lm-evaluation-harness 是大模型论文报告 benchmark 数字的事实标准——MMLU / GSM8K / HellaSwag / ARC 全部在它上面跑
- 四层抽象 Task / Instance / LM / Filter 清晰职责分离,核心代码 4500 行
- LM 基类强制 logprob 接口,因此能精确做 multiple_choice 评测——这是它和 OpenAI evals 最根本的差异
- 214 个 task 通过 YAML 驱动,贡献门槛低;MMLU 57 个学科共享模板继承机制
- Metrics 库 30+ 个内置指标覆盖所有主流学术评测;
acc_norm长度归一化体现学术严谨度 - 5 个工程亮点(Filter 流水线 / Cache / 去污染 / Multi-task 并行 / 模板继承)是 fork 改造时该保留的财富
下一章我们看 ragas——RAG 评测的工程范式,与 lm-eval 完全不同的思路。
评论 0
还没有评论,来说两句吧。
评论加载失败,刷新重试。