ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SLM基准测试失败样本复盘:评测管线中的“假失败”与宽松判分方案

SLM基准测试失败样本复盘:评测管线中的“假失败”与宽松判分方案 如果评测脚本告诉你某个小型语言模型在基准测试里有一半题目答错了你会直接换模型还是先把失败样本翻出来看看最近一类的复盘结论是所谓失败样本里有一半其实包含了正确答案。这句话拆开就是SLM 的输出质量可能比 benchmark 分数显示的更好问题往往出在评测管线——比如输出被截断、格式和预期不符、答案被埋在推理过程里而判分脚本只会做严格的字符串匹配。这类情况在实际评测里非常常见。小型语言模型SLM没有大模型那么强的指令跟随能力经常出现“答案是对的但表达形式不符合评测脚本预期”的情况。如果你正在做模型选型、微调回归、RAG 效果评估或者想让本地 SLM 接进自动化流水线这篇文章值得直接收藏。我们会把“失败样本里其实有正确答案”这个现象拆开给出一个可落地的评测方案怎么部署一个本地 SLM、怎么跑批量推理、怎么写答案抽取与宽松判分逻辑、怎么用 LLM 裁判复核开放题以及怎么统计最终正确率。文章核心不是某个具体模型的分数而是一套评测方法。所有代码都能直接改成你自己的评测脚本不依赖任何闭源平台。1. 核心能力速览能力项说明讨论主题SLM 基准测试失败样本复盘与评测管线改进核心问题固定字符串匹配判分把包含正确答案的输出误判为失败解决方案输出保留、答案抽取、宽松匹配、LLM-as-Judge 复核、批量统计评测对象常见小型语言模型 SLM参数量通常在 0.5B 到 7B 之间服务部署Ollama 本地服务 / vLLM / Transformers 通用接口批量支持JSONL 输入输出支持并发批量评测接口能力OpenAI 兼容接口可接入自建评测平台运行环境Python 3.10CPU 可跑有 GPU 更快输出内容原始输出、抽取结果、匹配结果、裁判结果、统计报告适用场景模型选型、微调回归、RAG/Agent 评测、上线前验收这套方案设计的重点是“可审计”。评测结果不是只给一个正确率而是把每一条样本的原始输出、抽取后答案、判分依据全部保留下来方便随时回溯。这比单纯追求一个高分重要得多。2. 问题拆解为什么“失败”样本其实答对了先明确一个概念SLM benchmark 里的“失败”分两种一种是模型真的不会另一种是判分器判错的“假失败”。标题里说一半失败样本包含正确答案指的就是第二类。从常见评测经验看假失败通常出现在以下三种情况。2.1 输出被截断答案丢在尾巴上SLM 的生成受max_tokens限制。如果题目包含长上下文模型可能会先复述问题、写推理过程最后才给答案。当输出长度达到上限答案就被截掉了。评测脚本拿到不完整文本自然判失败。这不是模型不会是生成参数和题目不匹配。更麻烦的是很多 SLM 倾向先输出一段解释再给结论。如果评测脚本只匹配“答案”后面的字符串而模型这次没有按模板输出就会判错。实际人工看结论明明是对的。2.2 格式漂移模板匹配失效有的评测要求模型输出 JSON例如{answer: 北京}。SLM 在低温度下偶尔也会输出 Markdown 代码块、自然语言解释或者在 JSON 前后加了多余文字。如果判分脚本直接json.loads(whole_output)轻则解析失败重则把整段输出判成 0 分。这类问题在大模型上不明显因为大模型指令跟随能力强能稳定输出标准格式。SLM 做不到所以评测脚本必须做容错处理。2.3 表述差异语义等价判不了选择题里模型可能不输出选项字母而输出答案文本数值题里模型可能输出“9.0”标准答案是“9”地名题里模型可能输出“法国首都巴黎”标准答案是“巴黎”。字符串匹配全部判错但人工一眼就知道是对的。不要小看这类差异。SLM 评测中处理号的差异、单位差异、中英文标点差异、大小写差异都可能直接影响正确率几个百分点。这就是“评测胜率”被误读的常见原因——不是说模型不能打而是裁判尺子没校准。3. 评测流水线设计与环境准备要把“假失败”识别出来评测流程不能是简单的“输入问题-拿输出-比对答案”三段式而要升级为四段式流水线推理、采集、抽取、判分。3.1 四段式流水线推理阶段向 SLM 发送题目保留原始输出不做任何清洗。采集阶段记录原始输出、生成参数、耗时、实际输出长度。抽取阶段从原始输出里提取“最终答案”方法包括正则、JSON 解析、位置规则。判分阶段先做精确匹配和归一化匹配不通过的再走 LLM 裁判复核。关键点是第一阶段的原始输出必须完整落盘。很多评测脚本为了省磁盘只存判分结果导致后面复盘时没有证据。没有原始输出后面所有“宽松判分”都无从谈起。3.2 环境准备检查项说明操作系统Windows / Linux / macOS 均可评测脚本以 Python 为主Python 版本建议 3.10 以上本地模型服务Ollama、vLLM 二选一或直接使用 HuggingFace Transformers显卡可选。SLM 在 CPU 上也能跑但批量评测建议有 GPU磁盘空间单个 SLM 权重文件通常在 1GB 到 5GB 之间预留 20GB 以上更稳妥端口默认 11434Ollama或 8000vLLM冲突时改端口这些是通用检查清单具体版本号以你实际安装为准。第一次跑评测建议先用最小测试集不要直接上几千条样本。3.3 数据格式建议用 JSONL 组织评测数据一行一个样本{id: 1, question: 法国首都是哪里, gold: 巴黎, category: 地理} {id: 2, question: 9.0 和 9 哪个大, gold: 一样大, category: 数值}后面所有代码都围绕这个格式展开。评测结果也可以继续用 JSONL 保存方便用 pandas 或 jq 做统计。4. SLM 推理服务启动与接口验证这一步要先把本地 SLM 跑起来得到一个可调用的接口。下面以 Ollama 为例因为它部署最简单适合评测脚本快速验证。4.1 启动 Ollama 服务# 启动 Ollama 服务 ollama serve另开一个终端拉取一个常见 SLM 模型。模型名以你本地可用版本为准下面只是示例# 拉取一个约 1.5B 参数的小模型 ollama pull qwen2.5:1.5b拉取完成后验证模型已就绪ollama list4.2 用 OpenAI 兼容接口验证推理Ollama 默认会提供一个 OpenAI 兼容接口路径是/v1/chat/completions。评测脚本只需要用 HTTP 请求调用它不需要额外安装模型推理库。下面这段代码是最小调用示例import requests url http://127.0.0.1:11434/v1/chat/completions payload { model: qwen2.5:1.5b, messages: [ {role: system, content: 你是一个简洁的答题助手直接给出答案。}, {role: user, content: 法国首都是哪里} ], temperature: 0, max_tokens: 256 } resp requests.post(url, jsonpayload, timeout120) content resp.json()[choices][0][message][content] print(content)实际调用时model名称要替换成你ollama list里看到的模型名。如果服务端口改了url也要对应改。4.3 启动 vLLM 的通用参考如果你已经有 vLLM 环境也可以用类似方式启动python -m vllm.entrypoints.openai.api_server \ --model 模型路径或名称 \ --port 8000 \ --gpu-memory-utilization 0.7这个命令里的模型路径或名称需要按实际情况替换。vLLM 对 GPU 要求更高适合批量评测场景如果只是几十条样本Ollama 足矣。4.4 验证接口成功的标准返回的 HTTP 状态码是 200。resp.json()[choices][0][message][content]能取到非空字符串。输出内容不是报错信息例如model not found或connection refused。接口能跑通后面批量评测就简单了无非是循环调用同一个接口。5. 答案抽取让隐藏答案显形答案抽取是整个评测管线里最关键的一步。目标是从自由文本中把“最终答案”单独拎出来再交给判分器。注意抽取不是“美化模型输出”而是把模型已经给出的正确信息还原成可比较的形式。5.1 常见抽取规则带标记输出匹配答案、最终答案、Answer:等标记。JSON 输出尝试解析完整 JSON按answer字段取值。多行输出取最后一行非空文本因为很多模型习惯最后给结论。列表输出提取第一个或最后一个列表项按题型决定。一个通用抽取函数可以这样写import re import json def extract_final_answer(text: str) - str: if not text: return # 优先匹配带答案标记的文本 patterns [ r(?:答案|最终答案|answer)\s*[:]\s*(.), r(?:the (?:final )?answer(?: is)?)\s*[:]?\s*(.), ] for pattern in patterns: matches re.findall(pattern, text, flagsre.IGNORECASE) if matches: return matches[-1].strip() # 尝试解析 JSON 输出 try: obj json.loads(text) if isinstance(obj, dict): for key in (answer, result, value, 答案): if key in obj: return str(obj[key]).strip() except (json.JSONDecodeError, TypeError): pass # 兜底取最后一段非空文本 lines [line.strip() for line in text.strip().splitlines() if line.strip()] return lines[-1] if lines else text.strip()这个函数不完美但它能解决大部分“输出里面有答案但脚本没发现”的情况。实际使用时你应该先拿 20 条失败样本人工看一下再决定规则顺序。5.2 抽取结果也要落盘抽取之后不要只保存抽取结果。每个样本都要同时保留原始输出抽取后的答案抽取规则命中了哪一个正则 / JSON / 兜底这样如果抽取规则有 bug回过头还能复盘。评测最怕“判分错误叠加抽取错误”两层错误会完全掩盖模型真实水平。6. 宽松判分与 LLM-as-Judge 复核抽取完成之后判分器要经过多个档位从严格到宽松。先用低成本规则过滤再用高成本裁判处理剩余样本。6.1 第一档精确匹配def exact_match(prediction: str, gold: str) - bool: return bool(prediction and prediction.strip() gold.strip())这一段可以快速筛掉完全一致的样本。但注意SLM 评测里这一档覆盖率通常不高因为表述差异太常见。6.2 第二档归一化匹配def normalize_text(text: str) - str: 去掉大小写、标点、多余空白只保留核心字符。 text text.strip().lower() text re.sub(r[\s\u3000], , text) text re.sub(r[^\w\u4e00-\u9fff%.-], , text) return text def normalized_match(prediction: str, gold: str) - bool: if not prediction or not gold: return False return normalize_text(prediction) normalize_text(gold)归一化匹配能解决大小写、全角半角、标点、多余空格等问题。对选择题和数值题尤其有效。但要注意归一化不能解决同义表达比如“巴黎”和“法国首都”仍然匹配不上。6.3 第三档LLM-as-Judge 处理开放题归一化匹配还失败的样本交给 LLM 裁判。裁判模型通常用比被测模型稍大或同规模的模型。关键不是裁判模型多大而是裁判 prompt 是否中立。下面是一个面向判分的裁判函数JUDGE_PROMPT 你是评测员。判断以下模型回答是否“可以算作正确”。 判定标准模型回答与参考答案在语义上是否等价不要求逐字一致。 只输出 JSON{{correct: true 或 false, reason: 简短理由}} 题目{question} 参考答案{gold} 模型回答{prediction} def llm_judge(question: str, gold: str, prediction: str, judge_model: str qwen2.5:7b, url: str http://127.0.0.1:11434/v1/chat/completions) - dict: prompt JUDGE_PROMPT.format(questionquestion, goldgold, predictionprediction) payload { model: judge_model, messages: [{role: user, content: prompt}], temperature: 0, max_tokens: 128 } resp requests.post(url, jsonpayload, timeout120) content resp.json()[choices][0][message][content] start content.find({) end content.rfind(}) 1 try: return json.loads(content[start:end]) except (json.JSONDecodeError, ValueError): return {correct: False, reason: judge parse failed}裁判 prompt 里一个关键点是“不要求逐字一致”。如果不加这句话裁判模型会模仿严格匹配那第三档就等于白做。另外temperature必须设为 0否则同一道题可能被判定为忽对忽错。6.4 防止宽松判分变成无底线放水宽松判分不等于把所有模糊回答都判对。你需要做一次“负向校验”从判为正确的样本里随机抽 20 条人工确认确实正确。如果发现宽松判分把错误答案也放过就调整抽取规则或裁判 prompt。建议统计三档判分各自贡献了多少正确样本例如判分档位判定正确数占比精确匹配12040%归一化匹配8027%LLM 裁判6020%未通过4013%这样就能看出模型真实水平与“严格匹配”的差距有多大。很多场景下第三档是规避假失败的关键。7. 批量评测与结果统计实战评测脚本要支持批量否则几百条样本一条一条手动看没有意义。这里给一个完整的批量评测模板逻辑是读 JSONL → 调模型 → 抽取答案 → 分档判分 → 写回 JSONL。7.1 完整批量脚本模板import json import requests def generate(question: str, model: str qwen2.5:1.5b, url: str http://127.0.0.1:11434/v1/chat/completions) - str: payload { model: model, messages: [{role: user, content: question}], temperature: 0, max_tokens: 256 } resp requests.post(url, jsonpayload, timeout120) return resp.json()[choices][0][message][content] def evaluate_sample(sample: dict) - dict: question sample[question] gold sample[gold] raw_output generate(question) extracted extract_final_answer(raw_output) em exact_match(extracted, gold) nm False if em else normalized_match(extracted, gold) judge_result None if not em and not nm: judge_result llm_judge(question, gold, raw_output) final_correct em or nm or bool(judge_result and judge_result.get(correct)) sample[raw_output] raw_output sample[extracted] extracted sample[exact_match] em sample[normalized_match] nm sample[judge_result] judge_result sample[final_correct] final_correct return sample with open(samples.jsonl, r, encodingutf-8) as fin, \ open(results.jsonl, w, encodingutf-8) as fout: for line in fin: line line.strip() if not line: continue sample json.loads(line) result evaluate_sample(sample) fout.write(json.dumps(result, ensure_asciiFalse) \n)这个脚本没有做并发适合几十到几百条样本。如果你要跑几千条可以把evaluate_sample放进ThreadPoolExecutor做并发但要注意控制并发数保护本地模型服务不被压垮。7.2 统计结果批量跑完后可以用一段简单脚本统计total 0 correct 0 recoverable 0 # 严格匹配失败但最终判定正确 with open(results.jsonl, r, encodingutf-8) as f: for line in f: r json.loads(line) total 1 if r[final_correct]: correct 1 if (not r[exact_match] and not r[normalized_match]) and r[final_correct]: recoverable 1 print(ftotal{total}) print(ffinal_correct_rate{correct / total:.2%}) print(frecoverable_samples{recoverable}) print(frecoverable_ratio{recoverable / total:.2%})这里的recoverable_samples就是标题里说的“失败样本里其实有正确答案”的数量。如果这个数字很大说明你的评测判分标准过严模型能力被低估了。7.3 失败样本复盘清单对最终仍判定失败的样本按以下维度归类模型输出为空或截断。抽取规则没匹配中答案。归一化后仍然不一致。LLM 裁判明确判定错误。标准答案本身有歧义。最后这条经常被忽略。有时候不是模型答错而是题目和参考答案设计不合理。评测集质量差再好的判分器也白搭。8. 资源占用、性能观察与常见问题排查8.1 SLM 评测时的资源观察方法评测 SLM 与训练模型不同大部分消耗集中在推理阶段。需要注意三个点显存用nvidia-smi观察。SLM 显存占用比大模型低很多但具体数值取决于模型大小、上下文长度和并发数。跑批量评测前先观察基线占用。CPU纯 CPU 推理时SLM 也能跑但批量速度会明显变慢。建议先用 20 条样本估算单条耗时再估算总时长。上下文长度长上下文评测会显著增加显存和延迟。如果评测数据包含长文档建议单独分一组不要与短问答混在一起统计。max_tokens也是影响性能的关键。设置过小答案被截断设置过大模型可能生成一堆无关内容拖慢整体评测。更稳妥的做法是先用人工样本测试不同的max_tokens选择一个能让模型“刚好说完答案”的值。8.2 常见问题与排查方法问题现象可能原因排查方式解决方案启动后接口报 404服务未启动或端口不对检查ollama list、访问http://127.0.0.1:11434启动ollama serve或改脚本中的 URL模型名不识别model参数与本地模型名不一致执行ollama list查看可用名称替换脚本中的模型名输出被截断答案缺失max_tokens太小查看raw_output实际长度调大max_tokens或在 prompt 里要求先给答案抽取结果为空正则规则没覆盖模型输出格式打印原始输出人工观察格式增加抽取规则或先做格式归一化判分结果忽高忽低判分温度不为 0 或并发冲突检查temperature参数固定为 0必要时加随机种子LLM 裁判返回解析失败裁判模型输出不是合法 JSON打印reason字段增强 JSON 解析或要求裁判只输出纯 JSON批量任务卡住并发数过高或服务崩溃查看服务日志和进程 CPU/显存降低并发数增加超时时间失败样本人工看是对的脚本判错判分规则过严抽样失败样本人工复核加入归一化匹配和 LLM 裁判如果遇到“模型输出质量不稳定”的情况先看生成参数是否固定。评测核心是可控复现任何随机性都会污染结论。9. 最佳实践与后续方向评测管线改进不是一次性动作而是一个持续校准的过程。下面这些做法可以直接用到你的评测项目里。9.1 先做小样本人工校准正式跑大批量之前先人工标注 20 条样本。你亲自看模型输出确定常见的输出格式有哪些然后让抽取函数覆盖这些格式。这个动作成本很低能避免批量跑了两个小时后才发现规则完全不对。9.2 原始输出和判分结果分目录存放推荐目录结构evaluation/ ├── samples.jsonl # 原始评测集 ├── results.jsonl # 判分结果 ├── raw_outputs/ # 按 id 保存的原始输出 └── logs/ # 运行日志分开存放后失败复盘、抽检、回归对比都会方便很多。9.3 把“可恢复正确”作为单独指标评测报告里不要只写一个正确率。建议同时输出严格匹配正确率。宽松匹配正确率。恢复样本占比。人工抽检通过率。这样别人看你的 benchmark 结果时不会误会模型能力。尤其是对外发布“模型 benchamark 胜率”时必须说清楚判分标准。否则严格判分和宽松判分得出的结论可能完全不同。9.4 合规与授权提醒评测数据如果涉及私人信息、版权文本或未公开业务数据必须先脱敏或确认授权。不要拿未经许可的私有数据跑批量评测尤其是调用本地模型时输出结果可能保留原始数据片段。涉及 Agent benchmark 或工具调用评测时还要注意只使用授权账号和沙箱环境避免误操作外部系统。任何自动化评测都要控制在测试环境边界内。9.5 后续可以扩展的方向多判据仲裁用多个 LLM 裁判投票降低单裁判偏差。增量评测只重新评测受影响的样本节省推理成本。回归基线每次微调后自动跑同一个评测集对比“可恢复正确率”是否变化。在线评测面板把结果写入数据库生成趋势图方便团队查看。这套方法不局限于单个 SLM。你把它改一改就能用到 RAG 评测、Agent 评测、指令微调回归评测里。核心思想是一样的先保留原始证据再设计多档判分最后人工抽检校准。最后留一个建议下次再看到 SLM benchmark 分数不理想先不要急着换模型。把失败样本抽出来人工过一遍统计一下有多少条其实包含正确答案。这个动作成本很低但能把你对模型能力的判断校准一次。评测脚本判错的损失往往被误算成模型能力不足而找到并修复这些假失败才是最划算的优化点。
返回列表