文件切片(Chunking)是 RAG 表現好壞的第一關。切錯了,後面 Embedding 再強、Reranker 再準也救不回來。這篇教您三種策略與實作。
為什麼要切片?
- LLM context 有限,不能塞整本書
- 太大的 chunk 會稀釋相關度
- 切片可以保留來源 metadata(章節、頁碼)
策略 1:Fixed Size(固定大小)
最簡單:每 N 個 tokens 切一刀,相鄰 chunk 有 overlap。
from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=500,
chunk_overlap=50,
separators=["\n\n", "\n", "。", ",", " ", ""]
)
chunks = splitter.split_text(text)
優:簡單、快、可預測
缺:可能在句中切斷、語義不完整
建議參數:500 tokens + 50 overlap(中文)/ 800 + 100(英文)
策略 2:Semantic(語義切片)
用 embedding 偵測語義邊界——當相鄰句子的 embedding 距離大於門檻時切。
from llama_index.core.node_parser import SemanticSplitterNodeParser
splitter = SemanticSplitterNodeParser(
buffer_size=1,
breakpoint_percentile_threshold=95,
embed_model=embed_model
)
nodes = splitter.get_nodes_from_documents(docs)
優:語義完整、效果好
缺:慢、貴(每次切都要算 embedding)
策略 3:Hierarchical(階層式)
多層切:章 → 節 → 段。檢索時可以動態調整粒度。
# 結構化文件(有 H1, H2, H3)
def hierarchical_chunk(doc):
chapters = split_by_heading(doc, 'h1')
for chapter in chapters:
sections = split_by_heading(chapter, 'h2')
for section in sections:
paragraphs = split_by_size(section, 300)
yield {
'text': paragraph,
'metadata': {'chapter': ..., 'section': ...}
}
優:保留結構、利於檢索
缺:實作較複雜,適合有 H1/H2/H3 結構的文件
三種策略對比
| 策略 | 速度 | 品質 | 適合 |
|---|---|---|---|
| Fixed Size | 極快 | 中 | 大量純文字 |
| Semantic | 慢 | 高 | 高品質要求、量不大 |
| Hierarchical | 中 | 高 | 結構化文件(書、報告) |
實務建議
- 第一版用 Fixed Size 500/50,跑起來看效果
- 若 Hit Rate < 70%,試 Semantic
- 若文件本身有結構(章節清楚),用 Hierarchical
- 所有策略都要保留 metadata(來源、頁碼、章節)
中文切片的特殊問題
- 沒有 word boundary,分詞工具不可靠 → 直接用字元數切
- 句號、分號是好的切點
- 表格、清單建議整塊保留
- 標題與內文要連在一起,別切散
chunk_size 怎麼挑?
- 太小(< 200):缺乏語境
- 太大(> 1500):稀釋相關度
- 實務最佳:300–800 tokens
chunk_overlap 怎麼挑?
- 0:邊界資訊會丟
- 10–20%:剛好
- 50%+:重複、浪費儲存