RAG 的三个失效环节:检索、重排、生成各自会怎么坏

举报
yd_232401777 发表于 2026/10/24 08:35:53 2026/10/24
【摘要】 RAG 效果不好时,绝大多数人第一反应是"换个更好的 embedding 模型"。但这条链路有三个独立环节,每个环节的失效表现完全不同,用同一种手段去治注定无效。这篇把检索、重排、生成三段各自的失效机理拆开,给出可区分的诊断信号和分阶段评估方法。

前言

RAG 这套架构现在几乎是知识问答的默认方案。但它的调试体验很糟糕:最终输出不好,你不知道是哪一段坏的。

检索没找到、找到了但排错了、排对了但模型没用好——这三种情况从用户视角看完全一样,都是一句错误的回答。而它们的修法截然不同:第一种要改切分和索引,第二种要换重排,第三种要改提示结构和引用约束。用错了手段,你会陷入"换了个更强的模型,效果没变"的循环。

这篇文章试图把这三段拆开讲清楚,重点是每段的失效长什么样、怎么和另外两段区分开。

一、检索阶段:不是"没找到",是"找错了"

检索失效是最容易被误判的。因为我们习惯看"返回了什么",而不看"该返回什么"。

1.1 query 和文档不在同一个语义空间

这是最常见也最隐蔽的问题。

用户问的是问题,库里存的是陈述。这两者的 embedding 分布其实不一样。

举个典型例子。用户问:"为什么我的定时任务没有按时触发?"

库里有一段文档写的是:"调度器在检测到队列积压超过阈值时,会延后新任务的入队时间。"

这两句话在语义上是相关的,但向量相似度可能很低——因为前者是疑问语气、聚焦"失败现象",后者是陈述语气、聚焦"机制描述"。bi-encoder 把两句话各自压成一个向量再算余弦,中间丢失的信息就包括"提问 vs 陈述"这个语用差异。

常见的缓解手段有几种,代价不同:

手段做法代价
多查询扩展让模型把一个问题改写成 3-5 个不同表述,分别检索后合并检索次数翻倍,延迟上升
HyDE让模型先"编"一个假答案,用假答案去检索编错了会把检索带偏
关键词混合向量检索 + BM25 稀疏检索,结果融合需要维护两套索引
文档侧改写离线给每段文档生成"它可能回答哪些问题"一次性成本,索引体积变大

我倾向最后一种。因为它把成本放在离线阶段,在线延迟不变,而且对"提问 vs 陈述"这个差异是直接对症的。

1.2 chunk 切分破坏了语义边界

固定窗口切分(比如每 512 token 一刀)有个根本问题:它假设语义边界和 token 边界对齐,而这两者基本不相关。

被切断的典型形态:

  • 一个概念的定义在上块结尾、展开在下块开头
  • 表格的表头在上块、数据行在下块
  • 代码的函数签名在上块、实现在下块

后果是检索命中了一块"半截话",重排模型看到它也没法给出高分,最后生成阶段拿到一段没有主语的片段。

改善方向是按结构切而不是按长度切:Markdown 按标题层级切、代码按函数/类切、HTML 按块级元素切。做完结构切分之后,再对超长的块做二次切分,切的时候带上父级标题作为上下文前缀。

一个实用的技巧是重叠窗口 + 上下文注入:

块内容:<h2>调度器</h2> 在检测到队列积压超过阈值时,会延后新任务的入队时间。

把标题塞进块内容里,这一块的检索命中率会明显好于孤立的正文。因为标题提供了"这段在讲什么"的弱信号,而这个信号恰好是 query 里最常出现的词。

1.3 embedding 模型的域偏移

通用 embedding 模型是在开放语料上训的。如果你的库是内部技术文档、法律条文、医疗记录这类术语密度极高的文本,通用模型会把大量专有名词映射到相近的区域,导致"不同概念看起来很像"。

判断方法比较直接:找一批你确信不相关的文档对,算它们的相似度。如果两个八竿子打不着的东西余弦相似度到了 0.8 以上,说明这个模型的表示在你的域上塌缩了。

这时候要么换领域模型,要么做微调。微调 embedding 的成本比想象中低——用对比学习,正样本是同段落的不同表述,负样本用 in-batch 负例,几千对就能有可感知的提升。

二、重排阶段:分数不可比

重排(rerank)常被当成"锦上添花",实际上它解决的是检索阶段的一个硬伤:bi-encoder 无法建模 query 和 doc 之间的交互。

2.1 两种架构的目标差异

bi-encoder(检索)cross-encoder(重排)
输入query 和 doc 分别编码query + doc 拼接后一起编码
交互无,只在最后算余弦/点积有,注意力可以跨两者
复杂度doc 向量可离线算好,在线 O(1) 查表每个候选都要跑一次前向,O(N)
精度较低明显更高

所以标准做法是:用 bi-encoder 从百万级里粗捞出 top-100,再用 cross-encoder 精排到 top-5。这是精度和延迟的折中。

2.2 失效点一:截断

cross-encoder 有最大输入长度。当 query + doc 超过这个长度,通常的做法是从尾部截断。

问题在于,关键信息可能在中后段。一段 1000 字的文档,真正的答案在第 800 字,截断到 512 就正好切掉了。

这个失效很隐蔽,因为重排不会报错,只是给了一个偏低的分。表现出来就是"这段明明是对的,但没进 top-k"。

对策:重排前先确认文档长度分布。如果长尾很长,要么换支持更长输入的 reranker,要么在切分阶段就把块控制在重排窗口内(这也是为什么 chunk 大小不应该大于 reranker 的最大长度)。

2.3 失效点二:分数不可比

reranker 输出的是 logits 或归一化后的相关度。不同批次、不同 query 之间的分数不可直接比较。

有人会设一个全局阈值——比如"分数低于 0.3 就丢掉"。这个做法在跨 query 场景下是错的,因为分数分布随 query 变化。同一条文档,对 query A 得 0.2,对 query B 得 0.8,不代表它对 B 更相关,可能只是 B 的候选集合整体分数偏高。

正确的做法是用同一 query 内部的相对排序,或者用 top-1 与 top-k 的分数差来判断置信度:

gap = score[0] - score[k-1]

gap 大说明第一名明显突出,可以只取一条;gap 小说明候选之间难以区分,应该多取几条让生成阶段自己判断。

三、生成阶段:上下文给了,模型没用

这一段最反直觉。你把正确的文档放在上下文里,模型仍然可能答错。

3.1 位置偏差

关于长上下文利用有一个被反复验证的现象:模型对上下文开头和结尾的内容利用得更好,中间的内容容易被忽略。这个现象常被描述为 U 型曲线。

对你的 RAG 的直接含义是:如果 top-1 的文档被放在长上下文的中间位置,它可能还不如放在最后一条有用。

实践上的处理很简单:把最相关的放最前面,次相关的放最后面,中间的放不重要的。或者干脆控制上下文总量——5 条精排后的文档,好过 20 条粗排的。

3.2 冲突信息

检索回来的文档之间可能互相矛盾:旧版本文档说 A,新版本文档说 B。

模型面对冲突时不会主动报错,它倾向于综合两者给出一个模糊的答案,而这个答案通常两边都不对。

对策是在提示里显式要求处理冲突:

如果检索到的资料之间存在矛盾,不要自行调和。
请指出矛盾点,并优先采信标注了更新时间的较新资料。

同时给每段文档带上元信息(来源、更新时间),让模型有依据判断。

3.3 无引用约束

如果提示里只说"根据以下资料回答",模型在资料不足时会用参数里的知识补全,而补全的内容和资料混在一起,用户无法分辨。

加上引用要求之后,行为会明显改变:

回答中的每个事实性陈述后面,用 [1] [2] 标注来源编号。
如果资料无法支撑某个结论,直接说"资料中未提及",不要推测。

这个约束的价值不只是让答案可溯源,更重要的是它给了模型一个"承认不知道"的出口。没有这个出口,模型的默认行为是硬答。

四、分阶段诊断

要定位问题在哪一段,就得分开评估,而不是只看端到端效果。

4.1 三个指标

阶段指标怎么算
检索Recall@k标注的正确答案是否出现在 top-k 里
重排nDCG@k / MRR正确答案排在第几位
生成忠实度(faithfulness)答案中的陈述是否都能被上下文支撑

关键点:Recall@k 是上限。如果正确答案不在检索结果里,后面两段做得再好也是零。所以诊断顺序应该是先看召回,再看排序,最后看生成。

很多人直接跳到"换个更大的模型",但如果 Recall@20 只有 0.6,换模型顶多把剩下 40% 的失败率里的排序问题修一修,天花板还在。

4.2 定位表

症状可能原因优先检查
答案完全跑题检索召回失败Recall@k、embedding 相似度分布
答案沾边但不准重排把对的排下去了top-1 与人工标注对比
答案对一半上下文太长或位置不好上下文条数与顺序
答案编造缺引用约束提示里的引用要求
同一问题两次答案不同温度过高 / 无确定性约束temperature、结构化输出

做了什么

  1. 把 RAG 拆成检索、重排、生成三段,分别讲清失效机理
  2. 指出 query 与文档的"提问 / 陈述"语用差异是召回失败的常见根因,对比了四种缓解手段的代价
  3. 说明固定窗口切分破坏语义边界,给出按结构切 + 标题前置的做法
  4. 讲清 bi-encoder 与 cross-encoder 的架构差异,以及重排阶段的截断与分数不可比问题
  5. 指出生成阶段的位置偏差、冲突信息、无引用约束三个失效点
  6. 给出三阶段的独立指标与定位表,明确 Recall@k 是整体上限

一些体会

"换个更强的模型"是最贵的无效动作。 模型能力提升的是"给定上下文后的推理质量",它修不了"上下文里压根没有正确答案"。在动手换模型之前,先量一下召回率——这一步能省掉大量试错。

分数不要跨 query 比较。 这是个很容易犯的错,因为代码写起来很自然:if score < 0.3: drop()。但 reranker 的分数是相对的,全局阈值在不同 query 上的行为完全不一致。

长上下文不是免费的午餐。 能塞 128K 不代表该塞 128K。上下文越长,中间内容被利用的概率越低,成本还更高。精选 5 条经常好过全量 20 条。

给模型留"不知道"的出口。 不要求引用、不允许说"资料未提及",模型就只能硬答。这个约束看起来是格式要求,实际是幻觉控制的主要手段之一。

代码

分阶段评估的骨架(伪代码,指标实现用标准定义):

def evaluate_pipeline(queries, gold, retriever, reranker, generator, k_ret=20, k_rer=5):
    """
    queries: 问题列表
    gold:    {query: [正确文档 id 列表]}
    """
    recall_hits = 0
    mrr_sum = 0.0
    faithful = 0

    for q in queries:
        # ---- 阶段一:检索 ----
        cand = retriever.search(q, top_k=k_ret)          # -> [(doc_id, score)]
        gold_ids = set(gold[q])
        hit = [c for c in cand if c[0] in gold_ids]
        recall_hits += 1 if hit else 0

        # ---- 阶段二:重排 ----
        reranked = reranker.rerank(q, [doc for doc, _ in cand])[:k_rer]
        for rank, doc_id in enumerate(reranked, start=1):
            if doc_id in gold_ids:
                mrr_sum += 1.0 / rank
                break

        # ---- 阶段三:生成 ----
        answer = generator.answer(q, [doc_text[d] for d in reranked])
        if all_claims_supported(answer, [doc_text[d] for d in reranked]):
            faithful += 1

    n = len(queries)
    return {
        f"Recall@{k_ret}": recall_hits / n,   # 上限指标
        "MRR":             mrr_sum / n,        # 排序质量
        "Faithfulness":    faithful / n,       # 生成是否被上下文支撑
    }

all_claims_supported 需要模型辅助判断,常见做法是把答案拆成原子陈述,逐条检查是否有出处。这一步本身也是个 LLM 调用,成本不低,建议只在评估集上跑。

上下文组装的顺序控制:

def build_context(docs_with_scores):
    """最相关的放最前,次相关的放最后,避免关键信息落在长上下文中段"""
    if len(docs_with_scores) <= 2:
        return docs_with_scores
    head = docs_with_scores[0]
    tail = docs_with_scores[1]
    middle = docs_with_scores[2:]
    return [head] + middle + [tail]

这段看着像小技巧,但它解决的是"检索和重排都对了、答案还是错"这类最难查的问题。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。