在实际的 RAG 检索增强生成问答系统中,检索质量往往比 Prompt 技巧更早决定答案上限。很多项目第一次跑通时,用的是“文档切块 + 向量化 + 向量数据库召回 + 拼接 Prompt 让大模型回答”这条链路。演示环境里效果不错,一旦换成真实知识库,问题立刻出现:用户输入一个产品型号,向量检索返回一堆语义类似但型号不对的文档;用户换一种更自然的说法提问,关键词匹配又召回不到内容。这是因为单路检索有边界:稀疏向量检索擅长精确关键词,稠密向量检索擅长语义相似。要让问答系统更稳,通常把这两路结果合并到一起,而 RRF 倒数排名融合算法是实践中很常见的合并方式。
下面从概念、环境、代码、验证、排错五个方面,完整梳理一套 RAG 检索增强生成问答系统的落地思路。文章会提供可直接运行的 Python 示例,包括 BM25 稀疏检索、Embedding 稠密检索、RRF 融合、FastAPI 服务和本地大模型调用接口。适合正在做知识库问答、RAG 项目原型验证,或者想把检索层做扎实的开发者。
1. 先倒推问题:为什么 RAG 需要多路检索
1.1 从“幻觉”说起,理解 RAG 为什么多一个检索器
大模型在训练时学到的知识有时间截止点,也无法覆盖企业内部的私有文档。用户提问时,模型如果只凭参数里的记忆回答,就容易出现知识点过期、编造细节、引用了不存在的来源等情况。RAG 的思路不是让模型记住更多知识,而是在回答前先做一次检索,把相关资料作为临时上下文交给模型,再让模型基于资料生成答案。
RAG 全称是 Retrieval-Augmented Generation,中文叫检索增强生成。它通常分成四个阶段:
- 文档解析和切块,把知识库拆成适合检索的文本单元。
- 检索,从文本单元中召回与用户问题相关的候选内容。
- 增强,把召回结果拼接到 Prompt 中,附加上下文和生成规则。
- 生成,大模型基于上下文输出答案,并尽量引用来源。
这里的核心矛盾是:检索结果好不好,直接决定大模型看到什么。如果召回的前三名文档本身就离题,Prompt 写得再好,大模型也很难给出准确回答。所以在 RAG 项目里,检索器的质量比生成环节的调参更值得投入。
1.2 稠密向量检索的边界不是“有没有用”,而是“精确匹配”
稠密向量检索是目前 RAG 项目里最常见的一路检索。它用 Embedding 模型把文本编码成固定长度的连续向量,再用余弦相似度或内积找最相似的文本。它对同义改写、语义相近但关键词不同的查询更友好。例如用户说“怎么处理发票金额填错”,文档里写的是“发票金额录入错误如何处理”,查询和文档没有完全相同的关键词,但语义接近,稠密检索能把它找出来。
不过稠密向量检索在精确匹配场景下并不稳定。产品型号、合同编号、身份证号、人名、案号这类内容,本质上是“精确字符串”而不是“语义概念”。向量模型在编码这类字符串时,很容易把相近但不相同的编号推向相邻区域。结果就是用户输入“HT-2024-0001”,检索系统把“HT-2024-0002”也当成高度相似内容排在前面。
另一个问题是不同 Embedding 模型产出的分数区间差异很大。某个模型的余弦相似度 0.78 可能已经很高,另一个模型的 0.78 可能只是普通水平。如果直接拿这些分数和 BM25 分数做加权求和,需要先做大量分数校准。
1.3 混合检索与 RRF 融合的整体定位
混合检索的思路很直接:两路检索各自解决不同的问题,最后再合成一个候选列表。常见组合是 BM25 稀疏检索加上稠密向量检索,合并策略可以用 RRF,也可以用学习出来的 reranker。
整体流程可以按下面这条链路理解:
用户问题 -> 稀疏检索(BM25 关键词精确匹配) -> 稠密检索(Embedding 语义相似召回) -> RRF 倒数排名融合 -> 取 TopK 文档 -> 拼接 Prompt -> 大模型生成答案RRF 不直接比较两路分数,而是把每个文档在各自检索结果里的“排名”作为唯一的融合依据。这样天然避开了 BM25 分数和向量余弦分数不可比的问题。下面从代码开始,逐步搭建这条链路。
2. 准备环境与可复现示例数据
2.1 Python 环境与依赖
推荐使用 Python 3.10 或更高版本。先创建虚拟环境,再安装依赖。下面命令里的包名和版本是示例,实际项目落地