02.5 - 检索与重排序
定位: RAG Pipeline 的核心执行环节——从向量数据库中找出最相关文档,然后精排 面试高频度: ⭐⭐⭐⭐⭐ 考查方式: 检索策略、相似度计算、混合检索、重排序原理
一、这是什么?为什么需要它?
是什么
检索与重排序是 RAG 系统中从"查询"到"可用的上下文"之间的关键步骤:
查询 -> [第一步:粗检索] -> Top-20/50 候选 -> [第二步:重排序] -> Top-3/5 最终结果 -> 生成检索(Retrieval):从向量数据库中快速找到候选文档块 重排序(Reranking):用更强的模型对候选块精确排序
为什么需要两步?
为什么不能一步到位?
向量检索(ANN):速度快但精度有限
Embedding 模型将整个句子压缩成一个向量 -> 可能丢失细节
适合初筛:从百万中找到几十个候选(秒级)
重排序(Reranker):精度高但速度慢
Cross-Encoder 同时处理查询+文档 -> 理解更精确
适合精排:从几十个中找到最合适的几个(毫秒级/每个)
组合:兼顾效率与精度
ANN 粗筛(召回率高) -> Reranker 精排(精确率高)二、原理拆解
2.1 相似度计算方法
| 方法 | 公式 | 范围 | 特点 |
|---|---|---|---|
| 余弦相似度 | cos(A,B) = A*B/( | A | * |
| 点积 | A * B | (-inf, +inf) | 未归一化时受向量长度影响 |
| L2 距离 | A-B |
python
import numpy as np
def cosine_similarity(a, b):
"""余弦相似度:最常用的文本相似度计算"""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
def dot_product(a, b):
"""点积:如果向量已归一化,等价于余弦"""
return np.dot(a, b)
def l2_distance(a, b):
"""L2欧氏距离:越小越相似"""
return np.linalg.norm(a - b)经验:使用余弦相似度或点积(如果向量已归一化)。FAISS 默认用 L2,但对归一化向量效果等同。
2.2 混合检索(Hybrid Search)
向量检索的局限性:
纯向量检索的盲区:
查询:"iPhone 15 价格"
向量检索 -> 理解"iPhone 15"是手机,"价格"是cost -> 可能有不错结果
但:
查询:"ABC-123-XYZ"(产品编号)
向量检索 -> 无法理解这个编号,语义为空 -> 结果很差
解决方案:混合检索(向量 + 关键词)
向量检索:负责语义理解和同义词匹配
关键词检索(BM25):负责精确匹配专有名词和编号
融合:加权合并两个结果集混合检索流程:
+--------------+
| 用户查询 |
+------+-------+
|
+----------+----------+
| |
v v
+-------------+ +-------------+
| 向量检索 | | BM25 关键词 |
| (语义相似) | | (精确匹配) |
+------+------+ +------+------+
| |
v v
+-------------+ +-------------+
| 候选集 A | | 候选集 B |
+------+------+ +------+------+
| |
+-------+-----------+
|
v
+------------------+
| 融合排序 (RRF) |
+--------+---------+
|
v
+------------------+
| 最终 Top-K 候选 |
+------------------+融合算法 RRF(Reciprocal Rank Fusion):
python
def rrf_fusion(vector_results, keyword_results, k=60):
"""RRF 融合:排名倒数加权"""
scores = {}
for rank, doc in enumerate(vector_results):
doc_id = doc.id
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
for rank, doc in enumerate(keyword_results):
doc_id = doc.id
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)2.3 重排序(Reranking)
为什么需要重排序:
向量检索用 Bi-Encoder:
查询和文档分别编码 -> 两个独立向量 -> 余弦相似度
优点:文档向量可预计算(快!)
缺点:查询和文档的交互信息丢失
重排序用 Cross-Encoder:
查询和文档拼接在一起编码 -> 直接输出相似度分数
优点:查询和文档充分交互(精确!)
缺点:不能预计算(慢),需要逐对计算主流 Reranker 模型:
| 模型 | 语言 | 特点 |
|---|---|---|
| BAAI/bge-reranker-v2-m3 | 多语言 | 中文最优,推荐 |
| BAAI/bge-reranker-large | 英文/中文 | 性能均衡 |
| Cohere rerank | 多语言 (API) | 全托管,简单 |
| Jina Reranker | 多语言 | 支持长文本 |
2.4 完整的检索流水线
+----------------------------------------------------------------+
| RAG 检索流水线 |
| |
| 查询: "公司年假有多少天" |
| |
| 1. 查询预处理 |
| - 查询重写(HyDE / Multi-Query) |
| - 生成多个查询角度 |
| |
| 2. 多路检索 |
| - 向量检索: Top-30(语义相似) |
| - BM25: Top-30(关键词精确匹配) |
| - 合并: 60个候选 |
| |
| 3. 去重与过滤 |
| - 去除内容重复的文档块 |
| - 按 metadata 过滤(权限/分类/时间) |
| |
| 4. 重排序 |
| - Cross-Encoder 逐对计算 (查询, 文档) 分数 |
| - 按分数降序排列 |
| - 取 Top-3 作为 LLM 上下文 |
| |
| 5. 上下文组装 |
| [块3: "第3章 年假规定..."] [块1: "年假申请流程..."] |
| |
| LLM 生成最终回答 |
+----------------------------------------------------------------+三、图解全景
检索策略选择决策树
你的检索需求是什么?
|
+-- 需要语义理解(同义词匹配)?
| -> 必须用向量检索
|
+-- 需要精确匹配(专有名词/编号)?
| -> 必须用 BM25
|
+-- 两者都需要?
| -> 混合检索(Hybrid Search)
|
+-- 需要最高质量?
| -> 混合检索 + 重排序
|
+-- 延迟敏感?
| -> 仅向量检索(最快)
|
+-- 召回优先?
-> 多查询 + 混合检索 + 重排序检索质量 - 延迟权衡:
检索策略 质量 延迟
仅关键词 (BM25) ** 1ms
仅向量检索 *** 10ms
混合检索 (向量+关键词) **** 20ms
混合检索 + 重排序 ***** 80-200ms
多查询 + 混合检索 + 重排序 ****** 300ms-1s四、实战验证
4.1 混合检索 + 重排序完整代码
python
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
# 1. 创建向量检索器
vector_retriever = vectorstore.as_retriever(
search_kwargs={"k": 30}
)
# 2. 创建 BM25 检索器
keyword_retriever = BM25Retriever.from_documents(
documents=chunks
)
keyword_retriever.k = 30
# 3. 融合检索器(混合检索)
ensemble_retriever = EnsembleRetriever(
retrievers=[vector_retriever, keyword_retriever],
weights=[0.5, 0.5]
)
# 4. 添加重排序
from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import CrossEncoderReranker
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
reranker = CrossEncoderReranker(
model=HuggingFaceCrossEncoder(
model_name="BAAI/bge-reranker-v2-m3"
),
top_n=3
)
final_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=ensemble_retriever
)
# 5. 执行检索
results = final_retriever.invoke("公司年假政策是什么")
for doc in results:
print(f"内容: {doc.page_content[:100]}...")4.2 检索质量实验对比
| 策略 | Recall@3 | Precision | 延迟 |
|---|---|---|---|
| 仅向量检索 | 0.72 | 0.61 | 10ms |
| 仅BM25 | 0.58 | 0.55 | 1ms |
| 混合检索 (向量+BM25) | 0.81 | 0.68 | 20ms |
| 混合检索 + 重排序 | 0.89 | 0.82 | 100ms |
| 多查询 + 混合检索 + 重排序 | 0.93 | 0.85 | 500ms |
五、面试视角
| 问题 | 答案要点 |
|---|---|
| 为什么 RAG 需要重排序?向量检索不够吗? | 向量检索用 Bi-Encoder,查询和文档独立编码,缺少交互。重排序用 Cross-Encoder,查询和文档一起编码,交互充分。组合使用兼顾效率与精度 |
| 混合检索解决了什么问题? | 纯向量检索对专有名词/编号/缩写效果差(无法理解"ABC-123"的语义)。混合检索加入 BM25 做精确匹配,互补向量检索的盲区 |
| Bi-Encoder 和 Cross-Encoder 的本质区别? | Bi-Encoder 独立编码查询和文档(可预计算,快但精度有限);Cross-Encoder 联合编码(计算量大但精度高) |
| RRF 融合算法的原理? | Reciprocal Rank Fusion:对每个文档在不同检索方式中的排名取倒数再加权求和。排名越高贡献越大。简单有效,不需要调权重 |
| 如何评估检索质量? | Recall@K(相关文档是否在 Top-K 中)、MRR(第一个相关文档的排名倒数)、NDCG@K(考虑排序位置的加权精度) |
📚 相关链接
- **Embedding模型与向量化** — 检索的向量基础
- **向量数据库选型与对比** — 向量存储方案
- **HyDE与查询转换** — 检索前的查询优化
- **评估指标详解** — 检索质量评估指标
- <- 返回 **检索系统索引**