封面图:RAG 检索评测把查询、候选文档、排序指标、引用校验和修复循环连成一套系统

系列:生产级 LLM 应用方法论 04
日期:2026-06-18
适合读者:正在建设企业知识库、文档问答、客服助手、代码助手、研究助理或内部 Agent 的工程师、产品负责人和技术负责人。

摘要

前三篇分别讨论了 Prompt Engineering 为什么不够、Context Engineering 是什么,以及上下文压缩与排序怎么做。这一篇进入 RAG 系统里最容易被低估、但最决定上限的问题:检索质量到底怎么评测?

很多团队上线 RAG 时只看最终答案:“答对了吗?有没有幻觉?”这个视角太粗。最终答案错了,可能是检索没有召回正确文档,也可能是召回了但排在第 12 位进不了上下文,还可能是证据进了上下文但模型没有用,甚至可能是答案看起来正确却引用了不支持结论的来源。

所以 RAG 评测要拆成三层:检索是否找到了正确证据,排序是否把正确证据放到前面,生成是否忠实使用了证据。本文给出一套工程化评测方法:如何设计 gold set、如何算 Recall@k / MRR / nDCG / Citation Support,如何记录权限和时效性,如何用一段可运行脚本做离线评测,以及线上应该监控哪些信号。

目录

1. 为什么不能只看最终答案

RAG 的目标不是“让模型看起来引用了资料”,而是让答案能够被外部证据支撑。

如果只看最终答案,会把不同错误混在一起:

现象 表面表现 真正可能的问题
答案错误 模型说错政策、数字或步骤 检索没召回正确文档;文档过期;模型没用证据
答案太泛 只给常识,没有业务细节 召回结果弱相关;上下文缺少 must-have facts
引用存在但不支持结论 看起来有来源,点开不相关 citation 只是装饰,没有做支持性校验
同一问题多次回答不稳定 有时对,有时错 top-k 排序波动;chunk 重叠噪声大;reranker 不稳定
用户越权看到信息 答案引用了不该访问的文档 metadata filter 或权限层失效
旧政策覆盖新政策 答案沿用旧版本 freshness/filter/version priority 没做进检索层

这些问题不能靠“再写一句 prompt”解决。RAG 系统必须把检索、排序、上下文和生成拆开评测。

一句工程判断是:

RAG 的第一性评测对象不是答案,而是“证据链是否成立”。

如果证据链不成立,答案越流畅越危险。

2. 把 RAG 链路拆成三层

一个典型 RAG 请求可以拆成三段:

  1. 检索层:从知识库里找候选文档或 chunk。
  2. 上下文层:对候选结果做过滤、去重、重排、压缩和预算分配。
  3. 生成层:模型基于上下文回答,并给出引用或证据说明。

框架图:从 Query Set 到 Retriever、Top-k Results、Gold Evidence、Metrics、Citation Check 和 Fix Loop 的 RAG 评测闭环

对应的评测问题也不同:

层级 应该问的问题 典型指标
检索层 正确证据有没有被找出来? Recall@k、Hit Rate@k、Coverage
排序层 正确证据是否排在足够靠前的位置? MRR、nDCG@k、Context Precision
上下文层 进模型窗口的证据是否干净、不过期、不越权? Duplicate Rate、Freshness、Permission Violation
生成层 答案是否忠实、完整、可追溯? Faithfulness、Answer Correctness、Citation Support

OpenAI Retrieval 文档里提到,检索可以做 semantic search、query rewriting、attribute filtering、ranking options,也可以限制 max_num_results。这些不是“调参小项”,而是评测实验的变量。你应该能回答:打开 query rewrite 后 Recall@10 是否提升?把 score_threshold 提高后 Precision 是否提升但 Recall 是否下降?按部门和日期过滤后是否减少越权和过期证据?

3. 检索质量的核心指标

先定义一条样本:

{
  "query_id": "refund_001",
  "query": "跨境订单退款多久到账?什么情况需要人工审核?",
  "gold_doc_ids": ["refund_policy_2026_03"],
  "gold_chunk_ids": ["refund_policy_2026_03#chunk_07", "refund_policy_2026_03#chunk_09"],
  "required_facts": ["到账时间", "跨境支付需人工审核"],
  "user_scope": ["customer_service"],
  "as_of": "2026-06-18"
}

检索系统返回:

[
  {"chunk_id": "refund_faq_2025_10#chunk_03", "doc_id": "refund_faq_2025_10", "score": 0.83},
  {"chunk_id": "refund_policy_2026_03#chunk_07", "doc_id": "refund_policy_2026_03", "score": 0.79},
  {"chunk_id": "payment_policy_2026_01#chunk_12", "doc_id": "payment_policy_2026_01", "score": 0.74}
]

下面这些指标各自回答不同问题。

3.1 Hit Rate@k

Top-k 里是否至少出现一个 gold 证据。

适合回答:“这个问题有没有捞到一点正确材料?”

缺点也明显:只要命中一个就算成功,不关心是否命中全部关键证据,也不关心排第几。

3.2 Recall@k

Top-k 里命中的 gold 证据数量,占 gold 证据总数的比例。

如果一个问题必须同时找到“到账时间”和“人工审核条件”,Recall@k 比 Hit Rate 更有意义。很多 RAG 系统答不完整,不是模型不会总结,而是检索只找到了其中一个事实。

3.3 Precision@k

Top-k 里有多少比例是正确证据。

这个指标在上下文预算紧张时很重要。Recall 高但 Precision 低,意味着你确实把正确证据找到了,但也带进来大量噪声。模型在噪声里做推理,稳定性会下降。

3.4 MRR

MRR(Mean Reciprocal Rank)关心第一个正确结果排在第几位。

如果第一个正确 chunk 总是排在第 1 或第 2,MRR 高;如果正确 chunk 经常排在第 8 或第 12,MRR 低。对只取 top-3 或 top-5 进上下文的系统,MRR 非常关键。

3.5 nDCG@k

nDCG 适合 gold 证据有等级的场景。比如一个 chunk 是“直接回答问题的政策条款”,另一个只是“背景解释”,它们不应该同权。

如果你有人工标注的 relevance grade,例如:

  • 3:直接支撑答案;
  • 2:部分支撑;
  • 1:背景相关;
  • 0:不相关。

那么 nDCG@k 比简单 Recall 更能反映排序质量。

3.6 Coverage

Coverage 不是标准 IR 指标,但对业务 RAG 很实用。它回答:

问题需要的关键事实类型,是否都被证据覆盖?

例如退款问题需要:

  • 时间;
  • 渠道;
  • 异常条件;
  • 审核条件;
  • 政策版本。

只命中“时间”不够。Coverage 可以按 required_facts 计算。

3.7 Freshness

RAG 的错误常常不是“没找到”,而是“找到了旧版本”。

Freshness 可以按业务规则定义:

  • 是否命中最新版本文档;
  • 是否符合 as_of 时间;
  • 是否避开已废止政策;
  • 多版本冲突时是否选择优先级最高的来源。

3.8 Permission Violation

企业 RAG 必须把权限当作检索指标,而不是生成层道德提醒。

如果用户无权访问某些文档,检索结果里出现这些文档就已经是系统错误,即使模型最终没有引用。评测集应该包含不同 user_scope、部门、地区、客户等级和数据隔离场景。

3.9 Duplicate Rate

chunk overlap 会提升召回,但也会带来重复。重复结果太多,会挤掉其他证据。

Duplicate Rate 可以按 doc_id、相邻 chunk、文本相似度或 canonical fact 计算。它不一定越低越好,但高到一定程度就说明上下文预算被浪费。

3.10 Citation Support

Citation Support 属于生成层和证据层之间的指标。它问:

答案里的每个关键断言,是否能被引用来源直接支持?

这比“有没有引用”严格得多。RAG 系统最危险的状态是:答案看起来专业、有引用,但引用点开不支持结论。

4. 评测集应该怎么设计

不要从随机用户日志里随便抽 100 条问题就开始算分。那会让评测集偏向高频、简单、表述清楚的问题,掩盖真正会出事故的边界。

一个可靠的 RAG eval set 应该覆盖这些类型:

类型 例子 目的
直接事实 “企业版支持多少个管理员?” 测基础召回
多条件组合 “跨境订单退款多久到账,哪些情况要人工审核?” 测多证据覆盖
版本敏感 “2026 年 3 月后的退款政策是什么?” 测时效性
权限敏感 “某客户的合同折扣是多少?” 测 metadata filter
同义表达 “钱什么时候退回来?” 测 query rewrite 和语义召回
反事实/不存在 “能否 24 小时内保证退款?” 测拒答和不确定性
冲突证据 “新旧政策同时存在时按哪个算?” 测版本优先级
长尾术语 “SLA credit 怎么申请?” 测领域词表和 hybrid search
引用严格 “请给出处并引用原文依据。” 测 citation support

每条样本至少要有这些字段:

字段 作用
query_id 稳定追踪和回归测试
query 用户问题原文
intent_type 事实、流程、比较、总结、排障等
gold_doc_ids 正确来源文档
gold_chunk_ids 最小正确证据单元
required_facts 答案必须覆盖的事实
negative_doc_ids 不能被当成依据的相似干扰文档
user_scope 权限上下文
as_of 时间上下文
expected_answer_notes 生成层评测提示,不要求唯一答案

这里最费力的是 gold_chunk_ids。但如果没有这个字段,你很难判断检索到底对不对。只标答案不标证据,会把问题又推回“最终答案对不对”的粗粒度评测。

5. 一个最小 retrieval eval 数据格式

可以从 JSONL 开始,不要一上来做复杂平台。

{"query_id":"refund_001","query":"跨境订单退款多久到账?什么情况需要人工审核?","intent_type":"policy_qa","gold_doc_ids":["refund_policy_2026_03"],"gold_chunk_ids":["refund_policy_2026_03#chunk_07","refund_policy_2026_03#chunk_09"],"required_facts":["到账时间","跨境支付需人工审核"],"negative_doc_ids":["refund_faq_2025_10"],"user_scope":["customer_service"],"as_of":"2026-06-18","expected_answer_notes":"必须说明原支付渠道 1-5 个工作日;跨境支付订单进入人工审核;引用 2026-03 版政策。"}
{"query_id":"contract_014","query":"这个客户最新合同里的自动续约条款是什么?","intent_type":"contract_qa","gold_doc_ids":["contract_acme_2026_signed"],"gold_chunk_ids":["contract_acme_2026_signed#chunk_22"],"required_facts":["自动续约期限","提前通知窗口"],"negative_doc_ids":["contract_acme_2025_draft"],"user_scope":["legal","account_acme"],"as_of":"2026-06-18","expected_answer_notes":"只能使用已签署最新版合同,不能引用 2025 草案。"}
{"query_id":"support_033","query":"用户说登录后一直跳回首页,可能是什么原因?","intent_type":"troubleshooting","gold_doc_ids":["sso_runbook_2026_02","cookie_policy_2026_01"],"gold_chunk_ids":["sso_runbook_2026_02#chunk_11","cookie_policy_2026_01#chunk_04"],"required_facts":["SSO callback mismatch","SameSite cookie 配置"],"negative_doc_ids":[],"user_scope":["support_l2"],"as_of":"2026-06-18","expected_answer_notes":"需要给排查顺序,不能只给泛泛登录建议。"}

检索输出也建议记录成 JSONL:

{"query_id":"refund_001","results":[{"rank":1,"doc_id":"refund_faq_2025_10","chunk_id":"refund_faq_2025_10#chunk_03","score":0.83},{"rank":2,"doc_id":"refund_policy_2026_03","chunk_id":"refund_policy_2026_03#chunk_07","score":0.79},{"rank":3,"doc_id":"refund_policy_2026_03","chunk_id":"refund_policy_2026_03#chunk_09","score":0.76}]}

OpenAI File Search 文档里有一个实用点:默认情况下模型输出会带文件引用 annotation,但如果你要分析检索结果本身,需要在请求里显式 include file_search_call.results。做 eval 时不要只存最终 answer,要把搜索 query、top-k 结果、score、metadata、filters 和模型最终引用都存下来。

6. 离线评测脚本:先把问题定位清楚

下面是一段无第三方依赖的 Python 示例。它不负责调用检索服务,只负责读取 gold set 和 retrieval results,然后计算基础指标。真实项目里可以把它接到 CI、离线实验平台或 nightly job。

import json
import math
from collections import Counter
from pathlib import Path


def read_jsonl(path):
    with Path(path).open("r", encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if line:
                yield json.loads(line)


def dcg(relevances):
    return sum((2 ** rel - 1) / math.log2(i + 2) for i, rel in enumerate(relevances))


def evaluate(gold_path, result_path, k=5):
    gold = {row["query_id"]: row for row in read_jsonl(gold_path)}
    results = {row["query_id"]: row["results"] for row in read_jsonl(result_path)}

    totals = Counter()
    sums = Counter()
    failures = []

    for query_id, item in gold.items():
        rows = results.get(query_id, [])[:k]
        returned_chunks = [r["chunk_id"] for r in rows]
        returned_docs = [r["doc_id"] for r in rows]
        gold_chunks = set(item.get("gold_chunk_ids", []))
        gold_docs = set(item.get("gold_doc_ids", []))
        negative_docs = set(item.get("negative_doc_ids", []))

        chunk_hits = [chunk for chunk in returned_chunks if chunk in gold_chunks]
        doc_hits = [doc for doc in returned_docs if doc in gold_docs]

        hit_at_k = 1.0 if chunk_hits or doc_hits else 0.0
        recall_at_k = len(set(chunk_hits)) / max(1, len(gold_chunks))
        precision_at_k = len(chunk_hits) / max(1, len(rows))

        reciprocal_rank = 0.0
        for idx, row in enumerate(rows, start=1):
            if row["chunk_id"] in gold_chunks or row["doc_id"] in gold_docs:
                reciprocal_rank = 1.0 / idx
                break

        # 最小版 nDCG:gold chunk 记 3 分,gold doc 记 1 分,其他 0 分。
        rels = []
        for row in rows:
            if row["chunk_id"] in gold_chunks:
                rels.append(3)
            elif row["doc_id"] in gold_docs:
                rels.append(1)
            else:
                rels.append(0)
        ideal = sorted(rels, reverse=True)
        ndcg = dcg(rels) / dcg(ideal) if any(ideal) else 0.0

        permission_violation = any(doc in negative_docs for doc in returned_docs)
        duplicate_rate = 1.0 - (len(set(returned_chunks)) / max(1, len(returned_chunks)))

        totals["queries"] += 1
        sums["hit_at_k"] += hit_at_k
        sums["recall_at_k"] += recall_at_k
        sums["precision_at_k"] += precision_at_k
        sums["mrr"] += reciprocal_rank
        sums["ndcg_at_k"] += ndcg
        sums["permission_violation_rate"] += 1.0 if permission_violation else 0.0
        sums["duplicate_rate"] += duplicate_rate

        if recall_at_k < 1.0 or permission_violation:
            failures.append({
                "query_id": query_id,
                "query": item["query"],
                "recall_at_k": round(recall_at_k, 3),
                "returned_chunks": returned_chunks,
                "gold_chunks": sorted(gold_chunks),
                "permission_violation": permission_violation,
            })

    n = max(1, totals["queries"])
    summary = {name: round(value / n, 4) for name, value in sums.items()}
    return {"k": k, "summary": summary, "failures": failures[:20]}


if __name__ == "__main__":
    report = evaluate("retrieval_eval.jsonl", "retrieval_results.jsonl", k=5)
    print(json.dumps(report, ensure_ascii=False, indent=2))

这段脚本故意简单,因为第一版 eval 的目标不是“指标优雅”,而是帮助你把错误分层:

  • Recall 低:检索层没找到;
  • MRR 低:找到了但排序靠后;
  • Precision 低:噪声太多;
  • Duplicate Rate 高:上下文预算被重复 chunk 占用;
  • Permission Violation 高:权限过滤不能上线;
  • nDCG 低:排序没有把强证据放前面。

7. 诊断矩阵:指标差时应该改哪里

评测不是为了得到一个分数,而是为了决定下一步改哪里。

评测现象 常见原因 优先排查
Hit Rate@10 低 query 表达和文档表达差异大;索引缺文档;embedding 不适合领域 query rewrite、同义词词表、索引覆盖、hybrid search
Recall@10 高但 Recall@3 低 正确结果在后面 reranker、ranking options、domain boost
Precision@5 低 top-k 混入大量弱相关片段 score threshold、metadata filter、query decomposition
MRR 低 第一个正确结果太靠后 重排模型、标题/层级 metadata、文档结构化
nDCG 低 强证据没有排在弱证据前 人工 relevance grade、业务优先级、source authority
Duplicate Rate 高 chunk overlap 太大;相邻 chunk 都进上下文 结果去重、按 doc 限额、MMR、多样性约束
Freshness 差 旧文档没有废止标记;日期过滤缺失 as_of 过滤、版本优先级、索引生命周期
Permission Violation > 0 检索前未做权限过滤;只在生成层提醒模型 强制 metadata filter、租户隔离、审计日志
答案错但检索对 模型没有忠实使用证据;上下文排序差 引用约束、证据编号、答案后验校验
答案对但引用错 citation 没校验;引用只是文本装饰 claim-to-citation 检查、引用片段回放

这里有一个容易忽略的顺序:先修权限和时效性,再修召回,再修排序,最后修生成。

原因很简单:如果检索结果里已经有越权或过期内容,生成层越强,越可能把错误证据组织成可信答案。

8. 答案层评测:不要让模型“带着错引用答对”

RAGAS 等工作把 RAG 评测拆成了 answer relevancy、faithfulness、context precision、context recall 等维度;ARES 这类框架也强调用自动化方式评测 RAG 的检索和生成质量。这些方向的共同点是:不要把答案正确性和证据质量混成一个黑盒分数。

工程上可以把答案层评测拆成四个检查:

8.1 Answer Correctness

答案是否覆盖 required_facts,有没有明显事实错误。

这个指标可以由人工、规则、LLM judge 或混合方式完成。高风险任务不要完全依赖单一 LLM judge,至少要抽样人工复核。

8.2 Faithfulness

答案里的关键断言是否都来自上下文证据。

例子:

答案断言:跨境订单退款通常需要 1-5 个工作日,并需要人工审核。
证据 A:退款通常在 1-5 个工作日内退回原支付渠道。
证据 B:跨境支付订单需进入人工审核。

这就是可支持的。如果证据只说“部分异常订单需审核”,答案却写“所有跨境订单都需审核”,就不忠实。

8.3 Citation Support

每个引用是否支撑它旁边的句子。

不要只检查“答案末尾有没有来源列表”。更好的做法是让答案的每个关键句带 citation id,然后逐条校验:

{
  "claim": "跨境支付订单需进入人工审核。",
  "citation": "refund_policy_2026_03#chunk_09",
  "supported": true
}

8.4 Refusal / Insufficient Evidence

当检索不到足够证据时,系统是否会明确说“不足以回答”,而不是编一个看起来合理的答案。

这类样本必须进 eval set。否则系统会在有证据问题上得分很好,但在无证据问题上制造幻觉。

9. 线上监控:离线分数不等于线上可靠

离线 eval 是上线门槛,不是上线后的免死金牌。线上至少监控这些信号:

信号 说明
检索空结果率 空结果突然上升,可能是索引、权限或查询改写异常
平均 top-1 / top-5 score 分数分布漂移,可能说明数据或 query 变了
低分回答率 低检索置信度时仍生成答案,需要关注
引用点击率/反馈 用户是否点击来源,是否认为来源不相关
人工转接率 客服、法务、财务场景里很关键
版本命中率 新政策发布后,旧版本是否仍被命中
权限拒绝率 权限过滤是否异常宽或异常严
p95 延迟 reranker、query decomposition 和 top-k 扩大会影响体验
输入 token 分布 证据过长会提高成本,也可能挤掉规则
答案后验校验失败率 citation support 或 faithfulness checker 的失败趋势

更进一步,可以把线上失败样本回流到 eval set:

  1. 用户点踩或转人工;
  2. 记录 query、retrieval results、answer、citations、filters;
  3. 人工标注 gold evidence 和失败原因;
  4. 加入 regression eval;
  5. 下次改检索、chunking、reranker 或 prompt 时必须跑过。

这才是 RAG 质量持续提升的闭环。

10. 常见误区

10.1 只用公开 benchmark 判断业务 RAG

BEIR、KILT 等 benchmark 对理解信息检索和知识密集型任务很有帮助,但它们不能替代你的业务 eval set。企业知识库里最难的问题通常是权限、版本、内部术语、流程例外和文档质量。

公开 benchmark 可以用来比较底层检索器,但上线决策必须看业务 gold set。

10.2 把 top-k 调大当成万能解

top-k 变大通常能提高 Recall,但也会增加噪声、token、延迟和引用混乱。正确做法是同时观察 Recall、Precision、MRR、nDCG、答案 faithfulness 和成本。

10.3 只标 gold doc,不标 gold chunk

只标文档粒度会让指标虚高。一个 80 页 PDF 里有正确答案,不代表检索到这个 PDF 的任意 chunk 都算成功。

10.4 不测无答案问题

RAG 系统必须会说“不知道”。无答案问题、权限不足问题、证据冲突问题都应该进 eval。

10.5 把 LLM judge 当绝对真值

LLM judge 很适合扩大评测规模,但它本身也需要校准。建议做法是:

  • 关键指标保留人工标注样本;
  • LLM judge 输出理由和引用;
  • 抽样复核 judge 误判;
  • 每次改 judge prompt 也跑回归。

10.6 忽略数据摄取质量

如果 PDF 解析错、表格丢列、标题层级消失、扫描件 OCR 错误,检索评测会持续低分。不要只调 embedding 和 reranker,也要检查 ingest pipeline。

11. 落地路线图

如果从零开始,我建议按四周推进。

第 1 周:建立最小 eval set

  • 选 50-100 条真实业务问题;
  • 覆盖高频、长尾、无答案、权限、版本和冲突场景;
  • 标注 gold_doc_idsgold_chunk_idsrequired_facts
  • 固定第一版 JSONL 格式。

第 2 周:跑检索层基线

  • 记录 top-10 检索结果;
  • 计算 Hit Rate@k、Recall@k、MRR、nDCG;
  • 输出失败样本列表;
  • 对失败样本按原因分类:缺文档、chunk 不佳、query 不佳、排序不佳、权限/版本错误。

第 3 周:做排序和上下文实验

  • 比较不同 chunk size 和 overlap;
  • 比较 semantic、keyword、hybrid search;
  • 比较 query rewrite 开关;
  • 加 reranker 或业务 boost;
  • 加去重、source cap、metadata filter;
  • 每次只改一类变量,避免无法归因。

第 4 周:接答案层和线上监控

  • 加 answer correctness / faithfulness / citation support 检查;
  • 记录 file_search_call.results 或自建 retriever trace;
  • 把低置信度、用户差评、人工转接样本回流;
  • 在发布流程里设置回归阈值。

第一版阈值可以保守一点,例如:

指标 上线前建议
Permission Violation 必须为 0
Hit Rate@10 >= 0.90
Recall@5 >= 0.75
MRR 持续提升,不低于旧版本
Citation Support >= 0.85,且高风险场景人工抽检
无答案拒答准确率 >= 0.85

这些数字不是通用标准,只是帮助团队开始讨论 trade-off。真正的阈值要根据业务风险来定。

12. 发布前自检清单

上线前至少问完这些问题:

  • 是否有覆盖真实业务的 eval set,而不只是 demo 问题?
  • 是否标注了 gold chunk,而不只是 gold answer?
  • 是否包含无答案、权限、过期、冲突和长尾术语样本?
  • 是否能输出每个 query 的 top-k 结果、score、metadata、filter 和最终引用?
  • 是否分别看了 Recall、MRR、nDCG、Permission Violation、Duplicate Rate?
  • 是否有答案层的 faithfulness 和 citation support 检查?
  • 是否有失败样本回流机制?
  • 是否能在模型、embedding、chunking、reranker、prompt 变更后跑回归?
  • 是否明确低置信度时的降级策略:拒答、追问、转人工或扩大检索?
  • 是否记录了上线版本的知识库快照和评测报告?

如果这些问题答不上来,RAG 系统就还停留在“能演示”,没有进入“可运营”。

13. 总结

RAG 检索质量评测不是一个单一分数,而是一套定位问题的工具箱。

最重要的不是选择 Recall@k 还是 nDCG,而是建立这条链路:

业务问题 -> gold evidence -> top-k 检索结果 -> 排序指标 -> 上下文质量 -> 答案归因 -> 失败回流

当你能把一次错误回答拆成“没召回、排太后、证据过期、权限失效、引用不支持、模型没忠实使用证据”中的某一种,RAG 系统才真正可改进。

下一篇建议继续写:RAG 的引用与归因怎么做:让答案里的每一句关键结论都有可追溯证据

参考资料