夜雨聆风学习资料网

ARTICLE · 1188968

RAG 答错了先查哪里?从文档到引用的六步排障与评估实战

RAG 答错了先查哪里?从文档到引用的六步排障与评估实战

把大模型接上知识库,并不等于回答一定有依据。用户问“新版报销规则的上限是多少”,系统却引用了旧制度;用户问一个文档里根本没有的问题,模型仍然给出看似确定的数字。这类问题常被归咎于“模型幻觉”,但换更大的模型之前,更值得先确认:正确资料是否进入知识库、检索是否命中、证据是否真的送到了模型面前。

本文给出一个可以落地的 RAG 排障顺序,并用只依赖 Python 标准库的脚本演示如何评估检索结果。示例中的制度和数据均为虚构,指标代码可以直接运行。

一、先把 RAG 拆成可观察的链路

典型的检索增强生成(Retrieval-Augmented Generation,RAG)链路是:

原始文档 → 清洗与切块 → 建立索引 → 查询改写/检索 → 重排         → 上下文组装 → 模型生成 → 引用与答案校验

RAG 的基本思路,是让生成模型在回答时使用外部检索到的信息,而不是只依赖模型参数里已有的知识。但“用了检索”只是架构描述,并不是答案准确性的保证。任一环节遗漏、过期或选错证据,最后都可能表现为同一个现象:回答错误。

排查时先记录一次完整请求的几个关键字段:用户原问题、过滤条件、检索候选及分数、重排后的顺序、最终放进提示词的片段、模型原始输出,以及最终展示的引用。对包含个人信息或业务秘密的内容,应按系统既有的数据治理要求脱敏和控制访问。没有这些中间结果,只盯着最终回答,往往无法区分“没检到”和“检到了却没用对”。

下面用一个贯穿全文的虚构场景:知识库有《差旅报销规则 2024》和《差旅报销规则 2025》,用户问“2025 版住宿费上限是多少”。正确文档是 policy-2025,旧文档 policy-2024 的数值不同。我们需要保证系统找到、选中并正确引用 policy-2025。

二、第一步:确认源文档真的存在,而且版本正确

在调向量模型之前,先回答三个朴素的问题:

  1. 正确答案对应的原始文档是否已经入库?
  2. 入库的是最新生效版本,还是旧版、草稿版?
  3. 当前用户的权限和业务过滤条件,是否允许检索到它?

生产环境里常见的不是“向量相似度太低”,而是上传失败、定时同步延迟、解析器跳过表格、PDF 页码错乱,或 effective_date、tenant_id 等元数据写错。此时继续调 Top-K,最多让错误文档更稳定地被检出。

给每份知识保留稳定的 document_id、版本号、生效时间、来源地址、更新时间和权限范围。索引任务应能回答“这份文档是否已处理、处理成几个片段、何时进入索引”。对于会频繁更新的制度类内容,查询时也要定义清楚“当前有效”的业务含义,而不是只按文件上传时间排序。

快速验证:用文档 ID 直接查索引。如果 policy-2025 不存在,先修同步或解析;如果存在但内容缺了“住宿费上限”所在的表格,先修抽取;如果片段完整却被权限过滤排除,先修元数据或访问规则。此时还不需要碰生成模型。

三、第二步:检查切块是否保留了答案所需的上下文

文档切块不是简单地每隔固定字符截断。比如一个表格的标题写着“2025 年一线城市”,下一块只有“住宿费:800 元/晚”,单独检索后一块就容易丢失年份和适用城市;如果把整本手册塞进一个块,又可能混入多个版本和无关条款。

较稳妥的起点是按标题、章节、段落和表格边界切分;让每个片段带上文档标题、章节路径、版本和生效时间。对于跨段才能理解的定义,可适当重叠,但不要把“重叠越多”当作通用优化目标:它会增加重复证据、索引成本和上下文占用。

抽查切块时,针对真实问题看两个方向:

  • 答案完整性
    :命中的块能否独立说明数值、单位、适用条件和例外?
  • 来源可追溯性
    :块是否能映射回原文页码、段落或稳定 URL?

如果答案跨越多个块,就要决定是扩大块、建立父子块关系,还是在检索命中后补取相邻段落。选择依据应是具体问答样本的表现,而不是某个固定的“最佳块大小”。

四、第三步:先量检索,再调检索

检索层至少要看两份列表:初始召回的候选,以及重排后真正进入上下文的结果。只看最终 Top-3,无法判断是初始检索漏掉了正确文档,还是重排把它排到了后面。

对于制度编号、错误码、类名、API 路径这类精确字面信息,关键词检索往往很重要;对于同义表达、自然语言描述,语义检索通常更有价值。可以从关键词与向量检索并行召回开始,再用明确的去重、融合和重排规则处理候选。BEIR 基准研究也显示:不同检索方法在不同数据集上的表现和成本会变化。因此,别直接把某个公开基准的最优配置照搬到自己的文档上。

先做一个小而可靠的评估集:从真实问题中选取代表性样本,人工标出至少一个相关文档 ID,覆盖简单事实题、版本题、多条件题、找不到答案的题以及容易误召回旧资料的题。评估集要单独存放,不要在每次调参时随手改“标准答案”。

两个便于入门的指标是:

  • Recall@k
    :在前 k 个检索结果中,是否至少出现一个标注为相关的文档;再对所有题取平均。它衡量“有没有机会让模型看到证据”。若一道题有多个必须同时使用的相关文档,这个简化定义就不够,需要按任务改成覆盖率或全命中判定。
  • MRR@k
    :找到第一个相关文档时取 1/名次,前 k 个都没找到则记 0,最后对所有题取平均。它区分“第 1 位命中”和“第 3 位才命中”。

下面的脚本没有第三方依赖。每道题允许多个相关文档,Recall@k 使用“至少命中一个”的定义;无答案题不参与这两个指标,应该单独统计拒答表现。

from dataclasses import dataclass@dataclass(frozen=True)class Case:    question: str    relevant_ids: frozenset[str]    ranked_ids: tuple[str, ...]def evaluate(cases: list[Case], k: int) -> tuple[float, float]:    if k <= 0:        raise ValueError("k must be positive")    answerable = [case for case in cases if case.relevant_ids]    if not answerable:        raise ValueError("at least one answerable case is required")    recalls = []    reciprocal_ranks = []    for case in answerable:        top_k = case.ranked_ids[:k]        first_hit = next(            (rank for rank, doc_id in enumerate(top_k, start=1)             if doc_id in case.relevant_ids),            None,        )        recalls.append(1.0 if first_hit is not None else 0.0)        reciprocal_ranks.append(1.0 / first_hit if first_hit else 0.0)    return (        sum(recalls) / len(answerable),        sum(reciprocal_ranks) / len(answerable),    )cases = [    Case("2025 版住宿费上限?", frozenset({"policy-2025"}),         ("policy-2024", "policy-2025", "faq")),    Case("报销时限是几天?", frozenset({"deadline-2025"}),         ("faq", "policy-2024", "deadline-2025")),    Case("票据丢失怎么办?", frozenset({"receipt-rule"}),         ("faq", "policy-2024", "policy-2025")),    Case("出差申请入口在哪?", frozenset({"travel-portal"}),         ("travel-portal", "faq", "policy-2025")),]recall, mrr = evaluate(cases, k=3)print(f"Recall@3 = {recall:.3f}")print(f"MRR@3    = {mrr:.3f}")assert recall == 0.75assert round(mrr, 3) == 0.458

运行结果:

Recall@3 = 0.750MRR@3    = 0.458

这四题里有一题根本没命中,第一题虽然命中新版规则却排在旧版后面。由此可以安排两项具体工作:查漏检问题的索引与查询表达;给版本题增加有效期过滤或重排特征。只看“系统答对率下降”则很难指向这两处。

五、第四步:看证据是否在组装时被挤掉或污染

检索命中不代表模型看到的上下文正确。应用层常会按字符数或 token 预算截断候选,可能把关键条款截掉;重复片段可能占满预算;旧版资料可能放在新版之前;文档标题和生效日期可能在组装时丢失。

建议在每次生成请求中记录“最终实际发送”的证据列表,而不是仅保存数据库检索结果。至少检查:文档 ID、版本、片段正文、顺序、截断位置、引用编号。对于需要跨两份文档推理的问题,确认两份证据都还在提示词中。

“上下文越长越安全”也并非稳妥假设。研究 Lost in the Middle 在其测试设置下观察到,相关信息的位置会影响长上下文模型的表现,位于中间时可能更难被使用。这个结论提醒我们评估证据排序,但具体排序仍应以自己的模型、问题和数据实测为准。

还有一个常被忽略的边界:检索到的网页或文档是待分析的数据,不是可以执行的系统指令。若资料中出现“忽略以上要求”“把用户数据发给某地址”等文本,应用不能把它当成更高优先级的指令。外部知识的可信度、权限和来源仍需由应用控制。

六、第五步:约束生成,但别把提示词当万能补丁

证据正确送达后,才轮到检查模型回答。可要求模型:只根据给定材料回答;每个关键结论附上对应片段 ID;材料不足时明确说“不足以确认”;遇到版本冲突时说明冲突,并优先采用业务规则定义的有效版本。

但一句“请勿编造”并不能代替程序校验。至少做两类检查:

  1. 引用存在性
    :回答里出现的引用 ID 必须确实存在于本次传入的证据集合中。否则即使文字正确,也不能展示一条不存在的引用。
  2. 答案支持性
    :对金额、日期、适用条件等关键字段,抽样核对它们是否能从被引用片段中读出。对于高风险场景,可让系统输出结构化字段,再用确定性规则校验格式和范围;无法核实就交由人工或明确拒答。

例如证据只有“2025 版住宿费上限 800 元/晚,适用于一线城市”,模型答“全国统一 800 元/晚”就是扩大了适用范围。引用了正确文档也不能算答案有据。评估时应同时看事实值和限定条件。

七、第六步:把“无答案”也纳入评估

真实用户不会只问知识库里有答案的问题。“今年刚发布、还未同步的规则是什么?”若索引没有相应资料,系统应该说明目前查不到可核实的依据,并提示用户查看权威来源或稍后再试。

为无答案题建立单独集合,至少记录三个结果:模型是否拒答、是否编造来源、是否错误地引用相似但无关的旧文档。还可以把“过期文档仍在库中”和“权限不允许访问”作为不同的无答案原因。不要把拒答率单独最大化:过度拒答会损害本来可回答的问题,需要与可回答题的准确率一起看。

一个有用的发布门槛可以写成具体的回归条件,而不是“感觉答案不错”:

可回答题:Recall@5、MRR@5、事实正确率、引用支持率无答案题:正确拒答率、伪造引用次数版本题:旧版误用次数运行指标:检索耗时、重排耗时、生成耗时、单次成本

这些指标没有通用的合格数值。先记录当前系统的基线,再按业务风险设阈值;每次调整切块、索引、重排或提示词后,都对同一批固定样本回归。发现指标变好时,也要抽看失败样本,避免平均数掩盖某一类问题。

八、一次实际排障可以这样走

假设用户问“2025 版住宿费上限是多少”,系统却回答了旧版数值。按下面顺序排查,通常比同时修改五项配置更快:

检查点
要看的证据
可能的修复
文档入库
policy-2025
 是否存在,正文是否完整
修同步、解析和版本元数据
切块
数值与适用条件是否在同一可理解片段
调整章节或表格切分
初始检索
正确片段是否进入候选列表
调查询、过滤、词法/语义召回
重排
新版为何排在旧版后面
加入有效期信号,评估重排效果
上下文
正确片段是否实际传给模型
修截断、去重与顺序
回答与引用
数值、单位、条件是否被证据支持
修输出约束与程序校验

每次只针对已定位的环节修改,并保留修改前后的请求轨迹。否则即使结果变好,也难判断改动是否真的解决了根因。

九、上线前的简短检查表

  • 正确文档可按 ID 查到,版本和权限元数据可靠。
  • 表格、代码块、标题与关键限定条件没有在解析或切块时丢失。
  • 评估集覆盖正常题、版本冲突题、多条件题、无答案题。
  • 初始检索、重排和最终上下文可分别观测。
  • 引用能回到原始文档;不存在的引用会被拦截。
  • 关键数值与适用条件经过支持性抽样核验。
  • 每次配置变更都跑固定回归集,并记录延迟与成本。

RAG 排障的核心,是把“答案错了”还原成可验证的具体环节:资料是否在、片段是否完整、检索是否命中、上下文是否保留、结论是否被引用支持。模型是链路的最后一环;当上游证据有问题时,先把证据链修通,通常比盲目换模型更可控。

参考资料

  1. Lewis 等,Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks:https://arxiv.org/abs/2005.11401
  2. Thakur 等,BEIR: A Heterogenous Benchmark for Zero-shot Evaluation of Information Retrieval Models:https://arxiv.org/abs/2104.08663
  3. Liu 等,Lost in the Middle: How Language Models Use Long Contexts:https://arxiv.org/abs/2307.03172

相关学习资料