實測打臉:bge-m3 hybrid 沒贏 dense,還做不了料號搜尋

🧭 本文導覽
Part 一 — 起點與改進地圖
Part 二 — 方法論:能歸因才算改進
Part 三 — 機制與第一個實驗
Part 四 — 往下走
重點摘要(TL;DR)
  • 我們有一個會跑的工廠手冊 RAG(bge-m3 dense 檢索)。研究共識都說:開 bge-m3 沒用到的 sparse、做 dense+sparse hybrid,能免費拿到「型號/料號」精確匹配。我們就照規矩測了它。
  • 結果被實測打臉:hybrid 10 題沒贏過任何一題,嚴格輸給 dense(Content-Hit@5 從 90% 掉到 40%)。
  • 追根因:bge-m3 的 learned sparse 會把代碼裡的數字碎片(8286)權重壓成 0 —— 用它自己的評分函數驗,「D8286」查「D8286」的 sparse 分數是 0。所謂「免費料號匹配」在 bge-m3 上是假的。
  • 方法論:改進要能歸因——一次只動一個因子、全跑、比結果;兩因子就做 2×2。而 7~10 題的 gold set 統計上幾乎證明不了什麼(McNemar),擴充評估集本身是第一優先。
  • 心法:別信 hype,自己量。下一步是換「真正的 BM25」當 sparse 腿,看它能不能把料號救回來。本篇附可手算的 RRF 與 2×2、McNemar。
Part 一 — 起點與改進地圖

起點:一個已經會跑的 RAG,問題是「還能怎麼改」

我們手上是一個工廠設備維修手冊的 RAG 檢索系統:泵浦、空壓機、CNC、變頻器(VFD)、PLC、工業爐,約 700 本、混格式(PDF / Word / Excel / PPT / TXT / ODF),很多是掃描檔,故障排除表、保養週期表特別多。它現在的樣子是:

環節 現況 baseline
嵌入模型bge-m3(1024 維,多語)——但只用了它的 dense 一種輸出
檢索單一 dense 向量、cosine、top-k、按機台過濾、abstain 門檻
切法每份自動判:TOC(書籤)/ heading(字級)/ window(固定)/ table(逐列)
評估自建 gold set,量 Book-Hit + Content-Hit@k

定位很重要:我們負責的是 R(檢索)——把最相關的原文 + 出處撈出來給下游 agent,生成(G)是它自己的 LLM 在做。所以我們最在乎三件事:撈得準、撈得全、查不到就閉嘴(abstain)。現場工人拿錯資訊比拿不到更危險。

2024–2026 最新 RAG 改進地圖:哪些值得改,哪些是過度工程

我們把最新文章、論文、工程部落格掃過一輪,對「這個 case」逐項評估 CP 值。結論先講:最高槓桿幾乎都在上游(建庫),不在查詢時補救。

技術 解決什麼 對本 case
Hybrid:dense+sparse(bge-m3 原生)繁中型號/料號/故障碼,dense 會糊,sparse 精確匹配★★★★★ 幾乎免費,第一優先
表格專門處理(row 獨立 + 補機種/表名前綴)故障排除表被切碎,症狀↔處置對應斷掉★★★★★ 表格是查詢最大宗
Reranker(cross-encoder 精排)粗排完再細判相關性,抬升 top-3★★★☆☆ 值得重測,但挑對款、用對場景
Contextual Retrieval(LLM 生情境前綴)一段話脫離章節看不出在講哪台機★★★★☆ 對症,可用本地模型省錢
abstain 升級(NLI 校準)單一相似度門檻不可靠,LLM 看到任何 context 就想硬答★★★★☆ 安全情境核心
合成評估集(自動生 QA)人工 gold 量太少★★★★☆ 直接解本篇最痛的點
ColPali / ColQwen 視覺檢索掃描檔、接線圖跳過 OCR 直接檢索架構級分岔,看掃描佔比再決定
GraphRAG跨文件全域綜合問答❌ 單本查故障用不到,建庫貴 10-40x
一個重新定調:我們買了 bge-m3 這台三合一引擎(dense + sparse + ColBERT),只發動了 dense 一缸。上表把「開 sparse 做 hybrid」列為 ★★★★★ 第一優先——這是研究共識的預期。本篇 Step 1 就實際去發動 sparse 那一缸,用嚴謹的消融來驗證這個 ★★★★★ 到底真不真。劇透:結果很意外,見下面 Step 1。(表格裡的星等是「動手前的假設」,不是實測結論——這正是為什麼要自己量。)
Part 二 — 方法論:能歸因才算改進

鐵律:一次只動一個因子(不然你什麼都沒學到)

每一次「改進」都是一個因果宣稱:「因為我把 X 從 A 換成 B,命中率才升上去。」要成立必須(1)可歸因——這次只有 X 變;(2)可辨識於雜訊之上——差異大到不像量測誤差。先講第一條的數學。

你觀察到的永遠是差值。如果這一輪你同時把 A 從 a₀→a₁、B 從 b₀→b₁,你量到的 ΔM 在數學上恆等於三項之和:

ΔM = M(a1,b1) - M(a0,b0)

   = [ M(a1,b0) - M(a0,b0) ]                         ← A 的效應(在 b0 下)
   + [ M(a0,b1) - M(a0,b0) ]                         ← B 的效應(在 b0 下)
   + [ M(a1,b1) - M(a1,b0) - M(a0,b1) + M(a0,b0) ]   ← A×B 交互作用

你手上只有「一個數字」ΔM,右邊卻有三個未知貢獻項。
一條方程式解三個未知數 → 欠定,無解。

工廠手冊的具體例子:這一輪你同時把 embedding 換掉、又把 chunk 從固定切改成章節切,結果 Content-Hit 71%→86%。三種真相都與這結果相容:(甲)是新 embedding 的功勞;(乙)是章節切的功勞、換不換模型無所謂;(丙)只有兩個一起上才有效。一次雙改,甲乙丙在數據上完全無法區分——你換來一個「看起來變好」的幻覺,而它會在下一個專案背叛你。鐵律的本質:一次只動一個因子,等於強迫上式右邊只剩一項非零,ΔM 就唯一對應那一項。

Factorial:2×2 = 4 格,手算主效應與交互作用

「一次一因子(OFAT)」安全,但有個盲點:抓不到交互作用。當你懷疑兩個因子會互相影響(embedding × chunk 幾乎一定會),正確做法是把它們交叉全跑。這就是你說的「兩個節點 → 2×2 → 4 個結果」。用假想數字示範:

run A:rerank B:切法 Content-Hit
(1)關 −固定切 −y1 = 70
a開 +固定切 −y2 = 78
b關 −章節切 +y3 = 74
ab開 +章節切 +y4 = 90
# 主效應 = 跨另一因子的邊際平均差
Effect(A) = 1/2(y2+y4) - 1/2(y1+y3) = 1/2(78+90) - 1/2(70+74) = 84 - 72 = +12
Effect(B) = 1/2(y3+y4) - 1/2(y1+y2) = 1/2(74+90) - 1/2(70+78) = 82 - 74 = +8

# 交互作用 = 「A 的效應如何隨 B 改變」的一半
A 在 B=+ 的效應: y4-y3 = 90-74 = 16
A 在 B=- 的效應: y2-y1 = 78-70 = 8
Effect(A×B) = 1/2[(y4-y3) - (y2-y1)] = 1/2[16-8] = +4   (>0 → 兩者有加成)

交互作用 +4 且為正,代表 rerank 和章節切有加成——章節切乾淨之後,rerank 能撿到的正確段落更多。若這值 ≈ 0,兩因子獨立、可放心分開優化;若明顯非零,你不能把兩個 OFAT 的收益直接相加。這就是為什麼「兩個節點就做 2×2」:OFAT 只探索座標軸,factorial 探索整個空間,而且相同精度下更省(每個主效應用到全部 4 個資料點)。

殘酷現實:7 題 gold set 幾乎證明不了任何事

這是最反直覺、卻最該先懂的一段。當 gold set 只有 7 題,Content-Hit 的可能取值只有 0/7, 1/7, …, 7/7,也就是 0%、14%、29%、43%、57%、71%、86%、100%。所謂「71% → 86%」在物理上就是多對了 1 題。你的尺根本沒有 71 和 86 之間的解析度,顆粒度就是 14 個百分點。

更嚴謹地:新舊兩個 config 跑同一組題,是配對資料,正確工具是 McNemar 檢定,只看「一個對、另一個錯」的分歧題。假設新 config 修好 1 題、沒弄壞任何題(分歧數 b+c = 1):

分歧數少時用二項精確檢定:H0 下 b ~ Binomial(b+c, 0.5)
修好 1 題 (b=0, c=1):  雙尾 p = 2 × (0.5)^1 = 1.0     ← 完全不顯著
修好 2 題 (b=0, c=2):  雙尾 p = 2 × (0.5)^2 = 0.5     ← 還是遠不到 0.05

要多少題才抓得到「71%→86%(差 15pp)」這種差異?
n ≈ (1.96+0.84)^2 × [0.71×0.29 + 0.86×0.14] / 0.15^2 ≈ 113   ← 每個 config 約要 110~130 題
偵測 10pp 差異 → 約 250 題;偵測 5pp → ~1000 題(差異砍半,樣本量 ×4)
所以本篇所有實驗數字,都當「方向性線索」看,不是統計結論。這也直接說明為什麼「擴充評估集到上百題」不是可有可無的雜事,而是讓後面每一個實驗變得「可信」的地基——它本身就是最高優先的一個改進項。
Part 三 — 機制與第一個實驗

機制深挖:dense / sparse / hybrid 到底差在哪

Step 1 要比的就是 dense vs hybrid,先把機制講到能手算。bge-m3 是「一次 forward、共用同一個 backbone、接三個 head」,對同一段文字吐出三種資料結構完全不同的表示:

輸出 資料結構 / 幾何 相似度算法
dense一顆 1024 維向量(取 [CLS]、L2 正規化)= 單位球面上一個點內積 = cosine
sparse{token → 權重} 字典(每 token 過線性層+ReLU)= 25 萬維、只數十維非零共同 token 的權重乘積累加
ColBERT每 token 一向量 = 一疊點(矩陣)MaxSim(token 級軟對齊)

為什麼 dense 對「型號/故障碼」會糊?語意嵌入被訓練成「意思近 → 幾何近」。F0051F0052 進 tokenizer 後共用 F005 大量 subword,encoder 把共享 context 混進來,cos(F0051, F0052) 常高到 0.95+,對最近鄰搜尋幾乎是「同一個點」。而 sparse 裡它們是不同 token,F0051F0052 文件的 lexical 分數趨近 0——只要故障碼那個 token 對不上,貢獻直接歸零。這就是 hybrid 的理論價值:dense 管「意思對」,sparse 管「字要對」。(注意「理論」二字——Step 1 的實測會發現 bge-m3 的 learned sparse 有個要命的問題,讓這個理論優勢在代碼上根本兌現不了。)

順帶澄清一個常見誤解:bge-m3 的 sparse 不是內建 BM25。BM25 的權重來自語料統計(idf 是全語料常數);bge-m3 的 lexical 權重是神經網路依上下文學出來的 term 重要度,同一個詞在不同句子權重不同,概念上接近 SPLADE。對中文(無空格)還有個大好處:它用模型自帶 subword,不需要外掛 jieba 斷詞,dense 和 sparse 共用同一套 tokenization,不會兩套斷詞對不齊。

RRF 融合:為什麼只用排名、k=60 是什麼、手算一次

dense 分數(cosine,擠在 0.7~0.95)和 sparse 分數(權重累加,0~30 無上界)尺度完全不可比,直接相加會被 sparse 大數值支配。RRF(Reciprocal Rank Fusion)的解法:丟掉原始分數,只用排名

RRF(d) = Σ  1 / (k + rank_i(d))        k=60(Milvus 預設)
         i(每條檢索路)

查「變頻器 F0051 故障」,兩路各回 top-4(未進榜=該路貢獻 0):
  文件                       dense排名   sparse排名
  Doc-A (F0051 手冊)            3          1
  Doc-B (F0052 手冊)            1          -
  Doc-C (變頻器通用故障排除)      2          4
  Doc-D (F0051 維修工單)         -          2

Doc-A = 1/(60+3) + 1/(60+1) = 0.01587 + 0.01639 = 0.03226
Doc-C = 1/(60+2) + 1/(60+4) = 0.01613 + 0.01563 = 0.03176
Doc-B = 1/(60+1) + 0         = 0.01639
Doc-D = 0        + 1/(60+2)  = 0.01613

融合排名: A > C > B > D

Doc-B:它在 dense 是第 1 名(因為 F0052 語意跟 F0051 超像,dense 糊掉了),但 sparse 完全沒進榜(F0051 這 token 對不上)。RRF 把它壓到第 3。這就是「dense 型號糊掉 → hybrid 修正」在數字上的體現。k=60 的作用是平滑:讓「在多條路都排前段」的文件靠累加勝出,而不是被單一路的第 1 名綁架。

但書(hybrid 不是萬靈丹):純語意改寫查詢(「一直重開機」vs 文件寫「反覆重啟」,字面零重疊)、跨語言、太泛的短查詢,sparse 這路貢獻遞減甚至變雜訊。理想是依查詢類型動態調融合權重——但那是後面的因子了。

Step 1 實測:dense vs hybrid(我們的第一個數據點)

方法嚴格照上面:只動「檢索模式」一個因子(dense vs hybrid),其餘全部釘死(同一份凍結 gold、同一切法 auto/700/120、同一組 chunk、同一台 CPU)。語料 9 份公開檔(6 本設備手冊 + docx/xlsx/xls)切出 1525 片,bge-m3 在 CPU 上編碼 dense + sparse。gold 10 題:7 題自然語言問句(「無法抽水的可能原因」)+ 3 題代碼/型號查詢(D8286RF008X00ADN80)——特別加代碼題,因為那正是 hybrid「應該」發威的主場。

指標dense(baseline)hybrid(dense+sparse RRF)Δ
Content-Hit@170%30%-40%
Content-Hit@390%30%-60%
Content-Hit@590%40%-50%
Recall@557%17%-40%
nDCG@50.8030.343-0.460
MRR0.7830.379-0.404
語料 1525 片、gold 10 題、CPU;dense 編碼 23.9s、hybrid(重用快取)13.5s。綠=hybrid 勝、紅=hybrid 輸。

逐題看:相關段落的排名(Content-Hit 命中位置)

平均分會把「修好幾題、弄壞幾題」壓成一個數字,所以逐題看排名的移動才是命根:

題目dense 命中排名hybrid 命中排名
泵浦·進水管氣密第3第8
泵浦·無法抽水第2第4
空壓機·排氣溫度第1第8
PLC·常見故障第1第1
工業爐·節能第1未進top10
docx·章節數第1第6
xlsx·Excel教學第1第1
PLC·D8286代碼第1第8
VFD·RF008X00A代碼第1第1
空壓機·DN80代碼未進top10未進top10
McNemar 就地算:分歧題:dense對/hybrid錯 b=5、dense錯/hybrid對 c=0(b+c=5)。二項精確雙尾 p≈0.06 → 仍不顯著。

hybrid 全盤皆輸:10 題沒贏過任何一題(上表 McNemar:dense 贏 5、hybrid 贏 0)。連它「應該」發威的代碼題 D8286 都輸(dense 命中第 1、hybrid 掉到第 8)。這跟研究共識完全相反,所以我們停下來追根因——絕不能把違反文獻的結論直接發出去。

追根因:bge-m3 的 learned sparse 把代碼的數字「當停用詞」丟掉

用 bge-m3 自己的評分函數 compute_lexical_matching_score 驗一個乾淨案例:query「D8286 是什麼特殊暫存器」對一段含「D8286」的文字。D8286 被切成 subword [D, 82, 86],然後看兩邊 lexical 權重:

query「D8286 是什麼特殊暫存器」的 lexical 權重:
   D=0.35,  存=0.04,  器=0.01     ← 只剩這些;82、86 這兩個數字碎片權重 = 0

真實 PLC 手冊裡含 D8286 的 chunk,對這個 query:
   compute_lexical_matching_score = 0.0000     ← bge-m3 自己算的,不是我們算的

對照 dense:
   同一個 query ↔ 同一段文字,cosine = 0.73     ← dense 反而穩穩命中

死穴講白:bge-m3 的 learned sparse(SPLADE 那一派)是訓練出來的 term 重要度,它學到「裸數字碎片沒語意」,於是把 8286 的權重壓到 0——像對待停用詞一樣。結果它根本沒在匹配「D8286 這個代碼」,只能靠周邊的「特殊/暫存器」等字。而真實手冊裡代碼往往排在一整欄代碼表格中、旁邊沒有這些語意字 → sparse 分數真的變 0、完全瞎。dense 把整段一起編碼,反而抓得到。

被打臉的 hype:「bge-m3 附贈 sparse = 免費的型號/料號精確匹配」——在 bge-m3 上是假的。它的 learned sparse 抑制代碼 token,給不了 BM25 那種精確匹配。想真的做料號搜尋,得上真正的 BM25 / exact-match 索引(這就是下一步)。同時,我們 7 題自然語言問句 dense 本來就飽和(C@3 90%),sparse 只會把表面字拉到不相關的簡介段——語意題 sparse 也幫倒忙。兩頭落空,hybrid 當然全輸。

誠實的第一格結論:在這個語料上,bge-m3 dense 單獨就是最強,等權 hybrid 反而扣分。這不是失敗的實驗——它是一個「實測打臉 hype」的成功實驗:我們用嚴謹消融、加上模型自己的評分函數,證明了一個被廣泛引用卻在 bge-m3 上不成立的說法。(一樣提醒:10 題統計上仍薄,McNemar p≈0.06;但「10 題 0 勝」的方向性夠強,足以否定「hybrid 免費就贏」。)

Part 四 — 往下走

路線圖:接下來逐步要開的因子

照「一次一因子、能歸因才算數」的紀律,往下的順序是:

Step 開的因子 設計
1(本篇)檢索模式 dense / hybrid(bge-m3 sparse)單因子 2 格 → hybrid 全輸
1b(下一步)換「真正的 BM25」當 sparse 腿bge-m3 sparse 做不了料號 → BM25 能不能救代碼題?hybrid 這次贏不贏?
2+ chunk size 700 / 450檢索 × size 的 2×2 = 4 格(看交互作用)
3+ 表格 row 前綴(機種/表名)切法升級,單因子
4+ reranker(bge-reranker-v2-m3)在候選上精排
擴充 gold set 到上百題讓上面每一步變「可信」的地基

每一步都是:只動一格 → 全跑 → 拿結果比 → 記進累積表。CPU 跑得慢沒關係,慢慢走,重點是每個數字都問心無愧地知道「它是誰的功勞」。這篇會隨實驗進展持續更新。

續集已發布 → RAG 消融續集:BM25 逆轉 hybrid、chunk size 白調、reranker 兩面刃——把這篇結尾的路線圖真的走一遍:換古典 BM25 讓 hybrid 反敗為勝、chunk size 調了等於沒調、reranker 是撈得全與撈得準的兩面刃。

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *