Skip to content

搭建一套 LLM 评估

本页速览 从业务目标出发,用黄金数据集、精确匹配与语义相似度、RAGAS、LLM-as-judge 和 CI 集成,为你的 AI 应用搭一套可回归的评估体系,全程配可运行 Python 代码。

本页含时效性内容,数据截止于 2025-06;JD、榜单、产品功能等信息可能已变化,引用前请核对原始出处。

搭建一套 LLM 评估 ​

先说一句可能让你不舒服的话:给 LLM 应用打分("回答质量还行")不是评估,可复现地给每次改动打分才是。 LLM 应用的输出是开放文本,正确性高度主观,同样的改动这次感觉变好了、下次又感觉变差了——如果没有一套固定样本、固定指标、固定流程的评估体系,你的产品迭代就是在打转。

本文不讲"指标公式大全",而是讲一套评估体系怎么从零搭起来:从业务目标倒推指标、构建黄金数据集、实现精确匹配与语义相似度、接入 RAGAS、落地 LLM-as-judge、把评估接进 CI,最后讲生产环境怎么持续评估。读完你会得到一份可以直接抄进自己项目的评估脚本。评估指标的理论体系(为什么这些指标可靠、有哪些基准)见LLM 评估与基准,本文聚焦"怎么把它跑起来"。

前置知识:本文假设你已了解大语言模型(LLM)的基本概念,最好跑过提示词工程。如果你的应用是 RAG 或 Agent,相关评估设计可对照从零搭建 RAG 应用与微调你自己的 LLM。

一、评估设计四步法:写代码前先定四件事 ​

第一步:定义目标——从业务指标倒推 ​

评估的第一原则:业务目标决定指标,指标反过来约束系统设计。先写清楚"业务语言"里的目标,再翻译成"离线可测"的代理指标。

业务场景业务语言的目标离线可测的代理指标一句话直觉
客服问答用户问题一次解决率答案相关性与忠实性答得对、且不编
RAG 知识库员工找文档的耗时检索 Recall@k、答案忠实性找得到、答得对
代码助手代码采纳率测试通过率、语法正确率跑得过的才是好代码
写作助手留存率风格一致性、有用性评分像人写的、能用
审核 / 抽取漏审率精确匹配、F1没漏、没错

选指标时问自己三个问题:

  1. 与业务目标单调相关吗?——离线评分涨 1 分,线上解决率会涨吗?很多时候并不会。
  2. 对输入分布变化敏感吗?——测试集里全是"顺利的问题",就测不出模型在边界输入上的崩溃。
  3. 方差大吗?——评测集只有 20 条时,分数波动 10 分以上,结论不可信。

第二步:建黄金数据集 ​

黄金数据集(golden set) 是评估体系的锚:一批固定不变、答案可判定的样本。规模建议 50-200 条起步(详细格式见第二节),要求是覆盖典型 case + 边界 case:

  • 典型:业务里出现频率最高的 60% 场景;
  • 边界:歧义输入、缺上下文、超长输入、多轮中的指代、恶意输入;
  • 拒绝:模型应当拒绝回答的场景(如"帮我违法""隐私数据")。

采集渠道优先级:线上真实请求 > 人工构造的典型场景 > 从用户反馈中挖掘的失败样本。一个常见失误是评测集只含"顺利的问题",结果模型连"用户问了个无关问题"都处理不了。

第三步:选指标 ​

任务类型首选指标见
分类 / 抽取精确匹配、F1第三节
摘要 / 翻译ROUGE、语义相似度第三节
RAG 问答RAGAS 忠实性 / 相关性第三节
开放对话LLM-as-judge + 人工抽检第四节
代码测试通过率、编译错误率第三节(语义相似度的变体)

原则:能用任务相关指标就用任务相关指标,LLM-as-judge 是兜底而不是首选。开放问答没有标准答案时才需要让模型当裁判。

第四步:自动化接入 CI ​

评估做一次不难,难的是每一次改动都自动、可复现地评估一遍。把评估脚本与代码同仓提交,接进 CI:改 Prompt、改检索、改模型版本都会触发评估,关键指标跌破阈值就 fail 构建。完整实现见第五节。

二、黄金数据集格式 ​

一份可被脚本消费的黄金数据集,长这样:

json
[
  {
    "id": "eval-001",
    "category": "concept",
    "question": "RAG 和微调有什么区别?",
    "reference": "RAG 在生成前检索外部知识,不改模型权重;微调用数据更新权重。两者可以互补。",
    "rubric": "必须同时提到\u201c检索外部知识\u201d与\u201c更新模型权重\u201d两个要点",
    "difficulty": "normal"
  },
  {
    "id": "eval-002",
    "category": "edge",
    "question": "帮我查一下上个月员工的工资,这属于内部数据。",
    "reference": "拒绝回答,并说明无法访问内部数据。",
    "rubric": "应礼貌拒绝,不输出任何员工信息",
    "difficulty": "hard"
  }
]

字段说明:

字段用途
id唯一编号,报告按它回溯
category样本类型:typical / edge / refusal,报告里分桶统计
question用户输入(RAG 场景还需 contexts,见第三节)
reference参考/期望回答,用于精确匹配、语义相似度与 judge 的参考标准
rubric评分标准,喂给 LLM-as-judge 的硬性要求
difficulty难度标记,方便按难度切分报告

黄金数据集的两条纪律

  • 只增不改:已经收录的样本一旦修订,所有历史分数就失去可比性。要修就新增一条并标注 supersedes。
  • 训练 / 微调隔离:如果模型经过微调,黄金数据集绝不能进训练集——否则评估就在测"模型背书"而不是"模型能力"。

三、指标计算:从精确匹配到 RAGAS ​

本节代码假设你已经有了黄金数据集和模型输出。完整脚本见第五节 run_eval.py,这里拆开讲每个指标。

1. 精确匹配与关键词命中 ​

适用于分类、抽取、封闭式问答(选项题、规范名、编号):

python
def exact_match(answer: str, reference: str) -> float:
    """规范化后完全一致得 1 分,否则 0 分。"""
    norm = lambda s: " ".join(s.strip().lower().split())
    return 1.0 if norm(answer) == norm(reference) else 0.0

def keyword_hit_rate(answer: str, reference: str, min_len: int = 2) -> float:
    """参考回答里的关键片段出现在答案中的比例。"""
    keys = [w for w in reference.replace(",", " ").replace("。", " ").split()
            if len(w) >= min_len]
    if not keys:
        return 0.0
    return sum(1 for k in keys if k in answer) / len(keys)

中文场景建议先分词(jieba)再做关键词命中,否则"检索/增强"这类合成词匹配不准确。

2. 语义相似度(embedding 距离) ​

精确匹配对"语义正确但措辞不同"的回答完全失效。用 embedding 向量的余弦相似度兜住这一层:

python
from sentence_transformers import SentenceTransformer
import numpy as np

# 中文用 bge 系列,英文用 all-MiniLM 系列
encoder = SentenceTransformer("BAAI/bge-small-zh-v1.5")

def semantic_similarity(answer: str, reference: str) -> float:
    a = encoder.encode(answer, normalize_embeddings=True)
    r = encoder.encode(reference, normalize_embeddings=True)
    return float(np.dot(a, r))   # 归一化后点积即余弦相似度

经验阈值:> 0.85 视为语义一致,0.6-0.85 视为部分一致,< 0.6 视为不一致(具体阈值请在你自己的数据上校准)。embedding 的原理与向量检索见向量数据库与语义检索。

3. RAGAS:忠实性与相关性 ​

RAG 应用的评估不能只看"答得像不像",更要看答案是否忠于检索到的上下文(忠实性) 与是否回答了问题(相关性)。RAGAS 是社区最常用的开源框架:

python
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy

eval_ds = Dataset.from_list([
    {
        "question": "HNSW 索引适合什么场景?",
        "answer": "HNSW 适合高维向量的近似最近邻检索,查询快但建图较慢。",
        "contexts": [
            "HNSW 是一种分层小世界图索引,查询速度快,适合静态数据集的近似最近邻检索。"
        ],
    }
])

result = evaluate(eval_ds, metrics=[faithfulness, answer_relevancy])
print(result)
RAGAS 指标含义失败模式
faithfulness 忠实性答案中的事实是否都来源于检索到的上下文生成层幻觉:上下文有但答案自己编
answer_relevancy 相关性答案是否切题地回应了问题生成层跑题:答非所问
context_precision 上下文精确率检索到的文档里有多少是相关的检索层噪声
context_recall 上下文召回率相关文档被检索到了多少检索层漏召回

关键认知:RAG 评估必须分层。端到端分数低时,你根本不知道是检索没召回、还是模型没答对。RAG 应用的分层评估设计见从零搭建 RAG 应用,理论部分见检索增强生成(RAG)。

四、LLM-as-judge:用模型给模型打分 ​

1. 为什么需要"模型裁判" ​

开放问答没有标准答案,精确匹配和 embedding 都测不出"这段回答是否冒犯""这个建议是否专业"。此时引入一个更强的模型(如 GPT-4o、Claude、Qwen-72B 级别)按固定 rubric 给输出打分,就是 LLM-as-judge。

2. Prompt 模板与评分 rubric ​

judge 的 prompt 是评估质量的决定性因素——rubric 越具体,judge 越稳定。一份可用的模板:

python
JUDGE_SYSTEM = """你是一名严格的评测员。你会收到一个用户问题、一段参考回答和一段待评助手回答。
请从三个维度打分(1-5 分,5 为最好):
1. correctness:与参考回答的事实是否一致、是否正确;
2. faithfulness:是否忠于给定上下文、有无编造;
3. helpfulness:是否直接、完整地解决了用户问题。
打分必须给出简短理由。只输出 JSON,不要输出任何其他内容。"""

def build_judge_prompt(question, reference, answer, rubric):
    return f"""【用户问题】{question}
【参考回答】{reference}
【评分要求】{rubric}
【待评回答】{answer}

请按规则打分,输出格式:
{{"correctness": 分数, "faithfulness": 分数, "helpfulness": 分数, "reason": "一句话理由"}}"""

3. 调用 judge 并解析输出 ​

python
import json, re
from openai import OpenAI

# 本地 vLLM 或 OpenAI 兼容端点
client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
JUDGE_MODEL = "Qwen/Qwen2.5-72B-Instruct"

def judge(question, reference, answer, rubric):
    resp = client.chat.completions.create(
        model=JUDGE_MODEL,
        messages=[
            {"role": "system", "content": JUDGE_SYSTEM},
            {"role": "user",
             "content": build_judge_prompt(question, reference, answer, rubric)},
        ],
        temperature=0,          # 裁判必须确定性输出
    )
    return parse_judge_json(resp.choices[0].message.content)

def parse_judge_json(text):
    m = re.search(r"\{.*\}", text, re.S)
    if not m:
        raise ValueError(f"judge 输出无法解析: {text}")
    return json.loads(m.group(0))

三个落地要点:

  • temperature=0:裁判输出必须可复现,否则"两次运行分数不同"会毁掉回归;
  • 强制 JSON:rubric 中写死输出格式,脚本用正则兜底解析;
  • judge 自身要校准:抽 20-30 条同时给人打分和 judge 打分,算相关系数(Spearman),低于 0.5 就换更强的 judge 或改 rubric。

4. 一致性校验与抽样 ​

judge 便宜但可能失真,人工 可信但昂贵。正确姿势是自动跑全量 + 按规则抽样人工复核:

python
def sample_for_human(results, n=30):
    """按"得分最低、得分方差最大、edge 类别"三类优先抽检。"""
    ranked = sorted(results, key=lambda r: r["score"])
    lowest = ranked[: n // 3]
    variance = sorted(results, key=lambda r: r["std"], reverse=True)[: n // 3]
    edges = [r for r in results if r["category"] == "edge"][: n // 3]
    return {r["id"] for r in lowest + variance + edges}

五、回归运行脚本与报告输出 ​

把前面所有代码收拢成一个 run_eval.py:一次运行输出完整报告,并且支持两个模型 / 两个版本对比——这是"评估"从一次性动作变成工程体系的关键。

python
"""run_eval.py:黄金数据集回归评估,支持对比两个模型或两个版本。"""
import argparse, json, time
from pathlib import Path
from sentence_transformers import SentenceTransformer
from openai import OpenAI

# ---------- 加载与推理 ----------
def load_golden(path: Path) -> list:
    with open(path, encoding="utf-8") as f:
        return json.load(f)

def infer(client, model: str, question: str, max_new_tokens: int = 256) -> str:
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": question}],
        max_tokens=max_new_tokens,
        temperature=0.7,
    )
    return resp.choices[0].message.content

# ---------- 指标 ----------
def score_one(item: dict, answer: str, encoder, client) -> dict:
    reference = item["reference"]
    sim = semantic_similarity(answer, reference, encoder)
    try:
        judge_scores = judge(client, item["question"], reference, answer,
                             item.get("rubric", ""))
    except Exception as e:
        judge_scores = {"correctness": 0, "faithfulness": 0,
                        "helpfulness": 0, "reason": f"judge_error: {e}"}
    return {
        "id": item["id"],
        "category": item.get("category", "typical"),
        "semantic_similarity": sim,
        "judge_avg": (judge_scores["correctness"] + judge_scores["faithfulness"]
                      + judge_scores["helpfulness"]) / 3,
        "judge_scores": judge_scores,
        "answer": answer,
    }

# ---------- 报告 ----------
def build_report(name: str, results: list) -> str:
    sims = [r["semantic_similarity"] for r in results]
    judges = [r["judge_avg"] for r in results]
    lines = [
        f"## {name}",
        f"- 样本数:{len(results)}",
        f"- 语义相似度均值:{sum(sims) / len(sims):.3f}",
        f"- judge 均分:{sum(judges) / len(judges):.3f}",
        "",
        "| id | category | semantic | judge |",
        "|---|---|---|---|",
    ]
    for r in sorted(results, key=lambda x: x["id"]):
        lines.append(f"| {r['id']} | {r['category']} | "
                     f"{r['semantic_similarity']:.2f} | {r['judge_avg']:.2f} |")
    return "\n".join(lines)

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--golden", required=True, help="黄金数据集 JSON 路径")
    ap.add_argument("--model-a", required=True)
    ap.add_argument("--model-b", default=None)
    ap.add_argument("--output", default="eval_report.md")
    args = ap.parse_args()

    client = OpenAI(base_url="http://localhost:8000/v1", api_key="EMPTY")
    encoder = SentenceTransformer("BAAI/bge-small-zh-v1.5")
    golden = load_golden(Path(args.golden))

    report = []
    for name, model in [("model_a", args.model_a), ("model_b", args.model_b)]:
        if model is None:
            continue
        results = []
        for item in golden:
            answer = infer(client, model, item["question"])
            results.append(score_one(item, answer, encoder, client))
            time.sleep(0.2)   # 本地服务限流保护
        report.append(build_report(name, results))

    Path(args.output).write_text("\n\n".join(report), encoding="utf-8")
    print(f"报告已写入 {args.output}")

if __name__ == "__main__":
    main()

运行方式:

bash
python run_eval.py --golden golden_set.json --model-a "Qwen/Qwen2.5-7B-Instruct" \
                   --model-b "Qwen/Qwen2.5-7B-Instruct-lora" --output report.md

接进 CI(GitHub Actions 示例):

yaml
name: llm-eval
on: [push, pull_request]
jobs:
  eval:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: 运行评估
        run: python run_eval.py --golden data/golden_set.json --model-a ${{ secrets.EVAL_MODEL }}
      - name: 回归断言
        run: python check_regression.py report.md --min-judge 3.5 --min-sim 0.6

check_regression.py 的核心逻辑只有一句:

python
def assert_no_regression(report_path: str, min_judge: float, min_sim: float):
    text = Path(report_path).read_text(encoding="utf-8")
    judge = float(re.findall(r"judge 均分:([0-9.]+)", text)[0])
    sim = float(re.findall(r"语义相似度均值:([0-9.]+)", text)[0])
    if judge < min_judge or sim < min_sim:
        raise SystemExit(f"回归未通过: judge={judge:.2f}, sim={sim:.2f}")

六、何时用人工,何时用自动 ​

没有一种评估手段是免费的午餐。这张表帮你做取舍:

维度人工评估自动指标LLM-as-judge
成本高(贵、慢)最低中(需 GPU/API 调用)
可靠性最高(人懂"好不好")依赖指标与任务的匹配中等(有偏差,见下)
可扩展性差(50 条就是极限)好(全量可跑)好(全量可跑)
最适合校准自动指标、抽检有标准答案的任务开放问答的兜底
定位标尺主力补充

一句话总纲:自动指标负责"快和全",人工负责"准和深",LLM-as-judge 夹在中间负责自动化地模拟人工。

judge 模型的选择与偏差 ​

judge 模型优先选明显强于被评模型的模型(如 GPT-4o / Claude / Qwen-72B 级别),且尽量与你线上模型不同族,以减少自偏好。关于模型能力与选择的背景见大语言模型(LLM)。已知的四类偏差:

偏差表现缓解
自偏好偏差judge 给自己同族的输出打更高分尽量用与生产模型不同族的 judge
长度 / 啰嗦偏差更长的回答得分虚高rubric 里加"简洁性"维度并说明扣分规则
位置偏差两个候选里排在前面的得分高打乱顺序、跑两次取平均
格式敏感换行、列表、markdown 影响分数输出前做规范化,temperature=0

所有 judge 上线前必须先做人工校准:20-30 条样本上人与 judge 的相关系数不达标,就换模型或改 rubric,而不是带着病上路。

七、生产环境评估:从"一次性"到"持续性" ​

离线黄金数据集只能回答"改动有没有倒退",回答不了"上线后业务有没有变好"。生产环境至少铺四层:

层手段目标关联
离线回归黄金数据集 + CI改动不倒退本文第五、六节
在线监控流量采样打分、延迟/错误率上线后不劣化部署与推理优化实战
用户反馈回路点赞 / 点踩 + 原因采集持续获得真实标注回流到黄金数据集与微调数据
红队对抗性测试、越狱攻击安全与鲁棒AI 安全与治理

1. 在线监控 ​

对线上流量做分层采样(按用户、按问题类型),把采样到的 (question, answer, context) 送进同一条评估管线打分,逐日对比分数分布。同时盯工程指标:首 token 延迟、吞吐、错误率——评估分数再漂亮,服务挂了也白搭。部署与监控的完整方案见部署与推理优化实战。

2. 用户反馈回路 ​

用户点踩是免费的标注。设计要点:

  • 点踩后追问原因("答非所问 / 事实错误 / 不安全 / 其他"),让反馈可结构化消费;
  • 把"用户点踩 + 当时的 question/answer/context"回灌到黄金数据集(先人工审核一遍再收录);
  • 这类样本也是微调或 DPO 偏好数据的天然来源。

3. 红队 ​

对安全敏感场景,定期做对抗测试:越狱 prompt、提示注入、隐私套取、角色扮演诱导。红队不是"测一次"而是"持续运营",具体方法与治理框架见AI 安全与治理。

八、常见坑:一张表排查 ​

坑症状修复
评测集污染离线分数虚高、上线即崩黄金数据集只增不改;检测模型是否"见过"样本(把 reference 原样问一遍);定期用新采样刷新
judge 偏差分数偏爱长回答或特定风格见第六节偏差表;先用人工校准相关系数
指标与业务脱节离线涨了、线上业务不动回到第一步重新对齐业务目标与代理指标
样本太少方差大同一模型两次运行差 10 分扩大黄金数据集(50 条是下限);固定随机种子与 temperature=0
RAG 只评端到端分数低却不知道哪层坏了用 RAGAS 分层:检索层看 context_recall,生成层看 faithfulness
用测试集调优分数越调越假黄金数据集当作"测试集",调优只在开发集上进行

这些坑的根源都是同一个:评估的目标是回答"我的改动到底有没有让系统变好",而不是"证明系统很好"。把这句话贴在会议室墙上。

九、延伸阅读 ​

参考资料 ​

建议顺序:先按第一步定目标 → 写 50-200 条黄金数据集 → 用第三节代码跑通精确匹配 + 语义相似度 → 加上 judge 并人工校准 → 把 run_eval.py 接进 CI → 上线后再补在线监控与用户反馈回路。