LLM 评测体系:从 Benchmark 到生产评估

作者:Maurice | 灵阙学院


一、为什么评测是 AI 工程的基石

许多团队在早期阶段依赖"感觉"来判断模型好坏——换了一个 Prompt,感觉回答更流畅了;切了一个新模型,感觉质量提升了。这种方式被业界称为 Vibes-Based Evaluation,是 LLM 工程中最危险的反模式。

没有评测,就没有迭代。原因很简单:

  • LLM 是概率模型,同一 Prompt 不同运行结果可能截然不同
  • 改善了 A 场景,极可能同时悄悄劣化了 B 场景(回归)
  • 模型版本更新(如 GPT-4o -> GPT-4o-mini)行为变化难以捕捉
  • 团队规模扩大后,"感觉好"无法传递,评测标准才能传递

评测体系的本质是:把主观判断转化为可量化、可重复、可自动化的信号。这与软件工程中单元测试的地位完全等价。

核心原则:先写 eval,再改 Prompt;先定指标,再选模型。


二、评测金字塔

LLM 评测不是单一维度,而是一个四层金字塔结构,从通用到专用,从离线到在线:

           ┌────────────────────────┐
           │    第四层:在线评测      │  A/B 测试 / 用户反馈
           ├────────────────────────┤
           │   第三层:自定义 Eval   │  针对你的业务场景
           ├────────────────────────┤
           │  第二层:领域 Benchmark  │  金融/医疗/法律/代码专项
           ├────────────────────────┤
           │  第一层:通用 Benchmark  │  MMLU/HumanEval/MATH/GSM8K
           └────────────────────────┘

第一层:通用 Benchmark 衡量模型的基础能力上限。适合在选型阶段快速筛选模型。包括语言理解、数学推理、代码生成等维度。

第二层:领域 Benchmark 针对特定行业的专项评测。例如金融领域的 FinBench、医疗领域的 MedQA、法律领域的 LegalBench。

第三层:自定义 Eval 这是最关键的一层,也是最被忽视的一层。通用 Benchmark 高分的模型,在你的业务场景里未必表现最好。自定义 Eval 直接对齐你的真实需求。

第四层:在线评测 真实用户数据是最诚实的评测。通过 A/B 测试、用户反馈收集、隐式信号(点赞/复制/重试)来持续监控生产质量。


三、主流 Benchmark 速查手册

3.1 通用能力 Benchmark

Benchmark 评测维度 题量 难度 开源
MMLU 多任务语言理解(57 个学科) 14K 中-高
HellaSwag 常识推理 / 句子补全 70K
ARC 科学常识(Easy/Challenge) 7.8K
WinoGrande 代词消歧 / 常识 44K
TruthfulQA 真实性 / 幻觉抵抗 817

3.2 数学与推理 Benchmark

Benchmark 评测维度 题量 难度 开源
GSM8K 小学数学应用题 8.5K 低-中
MATH 竞赛数学(AMC/AIME级) 12.5K
GPQA 博士级科学问答 448 极高
MathBench 中文数学(初中-大学) 3K+ 中-高

3.3 代码生成 Benchmark

Benchmark 评测维度 题量 难度 开源
HumanEval Python 函数生成 164
MBPP 基础编程任务 500
SWE-bench 真实 GitHub Issue 修复 2.3K 极高
LiveCodeBench 持续更新的竞赛题 动态

3.4 中文能力 Benchmark

Benchmark 评测维度 题量 难度 开源
C-Eval 中文综合能力(52 个学科) 13.9K 中-高
CMMLU 中文多任务理解 11.5K 中-高
GAOKAO 高考题目(语文/数学/英语) 2K+ 中-高
CMATH 中文数学 1.7K

3.5 Agent 能力 Benchmark

Benchmark 评测维度 场景 难度 开源
GAIA 通用 AI 助手任务(多工具) 网页/工具/文件
WebArena 网页操作自动化 电商/Git/Wiki
SWE-bench Verified Agent 修复真实 Bug GitHub PR 极高
AgentBench Agent 多环境综合 OS/DB/Web

注意:Benchmark 分数是参考,不是终点。SWE-bench Verified 满分模型不等于你的代码 Agent 就好用。


四、自定义 Eval 开发实战

4.1 评测框架选型

框架 特点 适合场景
OpenAI Evals 官方框架,YAML/JSON配置 快速上手,OpenAI 用户
Promptfoo 开源 CLI,多模型对比 本地开发,CI 集成
DeepEval Python SDK,LLM-as-Judge 内置 生产级,指标丰富
Langfuse 全链路追踪 + Eval 生产监控 + 评测一体
HELM 斯坦福出品,学术级 全面基准测试

4.2 用 Promptfoo 做多模型对比

Promptfoo 是目前开发者体验最好的本地 Eval 工具,支持 CLI 一键对比多个模型:

# promptfooconfig.yaml
providers:
  - openai:gpt-4o
  - anthropic:claude-opus-4-5
  - openai:gpt-4o-mini

prompts:
  - "请将以下客户投诉分类为:退款/换货/咨询/投诉 四类之一。\n\n投诉内容:{{complaint}}"

tests:
  - vars:
      complaint: "我买的手机屏幕有裂缝,要求退货退款"
    assert:
      - type: contains
        value: "退款"
  - vars:
      complaint: "请问这款产品支持哪些支付方式?"
    assert:
      - type: contains
        value: "咨询"
  - vars:
      complaint: "快递损坏了我的商品,我想换一个新的"
    assert:
      - type: contains
        value: "换货"
# 安装并运行
npm install -g promptfoo
promptfoo eval
promptfoo view  # 打开 Web UI 查看对比结果

4.3 用 DeepEval 构建生产级评测

from deepeval import evaluate
from deepeval.metrics import (
    AnswerRelevancyMetric,
    FaithfulnessMetric,
    ContextualPrecisionMetric,
)
from deepeval.test_case import LLMTestCase

# 定义测试用例
test_case = LLMTestCase(
    input="2024年中国GDP增速是多少?",
    actual_output=llm_response,
    expected_output="约5%",
    retrieval_context=["2024年中国GDP同比增长5.0%,超过政府工作报告目标"],
)

# 定义评测指标
metrics = [
    AnswerRelevancyMetric(threshold=0.7, model="gpt-4o"),
    FaithfulnessMetric(threshold=0.8, model="gpt-4o"),
    ContextualPrecisionMetric(threshold=0.7, model="gpt-4o"),
]

# 运行评测
evaluate(test_cases=[test_case], metrics=metrics)

4.4 LLM-as-Judge 模式

用强模型评测弱模型(或评测任意模型),是目前生产环境最实用的模式:

import openai

JUDGE_PROMPT = """你是一位严格的质量评估专家。请对以下 AI 回答进行打分。

【用户问题】
{question}

【AI 回答】
{answer}

【评分标准】
- 准确性(0-3分):事实是否正确
- 完整性(0-3分):是否回答了问题的所有方面
- 简洁性(0-2分):是否避免了冗余内容
- 格式(0-2分):是否结构清晰、易于阅读

请按以下 JSON 格式返回评分:
{{
  "accuracy": <0-3>,
  "completeness": <0-3>,
  "conciseness": <0-2>,
  "format": <0-2>,
  "total": <0-10>,
  "reason": "<评分理由,一句话>"
}}"""


def llm_judge(question: str, answer: str, judge_model: str = "gpt-4o") -> dict:
    client = openai.OpenAI()
    response = client.chat.completions.create(
        model=judge_model,
        messages=[
            {
                "role": "user",
                "content": JUDGE_PROMPT.format(question=question, answer=answer),
            }
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )
    import json
    return json.loads(response.choices[0].message.content)


# 使用示例
score = llm_judge(
    question="什么是 RAG?",
    answer="RAG(检索增强生成)是一种将外部知识库与 LLM 结合的技术...",
)
print(f"总分:{score['total']}/10,理由:{score['reason']}")

五、评测指标设计

5.1 分类任务指标

适用于意图识别、情感分析、文档分类等任务:

from sklearn.metrics import classification_report, f1_score

y_true = ["退款", "咨询", "退款", "换货", "投诉"]
y_pred = ["退款", "投诉", "退款", "换货", "咨询"]

# 宏平均 F1(对类别不均衡更公平)
macro_f1 = f1_score(y_true, y_pred, average="macro")
print(classification_report(y_true, y_pred))

5.2 生成任务指标

适用于摘要、翻译、问答等任务:

指标 全称 适合场景 局限
BLEU Bilingual Evaluation Understudy 机器翻译 不考虑语义相似
ROUGE Recall-Oriented Understudy 文档摘要 依赖参考答案
BERTScore 基于 BERT 的语义相似度 通用生成 需要 GPU 计算
G-Eval LLM-as-Judge 打分 开放生成 成本高
from rouge_score import rouge_scorer

scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=True)
reference = "大型语言模型是一种基于Transformer架构的深度学习模型"
hypothesis = "LLM是基于Transformer的大型语言模型"

scores = scorer.score(reference, hypothesis)
print(f"ROUGE-L: {scores['rougeL'].fmeasure:.3f}")

5.3 代码生成指标:Pass@k

Pass@k 衡量模型生成 k 个样本中至少有 1 个通过测试的概率:

import numpy as np
from typing import List


def pass_at_k(n: int, c: int, k: int) -> float:
    """
    计算 Pass@k 指标
    n: 每道题生成的样本数
    c: 通过测试的样本数
    k: 评测时取的 k 值
    """
    if n - c < k:
        return 1.0
    return 1.0 - np.prod(1.0 - k / np.arange(n - c + 1, n + 1))


# HumanEval 标准:n=200, k=1/10/100
print(f"Pass@1: {pass_at_k(200, 50, 1):.3f}")   # 约 25%
print(f"Pass@10: {pass_at_k(200, 50, 10):.3f}")  # 约 83%

5.4 自定义 Rubric 设计

对于业务专有任务,需要设计评分维度(Rubric):

RUBRIC_TEMPLATE = """
请对以下合同摘要进行评分(每项 0-5 分):

1. 关键条款覆盖率:是否包含合同金额、有效期、违约条款
2. 法律术语准确性:是否使用了正确的法律表述
3. 逻辑一致性:摘要内容与原合同是否一致
4. 可读性:非法律专业人士能否理解

【原合同片段】
{contract_excerpt}

【AI 生成摘要】
{summary}

请按 JSON 格式返回:{{"coverage": X, "accuracy": X, "consistency": X, "readability": X}}
"""

六、LLM-as-Judge 最佳实践

6.1 位置偏差消除

研究表明,LLM Judge 对答案在 Prompt 中的位置敏感——同样质量的回答,放在前面往往得分更高。消除方法:

def judge_with_swap(question: str, answer_a: str, answer_b: str) -> dict:
    """双向评测消除位置偏差"""
    # 正序:A先B后
    score_ab = judge_pairwise(question, answer_a, answer_b, order="AB")
    # 逆序:B先A后
    score_ba = judge_pairwise(question, answer_b, answer_a, order="BA")

    # 综合判断:只有两次都选同一个答案才认为有明显差异
    if score_ab["winner"] == "A" and score_ba["winner"] == "B":
        return {"winner": "A", "confidence": "high"}
    elif score_ab["winner"] == "B" and score_ba["winner"] == "A":
        return {"winner": "B", "confidence": "high"}
    else:
        return {"winner": "tie", "confidence": "low"}

6.2 多 Judge 投票

单一模型评测存在系统性偏差,多 Judge 投票可以提升可靠性:

from collections import Counter


def multi_judge_vote(question: str, answer: str, judges: list) -> dict:
    """多模型投票,取多数结果"""
    scores = []
    for judge_model in judges:
        score = llm_judge(question, answer, judge_model=judge_model)
        scores.append(score["total"])

    avg_score = sum(scores) / len(scores)
    variance = sum((s - avg_score) ** 2 for s in scores) / len(scores)

    return {
        "scores": scores,
        "average": avg_score,
        "variance": variance,
        "reliable": variance < 2.0,  # 方差小说明 Judge 之间高度一致
    }


# 使用 GPT-4o + Claude + Gemini 三模型投票
result = multi_judge_vote(
    question="解释量子纠缠",
    answer=model_response,
    judges=["gpt-4o", "claude-opus-4-5", "gemini-2.0-flash"],
)

6.3 与人工评测的一致性校验

在部署 LLM Judge 之前,必须用人工标注数据验证其准确性:

from scipy.stats import spearmanr


def validate_judge_correlation(human_scores: list, judge_scores: list) -> dict:
    """计算 LLM Judge 与人工评测的相关性"""
    correlation, p_value = spearmanr(human_scores, judge_scores)

    # 相关系数 > 0.7 才认为 Judge 可靠
    is_reliable = correlation > 0.7 and p_value < 0.05

    return {
        "spearman_r": correlation,
        "p_value": p_value,
        "reliable": is_reliable,
        "recommendation": "可以部署" if is_reliable else "需要校准 Judge Prompt",
    }

七、生产评测体系

7.1 CI 回归测试

每次 Prompt 或模型变更,自动触发评测套件,防止回归:

# .github/workflows/llm-eval.yml
name: LLM Regression Tests

on:
  pull_request:
    paths:
      - "prompts/**"
      - "src/llm/**"

jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Promptfoo Eval
        run: |
          npm install -g promptfoo
          promptfoo eval --config promptfooconfig.yaml --output results.json

      - name: Check Pass Rate
        run: |
          python scripts/check_eval_threshold.py \
            --results results.json \
            --min-pass-rate 0.85

      - name: Comment PR with Results
        uses: actions/github-script@v7
        with:
          script: |
            const results = require('./results.json');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## LLM Eval Results\n\nPass rate: ${results.passRate}`
            });
# scripts/check_eval_threshold.py
import json
import sys
import argparse


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--results", required=True)
    parser.add_argument("--min-pass-rate", type=float, default=0.85)
    args = parser.parse_args()

    with open(args.results) as f:
        results = json.load(f)

    pass_rate = results["stats"]["successes"] / results["stats"]["total"]
    print(f"Pass rate: {pass_rate:.2%} (threshold: {args.min_pass_rate:.2%})")

    if pass_rate < args.min_pass_rate:
        print("ERROR: Pass rate below threshold. Blocking merge.")
        sys.exit(1)

    print("OK: Pass rate above threshold.")


if __name__ == "__main__":
    main()

7.2 在线 A/B 测试框架

import hashlib
import random
from typing import Literal


def get_model_variant(
    user_id: str,
    experiment_id: str,
    traffic_split: float = 0.5,
) -> Literal["control", "treatment"]:
    """
    基于用户 ID 的确定性流量分配
    同一用户在同一实验中始终进入同一分组
    """
    hash_input = f"{user_id}:{experiment_id}"
    hash_value = int(hashlib.md5(hash_input.encode()).hexdigest(), 16)
    normalized = (hash_value % 10000) / 10000.0
    return "treatment" if normalized < traffic_split else "control"


# 在业务代码中使用
variant = get_model_variant(user_id="user_123", experiment_id="gpt4o_vs_claude")
if variant == "treatment":
    response = call_claude_opus(prompt)
else:
    response = call_gpt4o(prompt)

# 记录实验数据
track_event(
    user_id=user_id,
    experiment_id="gpt4o_vs_claude",
    variant=variant,
    response_time_ms=elapsed_ms,
    user_satisfied=user_clicked_thumbs_up,
)

7.3 成本-质量 Pareto 分析

import matplotlib.pyplot as plt


def plot_cost_quality_pareto(model_results: list[dict]):
    """
    绘制成本-质量 Pareto 曲线
    帮助决策者选择最优模型
    """
    fig, ax = plt.subplots(figsize=(10, 6))

    for model in model_results:
        ax.scatter(
            model["cost_per_1k_tokens"],
            model["quality_score"],
            s=200,
            label=model["name"],
        )
        ax.annotate(
            model["name"],
            (model["cost_per_1k_tokens"], model["quality_score"]),
            textcoords="offset points",
            xytext=(5, 5),
        )

    ax.set_xlabel("Cost per 1K tokens (USD)")
    ax.set_ylabel("Quality Score (0-100)")
    ax.set_title("Cost vs Quality Pareto Analysis")
    ax.legend()
    ax.grid(True, alpha=0.3)

    plt.tight_layout()
    plt.savefig("cost_quality_pareto.png", dpi=150)


# 示例数据
model_results = [
    {"name": "GPT-4o", "cost_per_1k_tokens": 0.005, "quality_score": 92},
    {"name": "GPT-4o-mini", "cost_per_1k_tokens": 0.0002, "quality_score": 78},
    {"name": "Claude 3.5 Haiku", "cost_per_1k_tokens": 0.0008, "quality_score": 85},
    {"name": "Gemini Flash 2.0", "cost_per_1k_tokens": 0.0001, "quality_score": 80},
]
plot_cost_quality_pareto(model_results)

八、评测驱动的开发流程(Eval-Driven Development)

类比 TDD(测试驱动开发),LLM 工程应该遵循 Eval-Driven Development(EDD)

传统流程(错误):
  写 Prompt → 主观测试 → 上线 → 出问题再修

EDD 流程(正确):
  定义任务 → 收集/构建 Eval 数据集 → 定义指标阈值
       ↓
  迭代 Prompt/模型 → 跑 Eval → 未达标则继续迭代
       ↓
  达标 → 代码 Review → 上线 → 在线监控闭环

8.1 Eval 数据集管理

# eval_dataset.py
from dataclasses import dataclass
from datetime import datetime
import json


@dataclass
class EvalCase:
    id: str
    input: str
    expected_output: str
    tags: list[str]
    created_at: str
    source: str  # "human_labeled" | "gpt4_generated" | "production_sample"


class EvalDataset:
    def __init__(self, name: str, version: str):
        self.name = name
        self.version = version
        self.cases: list[EvalCase] = []

    def add_case(self, case: EvalCase):
        # 防止重复
        if any(c.id == case.id for c in self.cases):
            raise ValueError(f"Duplicate case ID: {case.id}")
        self.cases.append(case)

    def save(self, path: str):
        data = {
            "name": self.name,
            "version": self.version,
            "created_at": datetime.now().isoformat(),
            "cases": [vars(c) for c in self.cases],
        }
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)

    @classmethod
    def load(cls, path: str) -> "EvalDataset":
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
        dataset = cls(data["name"], data["version"])
        dataset.cases = [EvalCase(**c) for c in data["cases"]]
        return dataset

8.2 防止 Eval 数据泄露

Eval 数据集绝对不能进入训练集,这是 Eval 失去意义的最根本风险:

import hashlib


def check_eval_contamination(
    eval_dataset_path: str,
    training_dataset_path: str,
) -> dict:
    """检查 Eval 数据是否泄露到训练集"""
    # 构建训练集的内容哈希表
    training_hashes = set()
    with open(training_dataset_path, encoding="utf-8") as f:
        for line in f:
            item = json.loads(line)
            content_hash = hashlib.sha256(
                item.get("input", "").encode()
            ).hexdigest()
            training_hashes.add(content_hash)

    # 检查 Eval 集
    contaminated = []
    eval_dataset = EvalDataset.load(eval_dataset_path)
    for case in eval_dataset.cases:
        content_hash = hashlib.sha256(case.input.encode()).hexdigest()
        if content_hash in training_hashes:
            contaminated.append(case.id)

    return {
        "total_eval_cases": len(eval_dataset.cases),
        "contaminated_cases": contaminated,
        "contamination_rate": len(contaminated) / len(eval_dataset.cases),
        "safe": len(contaminated) == 0,
    }

九、常见陷阱与规避策略

陷阱一:Benchmark 刷分 vs 真实能力

部分模型通过训练集中包含 Benchmark 题目(数据污染)来刷高分,这在 MMLU、GSM8K 上已有多次记录。

规避策略:

  • 优先看持续更新、题目不公开的 Benchmark(如 GPQA、LiveCodeBench)
  • 跑自定义 Eval,题目来自你的业务数据
  • 关注模型在 新发布 Benchmark 上的分数(尚未被污染)

陷阱二:Eval 集太小导致方差大

100 道题的 Eval 集,单次运行的结果可能因随机性波动 5-10%,让你误判模型好坏。

规避策略:

import scipy.stats as stats
import math


def minimum_eval_size(
    expected_pass_rate: float = 0.8,
    margin_of_error: float = 0.03,
    confidence_level: float = 0.95,
) -> int:
    """计算达到统计显著性所需的最小 Eval 集大小"""
    z = stats.norm.ppf((1 + confidence_level) / 2)
    p = expected_pass_rate
    n = math.ceil((z**2 * p * (1 - p)) / (margin_of_error**2))
    return n


# 如果期望通过率 80%,误差要求 ±3%,置信度 95%
min_size = minimum_eval_size()
print(f"最小 Eval 集大小:{min_size} 题")  # 约 683 题

陷阱三:LLM-as-Judge 的循环偏差

用 GPT-4o 评测另一个 GPT-4o 的回答,会产生自我偏好(Self-Preference Bias)。

规避策略:

  • 不要用同款模型做 Judge(如用 GPT-4o 评测 GPT-4o)
  • 优先用能力更强的模型做 Judge(如用 Opus 评测 Haiku)
  • 多 Judge 投票时,混合不同家族的模型
  • 定期用人工标注数据验证 Judge 的一致性

十、评测体系建设路线图

对于 AI 产品团队,评测体系的建设可分三个阶段推进:

阶段一(0-1 个月):建立基线

  • 选定 3-5 个与业务最相关的公开 Benchmark,确立当前模型的基线分数
  • 手工标注 100-200 个真实业务样本,建立第一版自定义 Eval 集
  • 用 Promptfoo 或 DeepEval 跑通本地 Eval Pipeline

阶段二(1-3 个月):自动化

  • 将 Eval 集扩充到 500 题以上
  • 接入 CI/CD,Prompt 或模型变更自动触发 Eval
  • 引入 LLM-as-Judge,降低人工评测成本

阶段三(3 个月以上):生产闭环

  • 上线在线 A/B 测试框架
  • 收集用户隐式反馈(点赞/重试/复制)作为在线信号
  • 建立 Eval 数据集版本管理,与训练集严格隔离
  • 定期做成本-质量 Pareto 分析,持续优化模型选型

核心结论:评测不是 AI 工程的附加品,它是 AI 产品质量的唯一可靠锚点。没有评测体系,所有的"优化"都只是碰运气。建立一套好的 Eval Pipeline,团队才能真正做到数据驱动、快速迭代。


Maurice | maurice_wen@proton.me