RAG 能否可靠工作,不能只靠“看起来回答得不错”来判断。一个回答错误,可能是检索阶段没有找到正确片段,也可能是证据已经找到却被模型误读,或者问题本身就不在知识库中。本篇只聚焦一个核心知识点:如何识别 RAG 的常见故障,并用一组小测试问题建立最基础的评估方法。示例不依赖网络、密钥或第三方包,只评估可独立验证的检索结果。

先把 RAG 问题分层

一个 RAG 请求至少包含“问题—检索—上下文—回答”四个环节。排查时不要一上来就修改提示词,而要先确认正确资料是否进入了候选集。

召回不足是指知识库中有答案,但检索结果没有包含对应片段。常见原因包括切分过大或过小、查询和原文用词差异明显、top_k 太小,以及过滤条件过严。这个问题属于检索层,应该通过提高召回数量、改进切分或调整表示来处理。

上下文噪声是指正确片段找到了,但同时塞入了许多相似、重复或无关内容。噪声会增加 token 消耗,也会让生成模型更难判断证据重点。增大 top_k 并不总是更好,应该观察前几名结果的精确度,并考虑重排、去重和上下文长度限制。

无依据回答是指提供的证据不足以支持答案,模型却给出了确定的结论。知识库问答应明确区分“证据支持”和“无法从资料确认”。如果问题超出知识库范围,拒答或说明未知,往往比猜测更可靠。因此评估不能只看检索指标,也要抽查回答是否忠于证据。

用小测试集描述预期证据

基础评估不需要一开始就准备很大的数据集。每条测试样例至少包含一个问题和一个或多个正确来源编号。例如:

1
2
3
4
5
cases = [
{"question": "如何重置密码?", "relevant": {"account.md"}},
{"question": "退款多久到账?", "relevant": {"billing.md"}},
{"question": "支持哪些语言?", "relevant": {"product.md"}},
]

relevant 表示人工确认的正确证据,不是模型生成的答案。它可以先只记录“哪篇文档包含答案”,以后再细化到片段编号。测试集要覆盖正常问题、同义表达和知识库没有答案的问题;否则评估分数可能很好看,却无法反映真实使用场景。测试集还应固定保存,不能每次为了提高分数而临时修改标准。

一个可运行的检索评估器

下面用词项重叠模拟检索器,重点不在算法效果,而在评估流程。把代码保存为 evaluate_retrieval.py 后运行即可。真实项目中,只需要替换 retrieve 函数,测试集和指标计算仍然适用。

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
47
48
49
50
51
52
from re import findall

DOCUMENTS = {
"account.md": "忘记密码时,打开账户设置,选择重置密码,并通过注册邮箱确认。",
"billing.md": "退款申请审核通过后,通常会在五个工作日内原路到账。",
"product.md": "当前产品支持中文、英文和日文界面。",
"shipping.md": "标准配送通常需要三到五个工作日。",
}

CASES = [
{"question": "怎样修改忘记的密码?", "relevant": {"account.md"}},
{"question": "退款什么时候能收到?", "relevant": {"billing.md"}},
{"question": "可以使用哪些语言?", "relevant": {"product.md"}},
]


def words(text):
return set(findall(r"[\u4e00-\u9fff]|[a-zA-Z]+", text.lower()))


def retrieve(question, k=2):
query = words(question)
scored = []
for source, text in DOCUMENTS.items():
score = len(query & words(text))
scored.append((score, source))
scored.sort(key=lambda item: (-item[0], item[1]))
return [source for score, source in scored[:k] if score > 0]


def evaluate(cases, k=2):
hits = 0
reciprocal_ranks = []
for case in cases:
results = retrieve(case["question"], k=k)
relevant = case["relevant"]
rank = next(
(index for index, source in enumerate(results, start=1)
if source in relevant),
None,
)
hits += rank is not None
reciprocal_ranks.append(1 / rank if rank else 0)
print(case["question"], "=>", results, "rank=", rank)

total = len(cases)
print(f"Recall@{k}: {hits / total:.2f}")
print(f"MRR@{k}: {sum(reciprocal_ranks) / total:.2f}")


if __name__ == "__main__":
evaluate(CASES, k=2)

代码中的 Recall@k 表示:正确来源至少出现在前 k 个结果中的测试样例比例。它回答“答案有没有机会进入上下文”。MRR@k 是平均倒数排名,正确来源越靠前,得分越高;如果第一条结果就是正确来源,该样例贡献 1,排在第二条则贡献 1/2

这里使用的是非常粗糙的字符和英文词项匹配,中文分词也没有真正完成,所以它不是生产级检索器。它的价值在于让“评估一个检索器”变成可重复执行的程序:每次改变切分方式、查询改写或 k,都能用同一批案例比较变化。运行输出中的每一行还会显示问题、结果列表和正确来源排名,便于定位具体失败样例,而不只是盯着一个总分。

指标低或高时分别检查什么

如果 Recall@k 很低,先逐条查看失败问题。确认正确文档是否真的被加入知识库,来源编号是否写对,过滤条件是否误删了文档;然后再检查切分边界和查询表达。若提高 k 后召回上升,说明候选集太窄,但也要同步关注上下文噪声和成本。

如果召回不错而回答仍然错误,问题可能发生在上下文组装或生成阶段。记录实际发送给模型的片段,检查是否被截断、顺序是否混乱、来源标记是否丢失,并要求回答只能依据给定证据。对“资料中没有答案”的测试问题,验证程序是否会明确说无法确认,而不是凭常识补全。

离线指标也有边界。人工标注的相关来源可能遗漏,词项检索的分数不能代表语义检索质量,单一测试集更不能证明生产环境可靠。较稳妥的做法是同时保留检索日志、固定测试集和少量人工回答评审,并在修改后比较新旧版本,而不是只追求某个指标变大。

常见问题

为什么不能只评估最终答案? 因为最终答案错了时,单看文字很难判断责任在检索还是生成。记录检索结果和来源排名,才能把故障定位到具体环节。

Recall@k 越高越好吗? 不一定。k 增大通常有助于覆盖正确证据,但会引入噪声、增加上下文长度和费用。应结合排名质量、延迟和回答评审选择合适的 k

没有真实用户数据怎么办? 先用人工编写的最小测试集,覆盖高频问题、同义问法、边界问题和无答案问题。上线后再脱敏采样真实问题,补充到回归测试集中。

指标下降就一定是代码坏了? 不一定。知识库内容、标注标准或测试问题变化也会影响分数。每次评估都应记录数据版本、检索参数和变更内容,保证比较是同口径的。

小结

RAG 的可靠性需要分层验证:先用测试集确认正确证据能否被召回,再检查候选噪声、上下文组装和回答是否有依据。Recall@k 适合观察覆盖率,MRR@k 适合观察正确结果的位置;它们不能替代人工评审,但能把模糊的“感觉变差了”变成可重复的诊断信号。下一步可以在保持测试集不变的前提下,比较不同切分策略、检索参数和重排方法。