做模型迭代的人大概都遇过这种场面:自己改了版微调权重,拿几道题手测感觉「明显变强了」,结果同事用另一套题一测说「退步了」,两个人谁也说服不了谁。问题不在模型,在评测过程本身没有纪律——题目不一样、提示词不一样、抽答案的规则不一样、解码参数不一样,任何一处变动都足以让分数飘出好几个百分点。OpenCompass 这类评测框架存在的意义,就是把这堆变量全部固化成配置文件,让「同一个模型、同一套题、同一套规则」变成可复现的事实,而不是口头结论。这篇内容面向的是已经能跑通推理、准备给模型做系统性体检的开发者,尤其是刚接触大模型评测、手里有几张卡但不知道从哪下手的人。我会按实际动手的顺序讲:环境怎么算账、三份配置怎么写、分数不对时怎么逐层往回查、主观评测和自有数据怎么接进去,最后是几个我踩过之后写进 checklist 的细节。
1. 先搞清楚评测链路里哪些环节会偷走你的分数
很多人把评测当成一个「跑个脚本出个数」的动作,实际上从模型权重到最终那一行数字,中间至少要经过六七个环节,每个环节都能独立地让分数偏移。OpenCompass 的设计思路是把这些环节全部显式化,代价是你得知道每个环节在哪、干什么。
1.1 分数不可比的四个隐形来源
第一个来源是提示词模板。同一个模型,few-shot 从 0 改到 5,中文选择题的准确率能差出 3 到 8 个百分点,这不是模型不稳定,是模板变了任务难度就变了。第二个是答案抽取规则。模型输出「答案是 B」,正则提取([A-D])可能拿到B,也可能因为前面有个「A、B、C、D 四个选项中」而错误命中A。第三个是解码参数,温度、top_p、max_out_len 任何一个变动都会影响长答案题型的完整性。第四个最容易被忽略:chat template。用基座模型的模板去套对话微调模型,或者反过来,损失是隐性的,你可能只是觉得「这次分数有点低」。
OpenCompass 把这四类东西分别放进了数据集配置的infer_cfg、后处理函数、模型配置的解码参数和 tokenizer 加载逻辑里。理解这个划分,比记住任何一个命令都重要,因为出问题时你就知道该翻哪个文件。
1.2 框架在整条链路上承担的角色
粗粒度地看,OpenCompass 干四件事:读配置、拼 prompt、批量推理、抽答案打分。它自己不训练模型,也不负责部署服务,所以它对模型的接入方式是「本地路径或 API 地址」两种。推理后端可以换,HuggingFace 原生、vLLM、LMDeploy 都支持,换后端不改变评测规则,这也是它比手写脚本靠谱的地方——你可以用 vLLM 快速刷一遍看趋势,再用 HuggingFace 原生跑一遍做最终确认,两次的题目和规则完全一致。
中间产物全部落盘:predictions/存模型原始输出,results/存结构化评分,summary/存汇总表。这个设计是排查问题的地基,后面第 4 节会重点用它。
2. 开跑之前先把算力和时间的账算明白
我见过太多人一头扎进去,配好文件就python run.py,跑了六个小时发现显存不够或者数据集没下全。这一步花的半小时,能省掉后面半天。
2.1 依赖安装的两个版本陷阱
OpenCompass 依赖mmengine的配置系统,这套东西对版本比较敏感。我的建议是老老实实建独立环境,用官方给的requirements装,不要往已有的训练环境里塞。常见的第一个坑是transformers版本过高或过低导致某些模型的trust_remote_code加载失败,表现是加载时报一个跟模型结构无关的AttributeError;第二个坑是datasets库版本与缓存格式不兼容,报的是数据文件读取错误但实际是库版本问题。
判断方法很简单:新建一个空环境,只装框架依赖,跑一个最小 demo 配置,能出分再往里加东西。别一上来就在大环境里装,装完发现有冲突,你连是哪个包引起的都定位不到。
另外算力上有个容易被忽略的点:评测是纯推理,不需要梯度,所以显存占用主要来自权重 + KV cache,跟训练完全是两个量级。一个 7B 模型在 FP16 下权重约 14GB,配 batch_size=8、输出长度 1024 的话,24GB 卡基本能压住;但如果你把 max_out_len 调到 4096,同样的 batch 就会 OOM。这个关系要心里有数。
2.2 时间成本的粗略估算方法
评测时间 ≈ 样本数 × 平均输出长度 / 吞吐。以中文选择题为例,样本通常几千条,输出短(基本十几个 token),主要开销在 prefill,vLLM 下一个 7B 模型单卡能跑到每秒几十条。但换成代码生成题,输出动辄几百上千 token,吞吐会掉一个数量级,这时候max_out_len设成 512 还是 2048,时间差好几倍。
我的习惯是先用--debug或者只挂一个数据集跑通,看日志里打印的每秒处理条数,再乘以总样本数估总时长。别信「大概半小时」,代码类评测跑一整天是常事。
提示:先用小样本确认链路通,再放大规模。全量跑之前,务必确认
predictions/里已经有内容,否则你可能在等一个根本不会产出结果的任务。
3. 三份配置撑起一次完整评测:模型、数据集、运行参数
OpenCompass 的配置文件本质是 Python 脚本,用read_base把官方预置的配置引入再覆盖。这个设计一开始会让人不适应,但用熟之后非常灵活——你可以在配置里写条件判断、循环生成多个模型条目,这是纯 YAML 做不到的。
3.1 模型配置:什么时候该切到 vLLM
模型配置的核心是四个字段:用哪个后端类、模型路径、解码参数、卡数分配。以对话模型为例,原生后端的写法大致是引入HuggingFacewithChatTemplate,指定path为本地目录,max_out_len给个合理值,batch_size按显存调,run_cfg里写num_gpus。切到 vLLM 只换类名和几个参数,加tensor_parallel_size,其余不变。
什么时候该切?我的经验是:数据量超过一万条样本,或者输出长度超过 512 token,就切 vLLM。原生后端优势在于兼容性最好,冷门模型结构、自定义 tokenizer 都能跑,适合做最终确认;vLLM 优势是吞吐,适合大批量刷分。但 vLLM 有个前提,模型结构得被它支持,某些魔改过的模型会加载失败,这时候退回原生后端,别硬刚。
有个细节值得单独说:abbr字段决定结果目录名,也决定汇总表里的模型名。评测不同版本时,abbr 里一定要带版本号,比如my-model-v3-sft,否则-r复用缓存的时候,旧结果会被当成新结果用,你看到的分数其实是上一版模型的。这个坑我栽过一次,白跑了两轮。
3.2 数据集的 reader / infer / eval 三段式
一个数据集配置拆成三段,理解这三段就理解了一切。
reader_cfg管「从原始文件里读什么」。指定输入列、输出列、训练划分和测试划分。中文选择题的原始数据可能是question、A、B、C、D、answer六列,input_columns里把前五列拼起来喂给模型,output_column指定answer作为标准答案。
infer_cfg管「怎么问」。prompt_template定义 few-shot 示例和提问格式,retriever决定示例从哪来(固定、随机还是按相似度检索),inferencer决定生成式还是困惑度式。选择题一般用生成式,让模型直接吐选项字母;某些基座模型对生成式不敏感,用困惑度式对比各选项的似然反而更稳。
eval_cfg管「怎么判分」。这里挂evaluator和可选的pred_postprocessor。后处理函数就是把模型输出清洗成标准答案的关键一环,比如把「我认为正确选项是:B」规整成B。
我的建议是:先看一遍数据集原始文件长什么样,再去读对应配置。很多人跳过这步直接抄配置,结果字段名对不上,报错信息还特别晦涩。
3.3 分片、缓存与并行度
运行时的几个参数值得单独拎出来。分片参数控制把任务切成多少份并发跑,理论上越大越快,但切得太碎会让日志难以阅读、汇总阶段变慢,一般设成卡数的整数倍比较合适。缓存复用参数在调试阶段是救命的——改个后处理规则重跑,前面的推理结果直接复用,几秒钟出分;但它同时也是污染源,改提示词、改模型路径之后必须清掉旧结果或换输出目录。
还有一个常被忽略的是评估细节落盘开关,打开之后每个样本的判定细节都会写进结果文件,包括模型输出、抽取到的答案、是否正确。排查问题的时候这个开关必须开。
4. 分数明显不对时,从 predictions 往上倒推的排查链路
这是整篇内容里我最想讲清楚的部分。评测出问题的时候,绝大多数人的第一反应是「模型不行」,但十次里有七八次是链路问题。下面这条链路是按信息量从大到小排的,照着走基本能定位。
4.1 第一步永远是打开 predictions 原文
predictions/目录下是模型逐条原始输出,格式是每行一个 JSON 对象,带样本 id 和输出文本。先抽十条看。看一眼你就知道问题在哪:如果输出是空字符串,那是推理阶段的问题(max_out_len 太小、stop token 配置错、生成被提前截断);如果输出是一段完整的分析但没说选项,那是提示词要求不明确;如果输出格式工整但答案明显是错的,那才是模型能力问题。
这个动作三十秒能做完,但能挡掉后面一小时的瞎猜。我现在的习惯是任何一个数据集出分,先眼睛扫十条 predictions,再去看 summary。
4.2 答案抽取失败的三种典型形态
第一种,过度抽取。模型输出「A 选项描述了……所以答案是 B」,正则贪婪匹配第一个[A-D]拿到A。修法是收紧后处理逻辑,优先匹配「答案是」后面的字母,匹配不到再退回全文查找。
第二种,格式错位。模型输出小写b或者带括号的(B),而判分器只认大写裸字母,导致大量样本判为未命中。表现是准确率低得离谱,比如只有随机猜测水平左右。
第三种,空抽取。模型输出太长或者措辞完全偏离预期,正则一条都没命中。这时候去看未命中样本的分布,如果集中在某几个主题上,可能是那部分题目的提示格式有问题。
import re def postprocess(text: str) -> str: # 优先匹配显式结论,避免被题干里的选项字母干扰 m = re.search(r'答案[是为::\s]*([A-Da-d])', text) if m: return m.group(1).upper() # 退一步:取结尾附近出现的最后一个孤立字母 tail = text.strip()[-40:] m = re.search(r'([A-Da-d])\s*[。.]?$', tail) return m.group(1).upper() if m else 'X'这段只是个示意,实际项目里要拿着你自己的 predictions 反复调,调到未命中率降到百分之一以下再开始看分数,否则分数没有意义。
4.3 模板错配与 tokenizer 问题
如果 predictions 里出现了明显的乱码、重复片段、或者每一条都在同一个位置被切断,八成是模板或 tokenizer 问题。对话模型用错了 chat template,表现通常是模型把整段 prompt 当普通文本续写,输出风格突变;tokenizer 版本不匹配,表现是特殊 token 变成奇怪符号。
还有一种隐蔽情况:模型的eos_token配置与生成时的 stop token 列表不一致,导致模型生成完答案之后还在自说自话,一直说到 max_out_len 才停。这种情况不会报错,只会让输出变长、时间变长、后处理变难。看一眼平均输出长度就能发现异常。
5. 主观题和自有数据:把评测从「通用榜」拉到「自己的业务」
公开榜单的分数只能说明模型在公开题上的表现,真正决定上线与否的,是它在你业务场景里的表现。这部分要解决的问题是:开放题怎么判、自己的数据怎么接。
5.1 用模型当裁判的三个偏见
开放题没有标准答案,常见做法是让一个能力更强的模型当裁判打分。这个方案能跑,但有三个偏见必须知道。位置偏见:两个答案 A 和 B 比较时,把顺序换一下裁判可能给出相反结论,解决办法是做双向对比取一致结果。长度偏见:裁判倾向于给更长的答案更高分,即使内容空洞,解决办法是在评分提示里显式约束「不因长度加分」。自我偏好:同源模型打分时会偏向自己家族的输出,解决办法是选一个来源不同的裁判模型,或者混用两个裁判取平均。
还有一个工程细节:裁判模型的调用是逐条串行的,几千条样本的裁判调用成本和时间都不低,实际做的时候我会先抽 200 条看评分分布是否合理,合理了再全量跑。
5.2 自有数据的接入路径
把自己的数据接进去,核心是三件事:转成框架认识的格式、写 reader 配置、选对判分方式。
格式上,最省事的是 JSONL,每行一个对象,字段自己定,但必须有一个标准答案字段。转换的时候注意编码统一用 UTF-8,中文乱码在评测里会变成「模型输出完全对不上」,查半天以为是模型问题。
reader 配置里指定输入列和标准答案列,划分直接给测试集。判分方式分三类:有精确答案的(数字、选项、实体名)用精确匹配加归一化;答案表述多样的(同义词、不同说法)用包含匹配或者用裁判模型;代码类用执行测试用例的方式。
这里有个经验:自有数据的第一版评测,答案最好能够精确匹配。哪怕你手工把开放题改造成选择题,也比一上来就上裁判模型靠谱。等链路稳了,再逐步引入主观评分。我见过太多人第一版就上裁判,分数波动巨大,最后连模型到底有没有进步都判断不了。
6. 几个踩过之后写进 checklist 的细节
最后这部分是我个人认为最值钱的部分,都是文档里不太会写、但实际会咬人的东西。
6.1 缓存复用是双刃剑
复用参数在调试阶段非常香,改后处理规则重跑几秒出结果。但它的判断依据是任务标识,只要你没改模型路径和数据集,它就认为可以复用。问题在于:你改提示词模板了,它不一定认。表现是「我明明改了 prompt,分数一点没变」。判断方法很简单,看日志里有没有「加载已有结果」之类的字样。
我的做法是分三个输出目录:debug-前缀的随便复用,run-前缀的每次全量,final-前缀的跑完立刻归档。听起来笨,但能避免「用了旧结果做了上线决策」这种事故。
6.2 数据污染的识别与自查
公开评测集在训练语料里出现,是分数虚高的最常见原因。自查方法有两类:一是扰动测试,把题目里的选项顺序打乱、把数字改掉,如果分数断崖式下跌,说明模型记住了原题;二是对比测试,同一能力用公开集和一个临时构造的、肯定没被收录的小集各测一遍,差距超过十个点就要警惕。
还有一个反向的坑:某些评测集的题目本身有标注错误,模型答对了但被判错。这种情况在中文数据里不算罕见,做错题分析的时候如果发现某道题的「标准答案」你自己都不认同,别急着调模型,先去看数据。
6.3 结果要落到一张能讲清楚的表
评测的最终产出不是一堆 JSON,而是一张能拿给团队看的对比表。我通常只保留四类信息:模型版本、数据集名、核心指标、样本数。指标一定要带样本数,因为样本量小的数据集波动极大,只看分数不看样本数就是耍流氓。
另外给个实操建议:同一个模型在同一数据集上跑两遍,看分数波动有多大。解码温度设为 0 理论上可复现,但不同的并行度、不同的推理后端仍然可能带来细微差异。知道你自己这套流程的噪声幅度,你才知道「涨了两个点」到底算不算涨。这个数我个人经验是在 0.5 到 1.5 个百分点之间,具体取决于题型。
最后分享一个我一直保留的习惯:每次评测完,随机抽二十道错题,人工看一眼模型到底错在哪——是审题错、推理错、还是格式错。这一步花不了十五分钟,但它给你的信息量远超所有汇总数字,因为汇总数字告诉你「差多少」,错题分析才告诉你「差在哪」。评测这件事真正的价值,从来不在于把模型排出个一二三名,而在于让你知道下一次该往哪个方向改。