Retrieval Pipeline
檢索 pipeline 設計,從 chunking、Dual Search、Rank Fusion 到 LLM Reranking,一層層精煉搜尋結果。
設計問題
延續本系列的主線,我們用這個問題當例子:
「幫我找 John 上週寄來、提到合約截止日的 email。」
系統有 10,000 封 email。你需要在幾秒內找到那幾封真正相關的信。這不是 Google 搜尋:你沒有 PageRank,沒有使用者行為資料,email 也不像網頁那樣有明確的站內連結結構。
核心問題:如何設計一條檢索 pipeline,既能精確匹配關鍵字,又能理解語意,還能結合對話脈絡做出智慧判斷?
這篇聚焦在整體 pipeline 怎麼接起來
如果你想分開理解每個元件的底層原理,可以延伸閱讀 Chunking、 BM25、 Embeddings、 RRF 與 Re-ranking。
Pipeline 全貌
每一層解決不同的問題,一層層精煉,從萬級文件縮減到十幾個真正相關的結果。
第一層:Chunking
為什麼要做 chunking?
一封 email 可能很長,但用戶要的資訊可能只在其中一段。如果整封 email 作為一個搜尋單位:
- Embedding 被稀釋:一封信討論了五個主題,embedding 是五個主題的平均,跟任何單一主題都不太像
- 結果太粗:找到了一封 3000 字的信,但有用的只有其中 100 字
- Token 浪費:把整封信塞進 LLM 上下文,大部分內容是噪音
Chunking 策略
chunking 參數:
chunk_size = 1000 字元
chunk_overlap = 100 字元
separators = ["\n\n", "\n", " ", ""]- 1000 字元:大約是一段完整的論述。太小會失去語境,太大會稀釋重點
- 100 字元重疊:確保跨邊界的句子不會被截斷
- 層次分隔符:優先在段落邊界切,其次換行,最後空格
保留元資料
chunking 時保留原始 email 的元資料:
EmailChunk {
id: string ← 原始 email ID(可回溯)
index: number ← 第幾個 chunk
totalChunks: number ← 這封信共幾個 chunk
subject: string ← 信件主旨
from: string ← 寄件人
to: string ← 收件人
timestamp: string ← 時間
chunk: string ← 實際的文字片段
}元資料有兩個用途:
- 搜尋時:主旨和寄件人也參與搜尋(
emailChunkToText = subject + chunk) - 展示時:搜尋結果可以顯示「來自 John 的信,主旨是 ...」
Chunking 的 trade-off
| 參數 | 太小 | 太大 |
|---|---|---|
| chunk_size | 失去上下文、chunk 數量爆炸 | Embedding 被稀釋、搜尋不精確 |
| chunk_overlap | 邊界句子被截斷 | 冗餘增加、搜尋結果重複 |
1000/100 是經驗值。如果你的文件有特殊結構(如 markdown、code),可能需要專門的 chunking 策略。
第二層:Dual Search 雙重搜尋
BM25 關鍵字搜尋
BM25(Best Matching 25)是一個基於詞頻的排名演算法。它的核心思想:
- 一個詞在文件中出現越多次 → 越相關
- 一個詞在所有文件中都很常見 → 不那麼有區分力
- 文件越長 → 需要更多的出現次數才算「高頻」
擅長:
- 精確的名字:「John Smith」
- 具體的數字:「$150,000」
- 專有名詞:「Project Alpha」
不擅長:
- 同義詞:搜 "meeting" 找不到 "conference"
- 語意:搜 "budget discussion" 找不到 "we need to talk about money"
Embedding 語意搜尋
把文字轉成高維向量,用餘弦相似度衡量語意距離。
擅長:
- 概念搜尋:「關於預算的討論」→ 找到提到 "cost", "expense", "funding" 的內容
- 問答:「John 在信裡怎麼看這個 deadline?」→ 找到 John 表達意見的段落
不擅長:
- 精確匹配:搜 "John" 可能找到 "Johnny", "Jonathan" 或其他 John
- 數字和日期:embedding 不太擅長理解數值的精確意義
為什麼兩個都要?
| 查詢 | BM25 | Embedding | 兩者結合 |
|---|---|---|---|
| "emails from John" | 精確找到 John | 可能找到其他 John | BM25 主導 |
| "budget discussion" | 只找到有 "budget" 的 | 也找到 "cost analysis" | Embedding 補充 |
| "John 的預算建議" | 找到有 John + 預算的 | 理解「建議」的語意 | 互補最佳 |
這裡的召回率(recall)指的是:在所有真正相關的結果裡,系統成功找回了多少。假設資料庫裡其實有 10 段相關內容,最後只找回 6 段,召回率就是 60%。
單用任一種都有盲點。雙重搜尋的代價是多一次搜尋,但換來的是大幅提升的召回率。
第三層:Rank Fusion 排序融合
兩次搜尋產生兩份排名。問題是:如何合併?
為什麼不能直接用分數?
BM25 的分數可能是 0-15,Embedding 的分數(cosine similarity)是 -1 到 1。直接加起來沒有意義。BM25 的 5 分和 Embedding 的 0.8 分,哪個「更相關」?
Reciprocal Rank Fusion (RRF)
RRF 的聰明之處:完全不看分數,只看排名。
RRF_score(item) = Σ 1/(k + rank_i)k是平滑常數(通常 60),避免排名第一的項目權重過大rank_i是項目在第 i 個排名中的位置
舉例:某個 chunk 在 BM25 排第 3,在 Embedding 排第 7:
RRF = 1/(60+3) + 1/(60+7) = 0.0159 + 0.0149 = 0.0308另一個 chunk 在 BM25 排第 1,但 Embedding 沒找到(rank = ∞):
RRF = 1/(60+1) + 0 = 0.0164兩個排名都靠前的 → 分數高。只在一個排名靠前的 → 分數中等。這正是我們要的。
為什麼選 RRF 而非其他融合方法?
| 方法 | 優點 | 缺點 |
|---|---|---|
| 分數正規化 + 加權平均 | 保留分數資訊 | 需要調權重,不同查詢最佳權重不同 |
| 取交集 | 高精確度 | 低召回率,一個系統沒找到就丟了 |
| 取聯集 | 高召回率 | 沒有排序 |
| RRF | 不依賴分數、無需調參、穩定 | 丟失了分數的精細資訊 |
為什麼 RRF 適合這裡
RRF 是「足夠好且不需要調參」的方案。在生產環境中,不需要調參這一點極其寶貴。
第四層:LLM Reranking 智慧重排
為什麼 RRF 之後還需要重排?
RRF 給出了統計意義上的最佳排序,但它不理解:
- 用戶的真正意圖:「John 的建議」指的是 John 給出建議的那封信,不是提到 John 名字且碰巧有「建議」二字的內容
- 對話脈絡:前面聊了半小時房子的事,現在問 "他怎麼說",指的是關於房子的意見
- 答案的充分性:這個 chunk 是否真的能回答問題,還是只是主題沾邊
設計
把 RRF 的前 N 個候選(如 30 個)餵給 LLM,讓它根據查詢和對話歷史選出真正相關的:
輸入:
- 對話歷史(提供脈絡)
- 搜尋查詢
- 30 個候選 chunk(帶 ID、主旨、內容)
輸出:
- 相關的 chunk ID 列表(可能只有 5 個,甚至 0 個)關鍵設計決策
1. 使用輕量模型
Reranking 不需要強推理能力。它只需要判斷「這段文字是否回答了這個問題」,用最便宜最快的模型即可。
2. 返回 ID 而非文字
讓 LLM 返回「哪些相關」的 ID 列表,而非重新排序或生成摘要。這最小化了 output token,也避免了幻覺。
3. 允許返回空列表
如果沒有真正相關的結果,返回空比返回垃圾更好。搜尋工具回傳「沒找到」,Agent 可以嘗試不同的搜尋策略。
4. 包含對話歷史
把對話歷史傳給 reranker,讓它理解「他」指的是誰、「那件事」是什麼事。這是 reranker 相比純統計方法的最大優勢。
5. 候選數量限制
30 個候選是 trade-off:
- 太少 → 可能漏掉相關結果
- 太多 → input token 成本上升,且 LLM 在長列表中的判斷力下降
搜尋工具的三步工作流
檢索 pipeline 解決了「如何找到相關的 chunk」。但 Agent 如何使用它?
問題
直接返回所有 email 的完整內容會消耗大量 token。大部分情況下,Agent 只需要先知道「有哪些相關的信」,再決定哪些要點開看全文。
解法:三步工作流
這個設計讓 Agent 像人一樣搜尋 email:先掃一眼寄件人、主旨與 snippet,再點開真正需要的信細讀。
Token 節省
假設搜尋返回 10 個結果,每封 email 平均 500 token:
- 全量返回:10 × 500 = 5,000 token
- 三步工作流:10 × 50 (snippet) + 2 × 500 (只讀兩封) = 1,500 token
節省了 70% 的 token。在高頻使用場景下,這是巨大的成本差異。
生產考量
索引預建
pipeline 中最慢的部分是 Embedding 生成。在生產環境中:
- 新資料進入時就生成 embedding(寫入路徑),而非搜尋時才生成(讀取路徑)
- 使用訊息佇列異步處理:
新 email → 佇列 → embedding worker → 存入向量 DB - 搜尋時直接查向量 DB,不需要等待 embedding 生成
向量資料庫 vs 記憶體搜尋
| 方案 | 適用場景 | 限制 |
|---|---|---|
| 記憶體搜尋 | < 10k 文件,單用戶 | 啟動慢、不可擴展 |
| 本地檔案快取 + 記憶體搜尋(原型) | < 100k 文件,單機驗證 | 記憶體受限,不是正式索引 |
| 向量資料庫(Pinecone/pgvector) | 任意規模 | 額外的基礎設施 |
原型階段用記憶體搜尋完全可以,本地檔案快取也能幫你節省 embedding API 成本。但一旦文件超過萬級、需要多用戶,或必須支援穩定查詢延遲,就應該遷移到向量資料庫。
Evaluation
檢索品質的評估需要:
- 測試資料集:一組查詢 + 預期答案
- 指標:Precision@K、Recall@K、MRR(Mean Reciprocal Rank)
- A/B 測試:新的 chunking 策略 / 搜尋參數 vs 舊的
沒有持續的 evaluation,你不知道改了什麼會讓搜尋變好還是變差。
重點總結
| 層級 | 職責 | 輸入規模 → 輸出規模 |
|---|---|---|
| Chunking | 切分長文件 | 10k emails → 50k chunks |
| BM25 + Embedding | 雙重搜尋 | 50k chunks → 2 × top 30 |
| RRF | 融合排名 | 2 × 30 → 30 |
| LLM Reranking | 語意精煉 | 30 → 5~10 |
| 三步工作流 | Token 節省 | 5 |
每一層都在縮減候選範圍,同時提升精確度。這就是為什麼叫 pipeline:它會一層層過濾。
Last updated on