先看一个最基础的 RAG 检索流程:
css
用户 Query
↓
Embedding 模型
↓
向量检索(Dense Retrieval)
↓
召回 Top K 文档
↓
LLM 生成答案
看着挺合理对吧?但实际跑起来你会发现:只靠 Dense Embedding,翻车概率不低。
问题出在哪?看一个场景就懂了。
Query:iPhone 15 支持快充吗?
Dense 检索召回:
| 召回结果 | 问题出在哪? |
|---|---|
| 《iPhone 14 快充测试》 | ❌ 型号不同,用户要的是 15 |
| 《安卓手机快充功率对比》 | ❌ 说的是安卓,不是 iPhone |
Dense 抓到了"iPhone"和"快充"这两个语义核心,但没区分"15"和"14"、"iPhone"和"安卓" 。
它知道这些词"意思相近",但分不清是不是用户要的那个精确东西。
这就是 Dense 的核心问题:
它懂"意思像不像",但搞不定"关键词对不对"。
所以,我们需要另一种检索方式------不关心"意思",只关心"词"。
这就引出了这次要讲的两种检索方式:Dense 和 BM25。
一、Dense Retrieval:解决"意思像不像"
Dense Retrieval 的核心思路是:把 Query 和 Document 都映射到同一个向量空间,然后算相似度。
vbnet
Query: "LOL 怎么注册账号?"
↓ Embedding
[0.23, 0.87, -0.45, ...]
↓ 余弦相似度
Document: "英雄联盟账号注册方法"
✅ 擅长场景
| 场景 | 示例 |
|---|---|
| 同义词替换 | "LOL" ↔ "英雄联盟" |
| 近义表达 | "注册" ↔ "创建账号" |
| 口语化 Query | "怎么搞" ↔ "操作方法" |
| 措辞不一致 | Query 和文档说法不同 |
❌ 不擅长场景
| 场景 | 示例 |
|---|---|
| 精确数字/版本号 | "1.16.0" vs "1.17.0" |
| 专有名词拼写 | "Qdrant" vs "Qdrant"(拼错就凉) |
| 低频长尾词 | 罕见术语容易被"平滑"掉 |
一句话总结:Dense 关注的是"意思像不像",而不是"词对不对"。
二、BM25:解决"关键词对不对"
BM25 是经典的词法检索算法,它不靠向量,靠的是词频 + 逆文档频率。
它的核心逻辑很简单:
vbnet
Query: "Qdrant 1.16.0 Sparse Vector"
↓ 分词
["Qdrant", "1.16.0", "Sparse", "Vector"]
↓ 匹配
Document: "Qdrant 1.16.0 新增 Sparse Vector 支持"
✅ 四个关键词全部命中
BM25 打分公式(不用背,但要理解)
BM25 主要考虑四个因素:
| 因素 | 说明 |
|---|---|
| 词频(TF) | 这个词在文档里出现了几次 |
| 逆文档频率(IDF) | 这个词在整个文档集合里有多罕见 |
| 文档长度 | 短文档匹配相同词,得分更高 |
| 饱和机制 | 词频高到一定程度后,收益递减 |
✅ 擅长场景
| 场景 | 示例 |
|---|---|
| 精确匹配 | 版本号、产品名、ID |
| 专有名词 | "Qdrant"、"Sparse Vector" |
| 长尾关键词 | 罕见术语 |
❌ 不擅长场景
| 场景 | 示例 |
|---|---|
| 同义词 | 搜"LOL"匹配不到"英雄联盟" |
| 语义泛化 | 搜"登录问题"匹配不到"认证失败" |
三、Dense vs BM25:一个互补的关系
看到这里你会发现,Dense 和 BM25 正好互补:
| 对比维度 | Dense | BM25 |
|---|---|---|
| 匹配粒度 | 语义级别 | 词级别 |
| 核心优势 | 泛化能力强 | 精确匹配强 |
| 典型问题 | 对精确词不敏感 | 对同义词无感知 |
Dense 懂语义,BM25 认关键词,两者正好互补。
这就引出了下一个问题:
能不能让它们一起上?
四、Hybrid Search:两条腿走路
既然各有优劣,那不如一起上:
css
Query
│
┌───────┴───────┐
↓ ↓
Dense BM25
↓ ↓
Top 20 Top 20
└───────┬───────┘
↓
结果融合
↓
Top K
一个例子说明白
Query:LOL账号注册失败怎么办?
| 检索方式 | 召回结果 |
|---|---|
| Dense | D1: 英雄联盟账号无法登录怎么办? D2: 游戏账号登录异常处理方法 |
| BM25 | D1: 英雄联盟账号无法登录怎么办? D3: LOL账号注册失败处理方法 |
- 只用 Dense:漏掉了包含 "LOL" 和 "注册失败" 的 D3
- 只用 BM25:漏掉了语义相关的 D2
- Hybrid:D1 + D2 + D3 全都有
Hybrid 的目的:让不同检索器互相补充,提高召回的完整性。
五、RRF:两个结果怎么合并?
Hybrid 把 Dense 和 BM25 的结果拿回来了,但新问题来了:
ini
Dense 得分: D1 = 0.92, D2 = 0.87
BM25 得分: D1 = 15.3, D2 = 12.8
两个分数不在同一个量纲上,不能直接相加!
RRF 的核心思想
RRF(Reciprocal Rank Fusion)不看原始分数,只看排名:
scss
RRF(d) = Σ 1 / (k + rank_i(d))
其中:
rank_i(d):文档 d 在第 i 个检索器中的排名k:常数(通常取 60,用于平滑)
举例计算
假设 k = 60:
| 文档 | Dense 排名 | BM25 排名 | RRF 分数 |
|---|---|---|---|
| D1 | 1 | 3 | 1/61 + 1/63 ≈ 0.0323 |
| D2 | 2 | 1 | 1/62 + 1/61 ≈ 0.0325 |
D2 在两个检索器中都排名靠前,RRF 融合后排得更靠前。
RRF 的本质:不管分数怎么算,只看谁被排得靠前,综合多个排序来重新排名。
六、Reranker:从"找全"到"排准"
到这里,我们已经有了 Hybrid Search 召回的候选集(比如 Top 100)。但这里面依然混杂着:
非常相关 ⭐⭐⭐
比较相关 ⭐⭐
有一点相关 ⭐
完全不相关 ❌
Retrieval 阶段负责"尽可能找全",Reranker 阶段负责"尽可能排准"。
工作流程
css
Top 100(Hybrid 召回)
↓
Reranker 模型
(如 Cohere Rerank、BGE-Reranker)
↓
逐对计算 Query-Document 相关性
↓
重新打分 & 排序
↓
Top 5(给 LLM)
一个例子
Query:Qdrant 如何修改 Vector Size?
| 文档 | 原始排名 | Reranker 打分 | 最终排名 |
|---|---|---|---|
| D2: Qdrant Vector Size 修改方法 | 5 | 0.96 | 🥇 第1 |
| D1: Qdrant Collection 创建方法 | 3 | 0.71 | 🥈 第2 |
| D3: Qdrant 是什么 | 8 | 0.43 | 🥉 第3 |
| D4: 向量数据库介绍 | 10 | 0.31 | 第4 |
Reranker 能把真正最相关的文档提到最前面。
Retrieval vs Reranker
| 对比维度 | Retrieval | Reranker |
|---|---|---|
| 目标 | 找得全(高召回) | 排得准(高精度) |
| 处理量 | 百万级 → Top 100 | Top 100 → Top 5 |
| 模型类型 | 双塔/向量模型 | 交叉编码器(更重但更准) |
| 速度 | 快 | 慢(但只处理少量候选) |
七、完整流程一览
把每一层串起来:
css
Query
│
┌──────────┴──────────┐
↓ ↓
Dense BM25
(语义匹配) (关键词匹配)
↓ ↓
Top 20 Top 20
└──────────┬──────────┘
↓
RRF
(排名融合,统一排序)
↓
Top 100
↓
Reranker
(交叉编码器精排)
↓
Top 5
↓
LLM
(生成最终答案)
各层职责一览
| 阶段 | 技术 | 职责 |
|---|---|---|
| 召回层 | Dense | 理解语义,找意思相近的 |
| 召回层 | BM25 | 精确匹配,找关键词命中的 |
| 融合层 | RRF | 统一不同检索器的排名 |
| 精排层 | Reranker | 逐对判断,挑出最相关的 |
| 生成层 | LLM | 基于精排结果生成答案 |
八、一句话记忆法
Dense 找"意思像"的,BM25 找"词匹配"的,Hybrid 全都要,RRF 统一排名,Reranker 精挑细选,LLM 生成答案。