lm-evaluation-harness 实战笔记:把 LLM 评测从「跑分」变成可复现的工程

在 LLM 质量保障的日常里,最常被问到的问题之一是:"这个模型到底行不行?"

但"行不行"从来不是一个数字能回答的。同样是"MMLU 78 分",few-shot 设成几?prompt 模板用什么?是不是 chat 格式?温度多少?有没有踩到测试集污染?——任何一项没对齐,跨团队、跨模型、跨版本的数字都不可比。

lm-evaluation-harness 就是为消灭这种"不可比"而生的。它是 EleutherAI 维护的 LLM 评测框架,也是 HuggingFace Open LLM Leaderboard 背后的评测引擎。这篇文章是我把它真正用进质量体系的实战笔记,不堆概念,只讲怎么跑、怎么定制、怎么避坑。

它解决什么问题

一句话:把"评测"固化成可版本化、可复现、可对比的工程件。

  • Task(评测任务)用 YAML 声明,数据集、prompt 模板、few-shot、指标全部显式化
  • Model(被测对象)通过统一后端接入:HuggingFace 本地模型、vLLM、OpenAI 兼容 API 都能跑
  • 输出结构化结果(JSON),可直接入库、diff 对比版本差异

这意味着评测不再是"人肉跑一遍脚本记个分",而是流水线里的一等公民:模型换版本、prompt 改一版、数据集加一批 case,跑同一套 task 就能给出可解释的回归结论。

安装与最小上手

pip install lm-eval

# 以 HuggingFace 后端跑一个 7B 模型
lm_eval \
  --model hf \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct,dtype=bfloat16 \
  --tasks mmlu,gsm8k \
  --num_fewshot 5 \
  --batch_size auto \
  --output_path results/

跑完会得到每个 task 的 acc / exact_match 等指标,以及每条样本的原始输出(samples),方便失败 case 回放。这一步已经解决了"口径可复现":把命令行和版本写进 CI 配置,任何人重跑结果一致。

核心概念:Task 是一等公民

一个 task 的 YAML 大致长这样:

task: my_company_qa
dataset_path: json
dataset_kwargs:
  data_files:
    test: data/qa_test.jsonl
output_type: generate_until
test_split: test
doc_to_text: "问题:{{question}}\n回答:"
doc_to_target: "{{answer}}"
metric_list:
  - metric: exact_match
    aggregation: mean

关键字段:

  • doc_to_text / doc_to_target:prompt 模板与参考答案的映射,这是评测口径的核心,改一个字结果就可能漂移
  • output_type:multiple_choice(选择题,看选项 token 概率)或 generate_until(生成式,匹配答案)
  • num_fewshot:few-shot 示例数,必须在 config 里显式声明
  • metric_list:指标与聚合方式

所以真正专业的评测实践是:自定义 task 承载业务 golden set,而不是只跑公开 benchmark。公开榜看"通用能力水位",自定义 task 看"业务场景生死"。

把业务数据接进来

以我们对话质检场景为例,把 300 条人工标注的"问题 + 期望回答要点"转成 JSONL:

{"question": "退款多久到账?", "answer": "通常 1-3 个工作日,原路退回"}
{"question": "会员能退吗?", "answer": "虚拟商品不支持退款,可申请补偿"}

再用上面的 YAML 声明一个 task,评测时 --tasks my_company_qa 即可。相比手写脚本,好处是:数据、prompt、指标三件套被锁进同一个文件,改一处 diff 一目了然。

工程化:把它接进流水线

单次跑分没意义,持续跑、可对比才有意义。几点实操建议:

  1. 固定版本:lm-eval、数据集、模型 commit 全部锁定,评测环境用容器镜像
  2. 结果落库:--output_path 的 JSON 解析后写入评测平台,按"模型版本 × task × 日期"建索引
  3. diff 而非绝对分:新模型版本 vs 基线,看每个 task 的涨跌与失败样本变化,而不是只看总分
  4. 门禁:核心 task 掉分超过阈值(如 2%)直接卡发布——这就是质量门禁的入口
  5. 抽样复核:generate_until 类 task 的自动指标有误判,定期人工抽检失败样本,校准指标与人工判断的一致性

几个真实的坑

  • prompt 模板不一致:同一模型,问题:{q} 和 Q: {q} 能差好几分。模板必须进版本管理
  • few-shot 数不统一:公开分和自查分对不上,80% 的锅在 few-shot 设置
  • chat vs base 模型:Instruct 模型若未用 chat template 评测,分数会显著偏低;--apply_chat_template 按需开启
  • 数据污染:自建 golden set 要防止被训练数据"见过",评测集与训练集做去重
  • 采样参数:生成类任务建议贪心解码(temperature=0)保证可复现;否则每次结果有抖动
  • batch_size=auto 只是省心:显存吃紧时手动设小一点,避免 OOM 打断评测

在质量体系里的定位

我的理解是:lm-evaluation-harness 解决的是"可复现的批量回归",但它替代不了两件事——真实流量的线上评测,和人工的精细判断。

比较合理的分层是:

  • 公开 benchmark(MMLU/GSM8K 等):通用能力水位线,横向对比同类模型
  • 业务 golden set(自定义 task):验收标准,进发布门禁
  • 线上 A/B 与人工抽检:真实分布兜底,校准离线结论

三层各司其职,评测才不会沦为"跑分自嗨"。

小结

lm-evaluation-harness 的价值不在于它能跑多少 benchmark,而在于它把评测的"口径"变成了代码资产:task 可版本化、结果可对比、流程可进 CI。对于做 LLM 质量保障的人来说,它是把"我觉得还行"变成"数据证明有提升"的最短路径。

一句话建议:先跑通一个公开 task 建立手感,再把你自己的 300 条业务 case 变成第一个自定义 task——从那一刻起,评测才真正开始为你的业务服务。