在 Day 12,我們理解了 Embedding 這個將「語意」轉化為「數學座標」的魔法。在我們迫不及待地想把所有筆記都向量化之前,還有一個極其重要、卻也最常被忽略的準備工作 -- Chunking (文本分割)。
如果說 Embedding 是烹飪,那 Chunking 就是廚師在料理前的「備料」與「刀工」。你如何切割食材,將直接決定這道菜最終的口感與風味。同樣地,你如何分割你的筆記,也將直接決定你的 AI 助理最終的回答品質。
為什麼我們不能直接把一整篇 Notion 頁面,直接丟給 Embedding 模型呢?原因有三:
Embedding 模型的理解極限
Embedding 模型在處理小而精、語意集中的文本時,效果最好。如果你將一篇長達三千字的文章直接向量化,它的「語意座標」會變得非常模糊、平均化,失去了細節的精準度。就像問一台相機:「這張照片的主題是什麼?」,給它一張單人特寫,遠比給它一張千人大合照,更能得到精確的答案。
檢索的精準度 (最關鍵!)
使用者的問題通常是簡短而具體的,例如:「RAG 的 Reranking 是什麼?」如果你的知識庫裡,每個 Chunk 都是一篇長文,那麼包含「Reranking」的那個 Chunk,可能還混合了 Embedding、Chunking 等大量無關內容。這會「稀釋」掉關鍵資訊的語意權重,導致在向量搜索時,相關性分數下降,反而不容易被找到。
LLM 的上下文視窗 (Context Window) 限制
當我們檢索到相關的 Chunks 後,是需要把它們連同問題一起塞給 LLM 的。如果 Chunks 太大,很容易就塞爆了 LLM 的上下文視窗,導致無法提供足夠的參考資料,或是增加大量的運算成本。
針對 Notion 筆記,我們可以採用「段落優先,字數補充」的混合策略:
這樣做能兼顧:
我們可以在 src/chunk_utils.py 新增一個簡單的工具:
def chunk_text(text: str, max_len: int = 200, overlap: int = 50) -> list:
"""
將長文字切成多個 chunk,並保留 overlap
:param text: 原始文字
:param max_len: 每個 chunk 最長字數
:param overlap: chunk 與 chunk 之間的重疊字數
"""
if not text:
return []
chunks = []
start = 0
text_len = len(text)
while start < text_len:
end = min(start + max_len, text_len)
chunk = text[start:end]
chunks.append(chunk)
if end == text_len:
break
start = end - overlap # 往回 overlap 保留上下文
return chunks
# 範例
if __name__ == "__main__":
txt = "物件導向程式設計(Object-Oriented Programming, OOP)是一種程式設計範式..."
result = chunk_text(txt, max_len=20, overlap=5)
for i, r in enumerate(result):
print(f"Chunk {i+1}: {r}")
執行結果:
Chunk 1: 物件導向程式設計(Object
Chunk 2: 設計(Object-Oriented P
Chunk 3: Oriented Programming, O
...
今天,我們認識了 Chunking 策略,了解為什麼要在 Embedding 前切分文字,並且實作了一個簡單的 chunk_text 函式。
在 Day 14,我們將把這個 Chunking 工具串接到 SQLite → Embedding 流程裡,從 notion_blocks.block_text 自動生成向量,為真正的 語意檢索 (Semantic Search) 做好準備!