RAG 系統越做越複雜,搜尋結果的來源也越來越多:
- 查詢本身的 embedding → 向量搜尋結果
- HyDE 假設文件的 embedding → 另一組向量搜尋結果
- Multi-Query 擴展的 3-5 個子查詢 → 各自的向量搜尋結果
- BM25 全文搜尋 → 關鍵字匹配結果
每一路都有自己的結果集和排序。問題來了:怎麼把這六、七路結果合理地融合成一份?
問題的根源:分數無法比較
最直覺的做法是把所有分數加總,按總分排名。但這有個根本問題:不同來源的分數量綱不同。
- 向量搜尋的 cosine similarity:範圍 0.0 – 1.0,一般命中結果集中在 0.7–0.9
- BM25 分數:範圍可能是 0–50,分布取決於文件庫大小和詞頻
- Cross-Encoder:範圍 0.0 – 1.0,但分布與向量 cosine 不同
直接相加,BM25 的數值範圍就會主導結果,向量搜尋的細微差距被淹沒。
Score Normalization 的問題
一個改進是先做 Min-Max Normalization,把各路分數都縮放到 0-1:
normalized = (score - min) / (max - min)
然後加權相加:
final = w1 × normalized_vector + w2 × normalized_bm25 + ...
這看起來合理,但有幾個問題:
- Min-Max 受極值影響:一個異常高分的文件會把其他所有文件壓縮到很低的範圍
- 需要確定權重:
w1、w2怎麼設?不同查詢類型的最優權重可能不同 - Normalization 有意義嗎:向量分數 0.85 和 BM25 分數 0.85 代表的「好」是一樣的嗎?不一定
RRF 的解法
RRF(Reciprocal Rank Fusion)完全放棄比較分數,只看名次:
RRF_score(d) = Σ 1 / (K + rank_i(d) + 1)
rank_i(d):文件 d 在第 i 路結果中的名次(0-indexed)K:平滑參數,通常設 60- 如果文件在某路沒出現,不計入這路的分數(貢獻 0)
文件的 RRF 分數是所有路次的倒數名次之和。名次第 1 的文件得 1/(60+1) ≈ 0.016,名次第 10 的得 1/(60+10) ≈ 0.014,分數差距不大但有序。
K=60 的意義
K 是個平滑係數,防止第 1 名和第 2 名之間分數差距過大:
| K | 第 1 名 | 第 10 名 | 差距 |
|---|---|---|---|
| 0 | 1.0 | 0.091 | 11x |
| 10 | 0.091 | 0.048 | 1.9x |
| 60 | 0.016 | 0.014 | 1.1x |
K=60 讓名次的影響比較「溫和」,不讓單一路的第 1 名直接碾壓所有其他路的結果。這也是 Cormack et al. 原始論文建議的值,在各種搜尋場景表現穩定。
為什麼名次比分數更可靠
RRF 的核心假設:一個搜尋系統在自己的評分範圍內的排序是可信的,但分數的絕對值不可信。
BM25 說「這個文件在關鍵字匹配上排名第 3」,這個排名是有意義的。但 BM25 說「這個文件分數 23.7」,這個數字在向量搜尋的世界裡沒有任何意義。
名次是一個通用語言,所有搜尋系統都能說同一種語言。RRF 就是利用這個通用語言做融合。
多路融合的實際效果
在 NobodyClimb 的搜尋場景,複雜查詢時最多有 6 路並行搜尋:
路 1:query embedding → 向量搜尋
路 2:HyDE embedding → 向量搜尋
路 3:sub_query_1 embedding → 向量搜尋
路 4:sub_query_2 embedding → 向量搜尋
路 5:sub_query_3 embedding → 向量搜尋
路 6:BM25 全文搜尋
一個文件如果在 6 路中都出現且名次都高,它的 RRF 分數遠超過只在 1-2 路出現的文件:
文件 A(6 路都排第 1):6 × 1/(60+1) ≈ 0.098
文件 B(只在 1 路排第 1):1 × 1/(60+1) ≈ 0.016
這個特性很直觀:越多不同角度的搜尋都認為相關,就越可能真的相關。
RRF 的限制
RRF 不是萬能的,幾個已知的弱點:
1. 忽略分數差距
向量搜尋中第 1 名(0.95)和第 2 名(0.60)的差距很大,但在 RRF 裡,兩者只差 1/(61) - 1/(62) ≈ 0.0003。如果這個分數差距是有意義的信號,RRF 會把它丟掉。
2. 各路品質不等
如果某一路搜尋品質很差(比如 Multi-Query 生成了一個離題的子查詢),它的結果也會參與 RRF,可能引入噪音。處理方式:在 Multi-Query 生成後做品質過濾,把太偏離原始查詢語義的子查詢丟掉,不讓它參與搜尋。
3. 計算複雜度
路數越多,需要合併的候選越多。6 路各取 Top-20,合併後有多達 120 個候選(去重後通常 30-50 個)。後續的 Cross-Encoder Reranking 要對這些候選逐一評分,候選數影響 Reranker 的延遲。
實際上,Cross-Encoder 是在 RRF 之後、只對 Top-N(例如 30 個)做精排,不需要對所有 120 個都跑。
整體架構中的位置
[多路搜尋結果]
├ 向量搜尋 (query) → [d1, d3, d7, ...]
├ 向量搜尋 (HyDE) → [d2, d1, d5, ...]
├ 向量搜尋 (sub_q1) → [d3, d1, d9, ...]
├ 向量搜尋 (sub_q2) → [d7, d4, d1, ...]
└ BM25 → [d1, d6, d3, ...]
↓
[RRF 融合]
↓
Merged & Ranked List
↓
[Cross-Encoder 精排]
↓
Final Top-K
RRF 是第一次粗排(聚合多路信號),Cross-Encoder 是第二次精排(精確相關性評分)。兩者角色不同,缺一不可。
整體來說
RRF 的設計哲學是實用主義:與其試圖把不同系統的分數「對齊」(這在數學上很難做到又有意義),不如用名次這個更可靠的信號。實作也極其簡單,沒有需要調整的超參數(除了 K=60 這個幾乎不需要動的值)。
這種「把複雜問題簡化到最核心」的思路,是很多好設計的共同特徵。
參考資料
- Reciprocal Rank Fusion outperforms Condorcet and Individual Rank Learning Methods (Cormack, Clarke & Buettcher, 2009)
- RAG-Fusion: a New Take on Retrieval-Augmented Generation
- Large-Scale Validation and Analysis of Interleaved Search Evaluation (Chapelle et al., 2012)
- NobodyClimb 系統架構:Cloudflare 全端攀岩社群平台
- NobodyClimb AI 架構:20 節點 RAG Pipeline