如何评估 RAG 系统的效果?常用的评估指标有哪些?
结论:RAG 评估必须分层——检索层看"找没找到",生成层看"用得对不对",端到端看"用户满不满意"。 三层混在一起算一个总分,出了问题根本不知道该改哪。没有标注集就先手工造 50~100 条,这比任何调参技巧都值钱。
1. 三层指标体系
| 层 | 指标 | 回答的问题 | 计算依赖 |
|---|---|---|---|
| 检索 | recall@k、hit rate | 正确片段进了前 k 吗 | 查询 → 相关片段标注 |
| 检索 | MRR、NDCG@k | 排得够不够靠前 | 带序的相关性标注 |
| 生成 | 忠实度(Faithfulness) | 答案每句都有依据吗 | 答案 + 上下文 |
| 生成 | 答案相关性(Answer Relevancy) | 有没有答到点上 | 答案 + 问题 |
| 生成 | 上下文精确率/召回率 | 检索的噪声与遗漏 | 上下文 + 标注答案 |
| 端到端 | 答案正确率 | 最终对不对 | 标准答案 |
| 端到端 | 拒答准确率 | 该说"不知道"时说了吗 | 库外问题集 |
2. 几个公式,面试要能写出来
| 指标 | 公式 | 直觉 |
|---|---|---|
| recall@k | 命中数 / 相关总数 | 前 k 条覆盖了多少应有的证据 |
| MRR | mean(1 / 第一个相关片段的名次) | 第一个对的排多靠前 |
| NDCG@k | DCG@k / IDCG@k | 分级相关性下,排序质量如何 |
| 忠实度 | 有依据的句子数 / 总句子数 | 答案里有多少句是"编的" |
命中率(Hit Rate)只看"有没有",召回率(Recall)看"覆盖多少",两个别混用。
3. 评估集怎么造
| 来源 | 数量 | 说明 |
|---|---|---|
| 线上真实 query 采样 | 60% | 最能反映分布 |
| 业务方整理的典型问题 | 25% | 覆盖核心场景 |
| 故意构造的库外问题 | 15% | 测拒答能力 |
每条样本至少包含:question、relevant_chunk_ids(或 ground_truth)、should_refuse。
标注规范要写清"什么算相关"——是含答案,还是只沾边,否则两个人标出两套标准。
4. 检索层评估脚本
import numpy as np
def evaluate_retrieval(dataset: list[dict], k: int = 5) -> dict:
"""dataset: [{"question": str, "relevant": set[str]}]"""
recall, mrr = [], []
for item in dataset:
hits = store.query(query_embeddings=embed([item["question"]]), n_results=k)
ids = hits["ids"][0]
hit = [i for i, doc_id in enumerate(ids) if doc_id in item["relevant"]]
recall.append(len(hit) / len(item["relevant"]) if item["relevant"] else 0.0)
mrr.append(1.0 / (hit[0] + 1) if hit else 0.0)
return {"recall@k": float(np.mean(recall)), "mrr": float(np.mean(mrr)), "n": len(dataset)}
# 输出:{'recall@k': 0.82, 'mrr': 0.64, 'n': 80}
先把这个数拉到 0.85 以上,再动生成侧的提示词,顺序不能反。
5. 生成层:忠实度用 LLM 判定
import json
JUDGE = """逐句判断<答案>是否被<资料>支持,输出 JSON:
{"supported": 支持的句子数, "total": 总句子数, "unsupported": ["原句"]}"""
def faithfulness(answer: str, contexts: list[str]) -> float:
resp = llm.chat.completions.create(
model="gpt-4o-mini", temperature=0,
response_format={"type": "json_object"},
messages=[{"role": "system", "content": JUDGE},
{"role": "user",
"content": f"<资料>\n{chr(10).join(contexts)}\n</资料>\n\n<答案>\n{answer}"}],
)
r = json.loads(resp.choices[0].message.content)
return r["supported"] / max(1, r["total"])
成熟方案也可以直接用开源框架,它把忠实度、答案相关性、上下文精确率/召回率打包成一条流水线(各版本 API 有差异,以安装版本的文档为准):
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
result = evaluate(
dataset=samples, # 字段:question / answer / contexts / ground_truth
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result) # 各指标 0~1,按样本汇总
LLM 裁判适合做相对比较(A 方案 vs B 方案),绝对值要打折扣:同一份答案换个模型评分能差 0.1。
6. 指标好看但用户不满意的四种情况
| 现象 | 真实原因 | 该改哪里 |
|---|---|---|
| recall 高,答案仍空泛 | 块太大,证据被稀释 | 重新分块 |
| 忠实度高,但没回答问题 | 模型只复述原文 | 提示词要求"先结论后依据" |
| 正确率高,引用全错 | 引用编号未校验 | 加后置校验 |
| 离线全绿,线上投诉 | 分布漂移 | 线上采样回流评估集 |
7. 常见追问
Q:没有标注集怎么办? 先用 LLM 从文档反向生成"问题-答案"对,人工抽检 20% 修正;50 条就能看出方向,不必等 500 条。
Q:需要每次全量跑吗? 不必。回归集固定 50~100 条,每次改动跑一遍;全量评估放在版本发布前。
Q:线上怎么持续评估? 埋点采集空召回率、引用命中率、差评率,人工抽样每周 30 条灌回评估集,让集合跟着业务长大。
小结
评估是 RAG 的"仪表盘":没有它,所有优化都是玄学。先分层、再定量、最后才谈调优——顺序对了,每次改动都知道自己在改什么。