上周把一个 7B 量级的对话模型推到测试环境之前被问了一句最难回答的话它到底行不行。说感觉还行没人信说跑分不错又会立刻被追问跑的是什么分、和谁比、怎么跑的。这类问题躲不掉只能老老实实做一遍完整评测。这次我做的是一条可复现的评测流水线用配置驱动的评测框架 OpenCompass 把开源模型从能对话变成有据可查的分数。整件事的目标很朴素给定一个本地部署的模型权重用同一套提示词模板、同一套解码参数、同一套答案抽取规则跑完知识、推理、代码、长文本、指令跟随这几类基准最后产出一份别人拿着能复现、能对比、能挑刺的报告。适合谁看刚接手模型评测任务、手上只有一张卡、又不想把时间浪费在反复调环境上的同学也适合已经跑过一两次、但分数总觉得不对劲的人。下面我把整个流程按我实际做的顺序拆开讲包括那些文档里不会写、但会让你多熬两个晚上的细节。1. 先把评测这件事想清楚1.1 一次模型体检到底要回答哪几个问题很多人一上来就下载数据集、跑命令跑完拿到一个 62.3 的数字然后卡住了——这个数字意味着什么和谁比能不能写进报告问题出在顺序上应该先确定我要回答哪些问题再去选数据集。我这次给自己列了五类问题每一类都对应明确的决策场景。第一类是基础知识覆盖面。用户问历史、地理、常识模型答不上来就是硬伤。这类用中文选择题基准最合适四选一答案唯一抽取规则简单分数可比性最好。第二类是数学与逻辑推理。这里要区分会算和会推小学应用题和竞赛题是两个量级得分开跑否则平均分毫无意义。第三类是代码能力。代码评测不能只看能不能写出语法正确的片段还要看能不能通过单元测试所以要用带测试用例的执行式基准指标是 pass1。第四类是指令跟随与格式约束。这条最容易被忽略但对产品落地影响最大要求它输出 JSON 就输出 JSON要求它只回答选项字母就别絮絮叨叨解释一堆。这类能力用选择题基准是测不出来的必须用专门的约束型基准。第五类是长文本。把一篇两万字的文档丢进去问细节看它是不是只记得开头和结尾。这五类问题定下来后面的数据集选择、显存预算、时间成本才有依据。我特意没有把安全与价值观放进这次的技术评测里原因是这类评估高度依赖人工标注和场景定义用自动指标做出来的数字很容易误导人宁可在后面单独做一轮人工抽检也不要塞进自动流水线凑数。1.2 公开基准和自建题库到底该信哪个这个问题的答案不是二选一而是分工。公开基准的价值在于可比性——你说 68 分别人也知道 68 分大概是什么水平因为大家用的是同一套题、同一套抽取规则。它的软肋在于数据污染这些题目在互联网上流传了好几年训练语料里大概率出现过模型可能是背过而不是会做。自建题库的价值正好相反它干净、贴合业务但没有横向参照系你也说不清 75 分是高还是低。我这次的做法是两条腿走路主评测用公开基准保证分数能对外说同时准备一套 200 题左右的自建题全部来自真实业务问题只做内部参考不对外报分。自建题库有个实操上的坑要提醒题目必须自己写或者从真实对话里脱敏整理千万不要为了省事拿公开基准改几个数字。改数字这种做法看似规避了污染实际上往往把题目的逻辑改坏了答案变得有歧义最后评测出来的分数比不测还糟。还有一个细节自建题库要配套写评分要点尤其是开放题否则两个人打分能差出二十分。1.3 为什么最后选了配置驱动的评测框架选框架这件事我对比过三种路子。第一种是自己写脚本用 transformers 加载模型、循环喂数据、正则抽答案。好处是完全可控坏处是每换一个数据集就要重写一遍数据加载和答案抽取而且提示词模板散落在代码里过两周自己都记不清当时用的是哪版模板——这是最致命的模板不一致分数就没有可比性。第二种是直接用各个基准官方仓库的评测脚本。每条线单独跑能拿到最正宗的分数但五六个仓库的依赖版本互相打架环境能折腾一整天而且结果分散在一堆 json 里汇总全靠手工。第三种就是配置驱动的评测框架OpenCompass 属于这一类。它的核心设计是把模型数据集评测方式结果汇总四件事全部写成配置代码只负责执行。这么设计的好处很直接换模型只改模型那一段换数据集只改数据集那一段提示词模板和 few-shot 设置跟着数据集配置走天然保证一致性跑完之后结果统一落到一个输出目录自动生成汇总表。对我来说最有价值的一点是评测过程本身成了可版本管理的文件。配置文件提交到仓库谁在什么时候用什么参数跑的一清二楚。别人想复现拉下配置跑一遍就行不用问你当时那个 prompt 是怎么写的。这个特性在团队协作里价值极高比省下来的那点写脚本的时间重要得多。2. 环境准备显存账要先算明白2.1 显存估算权重只是零头KV Cache 才是大头这一步我建议所有人都先算账再动手不然就是下载完 14GB 权重、启动、OOM、改 batch size、再 OOM 的循环。显存占用分三块模型权重、KV Cache、激活值与临时缓冲。模型权重最好算。7B 参数bf16 精度每个参数 2 字节就是 14GB 左右如果做 int4 量化大约 4GB 到 5GB。这部分是固定的加载完就占着。KV Cache 是变量公式是2 × 层数 × KV 头数 × 头维度 × 序列长度 × 批大小 × 每元素字节数。注意这里用的是 KV 头数而不是注意力头数因为现在主流模型都用分组查询注意力KV 头数远小于注意力头数。拿一个典型的 7B 配置举例32 层、32 个注意力头、8 个 KV 头、头维度 128、bf16。单个 token 的 KV Cache 是 2 × 32 × 8 × 128 × 2 字节 128KB。听着不多但乘上序列长度就很可观2048 token 是 256MB8192 token 就是 1GB。再乘批大小批大小为 8、序列长度 8192 的时候KV Cache 要 8GB。如果这个模型用的是不分组的注意力KV 头数等于 32那同样的配置下 KV Cache 直接翻四倍到 32GB——这就是为什么老架构模型在长文本评测里特别吃显存。激活值和临时缓冲不好精确算我一般按权重加 KV Cache 总和的 15% 到 25% 预留。所以上面那个例子14GB 权重 8GB KV Cache 约 4GB 缓冲单卡 32GB 是舒服的24GB 卡就得把批大小降到 4 或者把生成长度压到 4096。注意生成长度max_out_len对显存和时间的影响是平方级的因为注意力计算随长度增长。有些基准的参考答案很长配置里默认给到 2048 甚至 4096实际大部分题目几百 token 就够。我第一轮就是把生成长度设成 2048 跑全量时间多花了将近一倍后来按数据集分别设 512 到 1024分数基本没变化。2.2 依赖版本对齐这一步别偷懒评测框架、推理后端、模型加载库三者的版本是有耦合关系的。我踩过的坑是框架最新版要求较新的模型加载库而较新的模型加载库又要求特定版本的深度学习框架装了新版之后推理加速后端编译失败回头降级又把框架搞崩。来回折腾了两轮。比较稳的做法是先建一个干净环境装推理加速后端的时候指定与当前深度学习框架匹配的版本装完立刻验证一句能不能加载模型跑出一条输出再装评测框架。顺序很重要——先保证推理链路通再装评测层出问题的时候容易定位是谁的锅。还有个小细节如果用的是编译型推理后端第一次启动会有一段编译时间几十秒到几分钟不等容易被误判成卡死。我一般会先跑一个只有三道题的迷你数据集确认整条链路通畅再上全量。2.3 目录结构约定省下以后找文件的时间这个习惯是从一次事故里养成的早期我把模型、数据集、输出全放在一个目录下某次清理缓存把数据集删了重新下载花了大半天。现在我固定四个目录分开模型权重目录、数据集缓存目录、配置文件目录、输出结果目录。输出目录再按日期_模型简称_备注命名。这么做的直接好处是断点续跑和对比。评测跑一半中断是常态输出目录结构规整的话重新执行同一条命令会自动跳过已完成的部分要做模型间对比直接拿两个输出目录的汇总表对齐就行。另外把数据集缓存目录单独设出来还有个隐藏好处多个评测任务共用同一份缓存不用每个任务下重复的数据。3. 配置文件拆解模型、数据集、汇总器三件套3.1 三种模型接入方式按场景选评测框架接模型一般有三种方式各有适用场景我用表格对比一下。接入方式适用场景优点代价直接加载本地权重单卡、小模型、快速验证配置最简单不用起服务速度慢无法并发长基准耗时极长接推理加速后端有多卡、要跑全量基准吞吐高长基准几小时能跑完需要额外部署服务版本耦合多接在线接口评测商业模型做基线对比零显存占用有调用成本有速率限制数据外发需评估我这次的做法是分阶段先用直接加载权重的方式跑一个小数据集把配置和答案抽取规则调通确认分数在合理区间然后再切到推理加速后端跑全量。这样调试成本最低因为配置调错的时候直接加载方式的报错信息最直白而加速后端经常只给你一个输出为空的结果很难判断是配置问题还是服务问题。3.2 配置文件逐段写结构比细节重要配置文件本质是一段 Python框架会执行它。结构上分成模型列表、数据集列表、汇总器三块。下面是我实际用的骨架字段名在不同版本间会有出入具体以你安装版本的示例配置为准。from opencompass.configs.datasets.ceval.ceval_gen import ceval_datasets from opencompass.configs.datasets.gsm8k.gsm8k_gen import gsm8k_datasets # 模型段一个列表可以放多个模型做同批对比 models [ dict( typeHuggingFace, # 直接加载权重的方式 abbrtarget-7b-chat, # 简称会出现在结果文件名里 path/data/models/target-7b-chat, max_out_len1024, # 与显存、耗时强相关按数据集调 batch_size4, run_cfgdict(num_gpus1), model_kwargsdict(trust_remote_codeTrue, dtypebfloat16), generation_kwargsdict(temperature0.0, do_sampleFalse), ), ] # 数据集段只跑子集先验证链路 datasets [] for d in ceval_datasets: d[abbr] ceval- d[abbr] datasets.append(d) # 汇总器结果怎么合并成一张表 summarizer dict( dataset_abbrs[ceval-*, gsm8k], summary_groups[ dict(nameknowledge, subsets[ceval-*]), dict(namereasoning, subsets[gsm8k]), ], )写这份配置有几个要点。abbr这个字段看着不起眼但它决定了所有输出文件名一旦改了历史结果就对不上所以项目开始前就要定好命名规范比如模型简写-数据集简写。generation_kwargs里的解码参数必须固定成贪心解码否则同一个模型跑两次分数能差出一两个点而且你会发现根本原因查不出来。还有一点配置里的模型路径建议写绝对路径。相对路径在不同工作目录下执行时行为不一致这个问题排查起来非常浪费时间表现是昨天还能跑今天找不到模型。3.3 数据集准备离线加载和提示词模板处理数据集下载是最容易卡住的一步。多数基准数据集托管在公共数据平台上网络不稳定的时候会反复超时。我的做法是先在网络状况好的时候把需要的数据集全部拉下来缓存到本地目录然后配置环境变量指向缓存目录之后所有评测都走离线。export HF_DATASETS_OFFLINE1 export HF_HOME/data/hf_cache提示词模板这块要特别注意。同一个数据集官方可能提供零样本和少样本两个版本少样本版本又会因为示例数量不同而产生不同变体。选择原则是要让自己的分数能和别人比就用该基准最广泛采用的那个版本。比如中文选择题基准社区里普遍用的是五样本版本你偏偏用零样本跑出来的分数低三到五分然后拿去和别人比只会得出错误结论。少样本示例还直接影响 token 消耗。五个示例可能就把提示词撑到两千 token再叠上生成长度显存和时间都会涨。如果只是为了内部看趋势、不做对外对比用零样本版本快得多但要清楚这个分数不能拿去横向比较。4. 开跑命令、分区与断点续跑4.1 启动命令与几个关键参数链路验证建议先用最小配置跑命令大致是这样python run.py configs/eval_target_7b.py \ --work-dir outputs/20240612_target7b_smoke \ --debug--debug会用极小的样本量跑一遍全流程主要是验证配置解析、数据加载、模型加载、答案抽取、结果汇总这几步都能走通通常几分钟出结果。这一步千万别跳过我见过太多人直接上全量跑了四个小时在最后汇总阶段报错前功尽弃。验证通过后跑全量关键是分批。全量配置一次性启动会把所有数据集排在队列里中间任何一个出错后面的都受影响。我习惯按能力维度拆成几个配置文件分别跑知识类一个、推理类一个、代码和长文本各一个。好处是某一批出问题不影响其他批而且可以按机器空闲情况灵活安排——晚上跑耗时的长文本白天跑轻量的选择题。# 知识类 python run.py configs/eval_knowledge.py --work-dir outputs/20240612_knowledge # 代码类单独一个输出目录 python run.py configs/eval_code.py --work-dir outputs/20240612_code4.2 主观评测怎么配才不至于失控客观题靠规则比对主观题开放问答、写作、摘要就必须用模型打分或者人工打分。用模型当裁判这件事我谨慎推荐可以作为初筛但不能作为最终结论。如果要用有几个约束必须写死。裁判模型的能力要明显强于被评测模型否则打出来的分基本是噪声打分的提示词里必须给出明确的评分维度和分档标准不能只说请打分分数要做重复实验同一个样本打三次取平均看方差有多大——方差大的话这套评分本身就不可信。我在实际操作里更推荐模型初筛 人工抽检的组合。让裁判模型先给全部样本打分然后按分数段分层抽样每个分数段抽十几条人工看一遍。如果发现某个分数段的判定明显偏了就回头修提示词。这套流程比纯人工省力又比纯自动可靠。4.3 结果目录和指标该怎么读跑完之后输出目录里通常有几个子目录预测结果、单数据集评分结果、汇总表、当前配置的快照。配置快照这个设计我非常认可——它把当时用的完整配置存了一份几个月后想复现直接拿这份快照就行不用去翻代码仓库。指标方面要注意区分。选择题基准通常报准确率代码基准报 pass1生成类任务报词重叠类指标这类指标只能反映表面相似度不能反映语义正确性看到高分不要高兴太早。汇总表会把同一个能力组下的多个数据集取平均这个平均值的解释力有限——比如知识类下面挂了五六个不同领域的数据集平均分 60 可能意味着每个都是 60也可能意味着三个 80 两个 30。一定要点进去看分项。注意汇总表里的平均值只对同一套数据集配置有意义。如果你这次跑了六个数据集、上次跑了八个两个平均值放一起比是错的。我一般会在报告里明确写出本次评测覆盖的数据集清单避免被人拿去和口径不同的结果对比。5. 分数怎么看才不骗自己5.1 分数不可比的四种常见情况这是我最想强调的一节。同样一个模型网上的分数和你跑出来的分数差五分以上是常态绝大多数不是模型的问题而是口径问题。我总结出四种最常见的不可比情况。第一种是提示词模板不同。中文选择题基准有的版本带请一步步思考有的要求直接输出答案前者分数通常更高因为模型有推理过程。第二种是少样本数量不同前面说过了零样本和五样本差三到五分很常见。第三种是答案抽取规则不同这是最隐蔽的。选择题基准里模型经常输出答案是 A因为……抽取规则要能从长文本里抠出那个 A。规则严格一点模型明明答对了但格式不对就算错规则宽松一点又能把一些蒙对的救回来。不同实现对同一个模型能差出两三个点。第四种是解码参数和后处理不同。温度设 0.7 和设 0分数差别不小有的实现会对输出做后处理比如去掉重复内容、截断超长回答这些都会影响最终分数。结论很直接对比只能在自己可控的同一套流水线内进行。要做对外对比就必须用与对方完全一致的模板、样本数和抽取规则否则这个对比毫无意义。我后来的做法是自己维护一个基线模型每次评测都把基线一起跑所有结论都建立在相对基线的基础上这样即使口径和外界不同趋势判断也是可靠的。5.2 一次横向对比怎么做我的对比表长这样固定几列能力维度、基线模型、目标模型、差值、样本数、是否显著。能力维度基线模型目标模型差值样本数中文知识选择题58.262.74.5约 1.2 万小学数学推理41.345.94.6约 1300代码生成 pass118.722.43.7164长文本细节召回63.555.1-8.4200指令格式遵循71.274.83.6500这张表最有价值的信息不是那些正数而是那个负数长文本能力退化了八点多。如果只看总分这个退化会被淹没。顺着这条线索去查发现是训练数据的长度分布问题——短样本占比过高模型对长距离依赖的处理能力被稀释了。这就是分项评测的意义。另外样本数这一列必须写出来。代码基准只有 164 道题差 3.7 分大概就是六道题的差距随机性很大这种差异不能当结论用。样本数少于 500 的分数我一般只做趋势参考。5.3 顺着低分做 bad case 分析分数只告诉你哪里不行不告诉你为什么不行。真正能指导优化的信息在错题里。我一般的做法是按数据集导出预测结果然后做三种切分按错误类型切、按题目长度切、按模型输出长度切。按错误类型切最有价值。比如数学题的错误可以分成三类理解错题意、计算过程错、答案格式不对。第三类其实不算能力问题是格式问题改一下答案抽取规则或者加一句格式约束提示分数立刻能涨回来。前两类才是真需要改模型的。我这次跑完就发现数学题里有将近四分之一的错误其实是模型算对了但没把最终答案单独列出来抽取规则没抓到。调整抽取规则后这个基准的分数直接提高了一点八个点——什么都没改只是把评测做对了。按输出长度切也很能说明问题。如果错误集中在输出特别短的那部分说明模型有提前终止的倾向如果集中在特别长的部分往往是重复生成或者跑偏了。这两种病根完全不同。6. 踩坑实录与排查清单6.1 几个让我印象深刻的报错第一个是数据集加载时报字段缺失。原因是该数据集的某个子集结构和其他子集不一样配置里用通配符把整个数据集都挂上了。解决办法是逐个列出需要的子集而不是用通配符一把梭。这个坑的教训是通配符很方便但会让你不知道到底加载了什么。第二个是分数异常高。有个基准跑出来 95 分我第一反应是模型变强了第二反应是不对劲。查下来发现答案抽取规则里有条兜底逻辑抽不到答案时默认返回选项 A而这个数据集的正确答案分布里 A 占比偏高。修掉兜底逻辑、改成抽不到就判错之后分数回到 68。这件事之后我养成了一个习惯任何评测结果出来后先抽十条预测结果人工看一眼看看模型到底输出了什么、抽取规则抓到了什么。这一步五分钟能避免大量错误结论。第三个是评测跑到一半卡死。表现是进度条不动日志也没有新内容。排查后发现是某条样本的生成长度达到了上限还没结束推理后端在处理这种情况时进入了异常慢的路径。解决办法是在配置里给生成长度设一个合理上限同时加上停止词。6.2 常见问题速查表现象最可能的原因处理方式启动即显存不足批大小或生成长度过大按 KV Cache 公式重算先降到 1 试通数据集加载失败缓存缺失或网络超时设离线模式提前把数据拉到本地分数明显偏低提示词模板或样本数用错对照该基准的社区通用版本核对配置分数明显偏高答案抽取有兜底逻辑抽十条预测结果人工核对同配置两次分数不同解码用了采样固定为贪心解码中途中断后重跑很慢输出目录不一致复用同一输出目录利用已有结果跳过部分样本输出为空提示词超出上下文或触发停止词检查模板长度核对停止词设置主观评分波动大评分提示词过于宽松明确评分维度和分档重复打分取均值6.3 几条不太写在文档里的经验先跑小数据集再跑大数据集这个顺序不能反。大数据集动辄上万条跑一遍几个小时配置里任何一个细节错了都是白跑。用小数据集把链路调通边际成本极低。评测环境要和推理环境一致。我在本地用一套推理后端跑出来的分数和线上服务用的是另一个后端两次结果差了将近两个点。原因是两个后端对超长输入的处理策略不同。评测的意义是预测线上表现环境不一致评测就失去了一半价值。把基线模型固定在流水线里。每次评测都带上基线哪怕它已经跑过十遍。这样任何环境变化、依赖升级导致的分数漂移都能立刻发现——如果基线的分数也变了说明是流水线的问题不是模型的问题。留一份人工抽检记录。自动指标只是代理最终要落到人看了觉得行不行。我每次评测会留二十条样本的人工判断和自动分数做个对照。时间久了你会发现某些基准的自动分数和人工判断的相关性其实不高这时候就该调整对这个基准的信任度。7. 我自己的几点体会这套流程跑下来最大的感受是评测的难点从来不在跑而在跑对。装环境、敲命令、等结果这些都是体力活真正决定评测价值的是那几件需要判断力的事——模板选哪个、样本数给多少、抽取规则怎么写、分数怎么解读。这些判断做错了跑得再快也是错的。还有一个认识上的转变。以前我把评测当成一个交付前的检查环节跑个分交差。现在更愿意把它当成一个诊断工具分项分数告诉你去哪里找问题错题样本告诉你问题长什么样人工抽检告诉你自动指标值不值得信。这三件事串起来评测才真的能指导下一步该做什么而不是变成一个放在报告里好看的数字。后面如果要把这套东西用起来我会优先补两块一是把自建题库的评分要点写细让内部评测也能有一致的口径二是给几个关键基准建固定的错题集每次迭代都拿来跑一遍专门盯退化。