iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
AI Engineering

從零搞懂 RAG 評測之旅系列 第 24 篇

DAY24. 文件載入與切割

  • 分享至 

  • xImage
  •  

昨天我們裝好了 Ollama、模型與 Python 套件。今天正式寫第一段 RAG 程式:把 data/ 裡的文件讀進來,再切成適合檢索的小段落(chunks)。這是 RAG 流程的第一個環節,也是最容易被低估的一步,因為切得好不好,會直接影響後面的檢索品質。

今日目標

  1. 用 LangChain 載入 data/ 裡的文件
  2. 理解為什麼需要切割,以及 chunk_size、chunk_overlap 的意義
  3. 切割文件並檢查結果
  4. 實驗不同參數對 chunk 數量的影響

為什麼要切割?

直接把整份文件做成一個向量有兩個問題:

  • 檢索不精準:一份文件可能談好幾個主題,整份向量化後語意被「平均」掉,問到細節時很難命中。
  • 塞不進模型:檢索到的內容最後要放進 prompt,太長會超出模型的上下文限制,也會增加生成的負擔。

所以我們把文件切成大小適中的片段,每個片段各自向量化。檢索時只撈出最相關的幾段,既精準又省空間。

Step 1:載入文件

在專案根目錄建立 build_rag.py,先寫載入的部分:

from langchain_community.document_loaders import DirectoryLoader, TextLoader


def load_documents(data_dir: str = "data"):
    loader = DirectoryLoader(
        data_dir,
        glob="**/*.md",            # 若你的檔案是 txt,改成 "**/*.txt"
        loader_cls=TextLoader,
        loader_kwargs={"encoding": "utf-8"},
    )
    return loader.load()


if __name__ == "__main__":
    docs = load_documents()
    print(f"共載入 {len(docs)} 份文件")
    for doc in docs:
        print(doc.metadata["source"], "→", len(doc.page_content), "字")

執行:

uv run python build_rag.py

你應該會看到每份文件的路徑與字數。如果文件數量是 0,先檢查 data/ 位置與 glob 副檔名是否對得上。

Step 2:切割文件

接著在同一個檔案加入切割函式:

from langchain_text_splitters import RecursiveCharacterTextSplitter


def split_documents(docs, chunk_size: int = 500, chunk_overlap: int = 50):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", "!", "?", ";", ",", " ", ""],
        add_start_index=True,
    )
    return splitter.split_documents(docs)

這裡每個參數都值得了解:

參數 意義
chunk_size 每個 chunk 的最大長度,單位是字元數(預設以 len 計算),中文一個字算一個
chunk_overlap 相鄰 chunk 之間重疊的字元數,避免關鍵資訊剛好被切在邊界而遺失語意
separators 切割時依序嘗試的分隔符,會優先在段落處切,不行再退到句子、再退到字詞
add_start_index 在 metadata 記錄 chunk 在原文的起始位置,方便日後追查

RecursiveCharacterTextSplitter 的運作方式是「遞迴」:先用最優先的分隔符(段落)切,如果某一段還是超過 chunk_size,就換下一個分隔符繼續切,直到每段都夠小為止。這就是它最常被當作預設選擇的原因。

Step 3:檢查切割結果

把 __main__ 區塊改成:

if __name__ == "__main__":
    docs = load_documents()
    chunks = split_documents(docs)

    print(f"原始文件:{len(docs)} 份")
    print(f"切割後:{len(chunks)} 個 chunks")

    lengths = [len(c.page_content) for c in chunks]
    print(f"長度範圍:{min(lengths)} ~ {max(lengths)} 字,平均 {sum(lengths) // len(lengths)} 字")

    print("\n--- 第 1 個 chunk ---")
    print(chunks[0].page_content)
    print(chunks[0].metadata)

    print("\n--- 第 2 個 chunk ---")
    print(chunks[1].page_content)

重點是親眼看過 chunk 的內容。請檢查三件事:

  1. 句子有沒有被切斷在奇怪的地方
  2. 第 1 與第 2 個 chunk 的結尾與開頭是否有重疊(這就是 chunk_overlap 的效果)
  3. metadata 裡的 source 與 start_index 是否正確

Step 4:實驗不同參數

用不同的 chunk_size 比較切出來的結果:

if __name__ == "__main__":
    docs = load_documents()

    for size, overlap in [(200, 20), (500, 50), (1000, 100)]:
        chunks = split_documents(docs, chunk_size=size, chunk_overlap=overlap)
        print(f"chunk_size={size:>4}, overlap={overlap:>3} → {len(chunks)} 個 chunks")

你會觀察到:chunk_size 越小,chunk 數量越多。這個取捨沒有標準答案:

chunk 偏小 chunk 偏大
優點 語意集中,檢索精準 上下文完整,不易斷章取義
缺點 資訊破碎,單一 chunk 可能不足以回答問題 一段混入多個主題,檢索容易失焦,也佔用更多 prompt 空間

小結

今天完成了文件載入與切割,明天會讓這些 chunks 變得「可搜尋」:用昨天下載的 bge-m3 模型把 chunks 轉成向量,存進 Chroma 向量資料庫,並做第一次相似度搜尋。


上一篇
DAY23. 安裝 Ollama 與專案套件
下一篇
DAY25. Embedding 與向量資料庫
系列文
從零搞懂 RAG 評測之旅 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言