RAG 检索不能只靠向量 TopK:Dense + Sparse + RRF + Reranker 的完整链路

3 阅读11分钟

混合检索、BM25 稀疏召回、RRF 融合、Reranker 精排、Query Enhancement、多 Collection 联合搜索、优雅降级与召回调试日志。我想解决的不是“把库查一下”,而是“怎么把检索质量做成一条稳定的工程链路”。

前面几篇我写了文档加载、分块、向量化和向量索引。
如果说前面这些模块都在为 RAG 铺基础,那这一篇开始进入真正决定结果质量的核心层:检索查询

很多 RAG demo 到检索这一步时,都会采用一个最经典的流程:

  1. 问题做 Embedding
  2. 去向量库搜 topK
  3. 把结果丢给 LLM

这个流程当然能跑,但如果你真的做过线上场景,很快就会发现它的问题:

  • 关键词明确但语义不强的查询,纯向量不一定稳
  • 查询问法稍微绕一点,召回就可能偏
  • 候选里有很多近重复内容
  • 命中了相关文档,但排序不对
  • 多知识库场景下,结果很容易被某一类内容“淹没”

所以我越来越不相信“纯向量 topK 就够了”这件事。
RAG Pipeline Hub 里,我更想做的是一条质量治理链路,而不只是一个检索接口。

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

为什么纯向量检索通常不够

向量检索的优势很明显:

  • 能做语义匹配
  • 对表述变化更鲁棒
  • 比关键词匹配更像“理解问题”

但它也有天然短板:

1. 关键词精确性不一定强

比如某些专有名词、接口名、报错码、字段名,纯向量并不一定比关键词更可靠。

2. 查询表达稍有偏差就可能召回不稳

用户的问题往往不是最适合检索的表达。
很多时候,它需要先被改写、展开或拆成多个查询角度。

3. 候选集质量差,后面生成也没法救

如果召回结果本身混乱、重复、缺少覆盖性,那后面的 Prompt 和 LLM 再强,也很难凭空把答案补出来。

所以对我来说,检索模块不应该只做“查”,而应该做:

召回质量提升、候选结果治理、排序优化和可观测性建设。

我为什么把检索模块单独做成一条链路

这个项目里,检索查询模块是独立于向量索引模块的。

因为在我看来,索引层负责的是:

  • 向量怎么存
  • Collection 怎么管
  • 稠密和稀疏结构怎么准备

而检索层负责的是:

  • 问题怎么理解
  • 候选怎么召回
  • 多路结果怎么融合
  • 候选怎么精排
  • 最终上下文怎么交给生成模块

也就是说,它更像是一个检索编排层

所以这部分我没有写成“一个 search 接口 + 一次 Milvus 调用”,而是拆成了:

  • 检索服务
  • Query Enhancement 服务
  • BM25 查询服务
  • Reranker 服务
  • 历史与调试能力

因为我想解决的不是“能查”,而是“查得更像一个真正可用的 RAG 检索系统”。

这条检索链路现在长什么样

RAG Pipeline Hub 里,当前混合检索链路大致是:

用户查询
  -> Query Enhancement
  -> Dense Recall
  -> Sparse Recall
  -> 候选集合并
  -> RRF 融合
  -> 去重与过滤
  -> Reranker 精排
  -> 输出最终上下文

从工程视角看,这比“查一次向量库”复杂了不少。
但这种复杂,不是为了炫技,而是因为真实查询质量问题本来就不是单一方法能解决的。

Dense Recall 和 Sparse Recall 分别在解决什么

混合检索最容易被误解的一点是:
很多人以为它只是“多查一次”。

但其实 Dense 和 Sparse 在解决的是两类不同问题。

Dense Recall

也就是稠密向量召回。

它擅长:

  • 语义相近
  • 表达改写
  • 同义说法
  • 主题相关性

比如用户问题和文档不完全同词,但意思接近,Dense 往往会更有优势。

Sparse Recall

也就是稀疏向量 / BM25 风格召回。

它擅长:

  • 关键词强约束
  • 专有名词
  • 字段名 / 参数名 / 接口名
  • 某些需要精确字面命中的查询

如果只有 Dense,很多“关键词强”的场景可能不够稳。
如果只有 Sparse,又很容易丢掉语义扩展能力。

所以我更倾向于把它们理解成:

Dense 负责语义,Sparse 负责字面,混合检索负责把两者的优势拼起来。

为什么需要 RRF,而不是简单拼接结果

Dense 和 Sparse 都召回完之后,还不能直接把结果一股脑丢给生成模块。

因为它们的得分空间通常并不一致,直接拼接会很容易出现:

  • 某一路结果过于占优
  • 候选集顺序不稳定
  • 相关结果因为打分不可比而被压下去

所以这个项目里采用了 RRF,也就是 Reciprocal Rank Fusion

它的价值在于:

  • 不强依赖原始分数绝对值
  • 更关注“在各路召回里排得靠不靠前”
  • 融合效果通常比简单拼接更稳定

我很喜欢 RRF 的一个原因是:
它特别适合在“多路召回,但分数体系不统一”的场景下做一个可靠的中间融合层。

换句话说,RRF 不是最终排序,但它是一个非常适合做“粗排融合”的工程工具。

为什么还要上 Reranker

很多人做到 Dense + Sparse + RRF 就停了。
如果只是 demo,这已经不错了。

但如果你想继续提升质量,会发现一个问题:

召回和融合做得再好,最终候选排序仍然不一定最理想。

尤其当候选集里有这些情况时:

  • 多条内容都看起来相关
  • 有些 chunk 语义接近,但回答问题的直接性不同
  • 同主题内容很多,但只有一两条真正最有用

这时候 Reranker 的价值就会非常明显。

在这个项目里,Reranker 用来做的是:

对候选集进行更高质量的相关性重排序。

它不是负责“从零开始搜索”,而是负责:

  • 在候选中再做一轮精排
  • 把更直接回答问题的片段推上来
  • 压低一些看起来相关但实际没那么有用的内容

所以在我看来:

  • Dense / Sparse 更像召回层
  • RRF 更像融合粗排层
  • Reranker 更像最终质量校正层

这三者的职责其实很清晰。

Query Enhancement 为什么值得单独做

这是我在检索模块里非常看重的一块。

因为很多时候,用户的问题并不是“最适合搜索的写法”。

比如用户可能:

  • 问得很口语化
  • 省略了关键上下文
  • 把多个意图揉在一个问题里
  • 只给出一个模糊描述

如果直接拿原始问题去检索,召回质量可能就会受影响。

所以这个项目里单独做了 Query Enhancement,主要承担两件事:

1. Query Rewrite

把用户问题改写成更适合检索的表达。

2. Multi-query

围绕同一个问题生成多个查询变体,提升召回覆盖率。

这件事的意义很大,因为它相当于把“用户语言”和“检索语言”之间做了一层桥接。

而且我很喜欢它的一点是:

它解决的不是排序问题,而是更前面的“召回入口质量”问题。

很多时候,问题问得不对,后面所有检索优化都只能在一个偏掉的起点上继续努力。

为什么我做了“三层防御体系”

这部分是我觉得检索工程里特别容易被忽略、但实际非常重要的点。

因为在真实系统里,候选结果的常见问题不只是“不相关”,还有:

  • 结果过多
  • 结果过度重复
  • 结果缺少来源多样性
  • 某些边缘低质量内容被挤进来了

所以在这个项目里,我专门做了一个“三层防御体系”,大致包括:

1. 候选配额控制

先控制候选规模,避免后续精排和生成都被无效内容拖累。

2. 近重复内容去重

避免同一段内容或高相似片段反复进入候选集。

3. 动态阈值与来源多样性控制

尽量让最终结果既相关,又不过度集中在单一来源。

这套机制的价值在于,它把“检索结果质量”从单一分数问题,扩展成了一个更完整的候选治理问题。

我越来越觉得,RAG 检索想做稳,不能只看“有没有命中”,还要看:

  • 命中的内容是不是够全
  • 是不是够干净
  • 是不是足够多样
  • 能不能真正作为生成上下文使用

多 Collection 联合搜索为什么重要

如果只是一个小型 demo,一个 collection 通常就够了。

但一旦你进入多知识库、多模型、多业务域场景,就会很快遇到跨集合检索的需求。

比如:

  • 一部分内容是产品文档
  • 一部分是接口文档
  • 一部分是内部规范
  • 不同 collection 背后甚至可能绑定不同 embedding 模型

这时候,多 Collection 联合搜索就非常重要。

它解决的是:

  • 查询范围怎么跨知识库扩展
  • 不同来源内容怎么统一召回
  • 最终结果怎么在多个集合之间融合

这也是为什么我在项目里把 Collection 列表、联合搜索和相关元信息专门做成独立能力。

为什么优雅降级和调试日志不能缺

检索模块依赖很多外部能力:

  • Embedding
  • BM25 统计
  • Reranker
  • Query Enhancement LLM
  • Milvus

只要其中任何一个不稳定,检索流程都可能受影响。

所以我在这个模块里非常强调“优雅降级”。

比如:

  • 稀疏向量不可用时,自动降级为纯稠密检索
  • Reranker 不可用时,跳过精排
  • Query Enhancement 失败时,回退到原始查询

我不希望系统因为某个增强组件挂了,就直接不可用。

同时,我还很看重调试日志。
因为检索问题如果没有调试信息,通常非常难排查。

所以这个模块里专门保留了召回调试日志,记录:

  • 粗排情况
  • 精排情况
  • 最终召回情况

这不是锦上添花,而是检索系统能不能持续优化的基础。

这一层和普通“向量 topK 检索”最大的区别是什么

如果只做最小实现,检索层可以非常简单:

问题 -> 向量化 -> topK -> 返回结果

但在这个项目里,我想做的不是“最短路径”,而是一条更完整的质量链:

  • Query Enhancement
  • Dense Recall
  • Sparse Recall
  • RRF 融合
  • 候选治理
  • Reranker 精排
  • 联合搜索
  • 优雅降级
  • 调试日志

所以它和普通 demo 的差别,不是“多几个功能点”,而是:

它把检索从单点搜索动作,升级成了一条质量治理流程。

我对这个模块的一个核心判断

如果只让我总结一句话,我会说:

RAG 检索不能只靠向量 topK,真正稳定的检索系统应该是一条把召回、融合、精排、增强和质量控制串起来的完整链路。

这条链做得好,生成模块拿到的上下文才有质量基础。
这条链做不好,后面的 Prompt 再花哨,也只是让模型在不够理想的候选上继续发挥。

所以在 RAG Pipeline Hub 里,我才会愿意把检索模块单独做成一个真正有编排、有降级、有调试能力的服务层。

下一篇写什么

检索模块解决的是“怎么把对的上下文找出来”。
接下来最后一个问题就是:

这些上下文,怎么和用户问题一起组织成更像产品而不是实验的生成体验。

所以下一篇我会继续写:

《RAG 的最后一公里:流式生成、来源引用和历史记录应该怎么做》

项目地址:

  • GitHub: https://github.com/qingni/rag-pipeline-hub

如果这篇文章对你有帮助,欢迎:

  • 点个 star
  • 提个 issue
  • 留言说说你最常用的检索链路

如果你也做过混合检索、Reranker、Query Rewrite 或多知识库联合搜索,欢迎交流你的经验。
我越来越确信,很多 RAG 系统看起来差在“模型回答”,其实真正拉开差距的地方,常常就在检索这一层。