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, # 检索是否包含了正确答案(需要 ground truth)
)

# 准备评估数据
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(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 评估的核心要点:

  1. 分解维度:检索质量(Precision/Recall/MRR)和生成质量(Faithfulness/Relevancy)分开评估
  2. 使用框架:RAGAS 和 TruLens 提供了开箱即用的评估工具
  3. 构建好评估集:覆盖真实用例,包含有/无答案的各种情况
  4. 诊断先行:先确定是检索问题还是生成问题,再针对性优化
  5. 持续评估:建立评估 pipeline,跟踪质量趋势,及时发现回归

相关文章: