用 W-RAC 讓你的 RAG 省錢又好查

📌 本文重點

  • W-RAC 目標是在不犧牲 RAG 效果下大幅壓低 chunking 成本
  • 先把網頁拆成可定址的小單元,再用 LLM 只做「分組決策」
  • 保留結構化資訊有助於 debug、調優與控制向量庫容量

一件事先講清楚:W-RAC 的目的,就是在不犧牲 RAG 效果的前提下,把「為了 chunking 花給 LLM 的錢」砍到最低。

參考論文:Web Retrieval-Aware Chunking (W-RAC)


為什麼你現在的 chunking 很可能在燒錢

多數團隊做 RAG,會遇到幾個典型做法:

  • 固定長度切分(例如 500 tokens 一刀)
  • rule-based(照標題、段落、HTML tag 切)
  • 直接丟給 LLM「幫我分合理的 chunk」

這幾種方法的共通問題:

  1. 重複內容被 embed 很多次
    尤其是網頁側邊欄、導覽列、footer,幾乎每個 chunk 都有一份。

  2. LLM 成本被當 tokenizer 用
    用 LLM 來重寫、分段、產生摘要,等於把整個網站內容吃一遍,token 直接爆掉。

  3. 不好 debug
    一個查詢跑出來一堆 chunk,很難追:

  4. 這個 chunk 為什麼被這樣切?
  5. 是哪個頁面、哪個段落?

W-RAC 的核心想法:先把網頁拆成結構化「可標號的小單元」,LLM 只負責決定「哪些單元湊成一個 chunk」,完全不改寫原文。

這樣可以:

  • LLM 只看「結構化 outline + 短內容片段」,token 使用大幅下降
  • chunk 是由原始小單元組合而成,可追蹤來源、可重建原文
  • 重複小單元只存一次,減少向量庫容量

💡 關鍵: 先抽取可定址小單元,再用 LLM 只做分組規劃,可以在保留 RAG 效果的前提下,把 chunking 相關的 LLM token 成本壓到最低。


核心功能:W-RAC 在做什麼?

1. 把網頁拆成「可定址單元」

具體做法:

  1. 用瀏覽器自動化抓頁面(例如 Playwright)
  2. 保留 DOM 結構,轉成一棵樹:
  3. 節點:標題、段落、表格列、列表項目等
  4. 每個節點給一個 ID(例如 page_123.h2_3.p_2
  5. 把每個節點變成一個「原子單元」,包含:
  6. id
  7. tag(h1/h2/p/li/td…)
  8. text(文字內容)
  9. path(在頁面中的位置)

你可以實作的步驟:

pip install playwright beautifulsoup4
playwright install

用 Playwright 拉 HTML,再用 BeautifulSoup 做 DOM 清洗、節點提取,最後存成 JSON:

{
  "id": "page_123.h2_3.p_2",
  "tag": "p",
  "path": ["body", "main", "section[2]", "h2", "p[2]"],
  "text": "本方案適用於企業內部知識庫..."
}

行動:先做「乾淨的節點抽取」,還不要想 embedding 和 LLM,確保每個網頁能拆成穩定、可追蹤的小單元。


2. 用 LLM 做「分組決策」,而不是改寫文本

傳統 agent 會:把整頁文字丟進 LLM,請它「改寫 + 切 chunk」。

W-RAC 則是:

  1. 給 LLM 的不是全文,而是「節點清單 + 結構資訊」:
  2. 節點 ID
  3. 簡短前幾個字(preview)
  4. tag / 標題層級
  5. 要 LLM 回傳的,只是ID 分組規劃,例如:
[
  {"chunk_id": 1, "node_ids": ["...h2_1", "...h2_1.p_1", "...h2_1.ul_1"]},
  {"chunk_id": 2, "node_ids": ["...h2_2", "...h2_2.p_1"]}
]

範例提示詞(可直接改用自己的模型):

你是一個文件分組器。給你一個網頁節點清單,每個節點有:id、tag、text_preview、heading_level。

目標:
- 將相關的節點分成多個 chunk
- 每個 chunk 內容長度約 300–800 字
- 優先讓同一個小節(同一個 h2/h3 底下)的節點在同一個 chunk

輸出格式:只回傳 JSON 陣列,每個元素包含:
- chunk_id:整數
- node_ids:字串陣列

不要產生任何說明文字。

接下來你在程式裡做:

  • 根據 node_ids 把原子單元的 text 串起來
  • 生成真正要 embed 的 chunk 文本

行動:選一個便宜的小模型(例如本地 LLM 或雲端小模型),先在 1–2 個頁面上跑一輪「ID 分組」,確認輸出格式與 chunk 長度合理,再批次上線。


3. 保留結構化資訊,提升可觀測性與調優效率

因為每個 chunk 只是「小單元的組合」:

  • 每個 chunk 知道自己由哪些 node_id 組成
  • 每個 node_id 可以反查 DOM path → 原頁面位置

你可以做到:

  • 查詢輸出時,在後台顯示:
  • chunk 來源頁面 URL
  • 對應的標題、段落位置
  • 線上觀察「常被命中的 chunk」長什麼樣,是否太長或太短

簡單的監控策略:

  1. 在 RAG pipeline 中記錄:query / 命中 chunk_id / 來源 page_id
  2. 定期統計:
  3. 哪些頁面 chunk 命中率高但回答不精準 → 調整該頁 chunk 最大長度
  4. 某些 tag(例如 table)被切得太散 → 調整「表格應視為一組」的規則

行動:把 node_idchunk_id 一起寫入向量庫的 metadata,之後才能在 dashboard 上做查詢與可視化。


適合誰用:三個典型場景

1. 公司知識庫 / 文件中心

情境:

  • 你有 Confluence、Notion、GitBook 或自建 docs 站
  • 想做內部問答助手,但頁面一多,embedding 成本驚人

W-RAC 的落地方式:

  1. 用 Playwright 把內部 docs 網站轉成 HTML
  2. 用 BeautifulSoup 抓出 h1–h3、段落、列表,做節點抽取
  3. 用小模型做 chunk 分組
  4. 把完成的 chunk 丟進現有向量庫(如 OpenSearch、PGVector、Weaviate)

效果:

  • 導覽列、側邊欄只存一次,不會每個 chunk 重複
  • 每個問題能對應到具體章節,方便文件 owner 微調內容

💡 關鍵: 對於大量公司文件,W-RAC 可以避免重複 embed 導覽與樣板內容,顯著降低向量儲存與 embedding 成本。

2. 官網 FAQ / 產品說明頁

情境:

  • 產品 FAQ 分散在多個頁面、Accordion、tab 裡
  • 用固定長度切,很容易一個 chunk 內混到不同問題

W-RAC 做法:

  • 把每個 Q/A 區塊視為一個原子單元
  • LLM 分組時以「一問一答」為最小單位

好處:

  • 用戶問「退款怎麼算?」時,命中的 chunk 幾乎就是整個退款 FAQ,不會混到完全不相干的條款

3. 大量公開網頁做 RAG(爬站型應用)

情境:

  • 你在做垂直搜尋 / 行業資料聚合
  • 動輒幾十萬頁 HTML,要控制成本

W-RAC 的優點在這裡會放大:

  • LLM 只負責分組計畫,成本可比「全 LLM 分 chunk」少一個數量級(論文的主張)
  • 分組邏輯可重跑:
  • 想換 chunk 長度,只要重新跑 LLM 分組,不必重新爬網

行動:挑一個實際場景(知識庫、FAQ、或爬站),先對 50–100 個頁面做 W-RAC pipeline,算出:LLM token vs 傳統做法的差異,作為是否全面導入的依據。


怎麼開始:從技術棧到實作指引

建議技術棧

類別 推薦選項 備註
抓網頁 Playwright / Puppeteer Playwright 對登入、動態頁面支援較好
HTML 解析 BeautifulSoup / lxml 把 DOM 轉成節點樹、抽取文字與 tag
分組 LLM 任一小模型(如本地 Qwen/LLama) 只做規劃決策,不改寫文本,成本壓得很低
向量庫 PGVector / Qdrant / Weaviate 支援 metadata 查詢較重要,以便 debug
嵌入模型 開源 embedding 或雲端 embedding 固定長度輸入即可,和 W-RAC 本身無強耦合

行動:先決定你現有的向量庫與 embedding 方案,再把「抓網頁 + W-RAC 分組」當作前處理模組接進去。


最快上手路徑(簡化版流程)

  1. 抓一頁 HTML
  2. 用 Playwright:

“`python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(“https://your-site.com/faq”)
html = page.content()
browser.close()
“`

  1. 用 BeautifulSoup 拆節點
  2. h1–h3pli、table row 都變成節點
  3. 幫每個節點生成 idpath

  4. 呼叫 LLM 做分組

  5. 使用前面提供的提示詞
  6. 控制每次輸入的節點數量(例如 100–200 個),避免 context 太大

  7. 依照 node_id 組 chunk

  8. 按分組結果串 text,加入 metadata(node_idspage_url

  9. 寫入向量庫 + 接到現有 RAG

  10. 查詢時流程不變,只是每個 chunk 背後多了完整來源資訊

線上觀察 chunk 品質與調優策略

觀察重點:

  1. 回答不準時,回頭看 chunk
  2. 是否同一個 chunk 裡混了太多主題?
  3. 是否重要上下文被切開?
  4. 調整策略
  5. 提示詞裡明確告訴 LLM:
    • 表格應盡量保持在同一 chunk
    • 同一個 h2 底下不要分太多 chunk
  6. 調整每個 chunk 的目標字數(例如從 800 改成 500)

行動:在你的 RAG 後台加一個「檢視來源 chunk」按鈕,點下去就顯示該 chunk 的 node 列表與原頁面位置,方便你和 PM / 內容 owner 一起 review。


小結:把 LLM 用在刀口上

W-RAC 的重點不是多厲害,而是很務實:

  • LLM 只做「分組規劃」這種高價值決策
  • 文本本身盡量保持原樣,避免幻覺與重寫成本
  • 一旦你有了「結構化可定址單元」,後面調優都變簡單

如果你正在為 RAG 成本與效果卡關,先不要換模型,先把 chunking 換成類似 W-RAC 的流程,很可能就能省下一大筆 embedding/LLM 費用,還順便讓系統更好 debug。

🚀 你現在可以做的事

  • 選一個實際頁面,用 Playwright + BeautifulSoup 做出「乾淨節點抽取」JSON
  • 用一個便宜的小模型,照文中提示詞跑一輪「ID 分組」,實際組出 chunk 並寫入向量庫
  • 在你的 RAG 後台加上 chunk_id / node_id 的 metadata 顯示與檢視來源 chunk 按鈕,開始觀察與調整 chunk 策略

留言

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *