☰
大模型评测避坑指南:基准选择、数据污染与可复现流程
2026/10/27 7:54:59 网站建设 项目流程

1. 为什么"跑个分"不能解决评测问题

先讲一句不太好听的话:如果你只是拉一个开源评测脚本,把模型丢进去跑一遍 MMLU,拿到一个准确率数字,就宣布"这个模型表现不错",那这个结论的可信度可能非常低。问题不在脚本有没有 bug,而是评测这件事本身充满了隐藏变量——基准选得是否贴合模型定位、基准题是不是早就混进预训练语料、采样参数是不是公平、同一个模型在不同推理框架下跑分是否一致,这些环节任何一处出问题,最终数字都会失真。

我见过不少团队在汇报模型效果时,喜欢把跑分表格做得漂漂亮亮,但追问一句"这个分数是基于哪个版本跑的、用了什么 prompt、随机数种子是多少、评测数据有没有排除训练集重叠",对方就答不上来了。这在 LLM Training Lab 系列前面的文章里也反复强调过:训练是黑盒,评测是白盒,你唯一能客观判断模型变好还是变坏的手段就是评测。评测如果不可信,后面所有迭代都白做。

做评测,先得把三个核心问题想清楚:

第一,你的模型到底要解决什么问题。通用对话、代码生成、数学推理、中文知识问答,这些方向对应的评测基准差异很大,混着用会得出误导性结论。

第二,你的评测分数是给谁看的。内部研发迭代更关注细分能力的变化趋势,外部做技术选型或发报告更关注综合排名和可复现性,不同受众对评测严谨程度的要求完全不同。

第三,你的评测结果能不能复现。过一个月、换一个人、换一台机器,能不能跑出完全一致的分数,这直接决定评测数据有没有长期参考价值。

这篇文章就围绕这三点展开:基准该怎么选、数据污染怎么识别、评测流程怎么搭建才能做到可复现。内容全部来自我实际跑评测踩过的坑和总结出的方法,不堆理论,直接说可落地的方案。

2. 基准选择:选错基准等于白测

很多人在选基准时存在一个惯性思维:哪个基准名气大就用哪个。但"名气大"和"适合你的模型"完全是两回事。评测这件事,基准的选择本质上是在"验证模型能力"和"验证训练效果"之间做一个对齐。基准选偏了,你再怎么仔细跑流程,得出的结论对项目迭代也没有参考价值。

2.1 按能力维度挑基准,而不是按热度挑

模型评测的第一步,是把你关心的能力维度列出来,再逐个匹配候选基准。我一般把能力维度拆成下面几类,每个维度都有对应的主力基准:

能力维度主要基准评测方式注意事项
综合知识MMLU、MMLU-Pro、C-Eval、CMMLU选择题MMLU 某些科目已接近饱和,建议同步看 MMLU-Pro
数学推理GSM8K、MATH、AIME生成式需要关注 true shot 是否一致
代码生成HumanEval、MBPP、LiveCodeBench生成式LiveCodeBench 时间窗口较新,不易被污染
逻辑推理BBH、ARC-C、GPQA混合式难度跨度大,注意样本量
中文能力C-Eval、CMMLU选择题注意繁体/简体版本差异
长文本处理LongBench、L-Eval生成式注意上下文窗口上限
指令遵循IFEval规则校验比人工打分更可复现
真实任务SWE-bench、GAIA、AgentBench端到端成本高,但和业务接近

举例说明。如果你在做一个面向程序员的大模型,核心能力是代码生成和代码理解,那 MMLU 这种百科知识类基准就只是一个很边缘的参考维度。你更应该看 HumanEval、MBPP、LiveCodeBench 这三个基准,甚至再深挖一层,区分"从注释生成完整函数"和"从上下文补全代码片段"这两类任务。倒不是说 MMLU 不需要测,而是它的分数不能成为判断模型好坏的主导指标。

再举一个反面案例。我见过团队在微调一个客服领域模型时,拿着一份 C-Eval 分数提升的报告向老板汇报,理由是这个分数最直观。可实际上客服模型的核心能力是上下文理解、多轮对话一致性、回复是否贴合业务规则,C-Eval 考的是百科知识,两者之间几乎没有正向关系。这就是基准选偏了,测试结果对业务没有指导价值。

2.2 基准饱和与被"刷穿"的问题

基准选完还要判断一件事:这个基准是否已经被"打穿"了。所谓打穿,指的是领先模型已经在这个基准上拿到接近或超过人类水平的分,剩余区分度很低。这时候你拿这个基准去衡量两个模型的差异,会出现一种情况:明明一个模型的实际用户体验差了一大截,但它们在基准上的分数只差 0.5 个百分点。

最典型的是 HellaSwag。这个基准刚出来时用来测常识推理,后来饱和得非常快,一票模型都刷到了 90 分以上,再做模型间对比已经没有说服力了。MMLU 整体也从 2023 年的六十几分被刷到了现在接近九十分,虽然还有一定区分度,但建议同步观察 MMLU-Pro。MMLU-Pro 对原版 MMLU 做了三件事:增加选项数量、去掉容易被猜测的题目、补充更多需要多步推理的难题,区分度比原版好很多。

我自己的习惯是保留一组"稳定基准"和一组"新鲜基准"。稳定基准用来做历史对比,保证每个版本之间可以纵向看趋势;新鲜基准用来做横向 check,防止模型过拟合旧基准的分布。LiveCodeBench 就是典型的新鲜基准,它会动态追加新题,各大榜单上它的分数含金量也更高。坚持这个组合逻辑,比盯着一个名声响亮的基准更有用。

2.3 训练集与评测集的交叉隐患

选基准时很多人容易忽略一个前置检查:候选基准的题目,是不是已经出现在你的预训练语料或微调数据里了。这不是危言耸听,而是数据圈真实存在的常态。互联网上的评测题满天飞,爬虫抓一遍语料就能把它们全带走。如果模型在训练阶段见过评测题,评测分数就变成了记忆力的证明,而不是能力的证明。

这个问题的前置检查办法很粗暴:先把评测基准的题目和你的训练语料做一个重叠检测,再决定要不要用这个基准。检测方法后面有专门章节细讲,这里想强调的是流程意识——不是跑完评测再怀疑分数,而是在选基准的当下就完成污染筛查。把这个筛查动作固定到评测流程里,比出了结果再去抢救靠谱得多。

3. 数据污染:让高分可信度归零的隐形杀手

数据污染是我在评测里最头疼的问题。它不像环境配置错误那样容易发现,也不像基准选错那样能明显看出不合理。污染的可怕之处在于,分数看起来合情合理,甚至高得让人兴奋,但实际上是模型"背题"背出来的。一旦带污染数据的模型上线,真实场景立马原形毕露。

3.1 数据污染的四种典型形态

我把实际工作中遇到的数据污染归纳成四种形态,它们隐蔽程度逐级递增:

第一种,原题完整出现在训练语料。这种情况最直白。预训练阶段爬虫把评测题整段抓进了语料,评测时模型直接调取记忆。如果模型连选项顺序都一字不差地背下来了,那它在选择题上的表现会好到异常。最直接的验证方法是把选项顺序打乱重新测试,如果分数出现断崖式下跌,基本可以断定模型在背答案而不是做题。

第二种,评测题被改头换面后进入语料。有些数据集会做去重,会改写题目,比如把 "What is the capital of France?" 改写成 "The capital city of France is which one?",但这道题本身的知识点还是进了预训练。这种污染的隐蔽性在于文本重叠检测可能发现不了,但从行为上能看出来——相关知识点题目全对,但稍有变式的同类问题表现却一般,说明模型学的不是推理规则,而是题目形态。

第三种,指令微调阶段引入了带解析的评测题。模型在 SFT 阶段见过完整的 "题目 + 答案解析",这时候模型学的不是知识,而是模仿答题格式。它的症状是:输出结构完美,推理过程详细,但从中途某一步开始出现逻辑断裂,因为它在"回忆"而不是"推理"。

第四种,评测题以近重复文本形式出现。比如模型训练数据里有一篇论文,其中包含了一道物理题的数字和题干,但表述不同,连模糊查重工具都查不出来。这种污染只能靠样本级的对照测试来发现。

3.2 数据污染的检测手段

检测数据污染没有一招鲜,核心是组合使用多种方法。下面按成本从低到高列一下:

方法一:n-gram 重叠检测。用脚本把评测题拆成 8-gram 或 13-gram,和训练语料的抽样片段或完整索引做匹配。如果某道题的大段文本和训练语料高度重叠,就要警惕了。开源社区有不少现成工具,直接拉下来改改用就行。缺点很明显:只对原题和近原题有效,对改写型污染没辙。但低成本高覆盖率,值得作为第一道防线。

方法二:行为线索观察。直接在评测过程中观察模型输出的元信息。如果模型在答题时"不小心"说出了题目的来源,比如输出 "According to MMLU question 1234...",那就实锤了。实际操作中我见过模型真的输出类似 "This question is from the MMLU benchmark" 的情况,虽然不多,但一抓一个准。

方法三:对照组测试。这是我自己最常用的方法。把同一道题的题干做轻微改写,比如调换专有名词、改变数字条件、把选择题改成简答题,然后对比原题和改写题的分差。如果分差极大,大概率污染。这个方法做起来成本不低,但验证效果最直接。建议每个基准抽样 50 到 100 道题做对照测试,不用全量做。

方法四:交叉验证陌生样本。使用一些时间窗口较新、网络公开样本较少的基准作为"试金石"。典型例子就是 LiveCodeBench 和 MathArena,它们有意识地控制题目公开范围,天然具备抗污染能力。如果你的模型在旧基准上分数很高,但在这些新基准上表现平平,那旧基准的高分含金量要打个问号。

3.3 识别污染之后怎么办

确认污染之后,最忌讳的做法是假装没看见,直接把分数写进汇报。我的处理方式是分场景走:

  • 如果污染比例不高(小于 1%),且影响范围集中在某几个题目:剔除污染的题目重新评测,同时记录剔除清单,保证后续评测在同一题目集合上进行。
  • 如果污染比例较高或无法判断污染边界:放弃这个基准,选择替代基准或新版本基准,并在评测报告中注明替换原因。
  • 如果是训练阶段发现的污染:赶紧检查数据管线,定位评测数据进入训练数据的路径,给数据管线加一个"评测题黑名单",从源头隔离。

这里要特别提醒一点:经常看到有人为了提高汇报分数,故意不处理污染,甚至跑到榜单上用带污染的模型刷分。这种事短期看很爽,长期看没有任何价值。评测的目的是辅助决策,不是装饰门面,污染不处理,你后续的每一次训练迭代都会被带偏。

4. 搭建可复现的评测流程

选好基准、排除污染之后,接下来要解决的是流程问题。评测流程的可复现性,决定了你测出来的分数能不能从"某一次运行结果"变成"长期可信的参考数据"。我见过很多评测脚本,跑完只输出一个分数,不保留任何上下文信息,过两周再跑发现分数不一样,又找不到原因,最后只能归咎于"玄学"。实际上,评测结果不可复现的原因通常非常具体,下面拆开讲。

4.1 固定环境和依赖版本

可复现评测的第一个坎是环境。模型权重、依赖库、CUDA 版本、推理框架版本,任何一个变化都可能让最终分数产生可感知的波动。举个例子,Transformers 库某些版本的实现变动会影响特定模型的输出,VLLM 的版本迭代也会导致采样行为发生变化。你今天用 transformers 4.38 跑出的分数,换了 4.40 可能就变了。

我的做法是用 conda 为每个评测项目建立独立虚拟环境,同时把关键依赖的精确版本号记录在评测配置里。下面是一个我常用的环境配置片段,可直接参考:

conda create -n eval-lab15 python=3.10 conda activate eval-lab15 pip install torch==2.1.2 transformers==4.38.2 vllm==0.4.1 datasets==2.17.0 pip install lm-eval==0.4.2

环境固定后还要固定模型权重本身。我习惯每次评测前记录模型文件的 SHA256 哈希,防止模型文件被误替换或误改动。别觉得这一步多余,我真实遇到过训练好的模型在评测前被同事覆盖成旧版本,如果当时没查哈希,整个评测报告就报废了。

4.2 提示模板、采样参数与 Few-shot 设置

评测中出现的分数差异,很大一部分来自提示模板和采样参数的不一致。同一个任务,system prompt 加一句话,temperature 从 0 调到 0.7,few-shot 示例换了几条,分数变化可以高达 5 到 10 个百分点。这里我把需要用到的固定项整理出来:

  • System Prompt:必须固定为唯一版本,写入评测配置文件中,禁止在评测过程中临时手写。
  • Few-shot 示例:固定使用同一组示例,顺序不能变化。示例顺序本身会影响模型输出。
  • Temperature:建议设 0,使用贪心解码。如果确实需要采样,固定 temperature=0.7、top_p=0.9,并固定随机种子。
  • max_tokens:根据任务类型固定。选择题可以设置 64,代码生成建议 512 或更高。
  • Stop Words:需要针对不同任务设置停止符,避免模型输出多余的推理过程。

下面是一个可参考的评测配置 JSON 示例,把它和评测结果存放在一起:

{ "model": "/data/models/chat-7b-v3", "model_sha256": "a3f8f1cf08456f8a1c6c3d7a1e9f10f3b2e5a9c4d4e8f3a4f1b2c3d4e5f6a7b8c", "framework": "vllm", "framework_version": "0.4.1", "dtype": "bfloat16", "tasks": ["mmlu_pro", "gsm8k", "livecodebench"], "num_fewshot": { "mmlu_pro": 5, "gsm8k": 8, "livecodebench": 0 }, "sampling": { "temperature": 0, "top_p": 1.0, "max_tokens": 2048 }, "system_prompt": "You are a helpful assistant...", "seed": 42 }

4.3 评测脚本的核心骨架

评测脚本这部分,我强烈建议使用成熟框架,不要自己从头写。目前社区主流是 lm-evaluation-harness 和 OpenCompass,两者都支持大量基准、支持 vLLM/HF 后端,也支持批量跑任务。很多前人对齐过的细节,比如 GPU 并行策略、批次组织、指标归一化逻辑,你自己写一遍成本非常高。

下面是使用 lm-evaluation-harness 跑多个任务的示例命令:

lm_eval \ --model vllm \ --model_args pretrained=/data/models/chat-7b-v3,dtype=bfloat16,tensor_parallel_size=1 \ --tasks mmlu_pro,gsm8k,bbh \ --num_fewshot 5 \ --batch_size auto \ --output_path /data/evals/2025-01-15_chat-7b-v3 \ --limit 500

几个参数的实际含义需要展开解释。--limit 500表示每个任务只跑 500 道题,用于快速 sanity check;正式发布或训练迭代对比时,建议去掉--limit跑全量,虽然慢一些,但分数更可靠。--batch_size auto让框架根据显存自动调整批次,避免 OOM;但不同批次的精度偶尔会有细微差异。--num_fewshot 5是覆盖默认设置,因为某些任务的默认 few-shot 会变化,显式指定是保证可复现的好习惯。

如果做代码类基准,我建议再看看 bigcode-evaluation-harness,它专门针对 HumanEval 和 MBPP 做了很好的适配,包括同时计算 pass@1 和 pass@10 的及格率逻辑。

4.4 评测记录与结果追溯

评测跑完不记录过程数据,等于白跑。每次评测结束后,我要求自己至少保留三样东西:评测配置、原始日志、输出结果文件。这三样东西缺了任何一样,隔两周再看分数时就没法判断它到底有没有参考价值。

推荐把上面那个 JSON 配置和评测结果放在同一目录下,目录命名用"日期+模型名+分支"格式,方便日后检索。下面是我常用的目录结构:

/data/evals/ └── 2025-01-15_chat-7b-v3/ ├── eval_config.json ├── tasks/ │ ├── mmlu_pro_output.jsonl │ ├── gsm8k_output.jsonl │ └── bbh_output.jsonl ├── metrics_summary.json └── eval.log

还有一个小细节:如果评测脚本或者被评测模型的仓库本身有 git 版本管理,建议在评测配置里记录对应的 commit ID。这样将来有人问起"这个分数是哪个代码版本生成的",你能直接给出精准答案,而不是回一句"好像是上个月的版本"。

5. 常见问题与排查技巧实录

可复现评测流程落地过程中,百分之百会遇到各种奇怪问题。这部分我把自己和周围同事踩过的坑整理成速查表,尽量让看到的人少走弯路。

5.1 同模型同配置两次跑分不一样

我最早遇到这个问题时也怀疑是玄学,后来逐步排查发现主要是三个原因。第一个原因是显存不够时框架自动降低了批次,而不同 batch size 下部分算子计算的浮点累加顺序不同,会引入微小差异。第二个原因是 使用了非贪心解码,temperature 大于 0 时每次采样结果天然不一样,解决方案是固定 seed 或者直接用贪心。第三个原因是评测过程中有别的进程在抢占 GPU,导致张量并行产生细微差异。

排查路径很简单:先用批次统一加固定 seed 的贪心解码方案,如果分数还波动,就把监控打开看显存和其他进程。实话说,90% 的波动问题都能靠"固定参数 + 独占显卡"解决。

5.2 VLLM 和 HuggingFace Transformers 跑分不一致

VLLM 用起来比 Transformers 快几个量级,很多人图省事直接用 VLLM 跑评测。但要明确一点:VLLM 和 Transformers 的实现差异会导致同一模型产生不完全相同的输出,尤其在某些激活函数和采样细节上。如果你用 Transformers 精调模型,用 VLLM 去评测,得到的分数理论上不等于 Transformers 的结果。

我的建议是保持一致性:训练阶段的 sanity check 用 Transformers 跑,因为训练节点通常已经装好 Transformers;做正式评测或对外报告时统一用同一个推理框架。如果必须在两个框架间切换,先在同一个评测子任务上做一次对照测试,量化差异后记录在配置里,后续对比才公平。

5.3 评测中途 OOM 或进程崩溃

评测是一个很重的计算任务,跑十几个基准可能连续跑几小时甚至几天,中途 OOM 真的很崩溃。我的习惯是分任务提交而不是一次性全跑。每个基准单独一个命令,跑完保存结果再跑下一个。这样即使某个任务崩溃,也不会拖累已经完成的评测结果。

另外要留意一个隐藏问题:长时间评测时显存碎片会逐渐累积。虽然框架大多会做显存复用,但极端情况下还是会出现"显存没有满但分配失败"的情况。遇到这种问题,建议加一个定时清理脚本,或者把评测任务拆得更小,每完成一个任务就重启一次 Python 进程。

5.4 题目级失败:到底是谁的错

评测分数出来后,逐题看失败案例也是必要动作。我经常见到一种情况:某个任务分数低,但逐题检查发现模型输出是对的,只是后处理脚本没用对。比如代码生成任务,模型在代码块外加了说明文字,后处理没正确提取,被判了错误。这种失败不是模型能力问题,而是评测实现问题。

遇到这类问题,建议先看输出原文,再确认后处理逻辑,最后再下结论。我在排查类似问题时,会随机抽查 20 到 30 条"失败"样本,确认失败原因分布。如果发现大部分失败是因为输出格式解析问题,那就应该修正后处理逻辑并重新评测,而不是把这个低分当作真实能力结论。

6. 实操心得:我建议的小型评测清单

最后这部分,分享一套我现在做模型迭代时实际使用的评测清单。这套清单的目标是做训练过程中的快速回归,而不是发论文级的全面评测,所以综合考虑了成本和覆盖度。如果你刚接触评测,可以从这里开始搭自己的流程。

日常迭代回归集(每次训练完必跑):

  • C-Eval(中文通用能力,跑 5-shot,看中文场景基线)
  • MMLU-Pro(英文综合能力,跑 5-shot,比 MMLU 更有区分度)
  • GSM8K(数学推理,跑 8-shot,测推理是否退化)
  • HumanEval(代码生成,跑 pass@1,测代码能力)
  • BBH(逻辑推理,跑 3-shot,测通用推理)

这套组合跑下来大概需要 1 到 2 个小时(7B 模型、单卡),能在较短时间内覆盖知识、推理、代码、中文、逻辑五个核心维度。哪一项掉分了,就知道该往哪个方向排查。

发布前完整评测集(对外发报告或上线前跑):

  • 在回归集基础上,增加 MATH、GPQA、LiveCodeBench、IFEval、LongBench。
  • 全部跑全量样本,不做 limit 截断。
  • 运行三次取中位数,尽量降低偶发波动对结论的影响。
  • 将评测配置、日志、结果归档,并记录模型权重哈希和代码 commit ID。

关于运行多次取中位数,这也是我从实际经历里得到的教训。某次我拿同一份配置连跑两次,分数差出 0.8 个百分点,单看其中任何一次都会得出不同结论,最后取了三次的中位数,才得到相对稳定的参考值。评测成本允许的话,跑三次依然是性价比较高的选择。

写这篇内容之前,我又重新翻了一遍自己做过的几十份评测记录,发现那些踩坑的时间远比训练本身花得多,但换来的是现在拿到任何跑分都能迅速判断它可不可信。评测不是一个跑完就丢弃的脚本,而是整个训练闭环里最值得反复打磨的环节。希望这篇分享能帮你把评测这座地基打得稳一点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询