對應原題(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 再強也救不回來。

⚙️ 怎麼用

為什麼要切

  1. embedding 模型有 token 上限(多數 512–8192)
  2. 更重要的理由:一個向量代表的內容越多,語意越被稀釋。一整章的 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 咬過。

問題為什麼會壞怎麼處理
多欄 PDFPDF 沒有「欄」的概念,只有字元的座標。純文字抽取會依 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

面試官會挑的四個點:

  1. count 要用 tokenizer 不是 len() —— 中文一個字可能 1–3 個 token(見 分詞與 BPE),用字元數會估錯好幾倍
  2. 分隔符要有優先序且能遞迴降級 —— 單一片段自己超長時必須往下一層切
  3. overlap 要從「前一塊的尾巴」接過來,不是從原文重新切位置
  4. 程式碼區塊要先保護起來(實務上先用正則把 ``` 區塊抽出佔位,切完再還原)

⚠️ 注意 / 什麼時候不適用

  • 別先調 chunk size。先確認:解析對不對 → 有沒有加脈絡前綴 → 有沒有 hybrid + rerank。chunk size 通常是效益最小的旋鈕。
  • overlap 不是萬靈丹。它增加索引體積與重複命中,解決不了「缺脈絡」——那是 breadcrumb 與 contextual retrieval 的工作。
  • 語意切分很貴而且不穩定。每次重建索引結果可能不同,不利於做 A/B 對照。
  • 切塊策略改了就要重建整個索引。這是不可逆的決定,上線前多試幾種。
  • 中文沒有空白分隔,英文那套分隔符優先序要改(。!?; 與換行)。
  • 表格摘要會有幻覺。摘要只用來檢索,餵給模型的一定要是原表。

🧪 我實際套用的紀錄

  • 2026-09-20:從面試題庫展開。待辦:下次做 RAG 前,先把解析結果原樣印出來檢查一輪。

🔗 相關