進階 RAG:
父子檢索、混合搜尋與 Rerank
主線第 7 課的 RAG 是天真版:一句(段)一塊、只用向量、取前 3 名。 手冊一變大、問題一變刁,它就開始出包——而且出包的地方模型不會告訴你。 下面是同一個模型、同一份 90 句手冊的真實紀錄:選一題、撥三個開關, 看送進 prompt 的內容和模型的回答怎麼變。
實驗場是真的 Python,在你的瀏覽器裡跑、不用安裝。首次載入約需 30–60 秒—— 正好夠你把上面的開關撥一輪、讀完第 1 節。
先出考卷,再談招式
開場的開關板你撥過一輪就會發現:8 種組合裡沒有一種三題都拿滿。 父子檢索修好了除垢步驟,卻讓 E07 那題的模型說「手冊中沒有提到」; 混合搜尋把 E07 排到第一,卻把「摔到地上」那題的答案擠出前 3 名。 只看三題會被個案帶著走,所以這一課的實驗場配了一份考卷:41 題、每題標好答案在手冊哪幾句。
| 題型 | 題數 | 例子 | 難在哪 |
|---|---|---|---|
| 步驟題 | 8 | 除垢的完整步驟是什麼? | 答案橫跨同一節好幾句,少一句就是錯的操作 |
| 單點題 | 8 | 單份濃縮要放幾公克咖啡粉? | 答案就一句——天真版也答得好,當對照組 |
| 型號/故障碼 | 15 | 螢幕顯示 E07 怎麼辦? | E07、E17 差一個字就是另一回事 |
| 換句話說 | 10 | 摔到地上壞掉可以免費修嗎? | 使用者說「摔到地上」,手冊寫「摔落」 |
打分用三個檢索界的標準指標:第一名就對(hit@1)、recall@k(標準答案句有幾成進了前 k 名)、 MRR(第一個對的排第 r 名得 1/r 分,再平均)。天真版的成績其實不差—— 第一名就對 35/41、MRR 0.92。但換個問法就露餡:取前 3 名時, 8 題步驟題只有 1 題的步驟全部進了 prompt。「排對第一名」跟「答案完整」是兩回事。
父子檢索:用小塊找、用大塊給
切塊是 RAG 的第一個兩難。切小,向量只講一件事、對上了就很「尖」,但步驟題只撈到零碎幾句; 切大,步驟湊齊了,可是一塊裡混著好幾個主題,向量變成它們的平均,找單點答案反而失準,prompt 還一路變胖。 開場第 ① 題就是切小的代價——天真版撈到的 3 句都在除垢那一節,偏偏漏了第一步,模型的回答是:
整個流程沒叫你加除垢劑。語氣篤定、格式漂亮,照做卻等於白做。實驗場 2️⃣ 把考卷上的數字攤開(取前 3 名):
| 切法 | 步驟題:步驟全進 prompt | prompt 平均長度 |
|---|---|---|
| 一句一塊(天真版) | 1/8 | 94 字 |
| 固定 300 字一塊 | 7/8 | 859 字 |
| 固定 600 字一塊 | 7/8 | 1,734 字(全手冊 2,890 字的六成) |
| 父子檢索 | 8/8 | 327 字 |
實測:jina-embed 向量、41 題考卷,2026-09。固定大小=整句依序裝箱、裝滿就換下一塊(常見的預設切法)。
做法樸素得驚人:子塊記住自己屬於哪一節,檢索照常用子塊,最後一步換成父塊、去重。 LangChain 的 ParentDocumentRetriever(1.x 版起移到 langchain-classic 套件)、 LlamaIndex 的 AutoMergingRetriever 都是這個概念。用 sentence-transformers 手寫只要十來行(參考程式,不在課內執行):
但它不是萬靈丹。開場第 ② 題只開父子檢索,模型拿到的是三整節、十幾個長得很像的故障碼, 2B 小模型直接回「手冊中沒有提到。」——對單點題來說,展開的整節就是雜訊。 這也是考卷存在的意義:它會告訴你這招對哪一型題目划算。
混合搜尋:向量懂意思,BM25 認字
問「螢幕顯示 E07 怎麼辦?」,向量檢索的前 3 名是 E17(cosine 0.656)、E11(0.528)、 最後才是 E07(0.526)。在向量眼裡,E07 和 E17 幾乎是同一個東西。 這次模型還從第 3 句把答案撿了回來;可是只取第 1 名時,模型拿到的是 E17 那句,實測回答 「手冊中沒有提到。(註:您提供的段落僅針對 E17 錯誤…)」——同樣設定換成混合搜尋,第 1 名就是 E07,答對。
BM25 是搜尋引擎用了幾十年的關鍵字排序:一個詞在這句出現越多、在全手冊越稀有,分數越高。 e07 只出現在一句裡,所以一命中就排第一。中文沒有空格,常見做法是切字元 bigram (「除垢燈」→「除垢」「垢燈」,Lucene 的 CJK 分析器就是這樣切),英數代碼整個當一個詞。 兩張排名用 RRF(Reciprocal Rank Fusion)合併:每張清單的第 r 名得 1/(60+r) 分,加總再排—— 只看名次、不看分數,因為 cosine 落在 0–1、BM25 可以到十幾,直接相加會被 BM25 綁架。
考卷的結果很誠實(實驗場 3️⃣ 可以自己拉權重):型號題從 13/15 變成 15/15, 但換句話說題從 9/10 掉到 5/10——「摔到地上」跟手冊的「摔落」bigram 一個都對不上, BM25 只撈得到共用「可以」這種虛詞的句子,還把向量原本排對的答案往下拖。 全部 41 題:純向量 35、純 BM25 27、混合 33。把 BM25 的權重降到 0.25,型號題保住 15/15、總分回到 35—— 在這份手冊上,單靠混合搜尋並沒有比純向量好,只是把錯誤從一型搬到另一型。
那為什麼業界還是標配混合?因為它真正的角色是第一階段「撈得全」,排序交給下一節的 reranker。 Anthropic 在 2024-09 的 Contextual Retrieval 報告裡量過:在他們的評測語料上,(加了上下文的)向量+BM25 讓前 20 名的漏撈率降了 49%、再加 rerank 降了 67%。你的語料會落在哪一邊,只有你的考卷知道。 Qdrant、Elasticsearch 都內建混合搜尋與 RRF——想在真的向量庫上做,接本站的 Qdrant 課。
Rerank:讓 cross-encoder 把前 N 名重排
到目前為止的向量檢索都是 bi-encoder:問題和句子各自變成向量再比,句子向量可以事先算好,查詢只算一次問題——快, 代價是兩邊從頭到尾沒「見過面」。cross-encoder 把問題和一句話拼在一起丟進模型,直接輸出相關度, 能看出「問的是 WF-21、這句講的是 WF-12」這種細節。
| bi-encoder(向量檢索) | cross-encoder(reranker) | |
|---|---|---|
| 怎麼讀 | 問題、文件各自編碼成向量 | 問題+文件一起讀 |
| 能不能事先算 | 能——文件向量存進向量庫 | 不能——每個問題都要重跑每一對 |
| 速度 | 百萬筆也是毫秒級 | 本機 CPU 實測約 45 ms 一對 |
| 用在哪 | 第一階段:從全部文件撈前 N 名 | 第二階段:只重排這 N 個 |
考卷結果(bge-reranker-v2-m3,實驗場 4️⃣):向量 → rerank 前 20 名,第一名就對 35 → 37, E07、WF-21 兩題型號題都救回來了;但「出國兩個月不在家」原本排第一,被 reranker 排到第 5——它也會犯錯。 有趣的是 N 不是越大越好:只重排前 3 名反而拿到 38/41(MRR 0.955), 候選越多,reranker 越有機會把干擾句排上來。還有一條硬限制:它只能重排撈到的 N 個, 答案不在前 N 名,它無能為力——這正是第一階段要混合搜尋「撈得全」的理由。
時間成本(本機 CPU、14 執行緒實測):一對約 45 ms,20 個候選約 0.9 秒/每次查詢;有 GPU 會快一兩個數量級。 reranker 還有別的選擇,例如 jina-reranker-v3、Qwen3-Reranker——一樣先用考卷量過再換。
三招疊起來,再認識幾個兄弟
實驗場 5️⃣ 讓你把所有開關組起來、跟天真版並排比。spike 對 2,050 種組合做了格點搜尋, 這份手冊上最好的一組長這樣(怎麼組?那是 LEVEL 3 的挑戰):
| 管線 | 第一名就對 | 步驟題全進 | 其他題全進 | prompt 長度 |
|---|---|---|---|---|
| 天真版:向量、一句一塊、k=3 | 35/41 | 1/8 | 31/33 | 94 字 |
| 格點搜尋的最佳組合(LEVEL 3 的答案) | 38/41 | 8/8 | 32/33 | 237 字 |
多花約 140 字,換來每一題的步驟都完整、第一名多對 3 題。先透露一點:最佳組合裡沒有 BM25—— 在這份 90 句的手冊上,向量的前 20 名已經撈到 98% 的標準答案句,BM25 沒東西可以救; 換成幾萬句、型號滿天飛的真實文件,結論可能反過來。招式是固定的,最佳組合是量出來的。
還有幾招常跟這三招一起出現,知道有就好:
| 手法 | 做什麼 | 本課實測 |
|---|---|---|
| 子塊加章節標題 | 算向量前把「除垢|」這種章節名貼在每句前面,讓「最後裝回濾水芯」知道自己在講除垢 | 有測(5️⃣ 的開關):第一名就對 35 → 36,步驟題 5 → 7,但型號題 13 → 12——有得有失 |
| Contextual Retrieval | Anthropic 2024-09 提出:請 LLM 替每一塊寫 50–100 token 的上下文再貼上去,向量與 BM25 都用 | 沒測(每塊要呼叫一次 LLM);上面那招是它最省的簡化版 |
| Query 改寫/Multi-query | 讓 LLM 把問題改寫成幾種說法分別檢索,再用 RRF 合併 | 沒測 |
| HyDE | 讓 LLM 先「假裝回答」,拿假答案的向量去找真段落——假答案的用詞更像手冊 | 沒測 |
| Query/Document 前綴 | jina v5、e5、bge 這類模型要求問題與文件加不同前綴(sentence-transformers 的 prompt_name) | 有測:忘了加,第一名就對 35 → 30、換句話說 9 → 6——最便宜的一招,最常被漏掉 |
自己跑一遍:免費 CPU 就夠
本課每個數字都來自同一支腳本: spike_genai_rag_advanced.py。 它的 --local 模式不需要任何服務:在 CPU 上載入跟本課一樣的開源模型—— 向量用 jina-embeddings-v5-text-small-retrieval(CC BY-NC 4.0,非商用免費,商用要找 Jina 授權)、 rerank 用 bge-reranker-v2-m3(Apache-2.0)——把切塊、BM25、融合、rerank、考卷全部重跑一次。 Colab 或 Kaggle 的免費 CPU notebook 裡三格就好:
驗證範圍(2026-09):這三步用同樣的套件、在本機 CPU 限 2 執行緒(模擬免費機器)實跑過—— 不含第一次下載模型(約 3.5 GB)全程約 7.5 分鐘、記憶體峰值 4.7 GB,考卷分數跟本課完全一致 (35/41、33/41、38/41、最佳組合 237 字)。Colab/Kaggle 本身沒有實跑。 開場的 LLM 回答需要一個 OpenAI 相容的端點(設 LLM_URL,例如本機 Ollama), 那段只用 qwen3.5-2b 驗證過,換模型回答會不同。
本課名詞速查卡
| 父子檢索 | Parent-child/small-to-big:用小塊(一句)找、把它所在的大塊(整節)交給模型;記得去重。 |
| BM25 | 關鍵字排序:詞在這篇出現越多、在全部文件越稀有,分數越高;認字不認意思,型號/錯誤碼的救星。 |
| 混合搜尋+RRF | 向量+BM25 兩張排名,用 Reciprocal Rank Fusion 按名次融合(第 r 名得 1/(k+r),k 常用 60)。 |
| bi-encoder | 問題、文件各自編碼成向量再比——可以事先算、快,是第一階段檢索的主力。 |
| cross-encoder/rerank | 問題+文件一起讀、直接打相關度——慢但準,只重排第一階段的前 N 名。 |
| hit@1/recall@k/MRR | 第一名就對嗎/答案有幾成進了前 k 名/第一個對的排第 r 名得 1/r 分——考卷的三把尺。 |
換你動手
在實驗場 2️⃣ 把 top-k 拉到 1:「其他題:答案全進 prompt」哪一種切法最高?固定 300 字掉到多少?為什麼切大塊反而讓單點的答案找不準?
在 3️⃣ 把 BM25 權重從 1 往下拉,找出「型號題還保得住 15/15」的最低權重。這時 41 題第一名就對幾題?跟純向量比贏了嗎?
在 5️⃣ 組一條管線:第一名就對 ≥ 38/41、步驟題 8/8 全進、其他題 ≥ 31/33,而且 prompt 平均字數越少越好。你能壓到幾個字?
三題在實驗場最後一格都有折疊解答——先自己拉,再打開對照。
情境測驗
離開前試試看:下面的情境都真的會遇到。每題選一個你認為的最佳做法,選了馬上看得到解釋。
Q1 情境題
你的客服 RAG 一句一塊、取前 3 名。使用者抱怨:問「怎麼更換濾芯」「怎麼除垢」時,回答常常少一兩個步驟。最該先做的是?
步驟題的問題是「答案散在同一節好幾句」,父子檢索正好對症:比對用小塊(找得準),交出去用整節(步驟齊)。本課考卷上它把步驟全進的比例從 1/8 拉到 8/8,prompt 平均只要 327 字。A 也能湊齊步驟(7/8),但 prompt 膨脹到 1,734 字、單點題準度下降——大塊的向量被多個主題稀釋。B 會塞進一堆不相關的句子,而且 top-10 仍可能漏掉同一節的某一步。D 最危險:手冊沒給的步驟,更大的模型只會「補」得更像真的——開場第 ① 題的回答就是自信地漏掉加除垢劑那一步。
Q2 錯誤診斷
同事實作了父子檢索,印出送給模型的 prompt 開頭(實測輸出)如下,prompt 長度 573 字。最可能的問題是?
這段是實測輸出:問「除垢的完整步驟」時,向量檢索的前 3 名是除垢那一節的三句不同的話(所以 A 不對,子塊層級是正常的),但每一句都被換成同一個父塊。去重之後只剩一段、191 字——現在是 573 字,三倍的 token 費用、零新資訊,還把其他可能有用的節擠掉了。修法是換成父塊時按名次去重(例如 list(dict.fromkeys(...))),LangChain 的 ParentDocumentRetriever 也是這樣做的。C 是因噎廢食;D 是常見誤解——重複內容不會讓模型更準,只會更貴、更擠。
Q3 情境題
維修 App 的使用者常只打故障碼,例如「E07」。你發現向量檢索常把 E17、E11 排在 E07 前面。下列做法哪個最好?
代碼是「認字」的問題,BM25 天生擅長:本課考卷上它讓型號題從 13/15 變 15/15。但只加 BM25 會把換句話說題從 9/10 拖到 5/10,所以要配 reranker(向量或混合 → rerank 後型號題 15/15、換句話說 8/10),並且用同時含兩型題目的考卷把關。B 不保證有用——向量本來就是在比「意思」,E07 與 E17 的意思本來就很像。C 把換句話說題整個丟掉(純 BM25 在考卷上只有 3/10)。D 救不了檢索:E07 那句根本沒進 prompt 時,提醒模型也生不出來。
Q4 錯誤診斷
同事這樣做混合搜尋:hybrid = cos_scores + bm25_scores。上線後發現「混合」的排名跟純 BM25 幾乎一樣,換句話說題大量答錯。下面是實測輸出,根本原因是?
cosine 最多 0.89、BM25 可以到 17,相加等於「BM25 排序、cosine 只當小數點後的 tie-breaker」——實測 40 題裡 36 題的第一名跟純 BM25 一模一樣,總分從向量的 35 掉到 30,比兩者都差。RRF 只用名次,天生不怕尺度不同;要用分數就得先各自正規化(例如 min-max)再加權。A 調 BM25 參數改不了尺度差;B 看錯方向,向量本身是 35/41 的最好單項;C 兩邊同乘 100,比例完全不變。
Q5 情境題
公司知識庫有 5,000 段。同事說:「cross-encoder 最準,那就每次查詢都把 5,000 段全部 rerank 一遍。」在 CPU 上(實測約 45 ms 一對),你的建議是?
5,000 × 45 ms ≈ 225 秒——每問一題要等快四分鐘,A 不可行。兩段式是標準做法:bi-encoder/BM25 快速縮小範圍,cross-encoder 只精排少數候選(本課 20 個候選約 0.9 秒)。而且 N 大不一定好:本課考卷上只重排前 3 名拿到 38/41,前 20 名反而 37/41。D 做不到——cross-encoder 的分數取決於「問題+文件」這一對,問題事先不知道就沒得算,這正是它跟 bi-encoder 的根本差別。C 把排序責任丟給 LLM,prompt 變長變貴,干擾句也更多。