RAG 评估入门:Recall@k、MRR、nDCG、Faithfulness

RAG 评估入门:Recall@k、MRR、nDCG、Faithfulness

  • [RAG 评估入门:Recall@k、MRR、nDCG、Faithfulness 到底怎么用?](#RAG 评估入门:Recall@k、MRR、nDCG、Faithfulness 到底怎么用?)
    • [0. 为什么 RAG 一定要评估?](#0. 为什么 RAG 一定要评估?)
    • [1. RAG 评估要评什么?](#1. RAG 评估要评什么?)
    • [2. 评测集怎么做?](#2. 评测集怎么做?)
      • [2.1 一个评测样本应该包含什么?](#2.1 一个评测样本应该包含什么?)
      • [2.2 gold_evidence 怎么标?](#2.2 gold_evidence 怎么标?)
    • [3. 检索侧指标:Recall@k、MRR、nDCG](#3. 检索侧指标:Recall@k、MRR、nDCG)
      • [3.1 Recall@k:Top-k 里有没有"正确证据"](#3.1 Recall@k:Top-k 里有没有“正确证据”)
      • [3.2 MRR(Mean Reciprocal Rank):第一个正确结果排第几](#3.2 MRR(Mean Reciprocal Rank):第一个正确结果排第几)
      • [3.3 nDCG:考虑"相关程度"和"排序质量"](#3.3 nDCG:考虑“相关程度”和“排序质量”)
        • [3.3.1 DCG@k(核心思想)](#3.3.1 DCG@k(核心思想))
        • [3.3.2 nDCG@k(归一化)](#3.3.2 nDCG@k(归一化))
    • [4. 生成侧指标:Correctness vs Faithfulness](#4. 生成侧指标:Correctness vs Faithfulness)
      • [4.1 Correctness(正确性):答案对不对?](#4.1 Correctness(正确性):答案对不对?)
      • [4.2 Faithfulness(依据一致性):答案是否"严格基于检索证据"?](#4.2 Faithfulness(依据一致性):答案是否“严格基于检索证据”?)
    • [5. Faithfulness 怎么评?(三种常用做法)](#5. Faithfulness 怎么评?(三种常用做法))
      • [5.1 引用覆盖率](#5.1 引用覆盖率)
      • [5.2 句子级支持度打分](#5.2 句子级支持度打分)
      • [5.3 LLM-as-a-Judge](#5.3 LLM-as-a-Judge)
    • [6. Python:实现 Recall@k / MRR / nDCG](#6. Python:实现 Recall@k / MRR / nDCG)
    • [8. 如何解读指标?](#8. 如何解读指标?)
      • [8.1 常见组合与含义](#8.1 常见组合与含义)
      • [8.2 检索指标高,生成还是错?](#8.2 检索指标高,生成还是错?)

RAG 评估入门:Recall@k、MRR、nDCG、Faithfulness 到底怎么用?

适用场景:企业知识库问答 / 课程资料问答 / 文档助手 / 内部制度查询 / 产品手册助手

目标:把"感觉还行"的 RAG,变成"可量化、可对比、可迭代"的 RAG。


0. 为什么 RAG 一定要评估?

很多团队做 RAG 的常见现象是:

  • 改了 chunking / embedding / Top-k / rerank / prompt,主观感觉忽好忽坏;
  • "检索到了"但答案仍然错:Top-1 不准、或上下文不支持
  • "答得像真的"但其实胡编:缺乏 Faithfulness(依据一致性)约束
  • 无法解释:到底是 检索问题 还是 生成问题

评估的意义就是:
把 RAG 拆成可度量的子问题(检索 + 生成),用指标把改动和效果绑定起来。


1. RAG 评估要评什么?

一个典型 RAG 系统可以拆成两段:

  1. 检索侧(Retrieval):给定 query,返回 Top-k chunk 列表(可能带 rerank)
  2. 生成侧(Generation):基于 Top-n chunk 生成回答(可带引用)

因此评估也分成两类:

  • 检索侧指标:Recall@k、MRR、nDCG(回答"找得到/找得准")
  • 生成侧指标:Correctness(正确性)、Faithfulness(依据一致性)、Citation Coverage(引用覆盖率)(回答"答得对/答得有据可查")

2. 评测集怎么做?

2.1 一个评测样本应该包含什么?

建议每条样本至少有这三项:

  • query:用户问题
  • gold_evidence:正确证据(可以是 chunk id、段落、或文档+段落范围)
  • gold_answer:参考答案(可选,但做 Correctness 更方便)

示例(JSON 伪例):

json 复制代码
{
  "qid": "q_001",
  "query": "报销流程中差旅标准怎么规定?",
  "gold_evidence": ["docA#sec3#chunk12", "docA#sec3#chunk13"],
  "gold_answer": "差旅标准包括交通、住宿和伙食补贴,分别按员工级别与城市等级执行......"
}

2.2 gold_evidence 怎么标?

三种常见方式:

  1. chunk 级标注:直接标注"哪些 chunk 支持答案"(最推荐,最贴近 RAG)
  2. 段落/页码标注:先标注段落范围,再映射到 chunk(适合 PDF)
  3. 文档级标注:只标文档,不标段(太粗,容易高估检索效果)

3. 检索侧指标:Recall@k、MRR、nDCG

下面三项是 RAG 最常用的检索评估指标。

3.1 Recall@k:Top-k 里有没有"正确证据"

定义

对每个 query,如果 Top-k 检索结果里出现任意一个 gold_evidence,则记为命中。

最后对所有 query 求平均。

  • 优点:简单、最常用
  • 缺点:不关心排序(Top-1/Top-3/Top-10 的差异会被"掩盖")

3.2 MRR(Mean Reciprocal Rank):第一个正确结果排第几

定义

看第一个正确证据在检索列表中的排名 rank,得分为 1/rank

例如:

  • 正确 chunk 在第 1 名:得分 1
  • 在第 2 名:得分 0.5
  • 在第 5 名:得分 0.2
  • Top-k 都没有:得分 0

最后对所有 query 求平均。

  • 优点:非常关注 Top-1 / Top-3 是否准确(RAG 体验关键)
  • 缺点:只看"第一个正确",不看后面还有多少正确

3.3 nDCG:考虑"相关程度"和"排序质量"

在很多任务里,证据不止一个,而且相关性可能是分级的(比如:强相关/弱相关)。

nDCG(Normalized Discounted Cumulative Gain)能同时考虑:

  • 相关性分数(relevance)
  • 排名折扣(越靠前贡献越大)
3.3.1 DCG@k(核心思想)
text 复制代码
DCG@k = Σ_{i=1..k} (rel_i / log2(i+1))
  • rel_i:第 i 个结果的相关性(例如 0/1,或 0/1/2/3)
  • log2(i+1):排名折扣
3.3.2 nDCG@k(归一化)
text 复制代码
nDCG@k = DCG@k / IDCG@k
  • IDCG@k:理想排序(把最相关的都排在最前面)得到的 DCG

  • 优点:适合 多证据、多相关等级 的场景

  • 缺点:需要定义 rel(相关性怎么打分)


4. 生成侧指标:Correctness vs Faithfulness

4.1 Correctness(正确性):答案对不对?

  • 关注最终答案是否与事实一致、是否回答了问题
  • 需要 gold_answer(参考答案)或人工评审

4.2 Faithfulness(依据一致性):答案是否"严格基于检索证据"?

  • 关注答案中的关键结论是否都能在检索 chunk 中找到依据
  • 即使答案"碰巧正确",但如果证据里没有、模型自己推出来了,也可能在企业场景不可接受

5. Faithfulness 怎么评?(三种常用做法)

5.1 引用覆盖率

要求模型输出带引用,例如:

  • 每个要点后加 [doc#chunk]
  • 或者每句话后加引用

然后统计:

  • Coverage:关键句是否都带引用
  • Validity:引用的 chunk 是否真的包含支撑信息

优点:工程简单、效果立竿见影

缺点:引用可能"乱贴"(需要 Validity)


5.2 句子级支持度打分

思路:把回答拆成句子,对每句在 Top-n chunk 里找"最能支持它的证据",如果找不到就判为不支持。

可用两类"相似度/判别器":

  • Embedding 相似度(快,但可能误判)
  • NLI/LLM Judge(更准,但更慢/更贵)

5.3 LLM-as-a-Judge

让一个更强的模型充当评审,输入:(query, retrieved_context, answer)

输出:Faithfulness 分数 + 解释 + 不支持的句子。

注意:

  • 评审模型本身也可能偏差
  • 建议做 抽样人工复核,以及保持评审 prompt 固定


6. Python:实现 Recall@k / MRR / nDCG

下面给一个不依赖第三方评测库的最小实现,方便快速集成到自己的 RAG pipeline。

python 复制代码
import math
from typing import List, Dict, Set


def recall_at_k(retrieved: List[str], gold: Set[str], k: int) -> float:
    # 是否在 Top-k 内命中任意 gold evidence
    topk = retrieved[:k]
    return 1.0 if any(x in gold for x in topk) else 0.0


def mrr_at_k(retrieved: List[str], gold: Set[str], k: int) -> float:
    # Top-k 内第一个 gold 的倒数排名;没命中为 0
    topk = retrieved[:k]
    for i, x in enumerate(topk, start=1):
        if x in gold:
            return 1.0 / i
    return 0.0


def dcg_at_k(retrieved: List[str], rel_map: Dict[str, float], k: int) -> float:
    # DCG@k = Σ rel_i / log2(i+1)
    dcg = 0.0
    for i, x in enumerate(retrieved[:k], start=1):
        rel = rel_map.get(x, 0.0)
        dcg += rel / math.log2(i + 1)
    return dcg


def ndcg_at_k(retrieved: List[str], rel_map: Dict[str, float], k: int) -> float:
    # nDCG@k = DCG@k / IDCG@k
    dcg = dcg_at_k(retrieved, rel_map, k)

    # 理想排序:按 rel 从大到小
    ideal = sorted(rel_map.items(), key=lambda kv: kv[1], reverse=True)
    ideal_list = [cid for cid, _ in ideal]
    idcg = dcg_at_k(ideal_list, rel_map, k)

    return 0.0 if idcg == 0 else (dcg / idcg)


def evaluate_retrieval(dataset: List[Dict], k_list=(5, 10, 20)) -> Dict[str, float]:
    # dataset 每条包含:
    #   - retrieved: List[str]  (你的检索结果 chunk ids,已按分数排序)
    #   - gold_evidence: Set[str]
    #   - rel_map: Dict[str, float] (可选:用于 nDCG)
    report = {}
    for k in k_list:
        recall_scores, mrr_scores, ndcg_scores = [], [], []
        for ex in dataset:
            retrieved = ex["retrieved"]
            gold = set(ex["gold_evidence"])

            recall_scores.append(recall_at_k(retrieved, gold, k))
            mrr_scores.append(mrr_at_k(retrieved, gold, k))

            rel_map = ex.get("rel_map")
            if rel_map is not None:
                ndcg_scores.append(ndcg_at_k(retrieved, rel_map, k))

        report[f"Recall@{k}"] = sum(recall_scores) / max(1, len(recall_scores))
        report[f"MRR@{k}"] = sum(mrr_scores) / max(1, len(mrr_scores))
        if ndcg_scores:
            report[f"nDCG@{k}"] = sum(ndcg_scores) / max(1, len(ndcg_scores))
    return report


if __name__ == "__main__":
    # 一个最小样例
    dataset = [
        {
            "retrieved": ["c7", "c2", "c9", "c1"],
            "gold_evidence": {"c2", "c3"},
            # relevance 分级示例:c2 更关键
            "rel_map": {"c2": 2.0, "c3": 1.0},
        },
        {
            "retrieved": ["c4", "c5", "c6"],
            "gold_evidence": {"c6"},
            "rel_map": {"c6": 2.0},
        },
    ]
    print(evaluate_retrieval(dataset, k_list=(1, 3, 5)))

8. 如何解读指标?

8.1 常见组合与含义

  • Recall@20 高、MRR@5 低:说明"能找到"但"排不前",需要 rerank / 更强的融合策略
  • Recall@k 低:说明检索本身有问题(embedding、索引、chunking、过滤条件、Hybrid)
  • nDCG 上升但 Recall 不变:排序变好但覆盖没变,通常是 rerank 起效

8.2 检索指标高,生成还是错?

高概率是:

  • Top-n context 里有正确证据,但 prompt 没约束好(模型"自由发挥")
  • chunk 太长/噪声太多,模型没读到关键句
  • 证据分散在多个 chunk,缺少结构化整合(需要多段汇总或多跳检索)

此时应该上:

  • 更严格的"仅基于 context"约束
  • 引用输出 + 引用校验(Faithfulness)
  • 更强的 rerank / 更小更干净的 chunk
相关推荐
NAGNIP4 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab5 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab5 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP9 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年9 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼9 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS9 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区10 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈11 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang11 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx