AI 模型性能评测实战:用 lm-evaluation-harness 搭建标准化评测流水线

阿里云上云采购季 2核2G云服务器99元/年 续费同价

“明明说好了能打 80 分,为什么接上业务就翻车?”——模型排行榜的分数和实际表现差了十万八千里,原因很简单:评测基准不对。本文教你用工业级工具标准化评测大模型,摆脱「体感评测」的魔咒。


你为什么不能信排行榜?

HuggingFace Open LLM Leaderboard 是开发者选模型的重要参考,但它的「高分」模型到了你的场景可能一塌糊涂,原因有三个:

排行榜的坑 解释
数据泄漏 训练数据里混进了评测集,模型作弊了自己都不知道
评测单一 只测英文/数学/代码,你的中文客服场景没人测
Prompt 不匹配 你用的 System Prompt 和评测时不一样,输出天差地别
格式敏感 LLM 对 JSON 格式、Few-shot 示例数量极度敏感,差一点差很多

结论:自己的场景必须自己测。


lm-evaluation-harness 是什么?

lm-evaluation-harness 是 EleutherAI 维护的工业级 LLM 评测框架,HuggingFace Open LLM Leaderboard 用的就是它。

核心能力:

┌─────────────────────────────────────────────────────┐
│            lm-evaluation-harness                    │
│                                                     │
│  ┌─────────────┐  ┌───────────┐  ┌──────────────┐ │
│  │ 200+ 标准任务 │  │ 自定义任务  │  │ 多模型后端    │ │
│  │ MMLU/GSM8K  │  │ YAML 配置  │  │ HF/vLLM/API  │ │
│  │ HellaSwag   │  │ 本地数据集  │  │ OpenAI/本地   │ │
│  └─────────────┘  └───────────┘  └──────────────┘ │
│                                                     │
│  输出: JSON 分数 + 详细统计 + 置信区间                │
└─────────────────────────────────────────────────────┘

5 步搭建评测流水线

第 1 步:安装

# 创建虚拟环境
python -m venv eval_env
source eval_env/bin/activate  # Windows: eval_env\Scripts\activate

# 安装主库
pip install lm-eval

# 如果要测评本地模型,还需要这些
pip install transformers torch accelerate
pip install vllm  # 可选,推理加速

验证安装:

lm_eval --help
# 看到帮助信息就是成功了

第 2 步:跑第一个评测——MMLU

MMLU(Massive Multitask Language Understanding)是 57 个学科的综合知识测试,是目前最通用的模型能力基准。

# 用 HuggingFace 模型跑 MMLU(基础版)
lm_eval \
  --model hf \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct,dtype=bfloat16 \
  --tasks mmlu \
  --num_fewshot 5 \
  --batch_size auto \
  --output_path ./results \
  --log_samples

参数说明:

参数 含义
--model hf 使用 HuggingFace Transformers 后端
--model_args 模型路径 + 推理参数
--tasks mmlu 评测任务(可多个,逗号分隔)
--num_fewshot 5 每个问题前提供几个示例
--batch_size auto 自动检测最大 batch
--output_path 结果保存目录
--log_samples 保存每个样本的详细输出

第 3 步:多模型对比评测脚本

"""
多模型对比评测流水线
自动跑 MMLU / GSM8K / HellaSwag 三个基准测试
输出: 汇总 CSV + JSON 详细结果
"""
import subprocess
import json
import csv
from pathlib import Path
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor


# ==================== 配置区 ====================
MODELS = [
    {
        "name": "Qwen2.5-7B-Instruct",
        "pretrained": "Qwen/Qwen2.5-7B-Instruct",
        "dtype": "bfloat16",
    },
    {
        "name": "DeepSeek-R1-Distill-Qwen-7B",
        "pretrained": "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B",
        "dtype": "bfloat16",
    },
    {
        "name": "Llama-3.1-8B-Instruct",
        "pretrained": "meta-llama/Llama-3.1-8B-Instruct",
        "dtype": "bfloat16",
    },
]

TASKS = [
    {
        "name": "mmlu",
        "task": "mmlu",
        "fewshot": 5,
        "metric": "acc,none",
    },
    {
        "name": "gsm8k",
        "task": "gsm8k",
        "fewshot": 5,
        "metric": "exact_match,flexible-extract",
    },
    {
        "name": "hellaswag",
        "task": "hellaswag",
        "fewshot": 10,
        "metric": "acc_norm,none",
    },
]

OUTPUT_DIR = Path("./eval_results")
OUTPUT_DIR.mkdir(exist_ok=True)


def run_evaluation(model: dict, task: dict) -> dict:
    """运行单个 模型 × 任务 的评测"""
    model_name = model["name"]
    task_name = task["name"]
    output_file = OUTPUT_DIR / f"{model_name}_{task_name}.json"

    cmd = [
        "lm_eval",
        "--model", "hf",
        "--model_args", f"pretrained={model['pretrained']},dtype={model['dtype']}",
        "--tasks", task["task"],
        "--num_fewshot", str(task["fewshot"]),
        "--batch_size", "auto",
        "--output_path", str(OUTPUT_DIR),
        "--log_samples",
    ]

    print(f"🚀 开始评测: {model_name} × {task_name}")
    start = datetime.now()

    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600)
        elapsed = (datetime.now() - start).total_seconds()

        if result.returncode != 0:
            return {
                "model": model_name,
                "task": task_name,
                "score": "ERROR",
                "time_sec": elapsed,
                "error": result.stderr[-500:],
            }

        # 解析输出中的分数
        score = "N/A"
        for line in result.stdout.split("\n"):
            if task["metric"].split(",")[0] in line.lower().replace(" ", "_"):
                parts = line.strip().split("|")
                if len(parts) >= 3:
                    score = parts[2].strip()
                    break

        print(f"✅ {model_name} × {task_name}: {score} ({elapsed:.0f}s)")

        return {
            "model": model_name,
            "task": task_name,
            "score": score,
            "time_sec": elapsed,
        }

    except subprocess.TimeoutExpired:
        return {
            "model": model_name, "task": task_name,
            "score": "TIMEOUT", "time_sec": 3600,
        }


def generate_report(results: list[dict]):
    """生成评测汇总报告"""

    # 写入 CSV
    csv_path = OUTPUT_DIR / "leaderboard.csv"
    with open(csv_path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["model", "task", "score", "time_sec"])
        writer.writeheader()
        writer.writerows(results)

    # 写入 JSON
    json_path = OUTPUT_DIR / "leaderboard.json"
    json_path.write_text(json.dumps(results, indent=2, ensure_ascii=False))

    # 打印汇总表格
    print(f"\n{'='*70}")
    print("📊 模型评测汇总")
    print(f"{'='*70}")

    # 按模型分组
    models_data = {}
    for r in results:
        if r["model"] not in models_data:
            models_data[r["model"]] = {}
        models_data[r["model"]][r["task"]] = r["score"]

    # 表头
    headers = ["模型"] + [t["name"] for t in TASKS]
    header_line = "| " + " | ".join(headers) + " |"
    sep_line = "|" + "|".join(["------"] * len(headers)) + "|"
    print(header_line)
    print(sep_line)

    for model_name, scores in models_data.items():
        row = f"| {model_name} "
        for task in TASKS:
            row += f"| {scores.get(task['name'], 'N/A')} "
        row += "|"
        print(row)

    print(f"\n📁 详细结果: {OUTPUT_DIR}/")
    print(f"📄 CSV 汇总: {csv_path}")


if __name__ == "__main__":
    print(f"🔬 模型评测流水线启动 @ {datetime.now()}")
    print(f"   模型数: {len(MODELS)}, 任务数: {len(TASKS)}")
    print(f"   总评测数: {len(MODELS) * len(TASKS)}\n")

    # 串行执行(GPU 只能跑一个模型)
    results = []
    for model in MODELS:
        for task in TASKS:
            r = run_evaluation(model, task)
            results.append(r)

    generate_report(results)

第 4 步:自定义评测任务

标准任务不一定适合你的场景。lm-eval 支持用 YAML 定义自定义任务:

# custom_tasks/chinese_customer_service.yaml
task: chinese_customer_service
dataset_path: json
dataset_name: null
dataset_kwargs:
  data_files:
    train: ./data/cs_train.jsonl
    validation: ./data/cs_val.jsonl

# 指标
metric_list:
  - metric: exact_match
    aggregation: mean
    higher_is_better: true
  - metric: f1
    aggregation: mean
    higher_is_better: true

# 生成 Prompt
doc_to_text: |
  你是客服助手,请根据用户问题给出专业回复。

  用户问题:{{question}}
  回复:

doc_to_target: "{{answer}}"

# 输出解析
output_type: generate_until
generation_kwargs:
  until:
    - "\n用户"
    - "\n\n"
  max_gen_toks: 256
  do_sample: false
  temperature: 0.0

准备数据文件 data/cs_val.jsonl:

{"question": "我的订单什么时候发货?", "answer": "您好,订单付款后24小时内发货,您可以在订单详情中查看物流信息。"}
{"question": "可以退款吗?", "answer": "您好,支持7天无理由退款,请在订单页面提交退款申请。"}
{"question": "你们几点下班?", "answer": "您好,我们的客服工作时间是每天9:00-21:00。"}

运行自定义任务:

lm_eval \
  --model hf \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct \
  --tasks chinese_customer_service \
  --include_path ./custom_tasks \
  --output_path ./results

第 5 步:用 vLLM 加速评测

HuggingFace 默认推理太慢了,用 vLLM 可以直接提速 5-10 倍:

# 安装 vLLM
pip install vllm

# 用 vLLM 后端评测
lm_eval \
  --model vllm \
  --model_args pretrained=Qwen/Qwen2.5-7B-Instruct,dtype=auto,gpu_memory_utilization=0.9,tensor_parallel_size=1 \
  --tasks mmlu,gsm8k,hellaswag \
  --batch_size auto \
  --output_path ./results_vllm

性能对比:

后端 MMLU (14k 题) GSM8K (1.3k 题) 显存占用
Transformers ~45 分钟 ~8 分钟 16GB
vLLM ~6 分钟 ~1.5 分钟 18GB
加速比 7.5x 5.3x —

评测指标解读

核心指标速查

指标 含义 适用场景
Accuracy (acc) 精确匹配的正确率 分类、选择题
acc_norm 归一化准确率(忽略大小写/标点) HellaSwag 等
Exact Match (EM) 完全匹配率 数学、代码生成
F1 Score 精确率和召回率的调和平均 生成类、QA
Perplexity (PPL) 困惑度(越低越好) 语言建模能力
ROUGE-L 最长公共子序列 摘要、翻译

实战经验:怎么读分数?

MMLU 70%:基础常识不错,能理解各领域概念
MMLU 80%+:接近 GPT-4 水平,知识覆盖面广
GSM8K 80%+:数学推理达标,能处理初中数学
GSM8K 90%+:数学推理强,适合量化、数据分析场景
HellaSwag 85%+:常识推理优秀,不会犯低级逻辑错误

但是这些指标加了 Few-shot 后变化很大:

模型 MMLU (0-shot) MMLU (5-shot)
Qwen2.5-7B 71.2 74.8 (+3.6)
Llama-3.1-8B 65.3 68.9 (+3.6)
DeepSeek-R1-Distill-7B 70.5 71.1 (+0.6)

💡 DeepSeek-R1 对 Few-shot 不那么敏感,因为它本身推理能力就强。


自动化评测流水线

把上面的脚本接入 CI/CD,每次模型更新自动跑评测:

# .github/workflows/model_eval.yml
name: Model Evaluation

on:
  push:
    paths:
      - "models/**"
      - "eval/**"

jobs:
  evaluate:
    runs-on: [self-hosted, gpu]
    steps:
      - uses: actions/checkout@v4

      - name: Run evaluation
        run: |
          python eval_pipeline.py

      - name: Check score regression
        run: |
          python -c "
          import json
          results = json.load(open('eval_results/leaderboard.json'))
          for r in results:
              score = float(r['score'].replace('%',''))
              if score < 65:
                  raise Exception(f'{r[\"model\"]} {r[\"task\"]} score {score}% below threshold 65%')
          "

      - name: Upload results
        uses: actions/upload-artifact@v4
        with:
          name: eval-results
          path: eval_results/

👤 作者简介

一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~

博客:y1cloud.cn

发表评论