RAG 檢索準確率怎麼量?一個 27% 假象的除錯全記錄

重點摘要(先看這個)
  • 我以為 RAG 檢索很爛——「段級命中率」只有 27%。試了 reranker、query 前綴、hybrid 三招,全部撞在 ~36% 這道牆,還把 top-5 越調越差。
  • 差點下「調不動」結論前,我做了一件該一開始就做的事:把撈回來的內文印出來,自己判對不對。結果 10/11 ≈ 91% 一直是對的——27% 是一把壞掉的尺量出來的假象。
  • 根因是切片器把「表格」放到每頁最前面,害章節標籤標歪。改一行(表格接在內文之後)→ 標籤修對、出處誠實、命中率回到真實水準。
  • 三個心法:評估要兩段分開量對指標調參前先驗證指標本身撞牆時根因常在上游(切片),不在你正調的地方
🧭 本文導覽
Part 一 — 觀念打底
Part 二 — 怎麼量準不準
Part 三 — 除錯實錄
Part 四 — 收尾

這是一篇除錯實錄。主角是一套工廠設備手冊的 RAG(pgvector + bge-m3),但故事的核心跟你用什麼技術棧無關:我對著一個「看起來很低」的準確率調了老半天,最後發現準確率一直很好,是我量錯了。把每一步、每個數字、每段改了什麼、以及「R(檢索)到底怎麼做」全部攤開,當你是完全新手也看得懂。

PART 一 — 觀念打底

一片(chunk)到底存了什麼

很多人(包括一開始的我)以為:一本書切成很多片,每一片 = 向量 + 內容,就這樣。錯,少了一半。一片實際上還帶著「出處」。從資料庫拉一片真的出來看:

欄位 值(範例) 這是什麼
text「五、簡易故障排除 …症狀:泵浦漏氣→處置…」內容
embeddingvector(1024 維)向量
book_idpump_nantou出處:哪本書
section五、 簡易故障排除出處:哪一章節 ← 就是「標題」
page_from9出處:哪一頁

所以正確的公式是:一片 = { 內容 + 向量 + 出處(哪本書·哪章節·哪頁) }

為什麼非要記出處?因為向量和內容只回答「它說了什麼」,回答不了「它出自哪裡」。給人的答案一定要能講「出自《南投抽水機手冊》第五章故障排除第 9 頁」,對方才能翻到那頁自己核對、才敢信。沒有出處的答案 = 一段沒來源的文字,在工安場景(答錯=事故)不能用。這個「章節出處」就是後面整個故事的主角。

R 怎麼做:一次檢索的完整流程

RAG = R(檢索)+ A(增強)+ G(生成)。這篇只講 R——「拿到問題,怎麼找出該讀的那幾片」。流程只有三步:

  1. 把問題翻成向量:embedding 模型(bge-m3)只做這一件事——把一句話變成 1024 個數字。它不做搜尋。
  2. 資料庫找最近的幾片:pgvector 拿問題向量,跟庫裡每一片的向量算「餘弦距離」,回傳最近的 top-k。
  3. 回傳的片自帶出處:每片都帶著它的 book/section/page(見上一節)。
# 1) 問題 → 向量(normalize 後才能用 cosine)
qvec = model.encode([question], normalize_embeddings=True)[0]

# 2) pgvector 找最近的 top-k(<=> = 餘弦距離,越小越近)
#    注意回傳的每一片都帶 section / page = 出處
SELECT text, book_id, section, page_from,
       1 - (embedding <=> %(qvec)s::vector) AS score   -- 相似度(越大越像)
FROM manual_chunks
ORDER BY embedding <=> %(qvec)s::vector
LIMIT 5;

記住這裡冒出一個數字:score(相似度)——問題跟這片有多像,0~1。等一下會用到它做「查不到就閉嘴」。它跟後面的「命中率」是兩個不同的東西,先別混。

PART 二 — 怎麼量準不準

pgvector 的 cosine similarity 到底在算什麼

這是最多人有看沒有懂的一段,拆到最白。

1)每片、每個問題,都是 1024 維空間裡的一支「箭頭」。bge-m3 把一段文字變成 1024 個數字,你可以想成一支指向某方向的箭頭(向量)。意思相近的文字,箭頭方向也相近。

2)cosine 量的是「兩支箭頭的夾角」,只看方向、不看長短。夾角越小 → 越相似:

兩箭頭方向 cosine 相似度 意思
完全同方向1最像
垂直(90°)0無關
完全相反-1最不像
cosine similarity 向量夾角示意圖:問題向量與各候選片的夾角越小越相似
cosine 只看方向(夾角),不看長短:夾角越小 → cosine 越大 → 越相似。

3)公式與「normalize」為什麼重要。cosine 相似度 = 兩向量的點積 ÷ 各自長度:

cos(A, B) = (A · B) / (|A| × |B|)          # 點積 ÷ 兩者長度

# 小例子(用 3 維好算):
A = [1, 0, 0]   B = [1, 1, 0]
A·B = 1×1 + 0×1 + 0×0 = 1
|A| = 1,  |B| = √2 ≈ 1.414
cos = 1 / (1 × 1.414) ≈ 0.71     # 夾角 45°,中等相似

# 關鍵技巧:如果先把每個向量「normalize」成長度 1(|A|=|B|=1),
# 分母就變成 1 → cosine 直接等於點積 A·B,算起來又快又一致。
# 所以建庫和查詢都用 normalize_embeddings=True。

4)pgvector 的 <=> 是「cosine 距離」,不是相似度——差一個 1 減。

embedding <=> qvec        -- cosine 距離:0=最近(最像),2=最遠
1 - (embedding <=> qvec)  -- = cosine 相似度:1=最像,這就是我們的 score

-- 找最近的 5 片 = 照「距離」由小到大排,取前 5:
SELECT text, section, page_from,
       1 - (embedding <=> qvec) AS score   -- 給人看的相似度(越大越像)
FROM manual_chunks
ORDER BY embedding <=> qvec               -- 排序用「距離」(越小越近)
LIMIT 5;
一句話:<=> = 距離(越小越近),1 - <=> = 相似度(越大越像)。ORDER BY <=> LIMIT 5 就是「把跟問題方向最接近的 5 支箭頭撈出來」。

5)資料多才需要 HNSW 索引。最單純的做法是「問題向量跟庫裡每一片都算一次距離」(暴力法,O(N))——1251 片這種量級,暴力掃也才毫秒級。等到幾十萬片,就建 HNSW 索引(一種近似最近鄰的圖結構),不用全部比、查詢快很多。cosine 場景建索引用 vector_cosine_ops

順帶澄清「出處」有兩個角色(容易搞混):
放進向量的:我們把「書名/機種/章節」當前綴,跟內文一起餵給 bge-m3 → 幫它找得更準(方向更聚焦)。
存成欄位的:book / section / page 也原封存成資料庫欄位 → 給人核對出處用。頁碼這種只存欄位、不進向量。
同一個「章節」,在向量裡幫忙聚焦、在欄位裡當門牌,兩個用途。

進階:為什麼是向量、bge-m3 怎麼散、為什麼是球面不是球體

(數理背景取向;一般讀者可跳過,不影響前面理解。)

① 為什麼把文字轉向量? 目標是把「語意相似」變成「幾何相近」,才能用線代 + 最近鄰在大規模上算。依分布假說(Firth:you shall know a word by the company it keeps),語意由上下文決定 → 學一個映射 φ:文字 → ℝ1024,使語意相似 ≈ 內積大。之所以非「內積空間」不可(而非任意度量空間):內積 (a) 可微 → 可被梯度訓練;(b) 給線性結構,方向可編碼概念、具組合性;(c) 支援 ANN 索引做次線性搜尋。而 bge-m3 是用對比學習(InfoNCE)訓的:把 (query, 正例) 拉近、(query, 負例) 推遠,而「近/遠」就是用 cosine 定義——所以「內積 = 相關性」不是巧合,是訓練目標親手把幾何塑成這樣。
② bge-m3 怎麼「散布」? 架構是 XLM-RoBERTa encoder → 每 token 的 contextual 向量 → pooling(dense 取 CLS)→ 1024 維一點。空間的幾何是被對比損失雕出來的,不是天生均勻。而且關鍵:transformer embedding 出了名的 anisotropic(各向異性 / 表徵退化;Gao 2019、Ethayarajh 2019)——向量其實擠在一個窄錐裡、只佔球面一小塊。所以「散布」是一團團 cluster(≈ 語意主題),不是撒滿整顆球;這也是有些 pipeline 要做 whitening / 去中心化的原因。
③ 為什麼 normalize 到「球面」而不是留在「球體內」?(丟掉半徑 ‖v‖,四個硬理由)

(i) 方向載語意、半徑載雜訊。 經驗上 embedding 的方向帶語意,範數傾向跟詞頻 / 多 generic / 資訊量相關,不是核心語意。用裸內積 ⟨a,b⟩ = ‖a‖‖b‖cos θ,高範數向量會不管夾角就霸榜 top-k → 撈到「大聲的」不是「相關的」。normalize 消掉這個 magnitude bias,排序純看方向。
(ii) 訓練–推論一致。 模型用 cosine(normalized dot + 溫度)當損失訓,優化的是夾角;推論就必須也用 cosine(在球面上比),才對齊「被賦予意義的量」。用球體內歐氏距離 = 量一個模型沒被訓練過的東西。
(iii) 度量塌縮成一致。 球面上 內積 = cosine = 歐氏弦距 = 夾角 單調對應(‖u−v‖² = 2(1−cos θ))→ 一個 ANN 索引即可正確按 cosine 排。留在球體內、範數不一,則 L2 排序 ≠ cosine 排序 ≠ 內積排序,三者打架。
(iv) 本質是「商掉半徑」。 宣告 magnitude 那一維無語意 → 把它 quotient 掉;我們在乎的是從原點出發的射線(ray / 方向)不是點,normalize 就是取每條射線與單位球的交點當代表。球體內部那一維正是被判為 nuisance 的維度,所以塌掉。

附帶:normalize 後分數落在 [−1,1]、跨 query 可比,前面那個 abstain 門檻 0.52 才有意義。

兩段式評估:檢索 vs 生成,分開量

RAG 有兩個不確定點,最重要的一句話:分開量。只量端到端,出錯你不知道是哪一段爛。

階段 性質 怎麼量
① 檢索(問題→撈出該讀的段)確定性(同題同結果)Hit@k / MRR,可像單元測試反覆跑
② 生成(拿那些段→產答案)非確定性(每次不一樣)faithfulness(有無幻覺)+ 跑 N 次看變異 + LLM 當評審

如果你走抽取式(直接秀原文 + 出處、不讓模型自由生成),第二段的不確定性幾乎工程掉——答案就是那段原文、引用天生精確。所以量測重心壓倒性落在第一段:檢索。這篇也聚焦在這。

檢索評估怎麼做:gold set + Hit@k + abstain

檢索是確定性的(同一題→同一向量→同一排序),所以可以像考試一樣量。三個步驟:

  1. 出一份考古題(gold set):每題 = (問題 + 這題「應該」命中的出處)。例:「漏氣怎麼處理」→ 應命中 pump 的「故障排除」段。這步找老師傅標最準。
  2. :問題→向量→撈 top-k→每片帶出處。
  3. 比對:撈回來的出處,有沒有等於我標的「應該命中的出處」?有=命中(1),沒有=沒命中(0)。
# gold set:(問題, 應命中的 book, 應命中的 section 關鍵詞)
GOLD = [
    ("抽水機進水管漏氣怎麼處理", "pump_nantou", "故障排除"),
    ("空壓機排氣溫度過高怎麼辦", "compressor_hanbell", "排氣溫度"),
    # … 以及幾題「6 本裡根本沒有」的離題題(期望 abstain)
    ("員工年假怎麼申請", None, None),
]

# 對每題撈 top-k,檢查「對的 book 且 section 含關鍵詞」排第幾
def first_rank(rows, book, kw):
    for i, r in enumerate(rows, 1):
        if r.book_id == book and kw in (r.section or ""):
            return i          # 命中的名次
    return 0                  # 沒命中

算三種數字:Hit@k(正確出處有沒有在 top-k 裡)、MRR(排多前面)、abstain(離題題的相似度分數夠不夠低,能不能被門檻擋掉)。第一次跑的結果:

指標 數字 意思
Book-Hit@1(對的書)92%撈到對的手冊很穩
Section-Hit@1(對的段)27%top-1 常是「對的書、錯的段」?
Section-Hit@591%對的段幾乎一定在前 5,只是沒排第一
兩個「分數」千萬別混:
相似度分數(0.69、0.81…):pgvector 算向量距離,「問題跟這片多像」。用來做 abstain(低於門檻就說查不到)。
命中率(Hit@k)(27%、91%…):我的腳本算,「撈回的出處對不對」。用來量準確率。
這篇從 27 到 91 變的是命中率(對不對),不是相似度(像不像)。
PART 三 — 除錯實錄

調參四連敗:reranker / prefix / hybrid

段級 top-1 只有 27%,想調高。試了幾招常見的「撈完再重排」:

# ① cross-encoder reranker:撈 top-20 → 逐段重新打分重排
scores = CrossEncoder("BAAI/bge-reranker-v2-m3").predict([(q, chunk_text) ...])

# ② query 前綴:給問題加一句指示再編碼
qvec = encode(["為以下設備問題找相關的操作或故障排除段落:" + q])

# ③ hybrid(字面 + 向量,RRF 融合):技術術語靠字面精準命中
rrf = 1/(60 + 向量排名) + 1/(60 + 字面排名)
方法 Section-Hit@1 @5
baseline27%91%
+ reranker36%55% ↓
+ query 前綴36%64% ↓
+ hybrid36%73% ↓
訊號:四個不同方法全部撞在 ~36% 這道牆,還都犧牲 @5。 當「好幾個不相干的方法收斂到同一個數字」——通常代表天花板不在你正在調的地方。這時該停下來往別處找,而不是繼續加花招。我當時差點下「調不動」的錯結論。

轉折:先驗證你的「尺」——27% 是假象

下結論前,我做了一件該一開始就做的事:把每題 top-1 撈到的內文印出來,自己當老師傅判「這段到底對不對」,不看我標的關鍵詞

問:抽水機進水管漏氣怎麼處理
  期望詞[故障排除] → 比對 section 標題「三、啟動後注意事項」→ 不中 → 算 miss ✗
  但 top-1 的『內文』是:
    「[抽水機無法抽水] 症狀:泵浦漏氣 → 處置:清潔孔螺絲未鎖緊、逆止閥…」
  ← 內容 100% 正確!是被「錯的章節標題」騙了。
判內文的話,top-1 正確 ≈ 10/11 = 91%,不是 27%。 PLC、HMI、CNC、電弧爐節能全是同一回事:內容撈對了,是被「切歪的章節標題」騙了我的尺。

最貴的一課:對一個指標調參之前,先驗證那個指標本身是對的。 「27% / 調不動 / 撞 36% 的牆」全是我拿關鍵詞比對髒的章節標題量出來的假象。四招調參,全部在追一個幻影——檢索本來就 ~91% 好,而那個 91,其實從第一次量(Section-Hit@5=91%)就露過臉了。

根因:section 標籤為什麼髒(一個表格擺放 bug)

章節標題(section)是切書的時候記下來的:程式用「這片前面最近的那個標題」當它的 section。問題出在切片器組每頁文字的方式——它把該頁的表格攤平後放到頁首,結果表格跑到了「自己該屬的標題」前面:

這頁真實內容:   五、簡易故障排除(標題)  +  故障表:「漏氣→處置…」

原本(表格前置): 【故障表:漏氣→處置】  五、簡易故障排除(標題)…
                  ↑ 表格在「五、」標題前面 → 前面最近的標題是上一頁的「三、」
                  → section 被標成「三、啟動後注意事項」❌(錯的門牌)

注意:內容(text)和向量(embedding)完全沒問題、撈得到。壞的只有 section 這個「出處門牌」。這正是為什麼一個小小的位置 bug 有這麼大殺傷力——它是系統性的:每一頁有表格的地方都標歪,而且不會報錯,只會讓你的評估與出處默默失真。

修法 + 重建 + 前後對照

修法就一行:把表格從「放頁首」改成「接在內文之後」,它就跟到前面最近的正確標題。

# chunker.py 的 _page_text() —— 只改這一行
# 修前(表格前置 → 掛到前一個標題,標歪):
return ("\n".join(parts) + "\n" + body) if parts else body
# 修後(表格接在 body 之後 → 掛到正確標題):
return (body + "\n" + "\n".join(parts)) if parts else body

因為切出來的文字變了,要重新 build(重算向量)+ 重灌資料庫才生效。重建 6 本(1435 片)後實測:

指標 修前 修後
Book-Hit@192%100%
Section-Hit@1(關鍵詞尺)27%45%(真內文更高)
漏氣內容的出處❌ 啟動後注意事項✅ 五、簡易故障排除

要看清楚這一步「變的是什麼」:不是檢索變聰明了(內容本來就 ~91% 對)。變的是章節標籤/出處變誠實了——所以「量標籤對不對」的尺,數字自然跳上來。真正對使用者的意義是:出處不再騙人

PART 四 — 收尾

出處止血:以原文+頁為主 + abstain 門檻

根治(修切片)之外,serving 端也做了兩個「止血」改動:①出處以「原文+頁碼+分數」為主,section 標題為輔(它可能還有沒修乾淨的);②加 abstain 門檻,查不到就閉嘴

# 請求加一個 abstain 門檻(實測:in-scope 最低 0.65、離題最高 0.47 → 取 0.52~0.56)
class SearchBody(BaseModel):
    question: str
    k: int = 4
    min_score: float | None = None      # None = 不啟用(向後相容)

for r in rows:
    score = float(r.score)
    if body.min_score is not None and score < body.min_score:
        continue                        # 低於門檻 → 不回 = 查不到(答錯=工安,寧可閉嘴)
    results.append({
        "text": r.text, "page_from": r.page_from, "score": round(score, 4),
        "book_title": r.book_title, "section": r.section,   # section = 輔,勿當唯一權威
    })

思考邏輯:內文與頁碼是可信的(撈對了、頁也對),章節標題被切歪不可信——那就把可信的當主出處、可疑的當輔。再用門檻把離題擋掉:實測離題題最高分 0.47 < 該答題最低分 0.65,兩群無重疊,切得乾淨。這是「答錯=事故」前提下的止血:內容已經對了,先讓出處別騙人、該閉嘴就閉嘴。

每個數字到底在量什麼(防被騙表)

整個故事最容易讓人看走眼的,就是「一堆百分比到底在量什麼」。一張表看懂:

數字 它其實在量什麼 真相
92% / 100%對的「書」在 top-1一直很好
27%關鍵詞比對「章節標題」假的——標題髒
91%對的段在 top-5 / 內文判對真實品質,從頭就在
36%調參後的關鍵詞 @1追幻影,撞牆
45%修標籤後的關鍵詞 @1尺變準了(仍低估)

能穩定做好的是「上游」,不是查詢時的花招

很自然會有一個直覺:「要提升準確率,就去調查詢那端的演算法,或提問時多抓幾個關鍵詞讓向量更準。」——這正是這篇從頭到尾證明「低槓桿、常常在追幻影」的那條路。reranker、query 前綴、hybrid(就是「調算法 / 提問抓關鍵」)三招,全部撞在 36% 牆、還把 top-5 越調越差。

真正能「穩定做好」的,幾乎都在上游(建庫階段),不在查詢時補救:

槓桿 在哪一端 本文的證據
切片品質(邊界切乾淨、出處標對)build改一行表格擺放 → 出處誠實、Book@1 92→100%
餵給向量的文字(embed_text) 乾淨、有好章節前綴build文字髒 → 向量就髒;前綴幫聚焦
檢索範圍鎖定(只搜「這台機器」的手冊)build+serve硬過濾杜絕跨機台污染
一把準的尺(gold set)評估尺壞了,你連「有沒有變好」都不知道
⚠️ 查詢時 reranker / 前綴 / hybridserve本文全失敗,低槓桿
分工也講清楚:我們擁有 R,Agent 擁有 G。 API 只做 R——把最相關的原文 + 出處撈出來回傳;要不要生成一段話(G)、用什麼語氣,是消費端 Agent(它自己的 LLM)或一層「限縮 G」在做,不是這支 API。所以查詢那一端最值錢的兩件事,是「鎖定範圍(這台機器)」和「abstain(查不到就閉嘴)」,不是重排花招。R 的品質決定在「你怎麼切、怎麼標、餵什麼進向量」,不在「查的時候再補救」。

附:一個完全獨立的測試 kit,也重現了同一件事

後來我另做了一份零依賴的測試 kit(不同的簡單切法、numpy 記憶體索引、在 Kaggle GPU 上跑),拿同一批 6 本手冊、7 題重測。第一版我偷懶把「正確答案」標成 book 名 → 一跑 Hit@1 = 100%,漂亮。改成判「正確那一段的內文」後:

指標(同一批資料、同一組題) Hit@1
Book-Hit(撈到對的『書』)100%
Content-Hit(撈到對的『那一段』)71%(5/7)

一個完全不同的實作,卻獨立重現了「book 級 100% 是假安心、content 級才是真」。而且它老實抓出兩題(進水管漏氣、CNC 開機步驟)的正確段落根本不在 top-5——那個簡單 window 切法把答案切散了。

這不是特例。 只要你的「正確答案」判到 book 級,就會拿到虛高的分數;判到 content 級(人看內文),才看得到真相。連我自己做評估工具,都會不自覺滑回 book 級的漂亮數字——所以「先驗證你的尺」這句話,對做工具的人尤其要天天提醒自己。

附:多格式 + 自動判切法(700 本不用一本一本看)

前面都拿 6 本 PDF 講。但真實情況是:手上700 本、還混 Word / Excel / PPT / TXT / ODT。不可能一本一本開來看「這本該用哪種切法」。要兩個機制:多格式抽字 + 自動判切法

① 多格式抽取(零安裝優先)

格式 怎麼抽 需裝?
docx / xlsx / pptx / odt / ods純標準庫 zipfile+regex 拆 OOXML/ODF零安裝
pdfpymupdfpymupdf
txt / csv直接讀(試 utf-8/big5/gb18030)
xls(舊)xlrdpip install xlrd

② 自動判切法的決策樹(靠可量訊號)

是試算表(xls/xlsx/ods/csv)?  ── 是 ──▶  table(逐列切,表格是結構化的)
        │否
是 PDF?
   ├ 平均字/頁很低 且 多數頁有圖無字  ──▶  scanned(掃描/圖片型 → 先 OCR)
   ├ 有 ≥8 個書籤/目錄            ──▶  toc(依章節切,標題當出處)
   ├ 偵到明顯標題字級(大於內文)   ──▶  heading(依標題切)
   └ 都沒有                       ──▶  window(固定視窗保底)
        │否(docx/txt/pptx/odt…)
純文字類  ──▶  window

判斷依據全是可量的訊號:書籤數、字/頁密度、字級分布、副檔名。所以能對整個資料夾**批次自動判**。實跑一個混格式資料夾,每份自動選 + 印出原因:

自動選 為什麼
plc / vfd(pdf)toc有 89 / 120 個書籤
cnc / pump(pdf)heading偵到標題字級大於內文
docx / pptx / txtwindow無 PDF 結構訊號
一句話:一個資料夾丟進去,系統自己抽字、自己判每份該怎麼切、印出原因——700 本不用一本一本看。判斷靠可量訊號,不靠人肉。想微調某一類再手動指定切法即可。

③ 真的下載真檔來驗,不是嘴上「支援」

「支援多格式」很好講,但程式碼寫得出 from_xlsx(),不代表它切出來是對的。所以 notebook 裡真的去公開來源下載真的 Office 檔(filesamples.com 的 .xlsx / .docx / .xls),抽出來、切出來、再用一句檔案裡真的有的英文片語當 gold 去查,看檢索有沒有命中那一片。抽字量對得上、content-hit 命中,才算「這格式真的通」。

真檔 抽出 gold 片語(檔案裡真的有)
accessibility.docx36,855 字eight section headings
excel_tips.xlsx逐列30 Excel Functions in 30 Days
sample.pptx5,856 字
踩過的坑:一開始 demo「支援 Excel/Word」卻連一個真檔都沒下載——切的到底對不對根本沒驗過。沒有真檔的「支援」是空話。要嘛下載真檔驗,要嘛別說支援。

④ 讓別人也能一鍵跑:預設「對方環境是空的」

要把 notebook 丟上 Kaggle 給別人 Run All,一個根本認知:預設對方機器什麼都沒有。你本機有的套件、有的模型快取,對方通通沒有。所以第一格就 pip install / apt install 把環境裝齊,不要靠「我這台剛好有」。

真實的坑:我在載入 bge-m3 前塞了 os.environ['HF_HUB_OFFLINE']='1'——那是「我本機已經有快取」才成立的假設。搬到 Kaggle 空環境,這行反而禁止下載,直接 LocalEntryNotFoundError拿本機當基準,就是搬家會爆的原因。

嵌入模型(bge-m3,約 2GB)的載入寫成雙路徑,失敗給明確指引:先找有沒有掛上來的本地模型(不靠網路、最穩),沒有再線上下載,兩條都失敗就印出「怎麼辦」而不是丟一坨 traceback。

# 第一格:預設空環境,先裝齊(不靠「我這台有」)
!pip -q install pymupdf sentence-transformers numpy xlrd
!apt-get -qq -y install tesseract-ocr tesseract-ocr-chi-tra tesseract-ocr-chi-sim

# bge-m3 雙路徑:掛上來的本地模型優先 → 否則線上下載 → 都失敗給指引
def _find_local_bge():
    # Kaggle 模型掛得很深(.../bge-m3/<框架>/<變體>/<版本>/config.json),
    # config.json 常在 bge-m3 目錄再下好幾層 → 用「路徑含 bge+m3 的 config.json」定位才穩。
    import glob, os
    for cfg in glob.glob('/kaggle/input/**/config.json', recursive=True):
        low = cfg.lower()
        if 'bge' in low and 'm3' in low:
            return os.path.dirname(cfg)
    return None

_local = _find_local_bge()
try:
    if _local:
        print('用本地掛載模型:', _local); emb = SentenceTransformer(_local, device=device)
    else:
        print('線上下載 bge-m3(需 Internet On)…'); emb = SentenceTransformer('BAAI/bge-m3', device=device)
except Exception as e:
    msg = ('bge-m3 載入失敗 —— 兩解擇一:\n'
           '  (A) 右側 Add Input -> Models -> 搜 bge-m3 掛上(不吃網路,最穩)\n'
           '  (B) Settings -> Internet -> On(線上下載 ~2GB)\n'
           f'原始錯誤: {e}')
    print(msg)
    raise SystemExit(msg) from None   # from None:切斷鏈式例外,免得 Kaggle 的 IPython 格式器爆 NoneType
emb.max_seq_length = 512   # 封頂,防 VRAM 爆
拿模型的方式 動作 適合
(A) 掛 Kaggle ModelAdd Input → Models → 搜 bge-m3 掛上HF 被擋、要可重現,不吃網路
(B) 開 InternetSettings → Internet → On網路通、懶得掛模型
一句話:「能在別人空機器上一鍵跑起來」本身就是一種正確性。第一格裝齊環境、模型雙路徑載入、失敗給人話——這些不是額外功夫,是「可重現」的定義。

心法總結

  • 一片 = 內容 + 向量 + 出處。 出處(書·章·頁)不是裝飾,是「答案能不能被追、被信」的關鍵,尤其工安場景。
  • 評估要兩段分開量。 檢索(確定性,Hit@k 可回歸)、生成(非確定性,faithfulness)。混在一起你不知道哪裡爛。
  • 對指標調參前,先花 10 分鐘驗證指標本身是對的。 尤其你的「正確答案」是用某個 metadata 標籤(章節、分類)比對出來的——先確認那些標籤可信。我沒驗,結果對著一把髒尺調了半天、還差點下錯結論。
  • 不同方法收斂到同一個數,是「天花板不在你正調的地方」的強訊號。 該往上游(切片/標籤)或往量測本身找,不是繼續加檢索花招。
  • 一行改動的價值,不一定在「讓它變強」,而在「讓一個系統性 bug 消失」。 這次那一行讓章節標籤不再標歪 → 出處誠實、評估也終於量得準。

留言

發佈留言

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