iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
DevOps

30 天帶你實戰 LLMOps:從 RAG 到觀測與部署系列 第 8

Day08 - 文件清洗 (Cleaning) 與切片策略 (Chunking Strategies)

  • 分享至 

  • xImage
  •  

🔹 前言

昨天我們做了一個最小可行的 QA Bot,但知識庫的單位是「整句 FAQ」,格式非常乾淨。
然而真實情況下,文件來源可能包含:

  • Word / PDF / Markdown / HTML / JSON
  • 廣告文字、目錄、程式碼塊
  • 超長段落(> 2000 tokens)

如果不處理,用戶查詢得到的答案品質會非常差。

📌 舉個例子

原始文件:

公司制度:  
加班申請需事先提出,加班工時可折換補休。  
出差申請需填寫出差單,並附上行程與預算。  
報銷規則需要提供發票,金額超過 1000 需經理簽核。
  • 不切分: 當你查「報銷」,整份文件都被拉出來。
  • 切太細 (每 5 個字一塊) :LLM 只看到「報銷規則需要…」,但上下文缺失。
  • 合理切分 (句子為單位) :LLM 剛好能獲得完整規則。

🧩 與 Token 成本的關係

  • LLM 是依照 Token 收費的。
  • 假設每次檢索都送 3000 Token,30 次查詢就是 90000 Token,成本驚人。
  • 如果透過 Chunking,把平均檢索單位壓到 200 Token,每次只送相關片段,就能節省 90% 成本

所以在 RAG pipeline 裡,我們還需要再加入兩個步驟:文件清洗 (Cleaning)文件切片 (Chunking)

https://ithelp.ithome.com.tw/upload/images/20250922/20120069ZzMiylBaa7.png

1. 查詢嵌入 (Query Embedding):線上進行,把使用者的提問轉成向量,用來跟資料庫比對。(Day10 會聊到這塊)
2. 文件嵌入 (Doc Embedding):離線處理,把知識文件轉成向量並建立索引,讓線上查詢可以快速檢索。(Day09 會聊到這塊)


🔹 文件清洗 (Document Cleaning)

文件清洗的目標是把「可讀但雜訊太多」的原文,變成「乾淨、結構保留、容易切片」的文字。建議用下面這個安全順序處理:

1) 去雜訊(Noise Removal)

  • 移除:側欄/導覽、頁眉頁腳、廣告、版權、目錄區塊、頁碼。
  • 常見關鍵詞:目錄 / Table of Contents / 返回首頁 / 下一頁 / 版權 / Copyright / 廣告
  • HTML 來源:先用 readability / trafilatura 擷取正文,再做後續清洗;或用 BeautifulSoup 刪除 <nav>、<aside>、script、style>

2) 正規化(Normalization)

  • Unicode 正規化NFKC(全形→半形、符號統一)。
  • 空白:將 \t、\r、\u00A0、\u3000 → 單一空白;連續空行壓成最多兩行。
  • 標點:合併重複標點(如 !!!),保留句讀(。?!;)以利後續切句。
  • 數字/單位:將 10K10,0005k5,000;貨幣符號統一(例:NT$NTD)。

3) 結構保留(Structure-Preserving)

  • 保留 Markdown 標題(#)、清單(-/1.)、程式碼塊()。
  • 不要硬折行:一般行合併成段落,但保留標題/清單/空白行,避免破壞段落語意。

4) 語言過濾(Optional)

  • 文件多語時,偵測語言後只保留指定語言(如 zh/en)。

    ⚠️ 短句子易誤判,可對段落而非單句做語言濾除。

5) 移除重複段落與短片段過濾(Optional)

  • 刪除重複段落(hash 比對),丟棄訊息量過低的片段(如 < 10 字)。

🔹 Demo:文件清洗

這邊需要對照 GitHub Repocleaning_demo.py 參考,文中僅擷取片段。

主程式如下,我們會用幾個函式執行上述五步驟 (去雜訊、正規化、保留結構、語言過濾、以及去重):

if __name__ == "__main__":
    raw_html = """
    <html>
      <head><title>公司規章</title></head>
      <body>
        <nav>返回首頁|目錄|下一頁</nav>
        <aside>廣告:買一送一!</aside>
        <h1>公司制度</h1>
        <p>加班申請:需事先提出,加班工時可折換補休!!!</p>
        <p>出差申請:需填寫出差單,並附上行程與預算。  請參考「Table of Contents」。</p>
        <p>獎金上限為 10K 或 NT$5000,以較低者為準。</p>
        <script>alert('ads')</script>
        <footer>Copyright 2025</footer>

        <h2>清單範例</h2>
        <ul>
          <li> A 條款</li>
          <li> B 條款</li>
        </ul>

        <pre>
        Some      code block
        should   keep      spaces
        </pre>

        <p>English note: Budget cap is 5k only?!</p>

        <p>重複段落示例。</p>
        <p>重複段落示例。</p>

        <p>短</p>

        <p>
        {code}
        </p>
        <p>結束</p>
      </body>
    </html>
    """.replace("{code}", "```\\nprint('hi')\\n```")

    cleaned = clean_document(raw_html, is_html=True, lang_keep=("zh", "en"))
    print("=== 清洗後段落 ===")
    for i, p in enumerate(cleaned, 1):
        print(f"[{i}] {p}")

執行結果:

❯ python cleaning_demo.py
=== 清洗後段落 ===
[1] 加班申請:需事先提出,加班工時可折換補休!
[2] 出差申請:需填寫出差單,並附上行程與預算。 請參考「」。
[3] 獎金上限為 10,000 或 NTD 5000,以較低者為準。
[4] Some code block should keep spaces
[5] English note: Budget cap is 5,000 only?!

這一步的目的是把文件變得更乾淨,避免在檢索時被無用內容干擾。
但是光是乾淨還不夠,因為 文件往往太長,需要再進行「合理切片 (Chunking)」。


🔹 文件切片 (Chunking)

在清洗完文件後,接下來就是「如何切片」,文件切片的目的是讓文件單位更適合檢索與語意匹配。

為什麼需要 Chunking?

在 RAG 中,最核心的流程是:查詢 → 檢索 (Retriever) → 回答
那檢索到底是針對什麼單位做的?這就是 Chunking 的意義

如果不進行 Chunking,會遇到幾個問題:

  • 檢索不精準:一份 100 頁的 PDF 被當成「一個巨大的向量」,用戶問「報銷規則」,結果整份文件都被拉出來。
  • 成本高昂:LLM 收費是按 Token 計算,如果每次都送幾千 Token,成本會急劇上升。
  • 答案偏離:太多雜訊上下文會干擾 LLM,導致回答不聚焦。

所以我們需要把文件切成「合理大小」的片段:

  • 太大 → 檢索模糊,LLM 拿到太多無關上下文。
  • 太小 → 斷句破碎,LLM 無法理解語意。
  • 剛剛好 → 保留完整語意,並控制 Token 數。

🔹 常見的 Chunking 策略

1. 固定長度切片 (Fixed-size Chunking)

  • N 個字元 / tokens 切一次。
  • 簡單快速,容易實作。
  • 缺點:可能把一句話切斷,導致語意不完整。
def chunk_fixed(text, size=20, overlap=5):
    words = list(text)  # 以「字元」為單位
    chunks = []
    for i in range(0, len(words), size - overlap):
        chunks.append("".join(words[i:i+size]))
    return chunks

sample = """加班申請需事先提出,加班工時可折換補休。
出差申請需填寫出差單,並附上行程與預算。
報銷規則需要提供發票,金額超過 1000 需經理簽核。
員工請假需提前一天申請,緊急情況可事後補辦。
遲到超過三次需與主管面談,嚴重者列入考核。"""

print(chunk_fixed(sample, size=20, overlap=5))

🔄 Overlap 是什麼?

在 Chunking 時,我們常常會加上 Overlap(重疊區)
意思是:在切片的時候,每個 Chunk 之間保留一小段重疊文字,避免語意被硬生生切斷。

📌 舉例
假設一段文字有 50 個詞,我們設定 size=20, overlap=5

  • Chunk 1 → 詞 [0–19]
  • Chunk 2 → 詞 [15–34]
  • Chunk 3 → 詞 [30–49]

這樣可以確保:

  • 「跨區句子」的資訊不會完全丟失。
  • LLM 在檢索時,還能看見上下文

✅ 一般建議 overlap 大小:chunk size 的 10%~20%
例如:chunk size=200 tokens,overlap=20 tokens。


2. 句子切片 (Sentence-based Chunking)

  • 句號、換行 等標點符號切分。
  • 更符合語意,常用於 FAQ 或政策文件。
  • 缺點:長度不均,可能出現過長句子。
import re

def chunk_sentence(text):
    sentences = re.split(r"。|!|?|\n", text)
    return [s.strip() for s in sentences if s.strip()]


sample = """加班申請需事先提出,加班工時可折換補休。
出差申請需填寫出差單,並附上行程與預算。
報銷規則需要提供發票,金額超過 1000 需經理簽核。
員工請假需提前一天申請,緊急情況可事後補辦。
遲到超過三次需與主管面談,嚴重者列入考核。
"""

print(chunk_sentence(sample))

3. 語意切片 (Semantic Chunking)

  • 利用 Embedding 相似度 偵測段落邊界。
  • 如果相鄰句子語意差異很大,就切開。
  • 優點:更智慧,能保持語意連貫。
  • 缺點:計算成本較高。
import re

def chunk_semantic(text):
    # 先斷句(技術步驟,不特別當作一種方法)
    sentences = re.split(r"[。!?]", text)
    sentences = [s.strip()+"。" for s in sentences if s.strip()]

    chunks, cur, cur_lab = [], [], None
    for s in sentences:
        lab = "attend" if any(k in s for k in ATTEND) else "admin"
        if cur_lab is None or lab == cur_lab:
            cur.append(s); cur_lab = lab
        else:
            chunks.append("".join(cur)); cur = [s]; cur_lab = lab
    if cur:
        chunks.append("".join(cur))
    return chunks

sample = """加班申請需事先提出,加班工時可折換補休。
出差申請需填寫出差單,並附上行程與預算。
報銷規則需要提供發票,金額超過 1000 需經理簽核。
員工請假需提前一天申請,緊急情況可事後補辦。
遲到超過三次需與主管面談,嚴重者列入考核。"""

ADMIN  = {"加班","出差","報銷","發票","簽核","預算"}
ATTEND = {"請假","遲到","面談","考核","緊急"}

print(chunk_semantic(sample))

🔹 Demo:Chunking 結果比較

假設原始文本:

加班申請需事先提出,加班工時可折換補休。
出差申請需填寫出差單,並附上行程與預算。
報銷規則需要提供發票,金額超過 1000 需經理簽核。
員工請假需提前一天申請,緊急情況可事後補辦。
遲到超過三次需與主管面談,嚴重者列入考核。

不同策略的結果:

策略 範例執行結果
固定長度切片 ['加班申請需事先提出,加班工時可折換補休。出差申請需填寫出差單,並附上行程與預算。報銷規則需要提供發票,金額超過 1000 需經理簽核。與預算。']
句子切片 ['加班申請需事先提出,加班工時可折換補休', '出差申請需填寫出差單,並附上行程與預算', '報銷規則需要提供發票,金額超過 1000 需經理簽核', '員工請假需提前一天申請,緊急情況可事後補辦', '遲到超過三次需與主管面談,嚴重者列入考核']
語意切片 ['加班申請需事先提出,加班工時可折換補休。出差申請需填寫出差單,並附上行程與預算。報銷規則需要提供發票,金額超過 1000 需經理簽核。', '員工請假需提前一天申請,緊急情況可事後補辦。遲到超過三次需與主管面談,嚴重者列入考核。']

📊 三種切片策略比較

策略 優點 缺點 適用場景
固定長度切片 實作簡單、速度快 可能切壞語意,片段不自然 Demo、小規模測試
句子切片 保留語意自然、易懂 句子長度不一,可能過長 FAQ、政策文件、說明文件
語意切片 能保持語意連貫,把同一主題的多句話聚在一起 需要額外計算(Embedding/關鍵詞比對),成本較高 複雜文件、大型知識庫

🔹 量化指標(Quantitative Metrics)範例

📌 以下數字僅為示意範例,實際數值會依文件內容、切片策略與語料而有所不同。

指標 說明 清洗前 清洗後
平均 Chunk 長度 每個 chunk 的平均 Token 數 1200 tokens 180 tokens
長尾分佈 是否有超長段落 有(最大 5000 tokens) 無(控制在 300 以下)
重複率 相同段落是否多次出現 15% <1%
Top-k 命中率 檢索時,正確答案是否出現在前 k 筆 60% 85%

前言有提過 Token 成本可以透過 Chunking 來節省,這裡用數字量化來看對 token 成本的影響:

  • 未清洗:檔案平均 3000 tokens → 查詢一次傳給 LLM = 3000 tokens
  • 清洗 + Chunking:檔案切成 200 tokens → 查詢一次只取前 3 個相關片段 ≈ 600 tokens
  • 節省比例:(3000-600)/3000 = 80%

👉 清洗與合理 Chunking,能讓檢索更精準、上下文更聚焦。


🔹 現成解決方案比較

工具 / 框架 特點 適用情境 缺點
Unstructured 支援 PDF / Office / HTML / Email / 圖片 OCR,多格式解析 異質文件多的情境 安裝依賴較多
trafilatura 專精網頁正文擷取,效果好 網頁知識庫清洗 僅限 HTML
LangChain Splitter 提供 Recursive / Token-based / Markdown-aware 分割器 一般 RAG 開發 須依賴 LangChain 生態
Haystack PreProcessor 支援切片 + 過濾 + metadata pipeline 需要整合 DocumentStore 的場景 需學習 Haystack 架構

現成工具很好用,不過它們的核心其實就是在做:

  • 去雜訊(Noise Removal)
  • 正規化(Normalization)
  • 結構保留(Structure Preserving)
  • 切片(Chunking, Overlap)

如果對這些原理有點概念,會比較容易:

  • 理解問題:例如文件被切得太碎、或是有重要段落不小心被清掉。
  • 調整策略:不同語料可能需要不同 chunk size 與 overlap,沒有固定的公式。
  • 觀察成本:知道為什麼 Token 消耗變多,才有空間去思考怎麼節省成本。

🔹 小結

今天我們學到:

  1. 文件清洗: 去掉雜訊、標準化文字,讓知識更乾淨。
  2. Chunking 策略:
    • 固定長度 → 快速、簡單,但可能切壞句子。
    • 句子切片 → 語意自然,但長度不均。
    • 語意切片 → 智慧、連貫,但成本高。

在實務上,最常見的做法是先依照 句子切片 再透過適度 overlap。既保留語意完整,即使句子被切在邊界,依舊可以保有上下文,也能控制 Token 成本。有了乾淨、合理切片的文件,我們就能進入下一步:

👉 向量化與索引建立 (Vectorization & Indexing),把資料存進 Vector Database!

📚 延伸閱讀


上一篇
Day07 — 最小可行的 RAG QA Bot(Web 版 MVP)
系列文
30 天帶你實戰 LLMOps:從 RAG 到觀測與部署8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言