
上周有个做应用的朋友来问我模型换了个新版本客服场景的回答感觉好了不少能不能给个准话到底好在哪、好了多少。我问他怎么测的他说人工挑了二十来个问题聊了一遍觉得挺顺。这就是最典型的手工评测——成本不低结论却站不住。样本怎么挑的、prompt 是谁写的、同一句话换个问法答案会不会翻车、下个月还能不能复现出这个结论全是问号。大模型评测这件事本质上不是跑个分,而是一套完整的实验工程数据集要有版本、推理要有配置、指标要有口径、结果要能追溯。OpenCompass 就是冲着这件事来的——一个配置驱动的开源大模型评测框架把数据集、模型、评测器三件事解耦用一份 Python 配置文件描述整个实验跑完给你标准化的预测文件和指标汇总。这篇内容我按照自己实际搭环境、接数据集、调并行、读分数的顺序拆开讲适合两类人一类是刚接手给模型打分这件事、不知道从哪下手的新同学另一类是已经跑过几次、但结果总感觉不太对劲、想把口径和流程做扎实的老手。读完你应该能独立完成从零接入到结果解读的闭环也能避开我踩过的那些坑。1. 大模型评测为什么需要一套工程化框架1.1 手工评测的三类隐性坑先把手评测的坑摊开说因为只有理解痛点才知道框架里那些配置项为什么长成那样。第一类是样本选择偏差人工挑题的时候人会本能地挑自己觉得有代表性的题目而这批题目往往集中在模型能答上来的区间。你很难主动去挑那些刁钻、模糊、带陷阱的问题因为挑题的人自己也不确定标准答案是什么。结果就是二十道题的通过率虚高上线后用户随便一句话就把模型问住了。第二类是prompt 漂移。今天测试用的是请回答以下问题明天换成你是专业助手请回答模型表现可能差好几个百分点。更隐蔽的是多轮对话里的 system prompt、温度参数、max_tokens 截断长度任何一个变了分数就没法横向比较。手工测试几乎不可能把这几项全部锁死。第三类是不可复现。这话听着刺耳但确实是常态三个月后你回头想验证上一版是不是真的比这一版差会发现当时用的题目集存在谁的个人文档里、prompt 是手打的、模型权重已经被覆盖了。没有配置快照就没有对比的基础。工程化框架解决的正是这三件事题目来自固定数据集、prompt 写在配置里、每次运行的配置和结果一起落盘。你不需要信任任何人的感觉只需要信任那份可以重新跑一遍的配置文件。1.2 OpenCompass 的整体架构与设计取舍OpenCompass 的核心抽象就三层。数据集层描述拿什么题、怎么把题喂给模型、答案怎么比一份数据集的配置里会包含数据路径、字段映射、prompt 模板、检索器少样本示例怎么取、推理器生成式还是判别式和评测器。模型层描述模型在哪、怎么调、开几张卡、最大生成长度多少本地权重和 API 服务都在这一层统一。任务编排层负责把两边做笛卡尔积切分成若干推理任务分发给执行器跑完再把预测结果交给评测器打分。这个设计的关键取舍在于配置即实验记录。你写的那个.py配置文件本身就是实验的完整描述框架跑完会在输出目录里留一份配置快照。这意味着复现一个实验不需要读文档、不需要问人把配置文件拷走就行。对比那些靠命令行参数堆起来的评测脚本这个选择在多人协作场景下优势非常明显。第二个取舍是推理与打分分离。框架先把所有样本的预测结果predictions完整落盘再统一打分。好处是你可以拿同一份预测结果换不同口径重新算分不用重新推理坏处是磁盘占用会明显变大尤其是生成式任务一个几千样本的数据集跑出来几个 G 的 jsonl 很正常。所以我一般会在跑之前先看一眼输出目录所在盘的剩余空间。第三个取舍是执行器可换。单机多卡、多机、本地推理后端加速都是通过配置切换的不改业务代码。这对我们这种有的模型在本地、有的只能走 API的混合场景特别友好。1.3 评测到底该评什么先定口径再动手很多人上来就跑分跑完发现不知道该看哪个数。我的建议是先把评测目标写下来再决定跑什么。常见的四个维度维度关心的问题典型做法能力知识、推理、代码、长文本理解够不够用选 2 到 3 个公开基准 1 个自建业务集稳定性换个问法、换个顺序答案还对不对同一数据集跑 3 次不同随机种子看方差效率首 token 延迟、吞吐、单样本成本记录平均输出长度和推理耗时安全性拒答边界、敏感输入处理自建对抗样本集人工抽检这张表的价值在于提醒你公开基准分数不能直接对应业务效果。公开基准测的是通用能力你的业务集测的是场景适配。两者都要有但汇报的时候一定要分开说别把通用基准的高分当成业务可用性的证明。我见过太多团队被这一点坑过——通用榜单排名很靠前接进业务后一塌糊涂原因就是业务数据里的表达方式、专业术语、多轮上下文公开基准里根本没有覆盖。2. 环境准备与第一次跑通2.1 硬件与依赖盘点先说硬件。跑本地权重的话显存估算有个粗略公式参数量 × 精度字节数 KV Cache 开销 框架自身占用。7B 模型用 fp16 加载权重约 14GB加上 KV Cache 和推理框架的预留单卡 24GB 能跑batch_size 保守设 1 到 4。13B 在 fp16 下权重约 26GB24GB 卡就得靠量化或者切分。70B 级别基本只能走量化或者多卡张量并行。模型规模fp16 权重占用推荐单卡显存备注7B约 14GB24GBbatch 可开到 4 到 813B约 26GB40GB 或 2×24GB建议 bf16 加载34B约 68GB4×24GB必须并行70B约 140GB8×24GB 或量化优先考虑量化 单机多卡依赖方面Python 版本建议 3.10太高或太低在编译某些扩展时会出问题。PyTorch 版本跟你的 CUDA 驱动匹配别硬上最新版——评测框架里会依赖一些推理后端的特定版本版本组合不对会报很晦涩的错。我的习惯是先建独立虚拟环境装完 torch 后立刻验证torch.cuda.is_available()和实际可用显存确认没问题再装框架本体。提示如果你的机器是多人共用务必在虚拟环境里固定版本号别用不带版本约束的安装方式。评测任务一跑就是几个小时中途因为依赖被别人升级而崩掉重跑的成本很高。2.2 安装与目录结构认知安装一般两种方式直接从包仓库装或者拉源码后以开发模式装。我强烈推荐后者因为你早晚要改配置文件、要加自定义数据集读取逻辑、要写自己的评测器直接装包后面会很别扭。git clone 仓库地址 opencompass cd opencompass pip install -r requirements.txt pip install -e .装完之后目录结构里几个位置要记住configs/下是官方给的配置样例按数据集和模型分门别类是最快的抄作业来源opencompass/下是框架代码自定义数据集读取类、自定义评测器一般放这里或者在自己的项目目录里以继承的方式实现data/是数据集落地目录很多数据集需要提前下载解压离线环境下要手工放好outputs/是运行产物目录每次跑都会在这里建一个带时间戳的子目录。第一次用建议直接拿官方样例配置跑通不要一上手就写自己的配置。先确认框架能跑再排查我的数据为什么不对这两类问题的排查路径完全不同混在一起会让你怀疑人生。2.3 一行命令跑通最小闭环最小闭环大概是这样python run.py configs/eval_demo.py -w outputs/my_first_run --max-num 20-w指定工作目录--max-num 20限制每个数据集只跑 20 条。这个参数是我最常用的一个——配置写完先跑 20 条三分钟内就能看到端到端是否通畅比等两小时跑完再发现字段映射错了要划算得多。跑完你会看到类似这样的输出结构outputs/my_first_run/ ├── configs/ # 本次运行的配置快照 ├── logs/ # 推理和评测日志 ├── predictions/ # 每个模型每个数据集的原始预测 └── results/ # 打分结果含汇总表第一次跑通之后务必把predictions/里某个数据集的预测文件打开看几条。看什么看模型实际输出了什么、标准答案长什么样、后处理有没有把答案切歪。这一步我几乎每次接新数据集都会做因为分数正常但答案是错的这种情况太常见了光看汇总分数根本发现不了。2.4 配置文件的结构拆解一份配置文件通常由四段构成理解这四段的职责边界后面调什么都快from opencompass.models import HuggingFacewithChatTemplate # 第一段模型 models [ dict( typeHuggingFacewithChatTemplate, abbrmy-model-7b-chat, # 结果里的短名别重复 path/path/to/weights, # 本地权重路径 max_seq_len4096, max_out_len1024, batch_size4, run_cfgdict(num_gpus1), # 占几张卡 stop_words[|im_end|], # 停止符跟模板对齐 ) ] # 第二段数据集 from opencompass.configs.datasets.demo.demo_dataset import demo_datasets datasets demo_datasets # 第三段推理与评测的执行参数 from opencompass.configs.summarizers.example import summarizer四个关键字段abbr是结果里的标识字符集别用特殊符号否则汇总表里会出现列名错乱max_out_len直接决定推理耗时和显存峰值业务场景如果答案都很短把它从 1024 降到 256 能省一大半时间batch_size和num_gpus是紧耦合的单卡跑大模型时 batch 设太大反而会 OOM宁可小 batch 多跑一会stop_words不设对模型会一直生成到 max_out_len 才停既慢又容易把答案后面的解释混进判定结果。3. 数据集、模型与评测器三件套的配置实战3.1 数据集配置从公开数据到自建业务集接入一个新数据集本质上要回答五个问题数据在哪、字段叫什么、怎么拼成 prompt、答案怎么切出来、跟标准答案怎么比。OpenCompass 的数据集配置把这五件事拆成了path、reader_cfg、infer_cfg、eval_cfg四块其中reader_cfg和infer_cfg最容易出错。自建数据集最常见的格式是一行一条的 jsonl字段自己定。假设我的业务数据长这样{q: 订单超过七天还能退吗, a: 可以但需满足……, cat: 售后} {q: 发票怎么开, a: 在订单详情页……, cat: 财务}那我需要写一个 reader 把它映射到框架认识的结构from opencompass.datasets import BaseDataset from opencompass.openicl.icl_evaluator import AccEvaluator my_reader_cfg dict( input_columns[q], # 输入的字段 output_columna, # 标准答案所在字段 ) my_infer_cfg dict( prompt_templatedict( typePromptTemplate, templatedict(round[ dict(roleHUMAN, prompt{q}), ]), ), retrieverdict(typeZeroRetriever), # 不做少样本检索 inferencerdict(typeGenInferencer, max_out_len256), )这里有几个坑必须点出来。input_columns里列了几个字段prompt 模板里就必须有几个占位符少一个会报 KeyError多一个会静默渲染成字面量后者更麻烦——模型会一本正经地把{q}当成内容去回答。output_column指向的字段如果在你部分样本里是空的评测时会算成错所以要事先清洗。另一个重点是少样本示例的选取。ZeroRetriever表示零样本prompt 里只有问题。换成FixKRetriever会从训练集里固定取 K 条示例拼进 prompt。这两种方式的结果差异可能非常大尤其是小模型给几条示例能显著提升输出格式的正确率。但要注意示例一旦拼进去输入长度就变长了max_seq_len不够会被截断截断往往发生在 prompt 头部导致示例被切掉而问题保留这会让你误以为示例没用。3.2 模型接入本地权重与接口服务两条路本地权重的接入相对直接填path就行框架会按 HuggingFace 格式加载。这里最重要的一个参数是chat template。带对话模板的模型一定要用带模板的模型类否则你手写的 prompt 会跟模型训练时的格式不一致输出的质量会莫名其妙地差。判断方法很简单看权重目录里有没有tokenizer_config.json里的 chat_template 字段有就说明这个模型期望特定的对话格式。接口服务的接入适合那种模型部署在别处、只能走 HTTP 调用的场景。这种方式的坑集中在三处并发、限流和超时。并发设太高会被服务端限流返回一堆 429超时设太短长答案会被截断成半句话评测器拿到半句话自然判错。我的做法是把并发压到比较保守的值比如 8 到 16把超时设到 60 秒以上然后在日志里统计一下平均响应时间和失败率再决定要不要往上加。接入方式优点主要风险点调参建议本地权重完全可控、可复现显存吃紧、加载慢先小 batch 跑通再加接口服务不占本地资源、可测超大模型限流、超时、计费低并发起步看重试日志注意走接口服务时如果你的数据集有几千条务必先跑 20 条确认服务端的返回格式和你的后处理逻辑对得上。我遇到过一次服务端把答案包在多层 json 里后处理没剥干净导致整批分数接近零白等了一个下午。3.3 评测器选型规则匹配与模型裁判评测器决定了什么算对。选错了评测器前面所有工作都白费。常见三类精确匹配与包含匹配适用于答案形式固定的任务比如选择题、判断题、短答案抽取。这类评测器速度快、无成本但对模型的输出格式非常敏感。模型可能输出答案是 B也可能输出B还可能输出我觉得应该选 B因为……。所以这类评测器几乎都要配一个答案后处理用正则把选项字母或数字抠出来。后处理写得好不好直接决定分数可信度。规则化评测适用于数学、代码这类有客观验证方式的任务。数学题可以抽最终数值比对代码可以跑单元测试。这类评测器的好处是不会被表述方式干扰坏处是抽取逻辑要写得足够稳模型换个写法分数写小数、中文数字、带单位就可能抽不出来。模型裁判适用于开放问答、摘要、写作这类没有唯一答案的任务。做法是把模型输出和参考答案一起交给一个能力更强的模型让它按维度打分。这类评测器的成本最高而且本身带主观性同一个回答跑两次分数可能不一样。我的经验是用它做相对比较A 版本和 B 版本谁更好比做绝对打分可靠得多因为绝对分数会漂相对排序相对稳定。# 含有抽取逻辑的后处理示例 import re def extract_choice(text: str) - str: m re.search(r[(]?\s*([ABCD])\s*[)]?, text) return m.group(1) if m else Z # 抽不出来给个必然判错的占位符把抽不出来显式映射成一个不可能正确的值比让它默认算对要安全——至少你能通过这个占位符的计数统计出后处理的失败率。这个失败率如果超过几个百分点说明你的 prompt 或者后处理需要改而不是模型能力不行。3.4 并行与加速把两小时的活压到二十分钟配置跑通之后接下来就是优化时间成本。三个方向样本级并行靠num_gpus和batch_size。单机多卡时框架会把数据集切分到各卡上跑切分粒度是样本级所以卡数增加不会改变单条样本的推理结果温度设成贪心的情况下。这一点很重要并行不影响分数只影响耗时。如果你发现多卡跑出来的分数跟单卡不一样那多半是随机采样没固定种子。推理后端加速是提升幅度最大的一环。常规的 HuggingFace 逐条推理吞吐很低尤其是生成式任务。换成专门的推理加速后端比如支持连续批处理的那类同样的显存下吞吐能提升数倍。代价是后端对模型结构的支持面有限冷门结构可能加载失败需要提前验证。输出长度控制是最容易被忽视的一项。假设一个数据集有 2000 条样本max_out_len从 1024 降到 256如果模型大部分回答本来就在 200 token 以内那么实际耗时下降不明显但如果模型有大量刹不住车的长输出这个参数一改时间能砍掉一大半。跑之前先看 20 条的输出长度分布比拍脑袋设参数靠谱。# 先小样本探底看输出长度和耗时 python run.py configs/my_eval.py -w outputs/probe --max-num 20 # 确认无误后全量多卡并行 python run.py configs/my_eval.py -w outputs/full --num-gpus 44. 结果解读、可视化与复现性4.1 汇总表里每个数该看什么跑完之后results/下会有汇总表一般长这样模型数据集A(通用知识)数据集B(数学推理)数据集C(业务集)平均model-v162.441.855.253.1model-v264.139.561.755.1看这张表第一眼很多人会得出v2 更强的结论。但真正该关注的是分布而不是平均值v2 在数学推理上掉了 2.3 个点在业务集上涨了 6.5 个点。如果你的业务里数学推理占比不高这个交换是划算的反过来如果你的业务场景本身就依赖严谨推理那这个版本的提升是危险的。平均值把所有维度压成一个数恰好抹掉了最重要的信息。第二个要看的是样本量与置信区间。几百条样本上的 1 到 2 个百分点差异很可能是噪声。有个粗略的经验样本量在几百这个量级时准确率的标准误差大概在 1 到 2 个百分点上下所以两个模型差 1 个点别急着下结论。真想比较要么扩大样本量要么换多个随机种子各跑一遍看稳定性。第三个是失败样本的具体形态。把判错的预测捞出来看十几条你会发现信息量比整张表大得多是格式没对上后处理问题、是答案只差一个单位抽取问题、还是模型真的理解错了能力问题。这三种情况的处理方式完全不同但在汇总表里它们都只是错。4.2 结果解读最容易犯的三个错第一个错只看分数不看预测文本。我见过一次分数突然从 60 涨到 85 的情况兴奋了半天最后发现是后处理正则改宽了把模型输出里的解释文字也当成答案匹配上了实际上模型根本没答对。分数组件和预测文本要一起看。第二个错跨配置直接比分数。prompt 模板、少样本数量、max_out_len 任何一项变了分数就不可比。想对比两个版本除了模型权重之外的所有配置必须完全一致否则你测的是配置 模型不是模型。第三个错忽略拒答和空输出。有些模型在不确定时会输出我无法回答或者干脆空字符串。这类样本在精确匹配里算错在模型裁判里可能被算成部分正确。如果你的业务对拒答率敏感一定要单独统计这个比例别混在总分里。4.3 复现性把实验当代码管复现性的最低要求是配置文件 框架版本 数据版本 权重版本四样东西齐全。框架跑完会自动存配置快照这一项已经解决了剩下三项需要你自己记录。我自己的习惯是在项目里建一个experiments/目录每次正式跑都提交一份配置进去文件名带日期和目的描述比如20240712_baseline_v2_business_set.py。同时在跑结果的目录里放一个NOTE.md写清楚这次跑的动机、跟上次的区别、结论是什么。三个月后回头看这几十个字救你无数次。数据版本这一项最容易被忽视。同一个数据集如果上游更新了标注分数就会变而你完全不知道。所以自建数据集一定要在配置文件里写清楚版本标识最好把数据文件的哈希也记下来。# 记录数据指纹方便追溯 sha256sum data/my_business_set.jsonl outputs/full/data_fingerprint.txt5. 常见问题与排查实录5.1 故障速查表现象最可能原因排查动作报数据集路径不存在数据未下载或路径大小写错误检查data/目录确认解压完成分数接近 0 但预测有内容后处理没抽出答案或字段映射错位打开 predictions 看前 10 条分数接近满分答案字段泄漏进了输入检查input_columns是否包含答案OOMbatch_size 太大或 max_seq_len 过长先降 batch 到 1再降序列长度推理卡住不动生成式中途没有命中停止符补stop_words限制max_out_len多卡结果与单卡不一致随机种子未固定固定种子并统一温度设置接口调用大量失败并发过高触发限流降并发加指数退避重试汇总表缺少某模型abbr重名被覆盖改成唯一短名5.2 几个从踩坑里换来的经验先跑通再优化顺序不能反。我起初喜欢先调并行、再验证流程结果是参数一多出问题时不知道是并行配错了还是数据配错了。后来改成固定顺序单卡单样本跑通 → 单卡 20 条跑通 → 全量单卡 → 多卡并行。每一步都比上一步只多一个变量出问题一眼能定位。prompt 模板要跟模型的训练格式严格对齐。这一条我吃过最大的亏。当时用了一个对话模型但我手写了一个简单的拼接模板结果模型输出的全是重复的废话。换了带对话模板的模型类之后同样的权重、同样的数据分数直接涨了十几个点。教训是接入任何新模型之前先看一眼它期望的输入格式别默认所有模型都能吃同一种拼接方式。小样本探测一定要看输出长度分布。用--max-num 20跑完之后把预测文件里的输出长度统计一下。如果发现有大量样本正好等于max_out_len说明模型没能自然停止要么是停止符没配要么是模型本身刹不住。这种情况全量跑会非常慢而且截断的答案大概率判错。提示探测阶段建议把max_out_len设得比正式跑大一些先观察模型自然停在哪里再反过来定正式值。倒过来做会误判模型输出都很短。多版本对比时先把基线跑三遍。同一配置跑三遍的分数波动就是你的噪声水平。后面看到小于这个波动幅度的提升一律当成没提升。这一步花的时间不多但能避免你基于噪声做决策。别把评测结果当唯一依据。分数告诉你模型在固定题集上的表现不告诉你用户会怎么用。我一般的做法是分数筛掉明显不合格的版本剩下的候选版本再拿真实场景的对话日志抽一批做人工抽检。两关都过的版本才敢往线上推。业务集要持续迭代。上线之后遇到的badcase定期整理进业务评测集让这个集合跟着业务一起长大。三个月不更新的评测集测的其实是三个月前的业务问题。这件事没有终点只能当成一个长期习惯来养。