文件切塊的策略
一份文件只換成一個座標,意思會被平均掉、搜尋就不準。這堂課教你把文件切成大小剛好的「塊」:依照段落組合、相鄰重疊一點、每塊記下出處,為下一堂的搜尋打好基礎。
上一堂你看到 embedding 能把一句話變成座標,再用距離找出意思相近的句子。但你的資料不是一句一句的,而是一份份文件:員工手冊、產品說明、會議紀錄。
整份文件直接變成一個座標可以嗎?可以,但效果很差。這堂課就來解決這件事。
為什麼要切塊?
想像一份員工手冊,同時寫了請假、報帳、遠端工作。如果整份只換成一個座標,這個座標會落在三個話題的「中間」:什麼問題都有點像,但什麼問題都不夠像。
反過來,一句一塊也不行。「需在三天前提出。」單獨看完全不知道在講什麼,就算被找到,模型也沒辦法根據它回答。
好的塊像一張便利貼:拿出來單獨看,也知道它在說什麼。
所以切塊(chunking)就是把文件切成大小剛好的片段,每一塊各自變成一個座標。搜尋時找的是「最相關的那幾塊」,不是「最相關的那份文件」。
三個實用原則
1. 順著文件結構切
文件本來就有結構:標題、段落、清單。作者已經幫你把相關的內容放在一起了,順著它切,最不容易把一句話從中間切斷。
最簡單的做法是用「空行」找出段落,再把相鄰的段落組合起來,直到快超過字數上限,就開新的一塊。
2. 相鄰兩塊重疊一點
不管怎麼切,總有資訊剛好落在交界上。讓相鄰兩塊重疊一個段落,交界的內容兩邊都有,就不怕被切成兩半而找不到。
重疊會多存一點資料,換來的是不漏接。
3. 每一塊都記下出處
切開之後,每一塊要帶著自己的「身分證」:來自哪個檔案、屬於哪個標題。第四堂課要讓模型附上引用,靠的就是這些資料。
另外有一個小技巧:把文件標題加在每一塊的開頭。一塊寫著「需在三天前提出申請」的內容,加上「【請假規定】」之後,意思就清楚多了,搜尋也會更準。
準備練習用的文件
建立一個 docs 資料夾,放進三份虛構的員工手冊。內容可以直接複製,也可以換成你自己的文件(記得先拿掉個人資料)。
docs/leave.md:
# 請假規定
## 特別休假(特休)
到職滿六個月的同仁享有特休。天數依年資計算,請到人資系統的「我的假勤」頁面查詢。
特休請在預計休假日的三個工作天前,於人資系統提出申請,經直屬主管核准後生效。
當年度沒休完的特休,可以遞延到隔年六月底前使用;逾期未休的天數,依規定折算工資。
## 病假
因病需要請假時,請在當天上班時間開始前通知主管,回到公司後再補登系統。
連續請病假超過三天,需要附上醫療院所開立的證明。
docs/expense.md:
# 報帳規定
## 可以報帳的項目
因公務產生的交通費、文具與餐費可以報帳。交通費包含捷運、公車與高鐵。
計程車費只限晚上十點後下班,或需要攜帶大型器材時使用。
## 報帳流程
請在費用發生後三十天內,於報帳系統上傳發票或收據照片並填寫用途。主管審核後,財務部會在次月十號匯入薪資帳戶。
## 發票遺失
發票或收據遺失時,請填寫「支出證明單」並請主管簽名。每人每年最多使用三次。
docs/remote.md:
# 遠端工作辦法
## 申請資格
通過試用期的同仁,每週最多可以申請兩天遠端工作。
## 申請方式
請在前一週的週五前,於團隊行事曆登記遠端日期,並告知直屬主管。
## 遠端工作的規範
遠端工作日的上班時間與進辦公室相同,上午十點到下午四點之間需要保持通訊軟體在線。
公司筆電可以帶回家使用,但不可以連接公共 Wi-Fi 處理客戶資料。
寫一個切塊程式
建立 chunking.py。這個檔案之後幾堂課都會用到:
from pathlib import Path
MAX_CHARS = 150 # 每塊最多幾個字元(要配合 embedding 模型的長度上限)
OVERLAP = 1 # 相鄰兩塊重疊幾個段落
def split_paragraphs(text: str) -> list[str]:
"""用空行切出段落,並去掉頭尾空白。"""
return [p.strip() for p in text.split("\n\n") if p.strip()]
def make_chunks(text: str, max_chars: int = MAX_CHARS, overlap: int = OVERLAP) -> list[str]:
"""把段落依序裝箱:裝不下就封箱,並把最後幾段帶進下一箱。"""
chunks = []
current = []
for para in split_paragraphs(text):
if current and len("\n\n".join(current + [para])) > max_chars:
chunks.append("\n\n".join(current))
current = current[-overlap:] if overlap else []
current.append(para)
if current:
chunks.append("\n\n".join(current))
return chunks
def load_chunks(folder: str = "docs") -> list[dict]:
"""讀取資料夾裡所有 .md 檔,切成塊並記下出處。"""
results = []
for path in sorted(Path(folder).glob("*.md")):
lines = path.read_text(encoding="utf-8").splitlines()
if not lines:
continue
title = lines[0].lstrip("#").strip() # 第一行當作文件標題
body = "\n".join(lines[1:])
for i, chunk in enumerate(make_chunks(body)):
results.append({
"id": f"{path.name}#{i}", # 例如 leave.md#0
"source": path.name,
"title": title,
"text": f"【{title}】\n{chunk}", # 每塊開頭都加上標題
})
return results
if __name__ == "__main__":
for c in load_chunks():
print(f"--- {c['id']}({len(c['text'])} 字)")
print(c["text"])
執行:
python chunking.py
你會看到每一塊的編號、長度和內容。花一分鐘用眼睛讀一遍:每塊單獨看懂嗎?有沒有哪一塊只剩半句話?這個檢查比任何公式都重要。
逐段看懂
split_paragraphs:以「空行」作為段落的分界。Markdown 文件和多數文字檔都適用。make_chunks:像在打包行李。把段落一段一段放進箱子,下一段放不下了就封箱,再把最後一段(OVERLAP)複製到新箱子裡,達成重疊。load_chunks:每一塊都是一個字典,帶著id、source、title。id用「檔名#編號」,之後一看就知道出處。
切塊沒有標準答案
塊要多大、重疊多少,沒有放諸四海皆準的數字。不同文件、不同模型、不同問題,最佳值都不一樣。幾個起步的建議:
- 先從段落組合開始,大小配合 embedding 模型的上限。
- 表格、程式碼、條列清單盡量不要切開,切開後很難單獨看懂。
- 文件很長、結構清楚(例如有很多二級標題),可以改成「每個標題一塊」。
- 不確定時,選一個合理的設定先往下做。第五堂課會教你用測試題實際比較,讓數據告訴你哪個比較好,而不是靠感覺。
這是我的 chunking.py,會依照空行把 Markdown 文件切塊。請幫我新增一個選項:改成以「## 二級標題」為單位切塊,每塊開頭加上文件標題和該段的小標題,例如「【請假規定|病假】」。如果某個標題底下的內容超過 MAX_CHARS,再用原本的段落方式切。請保留原本的函式,讓我可以比較兩種切法。
- 建立
docs資料夾和三份手冊,執行chunking.py,數數看總共切出幾塊。 - 用眼睛讀每一塊,找出一塊「單獨看不太懂」的,想想是切太小還是切錯位置。
- 執行上面的長度檢查程式,確認沒有塊超過模型上限。
- 把
OVERLAP改成0再執行一次,比較前後的塊有什麼不同。 - 放一份你自己的文件進
docs(例如課程筆記或產品說明),看看這個切法適不適合它。
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。