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 伪例):

{
  "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(核心思想)
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(归一化)
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。

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
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐