向量检索的排序问题
上一篇我们用混合检索解决了"召回率"问题------BM25 加向量,能更全面地找到相关文档。
但召回只是第一步。找到文档之后,还有一个问题:这些文档按什么顺序喂给 LLM?
向量检索给出的排序,依据是 query embedding 和 doc embedding 的余弦相似度。这个相似度可以快速计算,但精度有限------它是两段文字的"粗略相似度",并不是真正意义上的"这篇文档有多适合回答这个问题"。
一个常见现象:检索 top-4,第 1 篇是泛泛的背景介绍,第 3 篇才是真正回答问题的文档。LLM 拿到的上下文开头是废话,关键信息埋在后面,生成质量自然下降。
Rerank 的思路很简单:先多召回,再精排。
向量检索召回 top-10(宁可多要,别漏掉),然后用一个更精准的模型对这 10 篇重新打分、重新排序,最终只保留 top-4 给 LLM。
Bi-Encoder vs Cross-Encoder
理解 Rerank,先要理解两种编码架构的区别。
Bi-Encoder(向量检索用的)
arduino
query → Encoder → query vector
doc → Encoder → doc vector
相似度 = cosine(query_vec, doc_vec)
query 和 doc 分别独立编码,相似度通过向量计算得出。优点是极快------doc 向量可以离线预计算,检索时只需算一次 query embedding,然后做向量比较。缺点是 query 和 doc 从不"见面",编码时各自不知道对方的存在,相关性判断较粗。
Cross-Encoder(Reranker 用的)
csharp
[query, doc] → Encoder → relevance score
query 和 doc 拼接在一起,同时输入模型,模型直接输出一个相关性分数。这样 query 的每个词都能和 doc 的每个词做注意力交互,相关性判断精准得多。缺点是慢------每对 (query, doc) 都要跑一次完整推理,无法预计算。
所以两者的正确用法是串联,而不是替代:
sql
向量检索(Bi-Encoder,快速召回)→ Reranker(Cross-Encoder,精准排序)
核心指标:context_precision
在 RAGAS 的 4 个指标里,context_precision 专门衡量排序质量:
ini
context_precision = 有多少个"对的"文档排在了"错的"文档前面
- context_precision = 1.0:相关文档全部排在不相关文档前面
- context_precision = 0.5:相关文档的排序很随机
- context_precision = 0.0:相关文档全部沉底
这正是 Reranker 要优化的指标。召回率(context_recall)关心"找没找到",精准率(context_precision)关心"排没排对"。
实验设计
复用之前的知识库(8 篇 RAG 技术文档)和测试集(8 条问题),对比两种策略:
| 策略 | 检索方式 | 最终返回 |
|---|---|---|
| 基准(Baseline) | 向量检索直接 top-4 | 4 篇 |
| Rerank | 向量检索 top-10 → Cross-Encoder 精排 → top-4 | 4 篇 |
两种策略最终都给 LLM 提供 4 篇文档,唯一区别是排序质量。
Reranker 使用 BAAI/bge-reranker-v2-m3,通过 SiliconFlow API 调用。
实现:自定义 SiliconFlowReranker
LangChain 没有内置 SiliconFlow 的 Reranker 封装,需要自己实现。继承 BaseDocumentCompressor,实现 compress_documents 方法:
python
import requests
from langchain_core.documents import Document
from langchain_core.documents.compressor import BaseDocumentCompressor
from typing import Sequence
class SiliconFlowReranker(BaseDocumentCompressor):
model: str = "BAAI/bge-reranker-v2-m3"
api_key: str = ""
api_base: str = "https://api.siliconflow.cn/v1"
top_n: int = 4
def compress_documents(
self,
documents: Sequence[Document],
query: str,
callbacks=None,
) -> Sequence[Document]:
if not documents:
return []
doc_texts = [d.page_content for d in documents]
resp = requests.post(
f"{self.api_base}/rerank",
headers={"Authorization": f"Bearer {self.api_key}"},
json={
"model": self.model,
"query": query,
"documents": doc_texts,
"top_n": self.top_n,
"return_documents": True,
},
timeout=30,
)
resp.raise_for_status()
reranked = []
for item in resp.json().get("results", []):
doc = documents[item["index"]]
doc.metadata["rerank_score"] = item["relevance_score"]
reranked.append(doc)
return reranked
接口说明:
SiliconFlow 的 /v1/rerank 接口格式:
json
// 请求
{
"model": "BAAI/bge-reranker-v2-m3",
"query": "中文场景用哪个 Embedding 模型",
"documents": ["文档1内容", "文档2内容", ...],
"top_n": 4,
"return_documents": true
}
// 响应
{
"results": [
{"index": 2, "relevance_score": 0.952, "document": {"text": "..."}},
{"index": 0, "relevance_score": 0.834, "document": {"text": "..."}},
...
]
}
index 是原始文档列表的下标,relevance_score 是 Cross-Encoder 给出的相关性分数(越高越好)。
串联:ContextualCompressionRetriever
有了 Reranker,用 ContextualCompressionRetriever 把它接在向量检索器后面:
python
from langchain_classic.retrievers import ContextualCompressionRetriever
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(
model="BAAI/bge-large-zh-v1.5",
api_key=os.getenv("EMBEDDING_API_KEY"),
base_url="https://api.siliconflow.cn/v1",
)
vectorstore = Chroma.from_documents(docs, embedding=embeddings)
# 召回阶段:多要一些
recall_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})
# 精排阶段
reranker = SiliconFlowReranker(
api_key=os.getenv("EMBEDDING_API_KEY"),
top_n=4,
)
# 串联
rerank_retriever = ContextualCompressionRetriever(
base_compressor=reranker,
base_retriever=recall_retriever,
)
调用时和普通 retriever 完全一样:
python
docs = rerank_retriever.invoke("中文场景用哪个 Embedding 模型?")
# 内部自动:向量检索 top-10 → reranker 精排 → 返回 top-4
实验结果
markdown
======================================================================
RAGAS 指标对比(向量检索 vs 向量检索 + Rerank)
======================================================================
指标 基准 (top-4) Rerank (10→4) 变化
──────────────────────────────────────────────────────────────────
context_precision 0.552 0.792 ↑+0.240 ◀ 核心指标
context_recall 0.500 0.688 ↑+0.188
faithfulness 0.688 0.854 ↑+0.167
answer_relevancy 0.429 0.381 ↓-0.049
======================================================================
结论:
✓ Rerank 将 context_precision 从 0.552 提升到 0.792
→ 更相关的文档排在前面,LLM 能看到更高质量的上下文
数字解读:
- context_precision +0.240:最显著的提升。基准检索时,0.552 意味着不少相关文档被排在了不相关文档后面;Rerank 后 0.792,排序质量大幅改善。
- context_recall +0.188:预期之外的收获。召回从 top-4 扩大到 top-10 后,本来担心精排会丢掉相关文档------实际上 context_recall 也提升了,说明放大召回本身就帮助找回了被向量检索漏掉的相关文档。
- faithfulness +0.167:排序质量改善后,LLM 获得了更高质量的上下文,幻觉率也随之下降。三个核心指标联动提升,这是 Rerank 带来的连锁反应。
- answer_relevancy -0.049:轻微下降,变化在误差范围内(LLM 评分本身有随机性)。整体来看不影响结论。
Reranker 的实际工作原理
用一个具体例子感受 Reranker 在做什么。
假设 query = "中文场景应该选哪个 Embedding 模型",向量检索返回了这 4 篇(按余弦相似度排序):
arduino
向量检索排序(余弦相似度):
1. doc-001 "RAG 技术介绍" ------ 泛泛介绍,提到 embedding 一词
2. doc-002 "向量数据库选型" ------ 相关,但重点是数据库
3. doc-003 "Embedding 模型推荐" ------ 直接回答问题 ✓
4. doc-005 "RAG 评估方法" ------ 无关
Reranker 重新打分后:
ini
Reranker 排序(Cross-Encoder 相关性分数):
1. doc-003 score=0.952 "Embedding 模型推荐" ✓
2. doc-002 score=0.621 "向量数据库选型"
3. doc-001 score=0.234 "RAG 技术介绍"
4. doc-005 score=0.089 "RAG 评估方法"
原来排第 3 的正确文档被提到了第 1 位。LLM 读到的上下文第一段就是核心答案,生成质量自然更高。
何时使用 Rerank
建议使用 Rerank 的场景:
- 知识库文档数量较多(> 100 篇),向量检索排序容易出错
- 查询涉及精确术语或特定概念,相关性判断要求高
- 对答案质量要求高,能接受略高的 API 成本和延迟
- 已经在用混合检索,想在排序阶段继续优化
可以跳过 Rerank 的场景:
- 知识库很小(< 20 篇),向量检索排序已足够准确
- 对延迟极敏感(Rerank 每次需要额外 API 调用)
- 查询都是宽泛的概念性问题,排序质量影响不大
成本估算: SiliconFlow 的 bge-reranker-v2-m3 按 token 计费,对 10 篇文档重排序,大约是向量检索成本的 3-5 倍。通常用在高价值查询或对质量要求高的场景。
完整代码
代码已开源:
核心文件:
rerank.py--- 基准检索 vs Rerank 的完整对比实验
运行方式:
bash
git clone https://github.com/chendongqi/llm-in-action
cd 11-rerank
cp .env.example .env # 填入 Embedding API Key 和 LLM API Key
pip install -r requirements.txt
python rerank.py
小结
本文通过代码实验展示了 Rerank 的价值:
- Bi-Encoder(向量检索):快速召回,但相关性判断是"独立视角",排序不够精准
- Cross-Encoder(Reranker):慢但精准,同时看 query 和 doc,直接输出相关性分数
- 两者串联(召回 + 精排):在本实验中,context_precision 从 0.552 提升到 0.792,同时 context_recall 和 faithfulness 也连带改善
在生产级 RAG 系统里,Rerank 通常是性价比最高的优化之一------它不改变任何数据、不调整任何 Prompt,只是让文档按正确顺序排列,LLM 就能看到更好的上下文。