混合检索、BM25 稀疏召回、RRF 融合、Reranker 精排、Query Enhancement、多 Collection 联合搜索、优雅降级与召回调试日志。我想解决的不是“把库查一下”,而是“怎么把检索质量做成一条稳定的工程链路”。
前面几篇我写了文档加载、分块、向量化和向量索引。
如果说前面这些模块都在为 RAG 铺基础,那这一篇开始进入真正决定结果质量的核心层:检索查询。
很多 RAG demo 到检索这一步时,都会采用一个最经典的流程:
- 问题做 Embedding
- 去向量库搜
topK - 把结果丢给 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 系统看起来差在“模型回答”,其实真正拉开差距的地方,常常就在检索这一层。