RAG 系统评估与优化实战
“没有度量就没有改进。RAG 系统的质量取决于你能否准确评估它的质量。”
一、RAG 评估的特殊性
为什么 RAG 评估比普通 NLP 评估更难?
1 2 3 4 5 6 7
| 普通 NLP 评估: 输入 → 模型 → 输出 → 与标准答案比较 ✓
RAG 评估: 输入 → 检索 → 读取 → 生成 → 与标准答案比较 ✗ ↑ ↑ 这里出错 这里也可能出错
|
RAG 系统涉及两个环节:检索质量和生成质量,任何一个出问题都会导致最终答案不对,但原因可能完全不同。
1 2 3 4 5 6 7 8 9 10 11
| 问题现象:"模型回答错误" 可能原因: ├── 检索失败 │ ├── 根本没召回相关文档 → 检索召回率低 │ ├── 召回的噪音太多 → 上下文稀释正确答案 │ └── chunk 切分不合理 → 关键信息被截断 │ └── 生成失败 ├── 模型没理解检索到的内容 → 生成理解力不足 ├── 幻觉:模型自己的知识和检索内容混淆 └── 回答风格不符合用户预期
|
二、RAG 评估框架
2.1 RAGAS(RAG Assessment)
RAGAS(RAG Assessment)是一个专门为 RAG 系统设计的评估框架,核心思想是分解评估维度,用 LLM 作为评估器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| from ragas import evaluate from ragas.metrics import ( faithfulness, answer_relevancy, context_relevancy, context_precision, context_recall, )
eval_dataset = [ { "user_input": "Transformer 是谁发明的?", "retrieved_contexts": [ "Transformer 由 Google 团队在 2017 年提出。", "《Attention Is All You Need》描述了 Transformer 架构。" ], "response": "Transformer 是 Google 团队在 2017 年发明的。", "ground_truth": "Transformer 由 Google 团队在 2017 年提出。" }, ]
result = evaluate( dataset=eval_dataset, metrics=[ faithfulness, answer_relevancy, context_relevancy, context_precision, ] )
print(result)
|
2.2 TruLens
TruLens 是另一个流行的 RAG 评估框架,提供更细粒度的”全过程追踪”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| from trulens.core import Feedback from trulens.providers.openai import OpenAI from trulens.apps.rag import RAG
provider = OpenAI()
context_relevance = Feedback( provider.context_relevance, name="Context Relevance" )
faithfulness = Feedback( provider.feedback_with_cot_reasons, name="Faithfulness" )
answer_relevance = Feedback( provider.answer_relevance, name="Answer Relevance" )
rag = RAG(rag_app, feedbacks=[context_relevance, faithfulness, answer_relevance])
result = rag.evaluate(input="Transformer 是谁发明的?")
|
三、核心评估指标
3.1 检索质量指标
Context Precision(上下文精确度)
衡量检索到的文档中,有多少比例是真正相关的。
1 2 3
| 示例: 检索结果:["文档A(相关)", "文档B(不相关)", "文档C(相关)", "文档D(不相关)"] Context Precision = 2/4 = 50%
|
Context Recall(上下文召回率)
衡量正确答案信息在检索到的上下文中出现了多少。
1 2 3 4 5
| ground_truth: "Transformer 由 Google 在 2017 年提出,作者包括 Ashish Vaswani..."
检索到的上下文:["Transformer 是注意力机制的变体...", "2017 年 NLP 领域取得突破..."]
Context Recall = "Google", "Ashish Vaswani" 这些关键实体被召回的比例 → 可能只有 50%
|
MRR(Mean Reciprocal Rank)
第一个相关文档出现在第几位。
1 2 3 4 5
| Query1: 正确答案在第 1 位 → MRR = 1/1 = 1.0 Query2: 正确答案在第 3 位 → MRR = 1/3 = 0.33 Query3: 正确答案在第 2 位 → MRR = 1/2 = 0.5
Mean MRR = (1.0 + 0.33 + 0.5) / 3 = 0.61
|
NDCG(Normalized Discounted Cumulative Gain)
综合考虑相关性和排名的评估指标,是搜索引擎的标准指标。
3.2 生成质量指标
Faithfulness(忠实度)
衡量回答中的每一个声明是否都能在检索到的上下文中找到依据。
1 2 3 4 5 6 7
|
上下文: "Transformer 由 Google 在 2017 年提出。"
回答A: "Transformer 是 Google 在 2017 年发明的。" ✅ 忠实(每个声明都有依据) 回答B: "Transformer 是 Google 在 2018 年发明的。" ❌ 不忠实(年份错了) 回答C: "Transformer 是 Google 发明的,广泛应用于 NLP。" ✅ 忠实(都有依据)
|
Answer Relevancy(回答相关性)
衡量回答是否真正回答了问题,而不是答非所问。
1 2 3
| 问题: "如何训练大模型?" 回答A: "训练大模型需要海量数据、GPU 集群和分布式训练框架..." → 相关 ✅ 回答B: "大模型在自然语言处理领域应用广泛..." → 答非所问 ❌
|
Hallucination Rate(幻觉率)
衡量模型产生了多少上下文中没有支持的内容。
1 2 3 4 5 6
| 幻觉检测流程: 1. 将回答拆解成独立的声明(claims) 2. 检查每个声明是否能在上下文中找到支持 3. 幻觉率 = 不被支持的声明数 / 总声明数
高幻觉率是 RAG 系统最严重的问题!
|
3.3 端到端指标
EM(Exact Match)
回答和标准答案完全一致的比例。过于严格,通常只用于客观题(如数学计算)。
ROUGE-L
衡量回答和标准答案的字面重叠度。适用于摘要类任务。
1 2
| ROUGE-L = LCS(回答, 标准答案) / max(|回答|, |标准答案|) LCS = 最长公共子串
|
LLM Judge(LLM 评估)
用强大的 LLM(如 GPT-4)来评估回答质量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| def llm_judge_evaluate(question: str, answer: str, ground_truth: str) -> dict: """用 GPT-4 评估 RAG 系统的回答质量""" prompt = f""" 你是一位专业的 RAG 系统评审员。请评估以下回答的质量。
问题: {question} 标准答案: {ground_truth} 待评估回答: {answer}
请从以下几个维度打分(1-10): 1. 准确性:回答是否正确? 2. 完整性:是否完整回答了问题? 3. 相关性:回答是否和问题相关? 4. 简洁性:是否简洁明了,没有冗余?
请给出每个维度的分数和总体评语。 """ response = gpt4.invoke(prompt) return parse_llm_response(response)
|
四、构建评估数据集
4.1 为什么评估数据集很关键?
1 2 3 4 5
| 好的评估数据集特点: ├── 覆盖真实用例(而不是虚构的问题) ├── 包含简单、中等、困难各种难度 ├── 有明确的 ground truth └── 有负例(没有答案的问题,测试系统是否会说"不知道")
|
4.2 人工构建评估集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| eval_samples = [ { "question": "《Attention Is All You Need》是谁写的?", "ground_truth": "Ashish Vaswani 等 Google 团队成员", "context_needed": "需要包含作者信息的段落" }, { "question": "LLaMA 2 和 GPT-3 哪个参数量更大?", "ground_truth": "GPT-3 有 175B 参数,LLaMA 2 有 70B,所以 GPT-3 更大", "context_needed": "需要同时包含两个模型的参数量信息" }, { "question": "Google 在 2015 年发布了大模型吗?", "ground_truth": "无法回答,Google 的 Transformer 模型发布于 2017 年", "context_needed": "需要包含 2015 年无大模型发布的事实" }, ]
|
4.3 用 LLM 自动生成评估集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| def generate_eval_dataset(documents: list[str], llm, num_samples: int = 50) -> list[dict]: """从文档库中自动生成评估问题""" prompt = f""" 你是一个问题生成专家。请根据以下文档,生成 {num_samples} 个测试问题。
要求: 1. 问题类型多样:事实型、推理型、总结型、对比型 2. 每个问题必须能从文档中找到答案 3. 同时生成"无答案问题"(文档中不存在答案) 4. 输出格式:JSON
文档示例: --- {documents[:3]} ---
输出 JSON 格式: [ {{"question": "...", "ground_truth": "...", "has_answer": true}}, {{"question": "...", "ground_truth": "无法回答", "has_answer": false}} ] """ response = llm.invoke(prompt) return json.loads(response)
|
五、RAG 优化实战
5.1 诊断流程
1 2 3 4 5
| 发现回答质量差 → 先判断是检索问题还是生成问题
方法:固定检索结果,只换生成模型 - 如果换了生成模型后质量提升 → 生成问题是主因 - 如果换了生成模型后质量不变 → 检索问题是主因
|
5.2 检索问题诊断与解决
| 问题 |
诊断信号 |
解决方案 |
| 根本没召回 |
top-k 文档中无相关内容 |
扩展检索源、调整 embedding 模型 |
| 召回噪音多 |
相关文档淹没在噪音中 |
增加重排序(ReRanker)、提高相关性阈值 |
| chunk 截断 |
关键信息被切成两半 |
调整 chunk 大小、使用句子窗口检索 |
| 主题漂移 |
召回文档和查询主题不一致 |
使用混合检索(向量+BM25) |
5.3 生成问题诊断与解决
| 问题 |
诊断信号 |
解决方案 |
| 幻觉严重 |
答案中有上下文中没有的信息 |
提示词优化、减少生成长度、添加强制引用 |
| 答非所问 |
回答跑题 |
优化 prompt、few-shot 示例 |
| 过于保守 |
频繁说”我不知道” |
调整置信度阈值、提供更多上下文 |
| 过于冗长 |
回答废话多 |
设置 max_tokens、增加简洁性要求 |
5.4 Prompt 优化实战
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| naive_prompt = """ 根据以下上下文回答问题。 上下文:{context} 问题:{question} 回答: """
optimized_prompt = """ 你是一个严谨的技术助手。请根据检索到的上下文回答问题。
要求: 1. 只使用上下文中的信息回答,不要添加你自己的知识 2. 如果上下文中有相关信息,必须引用(用[1]、[2]标注) 3. 如果上下文中没有答案,明确说"根据提供的信息无法回答"
上下文: {context}
问题:{question}
回答: """
self_check_prompt = """ 根据上下文回答问题后,请自我检查你的回答:
1. 回答中的每个声明是否都有上下文的直接支持? 2. 回答是否直接针对问题? 3. 是否有遗漏的关键信息?
上下文:{context} 问题:{question}
请先给出回答,然后进行自我检查。 """
|
六、持续评估(Continuous Evaluation)
6.1 为什么需要持续评估?
1 2 3 4 5 6 7 8
| 一次性评估 ≠ 持续质量保障
原因: - 数据在更新(新文档加入,旧文档修改) - 模型在迭代(新版本发布) - 用户在发现新 edge case
建议:每周跑一次完整评估,建立质量趋势图
|
6.2 建立评估 Pipeline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| import pandas as pd from datetime import datetime
class RAGEvaluator: def __init__(self, rag_system, eval_dataset): self.rag = rag_system self.dataset = eval_dataset self.history = [] def run_evaluation(self) -> dict: """运行完整评估""" results = [] for sample in self.dataset: answer = self.rag.query(sample["question"]) metrics = self.evaluate_sample(sample, answer) results.append(metrics) summary = { "date": datetime.now(), "avg_faithfulness": np.mean([r["faithfulness"] for r in results]), "avg_answer_relevancy": np.mean([r["answer_relevancy"] for r in results]), "avg_context_precision": np.mean([r["context_precision"] for r in results]), "avg_context_recall": np.mean([r["context_recall"] for r in results]), } self.history.append(summary) return summary def get_trend(self) -> pd.DataFrame: """查看质量趋势""" return pd.DataFrame(self.history) def detect_regression(self, threshold: float = 0.05): """检测质量是否下降""" if len(self.history) < 2: return current = self.history[-1] previous = self.history[-2] for metric in ["faithfulness", "answer_relevancy"]: diff = current[f"avg_{metric}"] - previous[f"avg_{metric}"] if diff < -threshold: print(f"⚠️ {metric} 下降 {abs(diff):.2%}!需要调查原因。")
|
6.3 A/B 测试
在生产环境中同时运行两个版本的 RAG,逐步放量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| class ABTestRAG: def __init__(self, rag_a, rag_b): self.rag_a = rag_a self.rag_b = rag_b self.results_a = [] self.results_b = [] def route(self, query: str) -> str: """随机分流(90% A,10% B)""" if random.random() < 0.1: result = self.rag_b.query(query) self.results_b.append(result) return "B", result else: result = self.rag_a.query(query) self.results_a.append(result) return "A", result def analyze(self): """分析 A/B 结果,决定是否全量上线""" score_a = np.mean([r["score"] for r in self.results_a]) score_b = np.mean([r["score"] for r in self.results_b]) if score_b > score_a and self.statistical_significance(len(self.results_a), len(self.results_b), score_a, score_b): print(f"✅ B 版本更优({score_b:.3f} vs {score_a:.3f}),建议全量上线") else: print(f"⚠️ 暂无明显差异,继续观察")
|
七、常见 RAG 评估问题
7.1 为什么 Context Recall 很难达到 100%?
1 2 3 4 5 6
| 原因: 1. 正确答案可能分散在多个文档中 2. 需要的不是单个事实,而是一段解释/推理过程 3. Ground truth 本身可能不完整
解决:承认 Context Recall 的局限性,关注 Answer Faithfulness
|
7.2 主观性如何量化?
用户说”回答好”vs”回答不好”是很主观的。
1 2 3 4
| 解决思路: 1. 让多个标注员独立打分,取平均 2. 用 LLM Judge 作为"第二意见" 3. 区分维度:准确性(客观)vs 可读性(主观)
|
八、总结
RAG 评估的核心要点:
- 分解维度:检索质量(Precision/Recall/MRR)和生成质量(Faithfulness/Relevancy)分开评估
- 使用框架:RAGAS 和 TruLens 提供了开箱即用的评估工具
- 构建好评估集:覆盖真实用例,包含有/无答案的各种情况
- 诊断先行:先确定是检索问题还是生成问题,再针对性优化
- 持续评估:建立评估 pipeline,跟踪质量趋势,及时发现回归
相关文章: