對應原題(Common ‧ RAG and Retrieval / Coding)
- What chunking strategy would you use for a large technical documentation corpus, and why? — Asked at: Glean
- How do you handle tables, figures and multi-column PDFs in a retrieval pipeline?
- Implement a text chunker with overlap that never splits a semantic unit. — Asked at: Harvey
🎯 什麼情境該想到我
當你「RAG 效果不好,而且懷疑問題出在資料進去的那一刻」的時候。切塊是 RAG 管線中最早、最便宜、也最常被草率帶過的一步——切壞了,後面 reranker 再強也救不回來。
⚙️ 怎麼用
為什麼要切
- embedding 模型有 token 上限(多數 512–8192)
- 更重要的理由:一個向量代表的內容越多,語意越被稀釋。一整章的 embedding 落在所有主題的平均位置,哪個查詢都不太像。
策略階梯(由粗到細)
| 層級 | 做法 | 適用 |
|---|---|---|
| 1. 固定長度 + overlap | 每 N 個 token(不是字元)切,重疊 10–20% | baseline。永遠先做這個當對照組 |
| 2. 遞迴分隔符 | 依優先序嘗試 \n\n → \n → 。 → ,直到塊夠小 | 一般文字,最常見的預設 |
| 3. ★ 結構感知 | 依 Markdown 標題層級 / 程式碼 AST / HTML DOM 切 | 技術文件的正解 |
| 4. 語意切分 | 算相鄰句子的 embedding 相似度,低於閾值就切 | 無結構的長文(訪談稿、會議紀錄) |
| 5. 命題式 / LLM 切分 | 讓 LLM 拆成獨立可理解的命題 | 品質最高、成本最高,通常只值得用在高價值語料 |
★ 技術文件語料的具體答案(Glean 那題)
這題問的是大型技術文件庫,所以「固定 512 token」是不及格的答案。要講的是:
1. 依標題層級切,並把 breadcrumb 前綴進每個 chunk
原文 : 「⋯設為 true 即可啟用。」
embed : 「REST API 指南 > 驗證 > OAuth 2.0 > 設定項
allow_refresh — ⋯設為 true 即可啟用。」
★ 這一招的效益通常大於調 chunk size。
技術文件充滿「它」「這個參數」「上述設定」——脫離了章節脈絡,這段文字對任何查詢都不像。
2. 不可切開的單位
- 程式碼區塊(切開的程式碼片段是噪音,不是資訊)
- 表格(連同表頭一起)
- 列表項、API 簽章、警告框
3. 太小就往上合併
只有一行的 ### 參數 標題塊沒有檢索價值。設下限(如 100 token),不足就併入父節點。
4. 每個 chunk 必帶 metadata
doc_id, 版本, 完整路徑, 更新時間, 語言, 權限標籤(見「權限感知檢索與引用歸因」)版本尤其關鍵——技術文件最常見的幻覺來源是檢索到舊版本的說明。
大小怎麼選
沒有魔法數字。 取決於 embedding 模型、查詢粒度、reranker 預算。
常見起點:256–512 token,overlap 10–20%,但必須用自己的評估集掃一輪(見 RAG 評估與營運)。
★ 小 chunk 檢索準、大 chunk 生成好 —— 怎麼兩者都要
這是切塊最核心的張力:
| 小 chunk | 大 chunk | |
|---|---|---|
| 檢索精準度 | 高(語意集中) | 低(稀釋) |
| 生成所需脈絡 | 不足(斷章取義) | 充足 |
三種解法(都是「檢索用小的、餵給模型用大的」):
| 方法 | 做法 |
|---|---|
| Parent-document / small-to-big | 索引小 chunk,命中後回傳它的父段落或整份文件 |
| Sentence-window | 索引單句,命中後回傳前後各 k 句 |
| ★ Contextual Retrieval(Anthropic, 2024) | 用 LLM 為每個 chunk 產生一段「這段在整份文件裡的位置與脈絡」,前綴進去再 embed |
Anthropic 自報的數字(在其測試語料上):contextual embedding 讓檢索失敗率降約 35%(5.7% → 3.7%);加上 contextual BM25 降約 49%;再加 reranking 降約 67%。
⚠️ 廠商自報、特定語料。方向可信,幅度要自己量。
成本上這招需要對每個 chunk 呼叫一次 LLM——用 前綴快取 Prompt Caching 把整份文件當共用前綴,成本可以壓下來。
📄 文件解析(tables / figures / 多欄 PDF)
這題考的是「你有沒有真的做過」——因為做過的人都被 PDF 咬過。
| 問題 | 為什麼會壞 | 怎麼處理 |
|---|---|---|
| 多欄 PDF | PDF 沒有「欄」的概念,只有字元的座標。純文字抽取會依 y 座標把左右欄交錯讀成一行 | 用版面感知解析(版面偵測模型先分出區塊,再逐區塊讀),或直接用 VLM 讀整頁 |
| 表格 | 抽成文字後行列關係全毀 | 轉成 Markdown / HTML 保留結構;整張表不切開;★ 另外用 LLM 生一段自然語言摘要一起 embed——因為使用者問「北區去年營收多少」,和表格裡的 北區 | 2025 | 3,241 在詞彙上幾乎不重疊 |
| 圖表 | 圖本身沒有可檢索文字 | 用 VLM 產生描述;caption 一定要與圖綁定(它常是全頁最有資訊量的一行) |
| 掃描件 | OCR 錯字會一路傳播到答案 | 記錄 OCR 信心分數,低信心的段落標記出來;別讓錯字進了索引還假裝沒事 |
| 頁首/頁尾/頁碼 | 每頁重複的樣板會污染 embedding | 解析時就移除 |
| 跨頁的表格與段落 | 被頁面邊界硬切 | 先做文件級的重組再切塊 |
一句話:RAG 的品質上限是由解析器決定的,不是由 LLM。
很多「RAG 效果差」的專案,真正的原因是 PDF 根本沒被正確讀出來——先把解析結果印出來看過一遍,這是最高投報率的除錯動作。
★ 一句判準:入庫方案跟著「結構在哪兒」走,不跟副檔名走
(補充自 https://juejin.cn/post/7690026779797733411,查證 2026-09-28)
不要問「PDF 該怎麼切」,要問「這份文件的結構在哪裡,我能不能保住它」:
| 結構在哪裡 | 入庫方案 |
|---|---|
| 寫在文本裡(Markdown 標題、HTML 標籤) | 直接按結構切,幾乎零損耗 |
| 在版面座標裡(PDF) | 先用版面解析「洗」成結構化文本,再回到上一列 |
| 在 XML 裡(docx/pptx 本質是 zip 裡的 XML) | 用 python-docx/python-pptx 按標題、章節、投影片取,不要用正則硬刮 |
| 在行列交叉裡(表格) | 見下方「表格」——保留結構或翻譯成語言,唯獨不能切碎 |
| 在語法樹裡(程式碼) | 按 AST/def/class 邊界切,每塊自帶「檔案路徑 + 函式名」metadata |
★ 表格:先問「查詢是什麼型別」,再決定入庫方式
上面的表格處理講的是「怎麼不切壞」,但更前面還有一個分岔——有些表格根本不該進向量庫:
| 查詢型別 | 正解 | 為什麼不用向量 |
|---|---|---|
| 精確數值(「2024Q3 華南營收多少」) | Text-to-SQL:讓 LLM 生成查詢直接打表 | embedding 會把 華南 2024Q3 12.7 稀釋成一團模糊語義,向量給不出精確數字 |
| 模糊語義(「哪個區域成長最快」) | 行敘事化:每一列轉成一句自然語言(「2024Q3,華南區營收 12.7 億,環比 +8%」)再嵌入 | 行級語義完整,向量才有用武之地 |
| 小表整體 | 整張表入庫,不切 | 切碎的表格兩半都無用 |
★ 這是很多 RAG 專案的根本誤判:把 Excel 當文件切塊進向量庫。
結構化資料的正解常常是 SQL,不是檢索——先判斷查詢型別,再決定要不要走向量。
版面解析:別自己啃 PDF,用現成的(查證 2026-09-28)
上一節說「用版面感知解析」,具體有兩條成熟路線:
| 工具 | 做什麼 | 實際規格 |
|---|---|---|
| RAGFlow DeepDoc | 版面辨識 + 表格結構辨識(TSR) | 把一頁分成 10 類組件:Text/Title/Figure/Figure caption/Table/Table caption/Header/Footer/Reference/Equation(deepdoc/README.md 查證)。表格走 TSR 還原層級表頭與合併儲存格 |
| MinerU(OpenDataLab) | PDF/圖片 → 保留結構的 Markdown | 表格轉 Markdown 表、公式轉 LaTeX;分 Flash / Basic / Standard / Advanced 四檔解析強度(README 查證,CLI --tier standard) |
MinerU 這條路線的價值在於「把問題轉回已解決的問題」——轉成 Markdown 之後,就回到本卡最上面那個「結構寫在文本裡」的理想情況。
💻 實作(coding 題:不切開語意單位的 overlap chunker)
import re
def chunk(text, max_tokens=512, overlap=64, count=len):
"""依分隔符優先序遞迴切分;永不切開一個語意單位。
count: token 計數函式(實務上用 tokenizer,別用 len(str))"""
SEPS = ["\n## ", "\n### ", "\n\n", "\n", "。", ". ", " ", ""]
def split(t, seps):
if count(t) <= max_tokens or not seps:
return [t]
sep, rest = seps[0], seps[1:]
parts = t.split(sep) if sep else list(t)
out, buf = [], ""
for p in parts:
cand = (buf + sep + p) if buf else p
if count(cand) <= max_tokens:
buf = cand
else:
if buf:
out.append(buf)
# ★ 單一片段自己就超長 → 用更細的分隔符再切一層
out.extend(split(p, rest) if count(p) > max_tokens else [p])
buf = "" if count(p) > max_tokens else p
if buf:
out.append(buf)
return out
chunks = split(text, SEPS)
# 加 overlap:把前一塊的尾巴接到這一塊開頭
if overlap <= 0 or len(chunks) < 2:
return chunks
out = [chunks[0]]
for i in range(1, len(chunks)):
tail = chunks[i-1][-overlap * 4:] # 粗估:1 token ≈ 4 字元
out.append(tail + chunks[i])
return out面試官會挑的四個點:
count要用 tokenizer 不是len()—— 中文一個字可能 1–3 個 token(見 分詞與 BPE),用字元數會估錯好幾倍- 分隔符要有優先序且能遞迴降級 —— 單一片段自己超長時必須往下一層切
- overlap 要從「前一塊的尾巴」接過來,不是從原文重新切位置
- 程式碼區塊要先保護起來(實務上先用正則把 ``` 區塊抽出佔位,切完再還原)
⚠️ 注意 / 什麼時候不適用
- 別先調 chunk size。先確認:解析對不對 → 有沒有加脈絡前綴 → 有沒有 hybrid + rerank。chunk size 通常是效益最小的旋鈕。
- overlap 不是萬靈丹。它增加索引體積與重複命中,解決不了「缺脈絡」——那是 breadcrumb 與 contextual retrieval 的工作。
- 語意切分很貴而且不穩定。每次重建索引結果可能不同,不利於做 A/B 對照。
- 切塊策略改了就要重建整個索引。這是不可逆的決定,上線前多試幾種。
- 中文沒有空白分隔,英文那套分隔符優先序要改(
。!?;與換行)。 - 表格摘要會有幻覺。摘要只用來檢索,餵給模型的一定要是原表。
🧪 我實際套用的紀錄
- 2026-09-20:從面試題庫展開。待辦:下次做 RAG 前,先把解析結果原樣印出來檢查一輪。
🔗 相關
- 稀疏與密集檢索 —— 切好之後怎麼索引
- Reranker 重排序 —— 切塊粗糙時的補救(但不是替代)
- RAG 評估與營運 —— 怎麼證明某個切法比較好
- 分詞與 BPE —— 為什麼要用 token 計數
- 前綴快取 Prompt Caching —— 讓 contextual retrieval 的成本可接受
- 工具-RAG檢索增強生成 —— 決策層:什麼時候該用 RAG
- 向量索引 ANN —— 切好、解析好之後,索引與向量庫產品怎麼選