Day 27 / 共 68 天 · 阶段 4 RAG

RAG 评测:给系统装一把尺子

前两天(Day 25/26)你学了检索和精排,还反复听我说"要用数据证明变好了"。今天就把那把"尺子"造出来:RAG 评测分两层——检索层(hit rate / MRR / NDCG / precision@k,量"该捞的段捞没捞到、排没排前")和生成层(faithfulness 有没有瞎编、relevancy 答得切不切题)。有了尺子,你后面每一次调参才不是碰运气;明天(Day 28)就把 D21-27 全部串成一个能跑的 RAG 小项目。

📍 你在阶段 4(RAG D21-28)的位置
D25 检索 D26 rerank&混合 D27 评测 D28 实战
💡 用一个类比兜住今天(今天全程沿用「考试与阅卷」的世界观) 评测 RAG = 给系统安排一场考试并阅卷测试集(问题+标准答案) = 出好的试卷和参考答案;检索层指标 = "找资料"这门课的分(该翻到的资料页翻到没、排在前几);生成层指标 = "写作文"这门课的分(答得对不对、有没有胡编);faithfulness 忠实度 = 作文有没有"照着资料写"而不是瞎编;relevancy 相关度 = 有没有跑题。没有考试和分数,你根本不知道昨天改的参数是让系统变强了还是变弱了——今天你从"凭感觉调"升级成"看分数调"。
L01

为什么评测是 RAG 的命门

🤔 痛点你把 chunk 改小了、又加了 rerank,感觉"好像变好了"?但只试了两三个问题、全靠肉眼。换一批问题呢?到底是真变好还是碰巧?
💡 本质没有评测 = 盲改。你以为在优化,其实可能在退步。评测就是"考试+打分":用一套固定试题反复考,每次改动后看分数升还是降。像医生不靠"感觉你气色好点了",而是量体温、验血——拿数字说话
这也是面试高频考点:面试官几乎必问"你怎么知道你的 RAG 好用?"。能答出"我有一套测试集 + 检索层和生成层指标,每次改动跑一遍看回归",立刻甩开一大片只会搭 demo 的人。
L02

两层指标:先看检索,再看生成

🤔 痛点RAG 答错了,到底是"资料没找对"还是"资料找对了但模型答歪了"?一个总分说不清病因。
💡 本质RAG 是"先检索、后生成"两步,所以评测也分两层:①检索层——该用的段落有没有被捞到、排得够不够前;②生成层——拿到段落后,答案答得对不对、有没有编。分层才能精准定位病因:检索层低分→去改切分/检索;生成层低分→去改提示词/模型。
分层评测:两门课分开打分 ①检索层(找资料) hit rate · precision@k MRR · NDCG ②生成层(写答案) faithfulness 忠实度 answer relevancy 相关度
图注:先确认资料找对了(左),再看答案写好了没(右)。答错时能一眼定位是哪门课拖了后腿。
L03

检索层①:hit rate 与 precision@k

🤔 痛点怎么用数字表达"该捞的段捞到了没"?
💡 本质先给每个测试问题标好"正确段落是哪几段"(标准答案)。然后:
hit rate(命中率):top-k 里只要有一个正确段,这题就算命中。统计"多少题命中"。像考试问"翻到的 5 页资料里,起码有一页是对的吗?"。
precision@k(前 k 精确率):top-k 里有几个是正确的 / k。像"翻的 5 页里,有几页真有用?"——更严格。
# 已知每题的"标准正确段落 id",和系统检索返回的 top-k id
def hit_rate(retrieved, relevant):
    # top-k 里只要碰到一个正确的,就算命中(1),否则 0
    return 1 if any(r in relevant for r in retrieved) else 0

def precision_at_k(retrieved, relevant):
    # 命中数 / 取回总数
    hit = sum(1 for r in retrieved if r in relevant)
    return hit / len(retrieved)

retrieved = ["d3", "d1", "d9"]   # 系统捞回的 top-3
relevant  = {"d1", "d5"}         # 标准答案:正确段是 d1、d5
print(hit_rate(retrieved, relevant))       # → 1  (里面有 d1,命中)
print(precision_at_k(retrieved, relevant)) # → 0.33(3 个里只有 1 个对)
👶 hit rate 高就够了吗不一定。hit rate 只问"有没有蒙对一个",很宽松;precision 问"有用的占比",更严。还有一个没细讲的 recall(召回率)——"所有正确段里,你捞回了几成"。三者一起看才全面。刚入门先盯 hit rate 和 precision@k 即可。
L04

检索层②:MRR 与 NDCG(在意排序)

🤔 痛点hit rate 只管"有没有捞到",不管"排第几"。可正确段排第 1 和排第 5,对模型答题的帮助天差地别!
💡 本质两个"在意名次"的指标:
MRR(平均倒数排名):看第一个正确段排在第几名,取它的倒数(排第 1 得 1、第 2 得 0.5、第 3 得 0.33…)再对所有题求平均。排得越靠前分越高。像考试"你最先翻到对的资料是第几页"。
NDCG:更全面,不仅看名次还看"每段有多相关"(可以分很相关/有点相关),越相关的排越前得分越高。
def reciprocal_rank(retrieved, relevant):
    # 找第一个正确段的名次(从1数),返回其倒数;一个都没有则 0
    for i, r in enumerate(retrieved, start=1):
        if r in relevant:
            return 1 / i
    return 0

print(reciprocal_rank(["d1", "d3"], {"d1"}))   # → 1.0  正确段排第1,满分
print(reciprocal_rank(["d3", "d1"], {"d1"}))   # → 0.5  正确段排第2
print(reciprocal_rank(["d3", "d9"], {"d1"}))   # → 0    没捞到

# 对整个测试集:把每题的 rr 求平均,就是 MRR
rrs = [1.0, 0.5, 0.0, 1.0]
print("MRR =", sum(rrs) / len(rrs))            # → 0.625
📝 举个例子:同样 hit=1,MRR 见真章 系统 A 把正确段排第 1(rr=1.0),系统 B 排第 5(rr=0.2)——两者 hit rate 都是 1,看不出差别;但 MRR 一比,A(1.0) 明显强于 B(0.2)。所以上了 rerank 之后,最该看的就是 MRR/NDCG 有没有涨——它俩正是衡量"精排效果"的尺子。
L05

生成层:忠实度与相关度

🤔 痛点资料找对了(检索层满分),模型却可能:①无视资料自己瞎编;②答得跑题;③一堆废话不回答重点。这些检索指标一个都测不出来。
💡 本质生成层看两件事:
faithfulness(忠实度):答案里每句话,是不是都能在检索到的资料里找到依据?找不到依据的就是"幻觉/瞎编"。像判作文"有没有照着参考资料写,而不是自己编"。
answer relevancy(答案相关度):答案切不切用户的题?有没有答非所问、绕圈子。像判作文"跑题没"。

👶 小白:这两个"答得对不对"没有标准答案,怎么打分?总不能人工一条条读吧?

👨‍🏫 老师:好问题!业界主流做法是 LLM-as-Judge——让另一个大模型当"阅卷老师":把"问题 + 检索到的资料 + 生成的答案"发给它,让它判断"答案的每句话在资料里有没有依据(忠实度)""答案切不切题(相关度)",打个分。这正是第 9 阶段(Day 50)的重头戏,今天你先知道"生成层可以自动评"就行。当然,关键场景仍要抽一部分人工复核,机器判官也会犯错。

📝 举个例子:检索满分也可能生成翻车 问:年假几天? 检索层完美命中了「第4.2条:满1年享5天年假」(hit/MRR 都满分)。但生成的答案却说成 10 天——资料里根本没这数,这就是忠实度不合格(瞎编);又或者答成"年假是一种法定福利……"绕了半天不说天数,这是相关度不合格(跑题)。可见检索和生成必须分开量:光看检索层,你永远发现不了这两种错。
👶 一句话记住四个词检索层看 MRR/precision(资料找得准不准),生成层看 faithfulness/relevancy(答案编没编、跑没跑题)。面试被问"RAG 怎么评测",把这两层四个词说清楚,就是漂亮的回答。
L06

现成工具 & 深潜入口

🤔 痛点这些指标都要自己手写吗?那也太累了。
💡 本质不用。成熟的 RAG 框架和评测库都内置了这些指标:你准备好"问题+标准答案"的测试集,调一个 evaluate() 就能一次性算出 hit rate、MRR、faithfulness 等一整排分数。手写(如本页 L03/L04)是为了让你看懂指标含义,真上项目直接用现成的。
生态里常见的 RAG 评测方案有 RAGASTruLens,以及 LlamaIndex / LangChain 各自内置的评测模块——名字先混个脸熟,用到哪个查哪个的文档即可,别死记。

其中 LlamaIndex 是最主流的 RAG 框架之一,它把"切分→嵌入→检索→评测"打包得很顺手,评测模块直接给你 hit rate/MRR 和忠实度/相关度。想把今天的概念落到能跑的代码、系统吃透 RAG 全链路,去看这套深潜教程:

🔗 想深入?去看《LlamaIndex 20 天精讲》→
学完记得回来继续 Day 28——明天我们要用今天造的这把"尺子",给一个真实的 PDF 问答项目验收打分。深潜是加餐,主线别断。
L07

今日小结 + 动手 10 分钟

🧠 今天你应该能回答

  • 为什么"没评测=盲改"?面试被问"你怎么知道 RAG 好用"该怎么答?
  • RAG 评测为什么要分检索层和生成层?分层的好处是什么?
  • hit rate 和 precision@k 分别衡量什么?哪个更严格?
  • MRR/NDCG 比 hit rate 多管了什么?为什么上 rerank 后要盯它俩?
  • faithfulness 和 answer relevancy 各查什么?一般用什么方式自动打分?

✋ 动手 10 分钟:给自己的检索器打个分

不用装任何库。把 L03/L04 的函数拼起来,给 Day 25 的迷你检索器造 3 道题的"标准答案",算出 hit rate 和 MRR:

# 3 道测试题:每题(系统返回的top-k, 标准正确段集合)
cases = [
    (["d1", "d3", "d9"], {"d1"}),      # 正确段排第1
    (["d5", "d2", "d1"], {"d1"}),      # 正确段排第3
    (["d7", "d8", "d9"], {"d1"}),      # 没捞到
]
def rr(ret, rel):
    for i, r in enumerate(ret, 1):
        if r in rel: return 1 / i
    return 0
def hit(ret, rel):
    return 1 if any(r in rel for r in ret) else 0

hr  = sum(hit(r, g) for r, g in cases) / len(cases)
mrr = sum(rr(r, g)  for r, g in cases) / len(cases)
print(f"hit rate = {hr:.2f}")   # → 0.67(3题命中2题)
print(f"MRR      = {mrr:.2f}")  # → 0.44
# 试试:把第2题的 d1 挪到第1位,再看 MRR 涨了没——这就是"优化后看分数"
明日预告 · Day 28:概念齐活!明天是阶段 4 的收官实战——用 llama-index/langchain 给一个 PDF 文件夹做端到端问答:摄入→切分→嵌入→建库→检索→生成,再用今天的指标验收。把 D21-27 学的全部串成一个能放进简历的小项目。
← Day 26 · rerank & 混合检索 Day 28 · RAG 实战小项目 →