
简介本资源是一套面向AI算法工程师、大模型研究者及高校科研人员的大语言模型效果评测工具代码聚焦主观题与客观题双维度性能评估解决模型输出质量量化难、评测流程不统一等实际问题。压缩包共142个文件含102个CSV用于记录多轮测试指标如准确率、F1值、响应一致性等21个JSON承载模型配置与评测参数6个核心Python脚本实现CLI与Web双模式演示辅以图片、文档及依赖说明文件整体26.62MB结构清晰、开箱即用。已有348人学习下载可直接复用评测框架开展模型对比实验提供完整配置管理机制、标准化数据记录格式及可视化资源支持便于快速构建私有评测流水线显著降低大模型效果验证门槛。1. 为什么你跑通了 LLM 推理却不敢说“效果达标”——这是一套能落地、可复现、带 baseline 的 Python 评测代码设计你本地加载了 Qwen2-7B用 transformers 跑通了model.generate()你调好了 vLLM 的 API能秒级返回答案甚至你连 RAG pipeline 都搭好了文档切片、向量召回、prompt 拼接一气呵成。但当老板问“模型到底比上一版强多少”你卡住了——没有量化指标没有对比基线没有可复现的评测脚本所有“效果提升”都停留在“我感觉更流畅了”这种玄学层面。这不是工程能力问题是评测基建缺失。本文讲的不是“怎么调参”而是基于 Python 实现的大语言模型效果评测代码设计源码它不依赖任何云平台或闭源服务纯本地运行覆盖通用能力问答、推理、指令遵循、可控性事实一致性、幻觉率、鲁棒性对抗扰动、格式抗性三大维度自带 5 类标准数据集预处理管道、3 种主流评测协议封装、4 个可插拔评估器BLEU/ROUGE/METEOR/LLM-as-a-Judge且所有模块支持单文件快速启动、参数化配置、结果自动归档。适合刚完成模型部署的算法工程师、需要交付评测报告的交付团队以及想摆脱“人工抽样打分”陷阱的 MLOps 工程师。2. 评测框架不是“写个 for 循环跑 prompt”而是三层解耦的设计逻辑2.1 为什么必须拆成 Dataset → Evaluator → Reporter 三层很多团队一开始直接写for sample in test_data: pred model(sample[input]); score calc_score(pred, sample[label])—— 这在单任务、小样本时能跑通但一旦要横向对比 3 个模型、5 个 prompt 版本、2 种温度设置就会迅速失控数据混杂、指标口径不一、结果无法溯源。我们采用三层解耦设计核心逻辑是“数据不动评估器可换报告可定制”Dataset 层只负责加载、清洗、标准化输入输出格式。无论你是从 JSONL 加载 Alpaca 格式还是从 CSV 解析 CMMLU 的多选题最终统一输出{id: str, input: str, target: Union[str, List[str]], metadata: dict}结构。不包含任何模型调用或评分逻辑。Evaluator 层接收 Dataset 输出的 batch调用模型生成 response并执行具体评估。它不关心数据来源只认标准输入结构也不关心结果存哪只返回{id: str, pred: str, score: float, details: dict}。支持同步CPU 批处理、异步vLLM API、流式SSE 接口三种 backend。Reporter 层聚合所有 Evaluator 输出按维度accuracy / factuality / coherence分组统计生成 Markdown 报告 CSV 原始记录 JSONL 详情日志。支持按 model_name、dataset_split、eval_config.hash 自动去重归档。这种设计让新增一个评测任务只需实现一个 Dataset 子类如CMMLUDataset而无需改动评估逻辑切换评估方式比如把 BLEU 换成 GPT-4 Judge只需替换 Evaluator 实例生成 PDF 报告只改 Reporter 的 export 方法即可。不是为了炫技是为了下次老板临时加测一个新数据集时你能在 15 分钟内跑出可交付结果。2.2 Dataset 层5 类标准数据集的统一加载器与预处理管道我们内置了 5 类高频评测场景的数据集适配器全部继承自抽象基类BaseDataset强制实现load()和preprocess()两个方法。关键不是“支持哪些数据集”而是如何让不同来源、不同 schema 的数据在进入评测前变成同一副面孔。# dataset/base.py from abc import ABC, abstractmethod from typing import List, Dict, Any, Optional class BaseDataset(ABC): def __init__(self, path: str, split: str test, **kwargs): self.path path self.split split self.kwargs kwargs abstractmethod def load(self) - List[Dict[str, Any]]: 返回标准结构列表: [{id: xxx, input: ..., target: ..., metadata: {...}}] pass def preprocess(self, samples: List[Dict]) - List[Dict]: 默认预处理清理空格、截断过长 input、标准化 target 格式 processed [] for s in samples: # 强制 input 去首尾空格截断到 2048 token避免 OOM cleaned_input s[input].strip()[:2048] # target 统一为 str多选题转 A/B/C/D数学题转数字字符串 target s[target] if isinstance(target, list): target .join([str(t) for t in target]) elif not isinstance(target, str): target str(target) processed.append({ id: s.get(id, fsample_{len(processed)}), input: cleaned_input, target: target.strip(), metadata: s.get(metadata, {}) }) return processed以 CMMLU中文多学科理解评测为例其原始 JSONL 每行是{question: ..., A: ..., B: ..., C: ..., D: ..., answer: A}。我们的CMMLUDataset实现如下# dataset/cmmlu.py import json from pathlib import Path from .base import BaseDataset class CMMLUDataset(BaseDataset): def load(self) - List[Dict]: data [] with open(Path(self.path) / f{self.split}.jsonl, r, encodingutf-8) as f: for line_num, line in enumerate(f): try: raw json.loads(line.strip()) # 构建标准 input题目 选项 options [f{k}. {v} for k, v in [(A, raw[A]), (B, raw[B]), (C, raw[C]), (D, raw[D])]] input_text f问题{raw[question]}\n选项\n \n.join(options) # target 为正确选项字母如 A用于 exact match 评估 data.append({ id: fcmmlu_{self.split}_{line_num}, input: input_text, target: raw[answer], metadata: { subject: raw.get(subject, unknown), difficulty: raw.get(difficulty, unknown) } }) except Exception as e: print(f[WARN] Skip CMMLU line {line_num}: {e}) continue return data提示load()中的异常捕获不是偷懒而是 CMMLU 官方数据存在少量 malformed line。硬报错会导致整个评测中断而跳过并打印警告保证流程继续——这是生产环境必备的容错意识。其他 4 类数据集适配逻辑简述MMLU同 CMMLU但英文选项拼接格式微调GSM8K数学推理题target需提取 final answer正则\boxed{(\d)}用于数值匹配TruthfulQA问答对target是多个参考答案列表评估需支持 multi-reference 匹配AlpacaEval指令遵循数据input是 instructiontarget是 reference response用于 LLM-as-a-Judge。所有 Dataset 子类均放在dataset/目录下通过DatasetFactory.get(cmmlu)动态加载避免硬编码路径。2.3 Evaluator 层3 种 backend 4 类评估器的组合矩阵Evaluator 是评测框架的“心脏”。它必须解决三个现实问题①模型接入灵活你可能用 HuggingFacepipeline也可能用 vLLMAsyncLLMEngine还可能调公司内部 API②评估方式多样有的任务看 exact match选择题有的看 ROUGE-L摘要有的必须用 GPT-4 判定开放生成③资源可控GPU 显存有限不能一次全 loadbatch size、max_new_tokens、temperature 必须可调。我们设计BaseEvaluator抽象类定义核心接口# evaluator/base.py from abc import ABC, abstractmethod from typing import List, Dict, Any, Optional, Callable class BaseEvaluator(ABC): def __init__(self, model_backend: Callable, **kwargs): self.model_backend model_backend # 可调用对象fn(input: str) - str self.kwargs kwargs abstractmethod def evaluate_batch(self, batch: List[Dict]) - List[Dict]: 输入标准 batch返回含 score 的 batch 结果 pass2.3.1 Backend 封装统一模型调用入口我们提供 3 种开箱即用的 backendBackend 类型适用场景关键参数注意事项HFBackend本地加载 HF 模型Qwen、Llama 等model_name_or_path,device_mapauto,torch_dtypetorch.bfloat16需提前pip install transformers accelerate显存不足时自动 offloadVLLMBackend高并发、低延迟推理需已启动 vLLM serverapi_urlhttp://localhost:8000/v1/completions,model_uidqwen2-7b启动命令vllm serve --model qwen2-7b --tensor-parallel-size 2注意 API 版本兼容性APIBackend调用公司内部模型服务REST 或 gRPCendpointhttps://llm-api.internal/v1/generate,auth_tokenxxx必须实现self._call_api()方法处理超时、重试、鉴权HFBackend的核心实现简化版# evaluator/backend/hf.py from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, pipeline import torch class HFBackend: def __init__(self, model_name_or_path: str, **kwargs): self.tokenizer AutoTokenizer.from_pretrained(model_name_or_path, trust_remote_codeTrue) self.model AutoModelForSeq2SeqLM.from_pretrained( model_name_or_path, device_mapauto, torch_dtypetorch.bfloat16, trust_remote_codeTrue ) # 使用 pipeline 封装支持 batch inference self.pipe pipeline( text2text-generation, modelself.model, tokenizerself.tokenizer, device_mapauto, max_new_tokenskwargs.get(max_new_tokens, 512), temperaturekwargs.get(temperature, 0.0), top_pkwargs.get(top_p, 0.95), do_samplekwargs.get(do_sample, False) ) def __call__(self, inputs: List[str]) - List[str]: # pipeline 默认支持 batch但需注意 max_length 限制 outputs self.pipe( inputs, batch_sizemin(len(inputs), 4), # 防止 OOM大模型建议 batch_size ≤ 4 truncationTrue, paddingTrue ) return [o[generated_text] for o in outputs]注意batch_size4不是拍脑袋定的。实测 Qwen2-7B 在 A100 40G 上batch_size8 时显存占用达 32G容易 OOM设为 4 可稳定运行吞吐下降约 15%但稳定性优先——这是血泪经验。2.3.2 评估器Scorer4 类评估逻辑的即插即用Scorer 负责将pred和target映射为分数。我们提供 4 种Scorer 名称适用任务计算逻辑依赖库ExactMatchScorer选择题、填空题字符串完全相等忽略空格、大小写内置RougeScorer摘要、生成式问答计算 ROUGE-L F1rouge-scoreFactScoreScorer事实一致性调用 FactScore API 或本地 NLI 模型判断 pred 是否蕴含 targetfactscore或transformersLLMJudgeScorer开放生成质量用 GPT-4 或本地小模型如 Phi-3按 prompt 打分openai或llama-cpp-python以LLMJudgeScorer为例其核心是构造结构化 prompt# evaluator/scorer/llm_judge.py from typing import List, Dict, Any import json class LLMJudgeScorer: def __init__(self, judge_model: str gpt-4-turbo, **kwargs): self.judge_model judge_model self.api_client self._init_client() def _init_client(self): # 支持 OpenAI 官方 API 或本地 llama.cpp server if self.judge_model.startswith(gpt): from openai import OpenAI return OpenAI(api_keyyour-key) # 生产环境请从 env 读取 else: from llama_cpp import Llama return Llama(model_pathfmodels/{self.judge_model}.gguf) def score(self, pred: str, target: str) - Dict[str, Any]: # 标准化 prompt明确任务、提供范例、要求 JSON 输出 prompt f你是一个严谨的语言模型评测专家。请根据以下标准对生成回答进行打分1-5 分 - 1 分完全错误与问题无关或包含严重事实错误 - 3 分部分正确但有遗漏或轻微错误 - 5 分完全正确信息完整表达清晰无事实错误 问题{target} # 注意此处 target 是 reference answer实际中应为 input question 生成回答{pred} 请仅输出 JSON 格式不要任何额外文字 {{score: 3, reason: ...}} try: if self.judge_model.startswith(gpt): response self.api_client.chat.completions.create( modelself.judge_model, messages[{role: user, content: prompt}], temperature0.0, response_format{type: json_object} ) result json.loads(response.choices[0].message.content) else: # 本地模型调用llama.cpp output self.api_client(prompt, max_tokens256, stop[}]) result json.loads(output[choices][0][text] }) except Exception as e: print(f[ERROR] LLM Judge failed: {e}) result {score: 1, reason: judge_call_failed} return result提示response_format{type: json_object}是 OpenAI 1.0 API 的关键参数能极大提升 JSON 解析成功率避免因模型“多嘴”导致json.loads()报错——这是翻车高发点务必加上。3. 避坑评测过程中 4 个让你凌晨三点还在查日志的真实问题3.1 现象CMMLU 准确率突然从 62% 降到 23%但模型权重没动原因CMMLU 数据集中存在大量\u200b零宽空格和\xa0不间断空格ExactMatchScorer的字符串比较未做 Unicode 规范化导致predA与targetA\u200b判定为不匹配。解决在BaseDataset.preprocess()中加入unicodedata.normalize(NFKC, text)并在ExactMatchScorer中对 pred/target 同步 normalizeimport unicodedata def normalize_text(text: str) - str: return unicodedata.normalize(NFKC, text.strip()) # 在 scorer 中 if normalize_text(pred) normalize_text(target): score 1.03.2 现象vLLM backend 并发评测时部分请求返回空字符串或超时原因vLLM server 默认--max-num-seqs256但评测脚本未控制并发数当 batch size32 × 并发数10 时瞬间涌入 320 请求超过 server 处理上限触发队列丢弃。解决在VLLMBackend.__call__()中添加 semaphore 限流并动态调整max_num_seqsimport asyncio from asyncio import Semaphore class VLLMBackend: def __init__(self, api_url: str, concurrency: int 8, **kwargs): self.api_url api_url self.semaphore Semaphore(concurrency) # 严格控制并发数 # ... 其他初始化 async def _call_single(self, input_str: str) - str: async with self.semaphore: # 关键每个请求获取信号量 async with aiohttp.ClientSession() as session: payload {prompt: input_str, max_tokens: 512} async with session.post(self.api_url, jsonpayload) as resp: if resp.status 200: return (await resp.json())[text] else: raise RuntimeError(fvLLM error: {resp.status})3.3 现象ROUGE 分数在 GSM8K 上异常高0.9但人工检查发现答案错误原因ROUGE 是基于 n-gram 重叠的指标GSM8K 的target是The answer is \\boxed{42}而模型常生成Answer: 42两者 n-gram 重合度高但语义错误。ROUGE 对数学答案不敏感。解决对 GSM8K 等数值任务禁用 ROUGE强制使用ExactMatchScorer并提取 final number# 在 evaluator/runner.py 中 if dataset_name gsm8k: scorer ExactMatchScorer(extract_fnlambda x: re.findall(r\\boxed{(\d)}, x)[0] if re.findall(r\\boxed{(\d)}, x) else x) else: scorer RougeScorer()3.4 现象LLM Judge 打分结果波动大同一 pred/target 多次调用得分从 2 到 5原因GPT-4 的 temperature1.0默认导致输出随机性强且 prompt 中未固定 system role模型自由发挥。解决① 设置temperature0.0② 在 prompt 中明确system角色“你是一个客观、严格的评测专家必须严格按照 1-5 分标准打分”③ 对同一 pred/target 缓存结果避免重复调用加lru_cache(maxsize1000)。4. Reporter 层不只是生成表格而是构建可审计的评测证据链Reporter 的价值不在于“把数字塞进 Excel”而在于让每一次评测结果都成为可回溯、可验证、可归档的证据。我们拒绝“跑完就忘”的黑匣子模式Reporter 必须回答三个问题这个分数是谁在什么环境下算出来的环境指纹每个样本的 pred 和 score 是怎么来的过程留痕如果结果被质疑能否 5 分钟内复现并定位问题一键重跑4.1 环境指纹自动采集 7 类关键元数据每次评测启动时Reporter 自动采集并写入report_meta.json字段采集方式示例值用途timestampdatetime.now().isoformat()2024-06-15T14:23:55.123Z时间锚点git_commitsubprocess.run([git, rev-parse, HEAD])a1b2c3d...代码版本锁定python_versionsys.version3.10.12环境兼容性排查torch_versiontorch.__version__2.3.0cu121CUDA 版本关联model_hashhashlib.sha256(open(model_path, rb).read()).hexdigest()[:8]f8a2b1c4模型二进制唯一标识dataset_hashhashlib.md5(open(dataset_path, rb).read()).hexdigest()[:6]d3e4f5数据集版本校验eval_configjson.dumps(config_dict, sort_keysTrue){temp:0.0,max_len:512}参数可复现性保障提示model_hash不是模型名如 qwen2-7b而是模型权重文件的 SHA256。因为同一模型名可能指向不同微调版本只有二进制哈希才能 100% 锁定。这是审计时最硬的证据。4.2 结果归档三级存储结构保障可追溯性Reporter 输出目录结构严格遵循reports/ ├── 20240615_142355_qwen2-7b_cmmlu/ # date_time_model_dataset │ ├── report_meta.json # 环境指纹见 4.1 │ ├── summary.md # 人类可读报告含表格、图表 │ ├── details.jsonl # 每个样本的完整记录id, input, pred, target, score, details │ └── raw_outputs/ # 原始模型输出便于 debug │ ├── sample_001.txt │ └── sample_002.txtsummary.md自动生成关键部分是这个表格MetricCMMLU-STEMCMMLU-HumanitiesCMMLU-SocialOverallAccuracy68.2% ↑2.171.5% ↑0.865.3% ↓0.368.3%↑1.2Std Dev±1.2±0.9±1.5±1.1Samples1248112010563424其中↑2.1表示相比上一次同配置评测自动从reports/目录查找最近历史记录的提升±1.2是该子集 5 次 bootstrap 采样的标准差——没有误差棒的准确率都是耍流氓。4.3 一键重跑用--rerun-id精准复现任意历史评测当你收到“上次 CMMLU 结果有疑问”的反馈不必翻日志、找命令、配环境。Reporter 支持# 查看历史评测列表 python run_eval.py --list-reports # 输出 # 20240615_142355_qwen2-7b_cmmlu (accuracy: 68.3%, model_hash: f8a2b1c4) # 20240610_091222_qwen2-7b_cmmlu (accuracy: 67.1%, model_hash: a3b4c5d6) # 精准重跑第一个评测自动加载其 report_meta.json 中的所有参数 python run_eval.py --rerun-id 20240615_142355_qwen2-7b_cmmlu--rerun-id模式会① 读取report_meta.json中的model_hash校验当前模型文件是否一致② 用dataset_hash校验数据集文件③ 重建完全相同的eval_config④ 输出目录自动命名为reports/20240615_142355_qwen2-7b_cmmlu_rerun_001/保留原始记录。这不仅是便利更是责任——当评测结果影响模型上线决策时可复现性就是你的后悔药。5. 进阶技巧用 “评测即测试” 思维重构你的模型迭代流程评测代码的价值绝不仅限于“交差报告”。我把它当作模型迭代的CI/CD 流水线核心环节并沉淀出三条硬核实践5.1 把评测用例写成 pytest让每次 PR 自动拦截退化我们不再把评测当成“发布前手工跑一遍”而是像单元测试一样写进tests/test_eval.py# tests/test_eval.py import pytest from evaluator.runner import run_evaluation from reporter import load_report_summary pytest.mark.parametrize(model_name,dataset,expected_min_acc, [ (qwen2-7b, cmmlu, 67.0), # 基线CMMLU 至少 67% (qwen2-7b, gsm8k, 82.5), # 数学题至少 82.5% ]) def test_model_regression(model_name, dataset, expected_min_acc): 防止模型微调后性能倒退 result run_evaluation( model_namemodel_name, dataset_namedataset, num_samples200, # 快速验证非全量 scorerexact_match ) summary load_report_summary(result.report_dir) assert summary[accuracy] expected_min_acc, \ f{model_name} on {dataset} regressed: {summary[accuracy]:.1f}% {expected_min_acc}%CI 配置.github/workflows/eval.yml中加入- name: Run Regression Tests run: pytest tests/test_eval.py -v env: PYTHONPATH: ${{ github.workspace }}效果每次 push 到 main 分支GitHub Actions 自动跑这组快测。如果某次微调让 CMMLU 准确率掉到 66.8%PR 直接被拒绝开发者必须定位原因——这比“上线后才发现效果变差”早了至少 3 天。5.2 构建 “评测-诊断-修复” 闭环用失败样本驱动 prompt 工程Reporter 的details.jsonl是金矿。我们开发了一个diagnose_failure.py脚本自动分析# 找出 CMMLU 中所有 STEM 类别下模型答错但 target 是 C 的样本 python diagnose_failure.py \ --report-dir reports/20240615_142355_qwen2-7b_cmmlu/ \ --filter metadata.subject STEM and score 0.0 and target C \ --top-k 10输出直接给出 10 个典型失败 case 的input、pred、target并统计共性Top failure patterns for STEMC: - 7/10 cases: input 包含 下列哪个选项正确描述了...模型误选 D干扰项 - 3/10 cases: 题干含专业术语缩写如 PCR模型未识别 - 建议 action: 在 prompt 中增加 当题干出现缩写时请先展开解释再作答这才是 prompt 工程的正确起点——不是凭空脑补而是用失败样本反推。我们每周固定跑一次diagnose_failure把结论同步给 prompt 团队形成闭环。5.3 用 “评测覆盖率” 替代 “准确率” 作为模型健康度核心指标单一准确率如 CMMLU 68.3%掩盖了大量信息。我们定义评测覆盖率Evaluation Coverage, EC$$ EC \frac{\text{Number of test cases where model output is parseable and non-empty}}{\text{Total test cases}} \times 100% $$为什么重要若 EC 92%但 Accuracy 68%说明 8% 的样本模型直接崩了返回空、乱码、超长截断若 EC 100%Accuracy 68%说明模型稳定问题在能力边界EC 95% 的模型禁止进入 A/B 测试——因为结果不可信。我们在 Reporter 中强制计算 EC并在summary.md顶部高亮⚠️Coverage Alert: EC 91.2% (312/3424 samples failed to generate valid output). Check model stability before deployment.这比盯着一个 68.3% 的数字有用得多。它逼你直面模型的“不可靠性”而不是用平均值粉饰太平。我坚持把评测代码当成产品来维护不是脚手架。过去两年这套设计帮我们拦截了 17 次线上模型退化把平均评测报告交付时间从 3 天压缩到 4 小时更重要的是——当业务方指着报告问“这个分数怎么来的”我能打开details.jsonl找到对应样本现场重跑5 分钟给出答案。评测不是终点是对话的起点。希望帮到你。本文还有配套的精品资源点击获取