本节导读:RAG 系统的检索质量如何量化?本节讲解检索评估的核心指标(Recall、Precision、MRR、NDCG)、构建评估数据集的方法、以及如何用 RAGAS 框架自动化端到端评估,让你对系统的检索质量做到心中有数。
检索评估要解决的核心问题是:你怎么知道你的检索系统变好了? 直觉判断("感觉更准了")不可靠,需要量化指标。检索评估分为两个层面:
1. 召回率(Recall@K):在前 K 个结果中,相关文档占所有相关文档的比例。
Recall 衡量的是"有没有漏掉"。RAG 系统中 Recall 至关重要——如果正确的文档根本没被召回,后续的重排和生成都无济于事。
2. 精确率(Precision@K):在前 K 个结果中,相关文档所占的比例。
Precision 衡量的是"有没有混入噪声"。当送入 LLM 的上下文中包含不相关文档时,LLM 可能被误导产生错误回答。
3. 平均倒数排名(MRR):第一个相关文档的排名的倒数的均值。
MRR 关注的是"最相关的文档排在第几位"。在 RAG 中,如果 Top-1 就是最相关的,生成质量通常最高。
4. 归一化折损累积增益(NDCG@K):考虑了相关性等级的排序质量指标。不同于前面三个指标只区分"相关/不相关",NDCG 可以处理"高度相关/部分相关/不相关"的多级标注。
其中 DCG(折损累积增益)给予高相关性文档更高权重,且排名越靠前权重越大。NDCG 是目前检索评估中公认最全面的单一指标。在实际项目中,我建议以 NDCG@5 作为主要优化目标,同时监控 Recall@10 防止漏召回。这两个指标基本覆盖了"排得对"和"找得全"两个核心诉求。
# 评估框架 # pip install ragas>=0.2.0 # RAG 端到端评估 # pip install ranx>=0.3.0 # 检索评估工具库 numpy>=1.24.0
前置知识:本教程 4.1 节(相似度计算)、基本的统计学概念(精确率、召回率)。
评估数据集是检索评估的基石——没有高质量的评估数据,所有指标都毫无意义。你需要准备三类核心信息:
import json from typing import Dict, List, Set # 评估数据集结构 eval_dataset = { "queries": { "q1": "RAG 系统中如何选择向量数据库", "q2": "什么是文档分割策略", "q3": "如何减少大语言模型的幻觉", "q4": "FAISS 的 HNSW 索引原理是什么", "q5": "提示工程在 RAG 中有什么用", }, # 每个查询对应的相关文档 ID(人工标注) "relevant_docs": { "q1": {"doc_101": 2, "doc_205": 1}, # 键为文档ID,值为相关等级 "q2": {"doc_302": 2, "doc_303": 1}, "q3": {"doc_401": 2, "doc_402": 1, "doc_403": 1}, "q4": {"doc_205": 2, "doc_206": 1}, "q5": {"doc_501": 2, "doc_502": 1}, }, } # 保存为 JSON with open("eval_dataset.json", "w", encoding="utf-8") as f: json.dump(eval_dataset, f, ensure_ascii=False, indent=2) print(f"评估数据集:{len(eval_dataset['queries'])} 个查询")
标注质量的建议:
import numpy as np from typing import Dict, List def recall_at_k( retrieved: List[int], relevant: Set[int], k: int ) -> float: """Recall@K:Top-K 中召回了多少相关文档。""" top_k = set(retrieved[:k]) if not relevant: return 0.0 return len(top_k & relevant) / len(relevant) def precision_at_k( retrieved: List[int], relevant: Set[int], k: int ) -> float: """Precision@K:Top-K 中相关文档的占比。""" top_k = set(retrieved[:k]) return len(top_k & relevant) / k def mrr(retrieved: List[int], relevant: Set[int]) -> float: """MRR:第一个相关文档排名的倒数。""" for rank, doc_id in enumerate(retrieved, 1): if doc_id in relevant: return 1.0 / rank return 0.0 def ndcg_at_k( retrieved: List[int], relevant_grades: Dict[int, int], k: int, ) -> float: """NDCG@K:考虑相关等级的归一化折损累积增益。""" def dcg(relevances: List[int]) -> float: return sum( (2**rel - 1) / np.log2(i + 2) for i, rel in enumerate(relevances) ) # 实际 DCG actual_rels = [ relevant_grades.get(doc_id, 0) for doc_id in retrieved[:k] ] actual_dcg = dcg(actual_rels) # 理想 DCG ideal_rels = sorted( relevant_grades.values(), reverse=True )[:k] ideal_dcg = dcg(ideal_rels) if ideal_dcg == 0: return 0.0 return actual_dcg / ideal_dcg # 批量评估示例 def evaluate_retrieval( retriever, eval_dataset: dict, k: int = 5 ) -> Dict[str, float]: """对检索系统进行全面评估。""" recall_scores, precision_scores, mrr_scores, ndcg_scores = [], [], [], [] for qid, query in eval_dataset["queries"].items(): # 执行检索 results = retriever.search(query, top_k=k) retrieved_ids = [r[0] for r in results] # 获取标注 relevant = set(eval_dataset["relevant_docs"][qid].keys()) relevant_grades = eval_dataset["relevant_docs"][qid] # 计算指标 recall_scores.append(recall_at_k(retrieved_ids, relevant, k)) precision_scores.append(precision_at_k(retrieved_ids, relevant, k)) mrr_scores.append(mrr(retrieved_ids, relevant)) ndcg_scores.append(ndcg_at_k(retrieved_ids, relevant_grades, k)) return { f"Recall@{k}": np.mean(recall_scores), f"Precision@{k}": np.mean(precision_scores), "MRR": np.mean(mrr_scores), f"NDCG@{k}": np.mean(ndcg_scores), } # 运行评估 metrics = evaluate_retrieval(my_retriever, eval_dataset, k=5) print("\n检索评估结果:") for name, value in metrics.items(): print(f" {name}: {value:.4f}")
RAGAS 是目前最流行的 RAG 评估框架,它不仅评估检索质量,还评估生成质量。
from ragas import evaluate from ragas.metrics import ( context_precision, context_recall, faithfulness, answer_relevancy, answer_correctness, ) from datasets import Dataset # 准备评估数据 eval_data = { "question": [], "answer": [], "contexts": [], "ground_truth": [], } # 对每个测试查询运行完整 RAG Pipeline for qid, query in eval_dataset["queries"].items(): # 1. 检索 results = rag_pipeline.retrieve(query, top_k=3) contexts = [r[2] for r in results] # 检索到的文档文本 # 2. 生成 answer = rag_pipeline.generate(query, contexts) # 3. 收集 eval_data["question"].append(query) eval_data["answer"].append(answer) eval_data["contexts"].append(contexts) eval_data["ground_truth"].append(ground_truths[qid]) # 运行 RAGAS 评估 dataset = Dataset.from_dict(eval_data) result = evaluate( dataset, metrics=[ context_precision, # 检索到的上下文中有多少与问题相关 context_recall, # 所需信息被检索到的比例 faithfulness, # 回答是否基于上下文(不幻觉) answer_relevancy, # 回答是否切题 answer_correctness, # 回答是否正确 ], ) print("\nRAGAS 端到端评估结果:") for metric_name, value in result.items(): print(f" {metric_name}: {value:.4f}")
RAGAS 各指标的含义与目标值:
| 指标 | 含义 | 及格线 | 优秀线 |
|---|---|---|---|
| context_precision | 检索精度 | >0.6 | >0.8 |
| context_recall | 检索召回 | >0.7 | >0.85 |
| faithfulness | 忠实度(不幻觉) | >0.8 | >0.9 |
| answer_relevancy | 回答相关性 | >0.7 | >0.85 |
| answer_correctness | 回答正确性 | >0.6 | >0.8 |
评估的目的不是"打个分",而是指导下一步优化方向。以下是一个实用的诊断框架:
诊断规则:
一个真实的诊断案例:在某企业内部知识库项目中,初始评估结果为 Recall@5=0.45, Precision@5=0.52, NDCG@5=0.38。按照诊断框架,Recall 和 Precision 都低说明嵌入模型与文档领域不匹配——该企业的文档以法律合同为主,通用嵌入模型无法很好理解法律术语的语义关系。切换到经过法律领域微调的 BGE-Legal 模型后,Recall@5 提升到 0.72, Precision@5 提升到 0.65。再引入 BGE-Reranker 重排后,NDCG@5 从 0.48 提升到 0.71。整个过程通过评估指标驱动,每一步改动都有数据支撑,而不是盲目尝试。
A:取决于你要做什么决策。如果只是快速验证"重排有没有用",20-30 个查询就能看出趋势。如果要精确比较两个检索策略的差异是否统计显著(p<0.05),通常需要 100+ 个查询。建议先从 30 个开始,有需要再扩展。标注 30 个查询的成本大约 2-4 小时(含文档阅读和相关性判断)。
A:是的,RAGAS 的 faithfulness、answer_relevancy 等指标需要 LLM 来判断。默认使用 OpenAI GPT-4,每个查询大约消耗 500-2000 tokens。30 个查询的评估成本约 0.5-2 美元。可以通过设置 RAGAS 的 LLM 参数切换到更便宜的模型(如 GPT-4o-mini)来降低成本,但评估准确性也会略有下降。
A:离线评估用标注数据集在本地跑,适合开发阶段快速迭代。在线评估用真实用户反馈(点击率、满意度评分、答案采纳率)来评估,适合上线后的持续监控。两者的指标不一定对齐——离线 NDCG 高的检索策略,在线满意度不一定最高(因为还涉及生成质量和用户体验)。建议离线评估作为主要优化工具,在线评估作为最终验证。
A:使用配对 t 检验或 Wilcoxon 符号秩检验。对同一组查询分别跑策略 A 和策略 B,得到两组指标分数,然后用 scipy.stats.ttest_rel 或 scipy.stats.wilcoxon 检验差异是否显著。p 值 <0.05 说明差异大概率不是随机的。样本量小于 30 时用 Wilcoxon 更稳妥。
一次性的评估只能告诉你"现在的状态",持续评估才能帮你"追踪趋势"。以下是一个可以集成到 CI/CD 中的自动化评估流水线:
import json from datetime import datetime from pathlib import Path class EvalTracker: """ 评估结果追踪器,记录每次评估的历史数据。 支持趋势分析和回归检测。 """ def __init__(self, log_dir: str = "eval_logs"): self.log_dir = Path(log_dir) self.log_dir.mkdir(exist_ok=True) def log(self, metrics: dict, config: dict = None, note: str = ""): """记录一次评估结果。""" record = { "timestamp": datetime.now().isoformat(), "metrics": metrics, "config": config or {}, "note": note, } filename = f"eval_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json" filepath = self.log_dir / filename with open(filepath, 'w') as f: json.dump(record, f, indent=2, ensure_ascii=False) return filepath def check_regression( self, metrics: dict, baseline: dict, threshold: float = 0.05 ) -> list: """检查是否有指标回归(下降超过阈值)。""" regressions = [] for key in baseline: if key in metrics: change = metrics[key] - baseline[key] if change < -threshold: regressions.append({ "metric": key, "baseline": baseline[key], "current": metrics[key], "change": change, }) return regressions # 使用示例 tracker = EvalTracker() current_metrics = { "Recall@5": 0.72, "Precision@5": 0.68, "MRR": 0.81, "NDCG@5": 0.74, } tracker.log(current_metrics, note="Day 9: 扩写后重新评估") # 检查是否回归 baseline = {"Recall@5": 0.70, "NDCG@5": 0.72} regressions = tracker.check_regression(current_metrics, baseline) if regressions: for r in regressions: print(f" {r['metric']}: {r['baseline']:.3f} -> {r['current']:.3f}")
这个追踪器做了三件有价值的事:
建议将这个评估流水线集成到每次模型更新、文档分割策略调整或嵌入模型切换之后自动运行,形成"改了就测、测了就记"的习惯。这在多人协作的 RAG 项目中尤其重要——它能防止某个人优化了检索但无意间降低了生成质量的问题。
本节系统讲解了 RAG 系统检索结果评估的完整方法论。从 Recall、Precision、MRR、NDCG 四大核心指标的定义与实现,到使用 RAGAS 框架进行端到端评估,再到用评估结果驱动迭代优化的诊断框架,形成了一个"评估-诊断-优化-再评估"的闭环。
核心认知:没有度量就没有优化。很多 RAG 项目失败的原因不是技术不行,而是无法量化"改了之后到底好了多少"。建立一套稳定的评估体系是 RAG 工程化实践的第一步。至此,本教程第 4 章的五大主题——相似度计算、检索策略、重排机制、混合检索和检索评估——全部讲解完毕。下一章第 5 章将进入 LLM 集成与优化,讲解如何将检索结果转化为高质量的最终回答。
关键词:RAG 知识库实战, 检索评估, Recall, Precision, NDCG, MRR, RAGAS, RAG 评估框架, 检索优化
难度:进阶
预计阅读:18 分钟