我们先来介绍几种检索方式,在 RAG(Retrieval-Augmented Generation,检索增强生成)框架中,稀疏检索器(Sparse Retriever) 和 密集检索器(Dense Retriever) 是两种核心的文档检索方式,它们的主要作用是:
从海量知识库中找出与用户输入相关的文档,供语言模型参考生成回答。
稀疏检索器通常基于 传统的倒排索引(Inverted Index) 和 词频统计特征,(特征值获取也可以使用学习的手段)比如:
其本质是:
特性 | 说明 |
---|---|
表达方式 | 单词级稀疏表示(one-hot 或 TF-IDF) |
索引方式 | 倒排索引,效率高 |
检索速度 | 快,适合大规模文档 |
训练需求 | 无需训练(规则或简单统计) |
表达能力 | 依赖关键词,不能捕捉语义相似(如“car”和“automobile”不会匹配) |
sklearn.TfidfVectorizer
+ cosine_similarity
我们使用一个 Transformer 编码器(如 XLM-RoBERTa、BERT 等)将文本变为 token 级别的隐藏表示。
对于一个输入文档 d=[t1,t2,…,tn]d = [t_1, t_2, \dots, t_n]d=[t1,t2,…,tn],编码器输出:
Hd=[h1,h2,…,hn]∈Rn×d \mathbf{H}_d = [h_1, h_2, \dots, h_n] \in \mathbb{R}^{n \times d} Hd=[h1,h2,…,hn]∈Rn×d
其中 hi∈Rdh_i \in \mathbb{R}^dhi∈Rd 是第 iii 个 token 的表示向量。
用一个额外的线性头来学习每个词的重要性(也可以使用静态的方式如TF-IDF 是静态、规则计算的,而模这里是 基于上下文、可学习的、更灵活的语义权重。):
wi=ReLU(Whi+b)∈R w_i = \text{ReLU}(W h_i + b) \in \mathbb{R} wi=ReLU(Whi+b)∈R
其中:
我们将每个文档表示成一个“词袋(Bag-of-Words)+ 权重”的形式:
sd={(ti,wi)∣wi>θ} \mathbf{s}_d = \{ (t_i, w_i) \mid w_i > \theta \} sd={(ti,wi)∣wi>θ}
例如:
文档d: "人工智能 正在 改变 世界"
→ 稀疏表示:
{"人工智能": 2.5, "改变": 1.8, "世界": 1.0}
我们将所有文档的稀疏向量合并,构建倒排索引:
词项 | 文档ID | 权重 |
---|---|---|
人工智能 | d1 | 2.5 |
改变 | d1 | 1.8 |
世界 | d1 | 1.0 |
AI | d2 | 2.2 |
倒排索引支持快速查找“哪些文档出现了这个词”。
假设用户发起一个查询:
query: "人工智能 改变 生活"
通过编码器和投影头,我们也生成:
sq={"人工智能":2.0,"改变":1.5,"生活":1.2} \mathbf{s}_q = \{ "人工智能": 2.0, "改变": 1.5, "生活": 1.2 \} sq={"人工智能":2.0,"改变":1.5,"生活":1.2}
我们就拿 query 的每个词去查倒排表,看在哪些文档中也出现,然后做打分:
打分公式(共现词 × 权重乘积)
对于某个文档 ddd,与 query 的稀疏向量 sq\mathbf{s}_qsq 的匹配分数为:
score(q,d)=∑t∈q∩dwq(t)⋅wd(t) \text{score}(q, d) = \sum_{t \in q \cap d} w_q(t) \cdot w_d(t) score(q,d)=t∈q∩d∑wq(t)⋅wd(t)
也就是:
文档稀疏向量:
{"人工智能": 2.5, "改变": 1.8, "世界": 1.0}
Query 向量:
{"人工智能": 2.0, "改变": 1.5, "生活": 1.2}
共同词:
总得分:
score=5.0+2.7=7.7 \text{score} = 5.0 + 2.7 = 7.7 score=5.0+2.7=7.7
密集检索器将查询和文档编码为 低维稠密向量,通常使用 神经网络(Transformer 编码器) 如:
其核心机制是:
q, d
特性 | 说明 |
---|---|
表达方式 | 语义向量(dense embedding) |
索引方式 | FAISS、ScaNN、Milvus |
检索速度 | 稍慢但可加速(ANN 索引) |
训练需求 | 通常需要监督或对比学习训练 |
表达能力 | 强语义理解,可处理同义词和语义变体 |
Query 编码得到:
Hq=[hq1,hq2,...,hqN] H_q = [h_q^1, h_q^2, ..., h_q^N] Hq=[hq1,hq2,...,hqN]
Document 编码得到:
Hd=[hd1,hd2,...,hdM] H_d = [h_d^1, h_d^2, ..., h_d^M] Hd=[hd1,hd2,...,hdM]
所有 token 两两点积:
Si,j=hqi⋅hdj S_{i,j} = h_q^i \cdot h_d^j Si,j=hqi⋅hdj
池化得到最终分数:
smulti=mean-pooling or max-pooling(S) s_{\text{multi}} = \text{mean-pooling or max-pooling}(S) smulti=mean-pooling or max-pooling(S)
维度 | 密集检索 | 稀疏检索 | 多向量检索 |
---|---|---|---|
输入表示 | 单向量 [CLS] |
每词一个 scalar 权重 | 每词一个 token 向量 |
匹配方式 | 句子整体匹配 | 关键词重合匹配 | token-token 对齐匹配 |
精度 | 中 | 高(关键词匹配准) | 高(语义细粒度) |
速度 | 快 | 中 | 慢 |
可解释性 | 中 | 高 | 中 |
检索效果 | 好(适合语义召回) | 好(适合专业问答) | 很好(适合复杂文本匹配) |
有了基础的铺垫我们来了解一下Embedding 模型和ReRank 模型区别
模型类型 | Embedding 模型(双塔模型) | ReRank 模型(交叉编码器) |
---|---|---|
核心任务 | 将文本编码为向量,计算相似度 | 精排,对候选文本进行语义重排序 |
输入方式 | Query 和 Document 分别编码 | Query 和 Document 一起输入 |
输出 | 向量 → 相似度打分(如余弦、点积) | 单个相关性得分(如 0~1,回归或分类) |
示例模型 | BGE, GTE, E5, MiniLM, mBERT(BiEncoder) | BGE-Reranker, GTR-Rerank, monoT5, Jina-Reranker(CrossEncoder) |
Query → Encoder → 向量Q
Doc → Encoder → 向量D
相似度(Q, D) = dot(Q, D) or cos(Q, D)
FAISS
、Milvus
、ScaNN
等做 ANN 检索[CLS] Query [SEP] Doc [SEP] → Encoder → [CLS]打分
[CLS]
token 表示对比维度 | Embedding 模型(BiEncoder) | ReRank 模型(CrossEncoder) |
---|---|---|
编码方式 | Query 和 Doc 分开编码 | Query 和 Doc 一起编码 |
相似度计算 | 向量相似度(点积、cosine) | 直接输出得分(回归/分类) |
可否离线 | ✅ 文档可离线向量化 | ❌ 只能在线处理 |
检索速度 | 非常快,适合大规模库 | 慢,不能直接检索 |
准确性 | 粗排(Recall 好) | 精排(Precision 好) |
应用场景 | 初始检索、ANN、语义匹配任务 | rerank、问答对齐、排序优化 |
模型体积 | 小(几十 M ~ 数百 M) | 大(通常是 Transformer 全模型) |
代表模型 | BGE-small, GTE-base, E5-base | BGE-Reranker, monoT5, Jina-Reranker |
假设你要问:
“中国最长的河流是什么?”
Embedding 阶段找到可能相关文档:
→ 因为 "黄河"
和 "中华文明"
在向量空间里可能也靠近,所以 Doc1 也被粗排进来。
ReRank 阶段会判断:
→ 最终输出顺序为:[Doc2, Doc3, Doc1]
为了衡量检索系统的有效性,主要依赖两个指标:、
我们可以通过prompt工程来提升rerank效果,不依赖标注数据,而是直接利用LLM的语言能力来评估查询与文档的相关性,通过prompt引导模型生成相关性评分。这种方法可以分为三种: pointwise, listwise, pairwise。
原理:Point-wise方法将重排序问题转化为回归或分类问题,独立地对每个查询-文档对进行相关性评分。
特点:
常见实现:
示例流程:
查询: "如何优化数据库性能"
文档1: "数据库索引优化技巧" → 评分: 0.8
文档2: "Python编程基础" → 评分: 0.2
文档3: "SQL查询优化方法" → 评分: 0.9
原理:Pair-wise方法通过比较文档对的相对相关性来进行排序,学习文档间的相对排序关系。
特点:
基于pairwise ranking prompting (PRP),有三种不同的变体,以优化文档排序和 rerank 过程:
PRP-Allpair:该方法对所有候选文档两两进行比较,计算它们之间的优先级关系。优点是理论上能够实现最精细的排序,但其缺点也十分明显——时间复杂度为 O(N2)O(N^2)O(N2),当候选文档数量 NNN 较大时,计算成本极高,导致效率严重下降,不适合大规模实时场景。
PRP-Sorting:该方法借助高效的排序算法(如快速排序或堆排序),通过对文档集合进行整体排序来间接实现排序目标。相较于 Allpair 方法,时间复杂度降低至 O(NlogN)O(N \log N)O(NlogN),显著提升了排序效率。该方法适用于对所有文档都需要排序的中等规模场景,平衡了准确性和计算开销。
PRP-Sliding-K:该方法专注于关注前 KKK 个最相关文档的排序,采用类似冒泡排序的思想,利用滑动窗口对文档进行局部比较和调整。由于 rerank 任务通常只关心 top-KKK 文档,且 KKK 相对较小,因此总体时间复杂度为 O(KlogN)O(K \log N)O(KlogN),在保证排序质量的同时,大幅降低了计算复杂度,特别适合实时性要求较高且只需输出少量高相关文档的应用场景。
原理:List-wise方法直接对整个文档列表进行优化,学习最优的排序顺序。
特点:
原理:基于BERT等预训练语言模型,通过fine-tuning学习查询-文档的相关性。
架构特点:
训练过程:
输入: [CLS] 查询 [SEP] 文档 [SEP]
输出: 相关性分数 (0-1)
原理:将查询和文档拼接作为输入,通过transformer模型学习两者的交互关系。
优势:
劣势:
原理:使用两个独立的encoder分别编码查询和文档,通过相似度计算进行排序。
优势:
劣势:
使用代码:
from FlagEmbedding import BGEM3FlagModel
# 初始化模型
model = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True)
# 准备数据
query = "如何优化机器学习模型性能"
documents = [
"机器学习模型调优技巧和方法",
"深度学习网络优化策略",
"Python编程基础教程"
]
# 重排序
rerank_scores = model.compute_score(
sentence_pairs=[(query, doc) for doc in documents],
batch_size=32,
max_length=512
)
# 排序结果
sorted_docs = sorted(zip(documents, rerank_scores),
key=lambda x: x[1], reverse=True)
BGE-M3(BAAI General Embedding Model 3)是智源研究院开发的多语言、多功能、多粒度的embedding模型,特别适用于RAG系统的rerank任务。
BGE-M3 是一个支持多种检索方式的 embedding 模型,它同时支持:我们最开始说的三种检索方式。
并使用自蒸馏的技术:
之前说的三种方法都会产生一个分数 s₁
, s₂
, s₃
:
这就是“自蒸馏”:模型用自己的多个子结果互相指导学习,就像自己教自己怎么融合知识。
(1)密集检索分数 sdenses_{\text{dense}}sdense
取 [CLS][CLS][CLS] token对应向量(Hq[0],Hd[0]H_q[0], H_d[0]Hq[0],Hd[0])
归一化后计算内积:
sdense=H^q[0]⋅H^d[0] s_{\text{dense}} = \hat{H}_q[0] \cdot \hat{H}_d[0] sdense=H^q[0]⋅H^d[0]
(2)稀疏检索分数 ssparses_{\text{sparse}}ssparse
计算每个 token 的权重(通过线性层等)
对 query 和文档的共现词,取最大词权重累加求和:
ssparse=∑w∈q∩dmaxweight(w) s_{\text{sparse}} = \sum_{w \in q \cap d} \max \text{weight}(w) ssparse=w∈q∩d∑maxweight(w)
(3)多向量检索分数 smultis_{\text{multi}}smulti
计算 query 和文档所有token两两向量点积
通过池化(max 或 mean)汇总成分数:
smulti=pooling({Hq[i]⋅Hd[j]}) s_{\text{multi}} = \text{pooling}(\{H_q[i] \cdot H_d[j]\}) smulti=pooling({Hq[i]⋅Hd[j]})
Ldense=InfoNCE(sdense),Lsparse=InfoNCE(ssparse),Lmulti=InfoNCE(smulti) \mathcal{L}_{\text{dense}} = \text{InfoNCE}(s_{\text{dense}}), \quad \mathcal{L}_{\text{sparse}} = \text{InfoNCE}(s_{\text{sparse}}), \quad \mathcal{L}_{\text{multi}} = \text{InfoNCE}(s_{\text{multi}}) Ldense=InfoNCE(sdense),Lsparse=InfoNCE(ssparse),Lmulti=InfoNCE(smulti)
L=Ldense+Lsparse+Lmulti \mathcal{L} = \mathcal{L}_{\text{dense}} + \mathcal{L}_{\text{sparse}} + \mathcal{L}_{\text{multi}} L=Ldense+Lsparse+Lmulti
先融合三种相似度分数生成“教师分数” sTs^TsT:
sT=λ1sdense+λ2ssparse+λ3smulti s^T = \lambda_1 s_{\text{dense}} + \lambda_2 s_{\text{sparse}} + \lambda_3 s_{\text{multi}} sT=λ1sdense+λ2ssparse+λ3smulti
对每种分数,计算它们和教师分数的 KL 散度蒸馏损失:
Ldense′=KL(softmax(sT)∥softmax(sdense)) \mathcal{L}'_{\text{dense}} = \text{KL}(\text{softmax}(s^T) \parallel \text{softmax}(s_{\text{dense}})) Ldense′=KL(softmax(sT)∥softmax(sdense))
同理计算 Lsparse′\mathcal{L}'_{\text{sparse}}Lsparse′ 和 Lmulti′\mathcal{L}'_{\text{multi}}Lmulti′
总蒸馏损失:
L′=Ldense′+Lsparse′+Lmulti′ \mathcal{L}' = \mathcal{L}'_{\text{dense}} + \mathcal{L}'_{\text{sparse}} + \mathcal{L}'_{\text{multi}} L′=Ldense′+Lsparse′+Lmulti′
Lfinal=L+L′2 \boxed{ \mathcal{L}_{final} = \frac{\mathcal{L} + \mathcal{L}'}{2} } Lfinal=2L+L′
用于模型反向传播和参数更新。