引言
在RAG系统中,向量检索负责从海量文档中快速召回候选结果,但召回结果的排序不一定准确。重排序(Reranking)作为第二步,对候选结果进行更精确的排序,将最相关的文档排在最前面。
2026年,重排序已经成为RAG系统的标配组件。一个好的重排序模型可以将RAG的准确率提升20-40%。本文将深入探讨重排序技术。
一、为什么需要重排序
1.1 向量检索的局限
向量检索使用双塔模型(Bi-encoder):查询和文档分别编码,通过向量相似度排序。这种方式速度快,但精度有限——因为查询和文档的交互发生在最后的相似度计算阶段。
1.2 重排序的优势
重排序使用交叉编码器(Cross-encoder):将查询和文档拼接在一起输入模型,模型可以充分理解查询和文档的关系。精度更高,但速度更慢。
向量检索: encode(query) · encode(doc) → similarity score (快,精度中等)
重排序: encode([query, doc]) → relevance score (慢,精度高)
1.3 两阶段架构
查询 → 向量检索(召回Top-100)→ 重排序(精排Top-10)→ 生成
向量检索负责"快而全",重排序负责"准而精"。
二、重排序模型
2.1 交叉编码器
class CrossEncoderReranker:
def __init__(self, model_name="cross-encoder/ms-marco-MiniLM-L-12-v2"):
self.model = load_model(model_name)
async def rerank(self, query, documents, top_k=10):
# 1. 构建query-document对
pairs = [(query, doc.text) for doc in documents]
# 2. 模型打分
scores = self.model.predict(pairs)
# 3. 排序
ranked = sorted(zip(documents, scores), key=lambda x: -x[1])
return [doc for doc, score in ranked[:top_k]]
2.2 LLM重排序
class LLMReranker:
def __init__(self, llm):
self.llm = llm
async def rerank(self, query, documents, top_k=10):
# 方法1: 逐个打分
scores = []
for doc in documents:
score = await self.score_document(query, doc)
scores.append(score)
# 方法2: 两两比较(更准确但更慢)
# ranked = await self.pairwise_rank(query, documents)
# 排序
ranked = sorted(zip(documents, scores), key=lambda x: -x[1])
return [doc for doc, score in ranked[:top_k]]
async def score_document(self, query, document):
"""使用LLM对query-document对打分"""
prompt = f"""
查询: {query}
文档: {document.text[:500]}
请评估文档与查询的相关性:
0 - 完全不相关
1 - 弱相关
2 - 部分相关
3 - 相关
4 - 高度相关
只输出数字。
"""
result = await self.llm.call(prompt)
return int(result.strip())
async def pairwise_rank(self, query, documents):
"""两两比较排序"""
# 使用类似冒泡排序的方法
for i in range(len(documents)):
for j in range(i+1, len(documents)):
comparison = await self.compare(query, documents[i], documents[j])
if comparison == "B is better":
documents[i], documents[j] = documents[j], documents[i]
return documents
2.3 ColBERT
ColBERT是一种延迟交互模型,介于双塔和交叉编码器之间:
class ColBERTReranker:
def __init__(self):
self.query_encoder = load_model("colbert-query-encoder")
self.doc_encoder = load_model("colbert-doc-encoder")
async def rerank(self, query, documents, top_k=10):
# 1. 编码query(多向量表示)
query_embeddings = self.query_encoder.encode(query) # [num_query_tokens, dim]
# 2. 编码documents(多向量表示,可以预计算)
doc_embeddings = [self.doc_encoder.encode(doc.text) for doc in documents]
# 3. 计算MaxSim
scores = []
for doc_emb in doc_embeddings:
# 对query的每个token,找到doc中最相似的token
sim_matrix = query_embeddings @ doc_emb.T # [num_q_tokens, num_d_tokens]
max_sim = sim_matrix.max(axis=1).sum() # 每个query token取max,然后求和
scores.append(max_sim)
# 4. 排序
ranked = sorted(zip(documents, scores), key=lambda x: -x[1])
return [doc for doc, score in ranked[:top_k]]
三、重排序策略
3.1 多模型集成
class EnsembleReranker:
def __init__(self):
self.rerankers = [
CrossEncoderReranker("cross-encoder/ms-marco-MiniLM-L-12-v2"),
CrossEncoderReranker("cross-encoder/stsb-roberta-large"),
LLMReranker(llm)
]
self.weights = [0.4, 0.3, 0.3]
async def rerank(self, query, documents, top_k=10):
all_scores = []
for reranker in self.rerankers:
scores = await reranker.score(query, documents)
# 归一化
scores = self.normalize(scores)
all_scores.append(scores)
# 加权融合
final_scores = np.average(all_scores, axis=0, weights=self.weights)
# 排序
ranked = sorted(zip(documents, final_scores), key=lambda x: -x[1])
return [doc for doc, score in ranked[:top_k]]
3.2 分层重排序
class TieredReranker:
async def rerank(self, query, documents, top_k=10):
# 第一层:快速重排序(轻量模型),从100→30
first_pass = await self.light_reranker.rerank(query, documents, top_k=30)
# 第二层:精确重排序(重量模型),从30→10
second_pass = await self.heavy_reranker.rerank(query, first_pass, top_k=top_k)
return second_pass
3.3 上下文感知重排序
class ContextAwareReranker:
async def rerank(self, query, documents, conversation_history=None):
"""考虑对话历史的重排序"""
# 如果有对话历史,扩展query
if conversation_history:
expanded_query = await self.expand_query_with_history(
query, conversation_history
)
else:
expanded_query = query
# 重排序
return await self.cross_encoder.rerank(expanded_query, documents)
四、模型对比
| 模型 | 类型 | 延迟 | 精度 | 适用场景 |
|---|---|---|---|---|
| MiniLM-L-12 | 交叉编码器 | 中 | 中 | 通用场景 |
| MiniLM-L-6 | 交叉编码器 | 低 | 中低 | 低延迟需求 |
| roberta-large | 交叉编码器 | 高 | 高 | 高精度需求 |
| ColBERT | 延迟交互 | 中 | 高 | 平衡精度和速度 |
| LLM (GPT-4o) | LLM重排序 | 很高 | 最高 | 最高精度需求 |
| BGE-reranker | 交叉编码器 | 低 | 中高 | 中文场景 |
五、优化
5.1 缓存
class CachedReranker:
def __init__(self, reranker):
self.reranker = reranker
self.cache = {}
async def rerank(self, query, documents):
cache_key = hash(query + str([d.id for d in documents]))
if cache_key in self.cache:
return self.cache[cache_key]
result = await self.reranker.rerank(query, documents)
self.cache[cache_key] = result
return result
5.2 批量化
class BatchReranker:
async def rerank(self, query, documents, batch_size=32):
scores = []
for i in range(0, len(documents), batch_size):
batch = documents[i:i+batch_size]
batch_scores = await self.model.score_batch(
[(query, doc.text) for doc in batch]
)
scores.extend(batch_scores)
ranked = sorted(zip(documents, scores), key=lambda x: -x[1])
return ranked
六、评估
class RerankerEvaluator:
async def evaluate(self, reranker, test_cases):
metrics = {
"ndcg_10": [],
"map": [],
"recall_10": [],
"latency_ms": []
}
for case in test_cases:
start = time.time()
reranked = await reranker.rerank(case.query, case.candidate_docs, top_k=10)
elapsed = (time.time() - start) * 1000
metrics["ndcg_10"].append(self.ndcg(reranked, case.relevant_docs, 10))
metrics["map"].append(self.average_precision(reranked, case.relevant_docs))
metrics["recall_10"].append(self.recall_at_k(reranked, case.relevant_docs, 10))
metrics["latency_ms"].append(elapsed)
return {k: np.mean(v) for k, v in metrics.items()}
结语
重排序是RAG系统中"投入产出比"最高的优化——用一个轻量级的重排序步骤,就能显著提升检索质量。
2026年的趋势是"LLM重排序"——随着LLM推理成本下降,越来越多的系统使用LLM进行重排序,因为LLM能理解更复杂的语义关系。
但记住:重排序不是万能的。如果向量检索阶段没有召回相关文档,重排序也无能为力。好的RAG系统需要在每个环节都做好——分块、编码、检索、重排序、生成,缺一不可。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。