Chapter 05

RAG 评估专题:四个指标定位"检索还是生成"

RAG 系统失败时的灵魂拷问:是检索没拿对,还是模型没用好?本章用 RAGAS 的 4 个核心指标把这个黑盒子拆成可诊断的模块。

RAG 的四条失败路径

一个"回答错了"的 RAG 请求,错可能错在 4 个地方:

用户问题 ──▶ [1] 检索 ──▶ [2] 过滤/重排 ──▶ [3] 生成 ──▶ 答案 │ │ │ │ 没拿到相关文档 拿到了但没用 / 乱用 失败 A: 相关文档压根没索引 失败 B: 索引了但检索打分没上去 失败 C: 检索上去了但被 reranker 丢掉 失败 D: 都拿对了,生成阶段幻觉/忽略上下文

一个好的 RAG 评估体系要让你能回答:"这次错了,错在 A/B/C/D 哪个?"而不是只能说"错了"。

RAGAS 的四象限

RAGAS(Retrieval-Augmented Generation Assessment)提出 4 个正交指标,构成 2×2 矩阵:

看检索质量看生成质量
面向上下文Context Precision / RecallFaithfulness
面向问题Context RelevancyAnswer Relevancy

指标 1:Faithfulness(忠实度)

定义
答案中的每个事实陈述是否都能在检索到的上下文里找到支持。衡量模型有没有胡编
计算
用 LLM 把答案拆成原子陈述 → 对每个陈述判断"上下文是否支持"(yes/no) → 支持比例。
信号
Faithfulness 低 = 模型幻觉 = 生成阶段问题。
FAITHFULNESS_PROMPT = """下面是一个问题、检索到的上下文、以及模型给出的答案。

问题: {question}
上下文: {context}
答案: {answer}

第一步:把答案拆成独立的事实陈述(每行一个)。
第二步:对每个陈述,判断上下文是否支持它(YES/NO)。

返回 JSON:
{{
  "claims": [
    {{"claim": "...", "supported": true}},
    {{"claim": "...", "supported": false}}
  ]
}}"""

def faithfulness(question, context, answer):
    result = judge_llm(FAITHFULNESS_PROMPT.format(...))
    claims = result["claims"]
    if not claims: return 1.0
    return sum(c["supported"] for c in claims) / len(claims)

指标 2:Answer Relevancy(答案相关性)

定义
答案是否真正回应了问题,还是跑题/答非所问。
计算(RAGAS 实现)
用 LLM 根据答案反推可能的问题(生成 n 个候选) → 每个候选问题与原问题计算嵌入余弦相似度 → 均值。
信号
Answer Relevancy 低 = 答非所问 / 过度模糊 / 前言不搭后语。

指标 3:Context Precision(上下文精度)

定义
检索回来的 top-k 上下文里,真正与问题相关的占比——且相关的排在前面
计算
用 LLM 判定每个 chunk 是否相关,按位置计算 MAP(Mean Average Precision)变体。
信号
Precision 低 = 检索召回了噪声 / 重排没工作好。

指标 4:Context Recall(上下文召回)

定义
生成正确答案所需的信息,是否已经全在检索上下文里。需要参考答案。
计算
参考答案拆成事实陈述 → 每个陈述判断能否从上下文推出 → 命中比例。
信号
Recall 低 = 该拿的文档没拿到 = 索引/检索阶段问题。
对比 Precision vs Recall Precision:我拿回来的,有多少是对的?
Recall:对的文档里,我拿回来了多少?
检索系统的常见失败模式是 Recall 低(关键文档没进 top-k)而 Precision 看起来还行(因为 top-k 里大多相关但都不是关键的)。

四个指标如何组合诊断

FaithfulnessAnswer RelevancyContext PrecisionContext Recall诊断
✅ 系统健康
⚠️ 生成阶段幻觉,prompt 没约束好
⚠️ 模型跑题,可能 prompt 不聚焦
⚠️ 召回进来但噪声大,缺少 reranker
⚠️ 检索没拿到关键文档,需改 embedding 或补索引
🚨 检索根本坏了,模型拿没用信息强答
💥 系统多处故障,从零排查

RAGAS 上手

from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision, context_recall
from datasets import Dataset

data = {
    "question": ["公司的退款政策是什么?"],
    "answer":   ["支持 7 天无理由退款,发起后 3-5 个工作日到账。"],
    "contexts": [["本公司支持 7 天无理由退款...", "另有会员等级说明..."]],
    "ground_truth": ["支持 7 天无理由退,3-5 天到账"],
}

ds = Dataset.from_dict(data)
result = evaluate(
    ds,
    metrics=[faithfulness, answer_relevancy, context_precision, context_recall],
)
print(result)
# {'faithfulness': 1.0, 'answer_relevancy': 0.95,
#  'context_precision': 0.85, 'context_recall': 1.0}

超越 RAGAS:更细粒度的检索指标

如果你能标注"哪些文档是黄金正确文档",可以用传统 IR 指标,比 RAGAS 更精确:

Hit@k / Recall@k
Top-k 结果里有没有黄金文档(任何一个命中就算)。
MRR(Mean Reciprocal Rank)
第一个命中文档的位置倒数的均值。越靠前越好。
nDCG@k
考虑相关性分级(不只是相关/不相关),且按位置折扣。最严格的排名指标。
def hit_at_k(retrieved_ids, gold_ids, k=5):
    return int(any(doc_id in gold_ids for doc_id in retrieved_ids[:k]))

def mrr(retrieved_ids, gold_ids):
    for i, doc_id in enumerate(retrieved_ids, 1):
        if doc_id in gold_ids: return 1.0 / i
    return 0.0

import numpy as np
def ndcg_at_k(retrieved_ids, gold_relevance, k=5):
    # gold_relevance: dict[doc_id -> 相关度 0/1/2/3]
    dcg = sum(
        gold_relevance.get(d, 0) / np.log2(i + 2)
        for i, d in enumerate(retrieved_ids[:k])
    )
    ideal = sorted(gold_relevance.values(), reverse=True)[:k]
    idcg = sum(r / np.log2(i + 2) for i, r in enumerate(ideal))
    return dcg / idcg if idcg else 0.0

生成阶段专项:Citation Accuracy

如果你的 RAG 要求带引用,应该额外评估:

# 简化版 citation 校验
def check_citations(answer_with_citations, contexts):
    # 答案格式: "xxx[1] yyy[2]", 每个 [n] 对应 contexts[n-1]
    claims_with_refs = parse_claims(answer_with_citations)  # 自行实现
    n_supported = 0
    for claim, ref_idx in claims_with_refs:
        if llm_entails(claim, contexts[ref_idx]):
            n_supported += 1
    return n_supported / len(claims_with_refs)

评估集的特殊要求

RAG 评估集除了常规字段,还要包含:

常被忽视的类别:无答案问题 RAG 评估集必须包含"知识库里没有答案"的问题。模型应该说"不知道",而不是胡编。15% 左右的评估集建议是此类。

本章小结