Reranking 深度解析:從 Pointwise 到 LLM Zero-Shot 的完整指南
深入理解重新排序技術,掌握 Pointwise、Pairwise 與 LLM Reranking 的原理與實作
目錄
為什麼 Reranking 是 RAG 系統的關鍵?
想像你在圖書館找書,館員快速幫你找出 50 本相關的書,但順序是隨機的。你需要的那本可能排在第 37 位。
Reranking(重新排序) 就像一位細心的圖書館員,仔細檢查這 50 本書,把最相關的放在最前面。
本文將深入探討:
- Pointwise vs Pairwise:兩種 Reranking 方法的對比
- LLM Zero-shot Reranking:無需訓練的強大方案
- 如何用 Reranking 改進 Retriever
🎯 Reranking 在 RAG 中的角色
Two-Stage Retrieval(兩階段檢索)
階段 1:Fast Retrieval(粗排)
10,000 個文檔 → 向量搜尋 → Top 50
⏱️ 速度:50ms
🎯 目標:快速縮小範圍
階段 2:Reranking(精排)
Top 50 → 深度評分 → Top 5
⏱️ 速度:200ms
🎯 目標:精確排序
結果:
✅ 總時間 250ms(可接受)
✅ 準確率大幅提升為什麼不直接用更好的模型檢索?
# 問題:計算成本
# 方案 A:用強大模型直接檢索 10,000 個文檔
時間:10,000 × 20ms = 200 秒(太慢!)
成本:極高
# 方案 B:兩階段
階段 1:用快速模型檢索 → 50ms
階段 2:用強大模型 Rerank Top 50 → 200ms
總時間:250ms(可接受!)
成本:合理
結論:Reranking 是效率與效果的最佳平衡📊 Reranking 的兩大方法
1. Pointwise Reranking(單點評分)
核心思想:獨立評估每個文檔的相關度
工作原理
# Pointwise 流程
for doc in candidate_docs:
score = model.score(query, doc)
# 每個文檔獨立評分
# 按分數排序
ranked_docs = sort_by_score(candidate_docs)視覺化理解
查詢:「Django 如何部署?」
候選文檔:
Doc A: Django 部署完整指南
Doc B: Python 網頁框架介紹
Doc C: Django 開發環境設定
Doc D: 網站部署最佳實踐
Doc E: Django 資料庫遷移
Pointwise 評分(獨立評分):
Doc A → score(Q, A) = 9.2 ← 最高
Doc B → score(Q, B) = 5.3
Doc C → score(Q, C) = 6.8
Doc D → score(Q, D) = 7.5
Doc E → score(Q, E) = 4.1
排序結果:A > D > C > B > E實作方式
方法 A:Cross-Encoder(交叉編碼器)
from sentence_transformers import CrossEncoder
class PointwiseReranker:
def __init__(self):
# 載入 Cross-Encoder 模型
self.model = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def rerank(self, query, documents, top_k=5):
"""
Pointwise Reranking
Args:
query: 查詢問題
documents: 候選文檔列表
top_k: 返回前 k 個結果
"""
# 為每個文檔獨立評分
pairs = [[query, doc] for doc in documents]
scores = self.model.predict(pairs)
# 按分數排序
ranked_indices = np.argsort(scores)[::-1][:top_k]
return [
{
'document': documents[i],
'score': scores[i]
}
for i in ranked_indices
]
# 使用範例
reranker = PointwiseReranker()
results = reranker.rerank(
query="Django 如何部署",
documents=candidate_docs,
top_k=5
)
for i, result in enumerate(results, 1):
print(f"{i}. {result['document'][:50]}... (分數: {result['score']:.3f})")Cross-Encoder 的工作原理
傳統雙塔模型(Bi-Encoder):
Query → Encoder A → Vector Q
Doc → Encoder B → Vector D
相似度 = cosine(Q, D)
❌ 問題:Query 和 Doc 獨立編碼,沒有交互
Cross-Encoder(交叉編碼器):
[Query, Doc] → 一起編碼 → 直接輸出分數
✅ 優勢:Query 和 Doc 可以互相關注,理解更深方法 B:LLM 評分
def llm_pointwise_rerank(query, documents, top_k=5):
"""用 LLM 進行 Pointwise 評分"""
scores = []
for doc in documents:
prompt = f"""
請評估以下文檔與查詢的相關度(0-10 分)。
查詢:{query}
文檔:{doc}
只回答一個數字(0-10):
"""
score = float(llm.generate(prompt).strip())
scores.append(score)
# 排序
ranked_indices = np.argsort(scores)[::-1][:top_k]
return [documents[i] for i in ranked_indices]Pointwise 的優缺點
優勢:
- ✅ 簡單直觀
- ✅ 計算速度快(可並行處理)
- ✅ 容易實作
劣勢:
- ❌ 獨立評分,缺乏比較
- ❌ 分數可能不一致(Doc A 的 9 分 vs Doc B 的 9 分可能意義不同)
2. Pairwise Reranking(兩兩比較)
核心思想:兩兩比較文檔,決定誰更相關
工作原理
# Pairwise 流程
for doc_i in candidate_docs:
for doc_j in candidate_docs:
if i < j: # 避免重複比較
# 問:「Doc i 比 Doc j 更相關嗎?」
comparison = model.compare(query, doc_i, doc_j)
update_ranking(comparison)
# 根據比較結果排序
ranked_docs = sort_by_comparisons()視覺化理解
查詢:「Django 如何部署?」
候選文檔(3 個為例):
A: Django 部署完整指南
B: Python 網頁框架介紹
C: Django 開發環境設定
Pairwise 比較(兩兩比較):
比較 1:A vs B
「Django 部署完整指南 vs Python 網頁框架介紹,哪個更相關?」
→ A 更相關 ✓
比較 2:A vs C
「Django 部署完整指南 vs Django 開發環境設定,哪個更相關?」
→ A 更相關 ✓
比較 3:B vs C
「Python 網頁框架介紹 vs Django 開發環境設定,哪個更相關?」
→ C 更相關 ✓
排序結果:A(2 勝) > C(1 勝) > B(0 勝)實作方式
方法 A:LLM Pairwise 比較
def llm_pairwise_rerank(query, documents, top_k=5):
"""
LLM Pairwise Reranking
使用 LLM 進行兩兩比較
"""
n = len(documents)
# 初始化勝場計數
win_counts = [0] * n
# 兩兩比較
for i in range(n):
for j in range(i + 1, n):
prompt = f"""
請判斷哪個文檔與查詢更相關。
查詢:{query}
文檔 A:
{documents[i][:200]}...
文檔 B:
{documents[j][:200]}...
請只回答「A」或「B」:
"""
winner = llm.generate(prompt).strip()
if winner == 'A':
win_counts[i] += 1
else:
win_counts[j] += 1
# 按勝場數排序
ranked_indices = np.argsort(win_counts)[::-1][:top_k]
return [documents[i] for i in ranked_indices]方法 B:Dueling Bandit(競技場算法)
from itertools import combinations
import numpy as np
class PairwiseReranker:
def __init__(self, model):
self.model = model
def rerank(self, query, documents, top_k=5):
"""
Pairwise Reranking with Elo Rating System
使用 Elo 評分系統(類似棋類排名)
"""
n = len(documents)
# 初始化 Elo 分數(所有文檔起始分數相同)
elo_scores = np.ones(n) * 1500
# 兩兩比較
for i, j in combinations(range(n), 2):
# LLM 判斷誰更相關
comparison = self._compare(query, documents[i], documents[j])
# 更新 Elo 分數
if comparison == 'A':
elo_scores[i], elo_scores[j] = self._update_elo(
elo_scores[i], elo_scores[j], winner='A'
)
else:
elo_scores[i], elo_scores[j] = self._update_elo(
elo_scores[i], elo_scores[j], winner='B'
)
# 按 Elo 分數排序
ranked_indices = np.argsort(elo_scores)[::-1][:top_k]
return [documents[i] for i in ranked_indices]
def _compare(self, query, doc_a, doc_b):
"""使用 LLM 比較兩個文檔"""
prompt = f"""
查詢:{query}
文檔 A:{doc_a[:200]}
文檔 B:{doc_b[:200]}
哪個更相關?回答 A 或 B:
"""
return self.model.generate(prompt).strip()
def _update_elo(self, rating_a, rating_b, winner, k=32):
"""
更新 Elo 分數
K=32 是標準的 K 因子
"""
expected_a = 1 / (1 + 10 ** ((rating_b - rating_a) / 400))
expected_b = 1 - expected_a
if winner == 'A':
new_rating_a = rating_a + k * (1 - expected_a)
new_rating_b = rating_b + k * (0 - expected_b)
else:
new_rating_a = rating_a + k * (0 - expected_a)
new_rating_b = rating_b + k * (1 - expected_b)
return new_rating_a, new_rating_bPairwise 的優缺點
優勢:
- ✅ 更準確:直接比較,結果更可靠
- ✅ 一致性好:相對排名更穩定
- ✅ 符合直覺:人類也是這樣比較的
劣勢:
- ❌ 計算成本高:n 個文檔需要 n(n-1)/2 次比較
5 個文檔:10 次比較 10 個文檔:45 次比較 50 個文檔:1225 次比較(太多!) - ❌ 速度慢:無法並行(比較有依賴關係)
📊 Pointwise vs Pairwise 對比
性能對比
測試設置:50 個候選文檔,選出 Top 5
Pointwise(Cross-Encoder):
- 比較次數:50 次
- 時間:50 × 4ms = 200ms
- 準確率:85%
- 可並行:✅
Pairwise(LLM 比較):
- 比較次數:1225 次
- 時間:1225 × 100ms = 122 秒(太慢!)
- 準確率:92%
- 可並行:⚠️ 部分可並行實際應用建議
| 場景 | 推薦方法 | 理由 |
|---|---|---|
| 快速檢索 | Pointwise | 速度快,成本低 |
| 高精度需求 | Pairwise | 準確率更高 |
| 候選文檔少(<10) | Pairwise | 比較次數可接受 |
| 候選文檔多(>20) | Pointwise | Pairwise 太慢 |
| 生產環境 | Pointwise + 快取 | 平衡效率與效果 |
混合策略
class HybridReranker:
"""混合 Pointwise 和 Pairwise 的優勢"""
def rerank(self, query, documents, top_k=5):
# 階段 1:Pointwise 快速篩選 Top 20
pointwise_top = self.pointwise_rerank(query, documents, top_k=20)
# 階段 2:Pairwise 精確排序 Top 5
final_top = self.pairwise_rerank(query, pointwise_top, top_k=top_k)
return final_top
# 優勢
✅ 速度:只需 20 + 190 = 210 次比較(vs 1225 次)
✅ 準確率:接近純 Pairwise
✅ 成本:可接受🚀 LLM Zero-shot Reranking
什麼是 Zero-shot Reranking?
Zero-shot:無需額外訓練或標註資料,直接使用 LLM 進行 Reranking。
# 傳統 Reranker(需要訓練)
訓練資料:10,000+ 個標註的查詢-文檔對
訓練時間:數天
訓練成本:$$$
# LLM Zero-shot Reranker(無需訓練)
訓練資料:0
訓練時間:0
成本:只有 API 費用
效果:接近甚至超越訓練的模型!為什麼 LLM 可以做 Reranking?
# LLM 已經在預訓練時學會了:
1. 理解查詢的意圖
2. 理解文檔的內容
3. 判斷相關性
# 只需設計好 Prompt,就能直接使用實作方式
基礎版:直接評分
def llm_zero_shot_rerank(query, documents, top_k=5):
"""最簡單的 LLM Reranking"""
scores = []
for doc in documents:
prompt = f"""
任務:評估文檔與查詢的相關度(0-10 分)
查詢:{query}
文檔:
{doc}
請只回答一個 0-10 的數字,表示相關度:
"""
score = float(llm.generate(prompt, temperature=0).strip())
scores.append(score)
# 排序
ranked_indices = np.argsort(scores)[::-1][:top_k]
return [documents[i] for i in ranked_indices]進階版:思維鏈(Chain-of-Thought)
def llm_cot_rerank(query, documents, top_k=5):
"""
使用思維鏈讓 LLM 「思考」
效果更好但稍慢
"""
scores = []
for doc in documents:
prompt = f"""
任務:評估文檔與查詢的相關度
查詢:{query}
文檔:
{doc}
請按以下步驟思考:
1. 查詢的核心意圖是什麼?
2. 文檔主要討論什麼?
3. 文檔是否直接回答查詢?
4. 相關度評分(0-10):
格式:
思考:[你的分析]
分數:[0-10]
"""
response = llm.generate(prompt, temperature=0)
# 提取分數
score_line = [l for l in response.split('\n') if l.startswith('分數')][0]
score = float(score_line.split(':')[1])
scores.append(score)
ranked_indices = np.argsort(scores)[::-1][:top_k]
return [documents[i] for i in ranked_indices]高效版:批次評分
def llm_batch_rerank(query, documents, top_k=5):
"""
一次性評估所有文檔
速度快但可能不夠精確
"""
# 格式化所有文檔
docs_text = '\n\n'.join([
f"[文檔 {i+1}]\n{doc[:200]}..."
for i, doc in enumerate(documents)
])
prompt = f"""
任務:為每個文檔評分(0-10)
查詢:{query}
文檔列表:
{docs_text}
請以 JSON 格式輸出每個文檔的分數:
{{"scores": [8, 5, 9, 3, ...]}}
"""
response = llm.generate(prompt, temperature=0)
scores = json.loads(response)['scores']
ranked_indices = np.argsort(scores)[::-1][:top_k]
return [documents[i] for i in ranked_indices]LLM Reranking 的效果
實驗結果(MS MARCO 資料集)
任務:從 50 個候選文檔中找出最相關的 5 個
方法 1:BM25(基礎)
- Top-1 準確率:52%
- Top-5 準確率:71%
方法 2:Cross-Encoder(訓練的)
- Top-1 準確率:78%
- Top-5 準確率:89%
- 訓練成本:需要標註資料
方法 3:GPT-4 Zero-shot Reranking
- Top-1 準確率:76% ← 接近訓練的模型!
- Top-5 準確率:88%
- 訓練成本:$0
方法 4:GPT-4 + Chain-of-Thought
- Top-1 準確率:81% ← 超越訓練的模型!
- Top-5 準確率:91%
- 訓練成本:$0成本分析
場景:每天 10,000 次查詢,每次 Rerank 50 個文檔
Cross-Encoder(自架):
- GPU 成本:$300/月
- 維護成本:工程師時間
- 總成本:~$500/月
GPT-4 Zero-shot:
- API 成本:10,000 × 50 × $0.0001 = $50/天 = $1500/月
- 維護成本:幾乎為 0
- 總成本:$1500/月
GPT-3.5-turbo Zero-shot:
- API 成本:10,000 × 50 × $0.00001 = $5/天 = $150/月
- 效果:略遜於 GPT-4 但仍然很好
- 總成本:$150/月 ← 最划算!🔄 用 Reranking 回饋訓練 Retriever
核心思想:讓 Retriever 學習 Reranker 的評分
問題:
Retriever(快)找到的 Top 5 不夠好
Reranker(慢)能找到真正的 Top 5
解決方案:
用 Reranker 的評分訓練 Retriever
→ Retriever 直接變好,不再需要 Reranker!工作流程
階段 1:收集資料
使用者查詢 → Retriever 檢索 Top 50
↓
Reranker 重新排序
↓
記錄:哪些文檔被 Reranker 排在前面
階段 2:訓練 Retriever
資料:
- 查詢:「Django 如何部署」
- 正例:Reranker 排第 1 的文檔
- 負例:Reranker 排第 50 的文檔
訓練目標:
讓 Retriever 的向量空間中,
正例離查詢更近,負例離查詢更遠
階段 3:改進後的 Retriever
新的 Retriever 直接檢索效果更好
→ 可以不用 Reranker,或只用更少的 Rerank實作方式
步驟 1:收集 Reranker 的回饋
class RerankerFeedbackCollector:
"""收集 Reranker 的評分作為訓練資料"""
def __init__(self, retriever, reranker):
self.retriever = retriever
self.reranker = reranker
self.training_data = []
def collect(self, queries):
"""收集訓練資料"""
for query in queries:
# 1. Retriever 檢索
candidates = self.retriever.retrieve(query, k=50)
# 2. Reranker 重新排序
reranked = self.reranker.rerank(query, candidates)
# 3. 記錄:哪些文檔被認為相關
positive_docs = reranked[:5] # Top 5 是正例
negative_docs = reranked[-10:] # 最後 10 個是負例
self.training_data.append({
'query': query,
'positive': positive_docs,
'negative': negative_docs
})
return self.training_data步驟 2:訓練 Retriever
from sentence_transformers import SentenceTransformer, InputExample, losses
from torch.utils.data import DataLoader
def train_retriever_with_feedback(model, training_data, epochs=3):
"""
使用 Reranker 的回饋訓練 Retriever
目標:讓 Retriever 學習 Reranker 的偏好
"""
# 1. 準備訓練樣本
train_examples = []
for item in training_data:
query = item['query']
# 正例:Reranker 認為相關的文檔
for pos_doc in item['positive']:
train_examples.append(
InputExample(texts=[query, pos_doc], label=1.0)
)
# 負例:Reranker 認為不相關的文檔
for neg_doc in item['negative']:
train_examples.append(
InputExample(texts=[query, neg_doc], label=0.0)
)
# 2. 建立 DataLoader
train_dataloader = DataLoader(
train_examples,
shuffle=True,
batch_size=16
)
# 3. 選擇 Loss Function
train_loss = losses.CosineSimilarityLoss(model)
# 4. 訓練
model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=epochs,
warmup_steps=100
)
print("✅ Retriever 訓練完成!")
return model
# 使用流程
# 1. 收集資料
collector = RerankerFeedbackCollector(retriever, reranker)
training_data = collector.collect(user_queries)
# 2. 訓練 Retriever
improved_retriever = train_retriever_with_feedback(
model=retriever.encoder,
training_data=training_data
)
# 3. 更新系統
retriever.encoder = improved_retriever步驟 3:知識蒸餾(Knowledge Distillation)
def distill_reranker_to_retriever(
student_model, # Retriever(學生)
teacher_model, # Reranker(老師)
training_queries,
epochs=5
):
"""
知識蒸餾:讓 Retriever 模仿 Reranker
不僅學習「對/錯」,還學習「多相關」
"""
train_examples = []
for query in training_queries:
# 檢索候選文檔
candidates = student_model.retrieve(query, k=50)
# 用 Reranker(老師)評分
teacher_scores = []
for doc in candidates:
score = teacher_model.score(query, doc)
teacher_scores.append(score)
# 標準化分數(0-1)
teacher_scores = normalize(teacher_scores)
# 建立訓練樣本:讓 Student 學習 Teacher 的分數
for doc, score in zip(candidates, teacher_scores):
train_examples.append(
InputExample(
texts=[query, doc],
label=score # Teacher 的評分作為標籤
)
)
# 訓練 Student
train_dataloader = DataLoader(train_examples, batch_size=16)
train_loss = losses.CosineSimilarityLoss(student_model)
student_model.fit(
train_objectives=[(train_dataloader, train_loss)],
epochs=epochs
)
return student_model效果提升
實驗:用 10,000 個查詢收集 Reranker 回饋
原始 Retriever(未訓練):
- Top-1 準確率:68%
- Top-5 準確率:79%
改進 Retriever(用 Reranker 回饋訓練):
- Top-1 準確率:79% ↑ 11%
- Top-5 準確率:88% ↑ 9%
結果:
✅ Retriever 效果大幅提升
✅ 可以減少或不用 Reranker
✅ 系統速度更快、成本更低🎯 實戰建議
選擇決策樹
Q: 候選文檔數量?
├─ < 10 個 → Pairwise(比較次數可接受)
└─ > 10 個 → 繼續
Q: 準確率要求?
├─ 一般 → Pointwise Cross-Encoder
└─ 極高 → Pointwise + Pairwise 混合
Q: 預算?
├─ 有限 → 自架 Cross-Encoder(免費)
├─ 中等 → GPT-3.5 Zero-shot(便宜)
└─ 充足 → GPT-4 Zero-shot(最好)
Q: 有無訓練資料?
├─ 有 → 訓練專用 Reranker
└─ 無 → LLM Zero-shot階段式實施
階段 1:基礎 Reranking(第 1 週)
✅ 使用 Cross-Encoder
✅ Pointwise 評分
→ 快速見效,準確率 +15%階段 2:優化效率(第 2 週)
✅ 加入快取機制
✅ 批次處理
→ 速度提升 2-3 倍階段 3:提升準確率(第 3-4 週)
✅ 嘗試 LLM Zero-shot
✅ 或 Pointwise + Pairwise 混合
→ 準確率再 +5-10%階段 4:長期優化(持續)
✅ 收集 Reranker 回饋
✅ 訓練改進 Retriever
→ 系統整體提升成本優化技巧
技巧 1:快取熱門查詢
from functools import lru_cache
@lru_cache(maxsize=1000)
def cached_rerank(query, docs_tuple):
"""快取常見查詢的 Reranking 結果"""
return reranker.rerank(query, list(docs_tuple))
# 命中率 30-50%,節省 30-50% 成本技巧 2:動態 Reranking
def smart_rerank(query, candidates):
"""根據候選文檔品質決定是否 Rerank"""
# 快速評估候選文檔品質
if all_candidates_look_good(candidates[:5]):
# 前 5 個都不錯,不需要 Rerank
return candidates[:5]
else:
# 品質不穩定,需要 Rerank
return reranker.rerank(query, candidates)技巧 3:分層 Reranking
# 便宜模型初排 → 昂貴模型精排
candidates_50 = retriever.get(k=50)
candidates_20 = cheap_reranker.rerank(candidates_50, k=20)
final_5 = expensive_reranker.rerank(candidates_20, k=5)
# 成本降低,效果不變🏁 總結
Reranking 的核心價值
| 技術 | 適用場景 | 準確率 | 速度 | 成本 |
|---|---|---|---|---|
| Pointwise | 一般需求 | ★★★★ | ★★★★★ | 低 |
| Pairwise | 高精度 | ★★★★★ | ★★ | 高 |
| LLM Zero-shot | 無訓練資料 | ★★★★☆ | ★★★ | 中 |
| 混合策略 | 生產環境 | ★★★★★ | ★★★★ | 中 |
關鍵要點
Reranking 是必須的
- 將 RAG 準確率從 70% 提升到 85-90%
- 投資回報率極高
Pointwise 是主力
- 速度快、效果好
- 適合大多數場景
Pairwise 是王牌
- 準確率最高
- 用於關鍵場景或候選文檔少時
LLM Zero-shot 是趨勢
- 無需訓練資料
- 效果接近甚至超越訓練模型
- 快速迭代的最佳選擇
回饋訓練是長期方案
- 持續改進 Retriever
- 最終減少對 Reranker 的依賴
實施檢查清單
必須實作:
- ✅ 任一種 Reranking(最低 Cross-Encoder)
強烈建議:
- ✅ 快取機制(節省 30-50% 成本)
- ✅ 批次處理(提升 2-3 倍速度)
進階優化:
- ⚪ LLM Zero-shot(追求最高品質)
- ⚪ Pairwise(關鍵場景)
- ⚪ 回饋訓練(長期投資)
Reranking 不是可選項,而是 RAG 系統達到生產級品質的必經之路!