为什么需要Rerank?
检索阶段的目标是高召回:宁可多召回一些无关内容,也不要漏掉相关内容(通常取Top-20或Top-50)。但直接把20个候选结果全部扔给LLM,会带来两个问题:
- 上下文窗口浪费:大量无关内容占用宝贵的token预算,挤掉真正重要的信息。
- 位置偏见:研究表明,LLM更倾向于依赖上下文开头和结尾的内容,中间部分容易被忽略("lost in the middle"问题)。
Rerank的职责是:在召回的20-50个候选中,精确判断哪些真正相关,筛选出最相关的Top-3或Top-5,再送入LLM。
Bi-encoder vs Cross-encoder
Bi-encoder(双编码器)— 用于初步检索
就是普通的Embedding模型。Query和Document分别独立编码,然后计算相似度。速度极快,可以预计算文档向量并存入向量库。缺点是query和document之间没有交互,精度有限。
Query → Encoder → 向量A ──┐
├─ 相似度计算 → 分数
Document → Encoder → 向量B ──┘
(两路独立编码,无交互)
Cross-encoder(交叉编码器)— 用于Rerank
将Query和Document拼接后一起输入模型,让两者在Transformer的自注意力机制中充分交互,输出一个精确的相关性分数。精度比Bi-encoder高得多,但因为无法预计算,必须对每个候选实时推理,速度慢10-100倍。
[CLS] Query [SEP] Document [SEP]
│
Cross-Encoder
│
相关性分数 (0-1)
(query和document充分交互,精度高)
主流Rerank模型
| 模型 | 类型 | 中文支持 | 调用方式 | 速度 |
|---|---|---|---|---|
| BGE-reranker-large | Cross-encoder | 优秀 | 本地 | 中等 |
| BGE-reranker-v2-m3 | Cross-encoder | 优秀 | 本地 | 中等 |
| Cohere Rerank | 专有模型 | 良好 | API | 快(云端) |
| Jina Reranker v2 | Cross-encoder | 良好 | API/本地 | 中等 |
Rerank的成本和延迟代价
Rerank是有代价的,需要权衡:
- 延迟增加:对20个候选做cross-encoder推理,通常增加100-500ms(本地GPU)或50-200ms(Cohere API)。
- API成本:Cohere Rerank约$2/1000次请求。
- 收益:在中文技术文档场景,Rerank通常能将Recall@3提升10-20个百分点,最终答案质量提升明显。
建议:延迟敏感场景可以用BGE-reranker本地部署(GPU推理,约100ms/次);成本敏感场景用Cohere;对延迟不敏感的后台处理场景任意选。
带Rerank的完整RAG链路代码
pip install sentence-transformers chromadb openai
from sentence_transformers import CrossEncoder
import chromadb
from openai import OpenAI
# ===== 初始化模型 =====
openai_client = OpenAI()
reranker = CrossEncoder("BAAI/bge-reranker-large") # 本地下载约1.3GB
# ===== 向量库初始化 =====
chroma = chromadb.Client()
collection = chroma.create_collection("docs_with_rerank")
documents = [
"BGE-M3最大支持8192个token输入",
"RAG通过检索增强生成,减少幻觉",
"Rerank使用Cross-encoder精确重排候选结果",
"向量数据库用HNSW加速近似最近邻搜索",
"BM25是基于词频的关键词检索算法",
"混合检索融合向量搜索和关键词搜索",
"Embedding模型将文本映射为高维向量",
"chunk切分策略影响RAG系统的检索质量",
]
def get_embedding(text):
resp = openai_client.embeddings.create(
model="text-embedding-3-small", input=text
)
return resp.data[0].embedding
# 建立向量索引
for i, doc in enumerate(documents):
collection.add(
ids=[f"doc_{i}"],
documents=[doc],
embeddings=[get_embedding(doc)]
)
# ===== 带Rerank的检索函数 =====
def rag_with_rerank(query: str, initial_k: int = 10, final_k: int = 3):
"""
两阶段检索:
1. 向量检索召回 initial_k 个候选
2. Cross-encoder Rerank,保留 final_k 个
"""
# 第一阶段:向量检索(宽召回)
query_embedding = get_embedding(query)
candidates = collection.query(
query_embeddings=[query_embedding],
n_results=initial_k,
include=["documents"]
)
candidate_docs = candidates["documents"][0]
print(f"[召回阶段] 召回 {len(candidate_docs)} 个候选")
# 第二阶段:Rerank精排
pairs = [(query, doc) for doc in candidate_docs]
scores = reranker.predict(pairs)
# 按Rerank得分排序
ranked = sorted(
zip(candidate_docs, scores),
key=lambda x: x[1],
reverse=True
)
top_docs = [doc for doc, score in ranked[:final_k]]
print(f"[Rerank阶段] 精选 {final_k} 个相关文档")
for doc, score in ranked[:final_k]:
print(f" [{score:.3f}] {doc}")
# 第三阶段:拼接上下文,调用LLM
context = "\n".join([f"- {doc}" for doc in top_docs])
prompt = f"""请基于以下参考内容回答问题。
参考内容:
{context}
问题:{query}
请给出简洁准确的回答。"""
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}]
)
return response.choices[0].message.content
# ===== 使用示例 =====
answer = rag_with_rerank("如何提升RAG系统的检索精度?")
print("\n=== 最终答案 ===")
print(answer)
小结:RAG链路中的职责分工
检索阶段(Bi-encoder/BM25):高召回,宽网捞鱼,取Top-20~50
↓
Rerank阶段(Cross-encoder):精排,精选Top-3~5
↓
生成阶段(LLM):基于精选上下文生成回答