RAG:把你的資料交給模型
LLM 的知識凍結在訓練截止日;你的文件每週都在變。檢索增強生成(RAG)把它解凍:查詢時抓你資料的相關切片、貼進提示詞,讓模型根據證據附引用回答。模型負責推理;索引負責記憶。
管線,一個完整實例
拿一本 12,000 token 的維修手冊,從頭到尾走一遍:
- **切塊。**先按標題切(章節保持完整),再對超過約 400 token 的章節重切,保留約 60 token 重疊(10–15%)。合理的區塊大小在 200–800 token。結果:約 35 塊。
- **嵌入。**每塊變成一個向量——bge-m3 每塊輸出 1,024 個浮點數。CPU 上整本手冊只要幾秒。
- **索引。**把向量+文字+元資料(來源檔、標題路徑)存進向量庫。
- **檢索。**嵌入問題,按餘弦相似度抓前 k = 5 塊(4–10 是合理範圍)。
- **生成。**組裝提示詞:指令、帶 ID 標籤的那 5 塊、問題。
按語意邊界切塊勝過固定視窗。與表頭列分離的表格、在第 4 步被切斷的編號程序,無論嵌入器多好都是檢索不到的垃圾。
兩個立刻回本的額外動作:把標題路徑(manual › §4 › Bolts)存成元資料以便精確過濾與引用;還有,查詢要用使用者輸入的原樣嵌入——「清理」前處理通常讓檢索更差,不是更好。
嵌入:多語的必要條件
嵌入器決定「相似」的意義。對韓文與韓英混合語料,選擇很窄——純英文嵌入器對韓文查詢會無聲失敗,回傳貌似合理但錯誤的區塊:
| 模型 | 維度 | 備註 |
|---|---|---|
| bge-m3 | 1024 | 多語、韓文強——安全的本機預設 |
| nomic-embed-text-v1.5 | 768 | 英文扎實、長上下文 |
| all-MiniLM-L6-v2 | 384 | 快、以英文為中心、韓文弱 |
嵌入模型很小(0.1–0.6B 參數);一個 CPU 核心就能嚼掉數千個區塊——CPU 推論。語料嵌入一次,查詢在提問時嵌入。每個索引的嵌入器保持固定:中途換模型會讓向量來自不同空間,相似度分數變成雜訊。
向量庫
| 儲存庫 | 適用時機 |
|---|---|
| sqlite-vec | 單人、最多約 1M 向量、零維運——它就是一個檔案 |
| ChromaDB | 本機優先的原型開發,帶元資料過濾 |
| Qdrant | 正式環境:過濾、混合搜尋、規模 |
三者都能在本機跑。從 sqlite-vec 開始;真的撞牆再搬,不要提前。
組裝提示詞
Answer only from the excerpts below. Cite every claim as [n].
If the excerpts do not contain the answer, reply "not in the docs".
[1] (manual §4.2) Torque the M8 bolt to 12 Nm after lubrication...
[2] (manual §7.1) Do not re-use stretch bolts...
Question: what torque spec applies to the M8 bolt?
引用讓使用者查證而不是信任。「文件裡沒有」這個出口很重要:沒有它,模型會編一個答案,而不是讓格式失敗。
混合搜尋與重排序
純向量搜尋會漏掉關鍵字搜尋一擊必中的精確字串——錯誤碼、函式名稱、料號。混合=BM25+向量,用倒數排名融合合併;它近乎免費,而且可靠地勝過任一單獨使用。
要品質,加重排序器:抓 20–50 個候選,用交叉編碼器(bge-reranker 家族——與 bge-m3 同樣的多語故事)重排序到前 5。重排序器把區塊與查詢一起讀,單向量嵌入器做不到。涵蓋多數需求的便宜配方:混合抓 25、重排序到 5。
診斷:檢索失誤還是生成失誤?
調任何東西之前,先回答一個問題:對的區塊有被檢索到嗎?
- 印出失敗查詢的前 k 塊,連同分數。
- 如果含答案的區塊不在清單裡,那是檢索問題——修切塊、嵌入,或調高 k。
- 如果它在清單裡但答案錯,那是生成問題——更大的模型、更好的提示詞、重排序把干擾項推出去。
這一個測試能省下數小時調錯半邊管線的時間。
失敗模式
| 症狀 | 原因 | 解法 |
|---|---|---|
| 檢索到對的文件、答案錯 | top-k 有干擾項 | 重排序;縮小 k |
| 答案在文件裡卻不見天日 | top-k 饑餓或邊界不良 | 調高 k;按標題切塊 |
| 自信但過時的答案 | 索引比文件舊 | 變更時重建索引 |
| 韓文查詢找不到英文文件 | 單語嵌入 | bge-m3 |
| 引用指到錯的區塊 | 區塊丟了來源元資料 | 每區塊存來源+標題 |
要測量,不要憑感覺
保留一組 20–50 個已知答案的真實問題(黃金集)。分開計兩個數字:檢索命中率(對的區塊在前 k 嗎?)與答案正確性。每次改區塊大小、嵌入器或 k 之後追蹤它們。沒有黃金集的 RAG 工作是占星術。
何時該改用微調
微調改變行為——格式、風格、術語。它不是加事實的好方法:模型無法引用它被調過的內容,而且新文件意味著新一輪訓練。經驗法則:事實 → RAG、行為 → LoRA,兩者都要只發生在提示詞已經榨乾之後:微調。
本機堆疊,從最小開始
Open WebUI 內建文件上傳與混合檢索——從那裡開始,你可能永遠不用寫管線程式碼。RAG 誠實的核心約 50 行;自己寫一次,你就知道那些框架在幹嘛:
chunks = chunk_by_header(read("manual.md"), max_tokens=400, overlap=60)
index = [(embed(c), c) for c in chunks] # done once
def answer(question):
hits = topk_by_cosine(embed(question), index, k=5)
ctx = "\n\n".join("[{}] {}".format(i, c) for i, (v, c) in enumerate(hits))
prompt = TEMPLATE.replace("{ctx}", ctx).replace("{q}", question)
return chat(prompt) # small local model is plenty
需要權限、排程刷新或評測框架時才伸手拿 LlamaIndex 或 LangChain——不是為了上面那個迴圈。特別是韓文文件,多語嵌入(bge-m3)不是選配,而且基於標題的切塊要像尊重 markdown ## 標題那樣尊重韓文文件結構(장/절 標題)。