Eric TechBlog
AIAI Assistant

Retrieval Pipeline

檢索 pipeline 設計,從 chunking、Dual Search、Rank Fusion 到 LLM Reranking,一層層精煉搜尋結果。

設計問題

延續本系列的主線,我們用這個問題當例子:

「幫我找 John 上週寄來、提到合約截止日的 email。」

系統有 10,000 封 email。你需要在幾秒內找到那幾封真正相關的信。這不是 Google 搜尋:你沒有 PageRank,沒有使用者行為資料,email 也不像網頁那樣有明確的站內連結結構。

核心問題:如何設計一條檢索 pipeline,既能精確匹配關鍵字,又能理解語意,還能結合對話脈絡做出智慧判斷?

這篇聚焦在整體 pipeline 怎麼接起來

如果你想分開理解每個元件的底層原理,可以延伸閱讀 ChunkingBM25EmbeddingsRRFRe-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       ← 實際的文字片段
}

元資料有兩個用途:

  1. 搜尋時:主旨和寄件人也參與搜尋(emailChunkToText = subject + chunk
  2. 展示時:搜尋結果可以顯示「來自 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 不太擅長理解數值的精確意義

為什麼兩個都要?

查詢BM25Embedding兩者結合
"emails from John"精確找到 John可能找到其他 JohnBM25 主導
"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 只需要先知道「有哪些相關的信」,再決定哪些要點開看全文。

解法:三步工作流

瀏覽

search / filter → 返回元資料 + snippet

判斷

Agent 審查 snippet,決定哪些需要完整內容

深入

getEmails → 只取需要的完整內容

這個設計讓 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 節省510 → 需要的 23 篇全文

每一層都在縮減候選範圍,同時提升精確度。這就是為什麼叫 pipeline:它會一層層過濾。

Last updated on

On this page