- 我們有一個會跑的工廠手冊 RAG(bge-m3 dense 檢索)。研究共識都說:開 bge-m3 沒用到的 sparse、做 dense+sparse hybrid,能免費拿到「型號/料號」精確匹配。我們就照規矩測了它。
- 結果被實測打臉:hybrid 10 題沒贏過任何一題,嚴格輸給 dense(Content-Hit@5 從 90% 掉到 40%)。
- 追根因:bge-m3 的 learned sparse 會把代碼裡的數字碎片(
82、86)權重壓成 0 —— 用它自己的評分函數驗,「D8286」查「D8286」的 sparse 分數是 0。所謂「免費料號匹配」在 bge-m3 上是假的。 - 方法論:改進要能歸因——一次只動一個因子、全跑、比結果;兩因子就做 2×2。而 7~10 題的 gold set 統計上幾乎證明不了什麼(McNemar),擴充評估集本身是第一優先。
- 心法:別信 hype,自己量。下一步是換「真正的 BM25」當 sparse 腿,看它能不能把料號救回來。本篇附可手算的 RRF 與 2×2、McNemar。
起點:一個已經會跑的 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 |
鐵律:一次只動一個因子(不然你什麼都沒學到)
每一次「改進」都是一個因果宣稱:「因為我把 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)
機制深挖: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 對「型號/故障碼」會糊?語意嵌入被訓練成「意思近 → 幾何近」。F0051 和 F0052 進 tokenizer 後共用 F、005 大量 subword,encoder 把共享 context 混進來,cos(F0051, F0052) 常高到 0.95+,對最近鄰搜尋幾乎是「同一個點」。而 sparse 裡它們是不同 token,F0051 對 F0052 文件的 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 名綁架。
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 題代碼/型號查詢(D8286、RF008X00A、DN80)——特別加代碼題,因為那正是 hybrid「應該」發威的主場。
| 指標 | dense(baseline) | hybrid(dense+sparse RRF) | Δ |
|---|---|---|---|
| Content-Hit@1 | 70% | 30% | -40% |
| Content-Hit@3 | 90% | 30% | -60% |
| Content-Hit@5 | 90% | 40% | -50% |
| Recall@5 | 57% | 17% | -40% |
| nDCG@5 | 0.803 | 0.343 | -0.460 |
| MRR | 0.783 | 0.379 | -0.404 |
逐題看:相關段落的排名(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 |
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 重要度,它學到「裸數字碎片沒語意」,於是把 82、86 的權重壓到 0——像對待停用詞一樣。結果它根本沒在匹配「D8286 這個代碼」,只能靠周邊的「特殊/暫存器」等字。而真實手冊裡代碼往往排在一整欄代碼表格中、旁邊沒有這些語意字 → sparse 分數真的變 0、完全瞎。dense 把整段一起編碼,反而抓得到。
誠實的第一格結論:在這個語料上,bge-m3 dense 單獨就是最強,等權 hybrid 反而扣分。這不是失敗的實驗——它是一個「實測打臉 hype」的成功實驗:我們用嚴謹消融、加上模型自己的評分函數,證明了一個被廣泛引用卻在 bge-m3 上不成立的說法。(一樣提醒:10 題統計上仍薄,McNemar p≈0.06;但「10 題 0 勝」的方向性夠強,足以否定「hybrid 免費就贏」。)
路線圖:接下來逐步要開的因子
照「一次一因子、能歸因才算數」的紀律,往下的順序是:
| 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 跑得慢沒關係,慢慢走,重點是每個數字都問心無愧地知道「它是誰的功勞」。這篇會隨實驗進展持續更新。
發佈留言