1. 为什么 RAG 需要专门的评估体系
RAG(Retrieval-Augmented Generation,检索增强生成)把「检索」和「生成」两个环节串成了一条流水线。它的输出质量同时取决于:
- 检索器有没有把真正相关的文档片段找出来;
- 生成器有没有忠实、完整地利用这些片段,而不是自由发挥。
这意味着传统 LLM 评估(只看答案好不好)不足以定位问题。一个答案错了,可能是检索没召回,也可能是召回了但模型没用上,还可能是模型用了但表达跑偏。只有把指标拆到环节上,才能知道该优化哪一段。
本文按「检索 → 生成 → 端到端 → 工程与安全」四层,梳理 Python RAG 项目里真正值得落地的评估指标,并给出可直接运行的实现代码。
2. RAG 评估的整体框架
#mermaid-svg-NBUNiHNfDjRl1VQk{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NBUNiHNfDjRl1VQk .error-icon{fill:#552222;}#mermaid-svg-NBUNiHNfDjRl1VQk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NBUNiHNfDjRl1VQk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NBUNiHNfDjRl1VQk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NBUNiHNfDjRl1VQk .marker.cross{stroke:#333333;}#mermaid-svg-NBUNiHNfDjRl1VQk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NBUNiHNfDjRl1VQk p{margin:0;}#mermaid-svg-NBUNiHNfDjRl1VQk .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster-label text{fill:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster-label span{color:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster-label span p{background-color:transparent;}#mermaid-svg-NBUNiHNfDjRl1VQk .label text,#mermaid-svg-NBUNiHNfDjRl1VQk span{fill:#333;color:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk .node rect,#mermaid-svg-NBUNiHNfDjRl1VQk .node circle,#mermaid-svg-NBUNiHNfDjRl1VQk .node ellipse,#mermaid-svg-NBUNiHNfDjRl1VQk .node polygon,#mermaid-svg-NBUNiHNfDjRl1VQk .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NBUNiHNfDjRl1VQk .rough-node .label text,#mermaid-svg-NBUNiHNfDjRl1VQk .node .label text,#mermaid-svg-NBUNiHNfDjRl1VQk .image-shape .label,#mermaid-svg-NBUNiHNfDjRl1VQk .icon-shape .label{text-anchor:middle;}#mermaid-svg-NBUNiHNfDjRl1VQk .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NBUNiHNfDjRl1VQk .rough-node .label,#mermaid-svg-NBUNiHNfDjRl1VQk .node .label,#mermaid-svg-NBUNiHNfDjRl1VQk .image-shape .label,#mermaid-svg-NBUNiHNfDjRl1VQk .icon-shape .label{text-align:center;}#mermaid-svg-NBUNiHNfDjRl1VQk .node.clickable{cursor:pointer;}#mermaid-svg-NBUNiHNfDjRl1VQk .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NBUNiHNfDjRl1VQk .arrowheadPath{fill:#333333;}#mermaid-svg-NBUNiHNfDjRl1VQk .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NBUNiHNfDjRl1VQk .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NBUNiHNfDjRl1VQk .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NBUNiHNfDjRl1VQk .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NBUNiHNfDjRl1VQk .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NBUNiHNfDjRl1VQk .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster text{fill:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk .cluster span{color:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NBUNiHNfDjRl1VQk .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NBUNiHNfDjRl1VQk rect.text{fill:none;stroke-width:0;}#mermaid-svg-NBUNiHNfDjRl1VQk .icon-shape,#mermaid-svg-NBUNiHNfDjRl1VQk .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NBUNiHNfDjRl1VQk .icon-shape p,#mermaid-svg-NBUNiHNfDjRl1VQk .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NBUNiHNfDjRl1VQk .icon-shape .label rect,#mermaid-svg-NBUNiHNfDjRl1VQk .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NBUNiHNfDjRl1VQk .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NBUNiHNfDjRl1VQk .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NBUNiHNfDjRl1VQk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 评估
评估
评估
评估
用户问题
检索器 Retriever
上下文 Context
生成器 Generator
最终答案
检索指标
Recall / Precision / MRR / NDCG
上下文指标
Context Precision / Recall
生成指标
Faithfulness / Answer Relevancy
端到端指标
正确性 / 语义相似度 / 人工评分
四层指标的关系是:检索指标决定上限,生成指标决定兑现率,端到端指标决定用户体感。
3. 检索层指标
检索层的输入是「问题 + 候选文档列表」,输出是「排序后的 Top-K 文档」。评估需要一份标注了相关文档(golden chunks)的测试集。
3.1 Recall@K(召回率)
Top-K 结果中命中的相关文档数 / 全部相关文档数。它回答的是「该找到的有没有找到」,是 RAG 里最关键的检索指标------召回不到,后面再强的模型也救不回来。
3.2 Precision@K(精确率)
Top-K 结果中相关文档数 / K。它衡量检索结果的「干净程度」。K 越大 Precision 通常越低,所以要和 Recall 一起看。
3.3 MRR(平均倒数排名)
第一个相关文档出现位置的倒数的平均值。适合「只需要一条正确证据」的场景,比如 FAQ 问答。
3.4 NDCG@K(归一化折损累计增益)
考虑相关性分级(如 0/1/2 三档)和位置折损,适合多文档、多相关度的复杂场景。
3.5 Hit Rate(命中率)
Top-K 中至少包含一条相关文档的查询占比。实现简单、解释直观,适合作为上线前的快速冒烟指标。
3.6 检索层指标实现
python
import math
from typing import List, Set
def recall_at_k(retrieved: List[str], relevant: Set[str], k: int) -> float:
if not relevant:
return 0.0
top_k = set(retrieved[:k])
return len(top_k & relevant) / len(relevant)
def precision_at_k(retrieved: List[str], relevant: Set[str], k: int) -> float:
if k == 0:
return 0.0
top_k = retrieved[:k]
return sum(1 for d in top_k if d in relevant) / k
def mrr(retrieved: List[str], relevant: Set[str]) -> float:
for rank, doc_id in enumerate(retrieved, start=1):
if doc_id in relevant:
return 1.0 / rank
return 0.0
def ndcg_at_k(retrieved: List[str], gains: dict, k: int) -> float:
"""gains: {doc_id: relevance_score},相关性分级 0/1/2"""
dcg = 0.0
for rank, doc_id in enumerate(retrieved[:k], start=1):
rel = gains.get(doc_id, 0)
dcg += (2 ** rel - 1) / math.log2(rank + 1)
ideal = sorted(gains.values(), reverse=True)[:k]
idcg = sum((2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(ideal))
return dcg / idcg if idcg > 0 else 0.0
def hit_rate(retrieved: List[str], relevant: Set[str], k: int) -> float:
return 1.0 if set(retrieved[:k]) & relevant else 0.0
批量评估时对测试集求平均即可:
python
def evaluate_retriever(testset, retriever, k=5):
metrics = {"recall": [], "precision": [], "mrr": [], "hit": []}
for item in testset:
retrieved = [d.id for d in retriever.search(item["question"], top_k=k)]
relevant = set(item["relevant_ids"])
metrics["recall"].append(recall_at_k(retrieved, relevant, k))
metrics["precision"].append(precision_at_k(retrieved, relevant, k))
metrics["mrr"].append(mrr(retrieved, relevant))
metrics["hit"].append(hit_rate(retrieved, relevant, k))
return {name: sum(vals) / len(vals) for name, vals in metrics.items()}
4. 上下文层指标
检索到了文档,不代表喂给模型的上下文就是好的。上下文层关注「送进 prompt 的那几段文字」的质量。
4.1 Context Precision(上下文精确率)
上下文中真正与问题相关的片段占比。上下文里塞太多无关内容,会稀释模型注意力,还可能引入干扰。
4.2 Context Recall(上下文召回率)
参考答案中的每个论断,能否在上下文中找到支撑。它衡量「上下文是否覆盖了回答问题所需的信息」。
4.3 Context Relevancy(上下文相关性)
上下文整体与问题的语义相关程度,通常用 LLM 或交叉编码器打分。
4.4 上下文层实现(基于 LLM 判定)
python
from openai import OpenAI
client = OpenAI()
def context_precision(question: str, contexts: List[str]) -> float:
"""逐条判断上下文片段是否与问题相关,返回相关占比"""
relevant = 0
for ctx in contexts:
prompt = (
f"问题:{question}\n片段:{ctx}\n"
"该片段是否包含回答问题所需的信息?只回答 是 或 否。"
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
if "是" in resp.choices[0].message.content:
relevant += 1
return relevant / len(contexts) if contexts else 0.0
提示:LLM 判定成本较高,建议先用小样本(100~200 条)校准 prompt,再批量跑。
5. 生成层指标
生成层是 RAG 评估里最容易被忽视、也最容易出问题的地方。核心是三个问题:有没有编、有没有答偏、有没有答全。
5.1 Faithfulness(忠实度 / 无幻觉率)
答案中的每个论断是否都能在给定上下文中找到依据。这是 RAG 的第一优先级指标------RAG 存在的意义就是减少幻觉,如果忠实度低,整个方案就失去了价值。
实现思路:把答案拆成若干原子论断,逐条判断能否被上下文蕴含。
5.2 Answer Relevancy(答案相关性)
答案是否切题。常见失败模式是「答得很流畅但答非所问」,尤其在多轮对话和模糊问题下高发。
5.3 Answer Correctness(答案正确性)
与标准答案在事实层面的一致性,通常结合语义相似度和事实重叠度计算。
5.4 Answer Completeness(答案完整性)
标准答案中的关键信息点,答案覆盖了多少。与 Correctness 互补:答对了但只答了一半,用户依然不满意。
5.5 生成层指标实现
python
def faithfulness(answer: str, contexts: List[str]) -> float:
"""把答案拆成论断,逐条验证是否被上下文支持"""
claims = split_into_claims(answer) # 可用 LLM 或分句实现
if not claims:
return 1.0
ctx_text = "\n".join(contexts)
supported = 0
for claim in claims:
prompt = (
f"上下文:\n{ctx_text}\n\n论断:{claim}\n"
"该论断是否能由上下文直接支持?只回答 是 或 否。"
)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
if "是" in resp.choices[0].message.content:
supported += 1
return supported / len(claims)
def answer_relevancy(question: str, answer: str) -> float:
"""反向生成:让模型根据答案反推问题,与原问题比较相似度"""
prompt = f"根据下面的答案,反推出最可能的问题:\n{answer}"
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
generated_q = resp.choices[0].message.content
return semantic_similarity(question, generated_q)
semantic_similarity 可以用 sentence-transformers 实现:
python
from sentence_transformers import SentenceTransformer, util
_model = SentenceTransformer("BAAI/bge-small-zh-v1.5")
def semantic_similarity(a: str, b: str) -> float:
emb = _model.encode([a, b], normalize_embeddings=True)
return float(util.cos_sim(emb[0], emb[1]))
6. 端到端指标
端到端指标直接面向用户体验,通常作为最终验收标准。
| 指标 | 含义 | 采集方式 |
|---|---|---|
| 答案正确率 | 人工或模型判定答案是否正确 | 标注集 + LLM 裁判 |
| 语义相似度 | 与参考答案的向量相似度 | 嵌入模型 |
| ROUGE / BLEU | 与参考答案的 n-gram 重叠 | 文本匹配 |
| 用户满意度 | 点赞/点踩、追问率 | 线上埋点 |
| 首字延迟 / 总耗时 | 响应性能 | 链路监控 |
| 拒答率 | 无法回答时的正确拒答比例 | 规则 + 人工抽检 |
其中 ROUGE / BLEU 在 RAG 场景参考价值有限:RAG 答案表述自由度高,n-gram 重叠低不代表答错。建议把它们作为辅助指标,主指标用语义相似度 + LLM 裁判 + 人工抽检。
python
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False)
def rouge_l(prediction: str, reference: str) -> float:
return scorer.score(reference, prediction)["rougeL"].fmeasure
7. 工程与安全指标
技术指标之外,RAG 上线还必须关注这些:
- 延迟:检索耗时、重排耗时、生成首 token 耗时、端到端 P95;
- 成本:单次查询的 token 消耗与 embedding 调用次数;
- 鲁棒性:同义改写、错别字、超长问题下的指标衰减幅度;
- 安全性:越权文档泄露率、Prompt 注入成功率、敏感信息命中率;
- 可追溯性:答案是否附带引用来源,引用是否真实存在。
8. 用 RAGAS 快速搭建评估流水线
手写指标适合理解原理,工程落地推荐直接用 RAGAS,它把上述大部分指标封装好了。
python
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
)
data = {
"question": ["RAG 的核心优势是什么?"],
"answer": ["RAG 通过检索外部知识增强生成,能降低幻觉并支持知识更新。"],
"contexts": [["RAG 结合检索与生成,可减少模型幻觉,并允许在不重训的情况下更新知识。"]],
"ground_truth": ["RAG 能降低幻觉,并支持知识库动态更新。"],
}
dataset = Dataset.from_dict(data)
result = evaluate(
dataset,
metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)
输出会给出每个样本的分数和整体均值,可直接接入 CI,作为每次改动的回归门禁。
9. 指标选型与落地建议
不同阶段的关注点不同,不必一上来就全量铺开:
- 冷启动阶段:先做检索 Recall@5 和生成 Faithfulness,这两个指标能覆盖 80% 的严重问题;
- 调优阶段:加入 Context Precision、Answer Completeness,定位「检索冗余」和「答不全」;
- 上线阶段:补齐延迟、成本、拒答率、引用准确率等工程指标;
- 持续迭代:把评估集固化为回归测试,每次改 chunk 策略、embedding 模型、prompt 都跑一遍。
几个容易踩的坑:
- 测试集太小:少于 100 条时指标波动极大,结论不可信;
- 只用 LLM 裁判:裁判模型本身有偏好,建议与人工抽检交叉验证;
- 忽略分块策略的影响:chunk size 变化会同时影响检索和生成指标,评估时要固定其他变量;
- 只看均值不看分布:P95 延迟和长尾失败案例往往比均值更有信息量。
10. 小结
RAG 评估的本质是分层归因:检索层看召回与排序,上下文层看信噪比,生成层看忠实与切题,端到端层看用户体感,工程层看成本与安全。
一个实用的最小指标集是:
- 检索:Recall@K、MRR
- 上下文:Context Precision
- 生成:Faithfulness、Answer Relevancy
- 端到端:答案正确率、P95 延迟
先把这套跑通、跑稳,再按业务场景逐步扩展,比一开始堆一堆指标却没人看要有效得多。