RAG 完全指南(二):Chunking 策略與向量資料庫選型

前言

上一篇我們建立了一個最基本的 RAG pipeline。
但實際上,Chunking 策略向量資料庫的選型會直接決定你的 RAG 系統品質。

這篇深入討論這兩個核心基礎建設。


Part 1:Chunking 策略

Chunking 是把長文件切成小片段的過程。切法不對,後面的搜尋再精準也救不了。

為什麼 Chunking 很重要?

想像你有一篇 10,000 字的技術文章,如果直接整篇丟進去,問「Python 的優點是什麼」,向量搜尋要在 10,000 字的「語意海洋」裡找到準確答案,難度極高。

好的 Chunking 原則:

  • 每個 chunk 應該是語意完整的單元(不要切斷句子、段落中間)
  • 大小適中:太小 → 資訊不夠完整;太大 → 搜尋精準度下降
  • 有適度重疊(overlap):避免邊界上的資訊遺漏

策略 1:固定大小切塊(Fixed-Size Chunking)

最簡單的方法,按字元數或 token 數切割。

 1from langchain_text_splitters import CharacterTextSplitter
 2
 3splitter = CharacterTextSplitter(
 4    chunk_size=500,
 5    chunk_overlap=50,
 6    separator="\n",  # 優先在換行處切割
 7)
 8
 9text = "你的長文字..."
10chunks = splitter.split_text(text)

優點:簡單、可預測、實作快速
缺點:可能把語意相關的句子切開
適合:快速原型、結構單純的文件


策略 2:遞迴字元切塊(Recursive Character Chunking)

這是最常用的預設策略。它會依照優先順序嘗試不同分隔符: \n\n\n. → 字元

 1from langchain_text_splitters import RecursiveCharacterTextSplitter
 2
 3splitter = RecursiveCharacterTextSplitter(
 4    chunk_size=600,
 5    chunk_overlap=60,
 6    # 預設分隔符順序:段落 → 換行 → 句子 → 空格
 7    separators=["\n\n", "\n", "。", ".", " ", ""],
 8)
 9
10chunks = splitter.split_text(text)

優點:比固定大小更尊重自然語言結構
缺點:chunk 大小仍然不均勻
適合:一般文章、說明文件、知識庫(大多數情況的首選


策略 3:語意切塊(Semantic Chunking)

根據「語意斷裂點」切割,而非字元數。比較相鄰句子的向量相似度,相似度突然下降的地方就是切點。

 1from langchain_experimental.text_splitter import SemanticChunker
 2from langchain_openai import OpenAIEmbeddings
 3
 4embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
 5
 6splitter = SemanticChunker(
 7    embeddings=embeddings,
 8    breakpoint_threshold_type="percentile",  # 超過第 95 百分位的語意差距就切
 9    breakpoint_threshold_amount=95,
10)
11
12chunks = splitter.split_text(text)

優點:每個 chunk 的語意完整性最高
缺點:需要呼叫 Embedding API(索引成本更高)、速度較慢
適合:高品質知識庫、法律文件、醫療資料


策略 4:文件結構切塊(Structure-Aware Chunking)

針對有明確結構的文件(Markdown、HTML、程式碼),按結構切割。

 1from langchain_text_splitters import MarkdownHeaderTextSplitter
 2
 3# 按 Markdown 標題層級切割
 4headers_to_split_on = [
 5    ("#",  "H1"),
 6    ("##", "H2"),
 7    ("###","H3"),
 8]
 9splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
10chunks = splitter.split_text(markdown_text)
11
12# 每個 chunk 會帶有 metadata,例如:
13# {"content": "...", "metadata": {"H1": "章節標題", "H2": "小節標題"}}

優點:chunk 有結構 metadata,可以用標題做 filter;語意非常完整
缺點:只適合有結構的文件
適合:技術文件、Wiki、程式碼說明文件


策略 5:父子切塊(Parent-Child Chunking / Small-to-Big)

儲存時用小 chunk(提升搜尋精準度),但回傳時用大 chunk(提供更多 context 給 LLM)。

 1from langchain.retrievers import ParentDocumentRetriever
 2from langchain.storage import InMemoryStore
 3from langchain_community.vectorstores import Chroma
 4from langchain_openai import OpenAIEmbeddings
 5from langchain_text_splitters import RecursiveCharacterTextSplitter
 6
 7# 子 chunk:用於向量搜尋(小)
 8child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
 9
10# 父 chunk:返回給 LLM(大)
11parent_splitter = RecursiveCharacterTextSplitter(chunk_size=800)
12
13vectorstore = Chroma(embedding_function=OpenAIEmbeddings())
14store = InMemoryStore()
15
16retriever = ParentDocumentRetriever(
17    vectorstore=vectorstore,
18    docstore=store,
19    child_splitter=child_splitter,
20    parent_splitter=parent_splitter,
21)

核心思想:小 chunk 搜尋精準,大 chunk 提供足夠的前後文脈
優點:兼顧搜尋精準度和 context 完整性
缺點:實作稍複雜,需要額外維護文件儲存
適合:高品質問答系統、企業知識庫


策略比較表

策略語意完整性實作複雜度索引成本適用場景
固定大小★★☆☆☆★☆☆☆☆快速原型
遞迴字元★★★☆☆★★☆☆☆一般文件(首選)
語意切塊★★★★★★★★☆☆高品質知識庫
結構感知★★★★☆★★★☆☆技術文件、Wiki
父子切塊★★★★☆★★★★☆企業問答系統

Part 2:向量資料庫選型

向量資料庫負責儲存和搜尋 Embedding 向量。選錯工具可能導致效能瓶頸或維護惡夢。

主流選項比較

ChromaDB — 開發首選

 1import chromadb
 2
 3# 本地模式(開發用)
 4client = chromadb.Client()
 5
 6# 持久化模式
 7client = chromadb.PersistentClient(path="./chroma_db")
 8
 9collection = client.get_or_create_collection(
10    name="my_knowledge_base",
11    metadata={"hnsw:space": "cosine"},  # 使用餘弦相似度
12)
13
14# 新增文件
15collection.add(
16    documents=["文件內容 1", "文件內容 2"],
17    embeddings=[[0.1, 0.2, ...], [0.3, 0.4, ...]],
18    metadatas=[{"source": "doc1.pdf", "page": 1}, {"source": "doc2.pdf", "page": 5}],
19    ids=["id1", "id2"],
20)
21
22# 查詢(帶 metadata filter)
23results = collection.query(
24    query_embeddings=[query_vec],
25    n_results=5,
26    where={"source": "doc1.pdf"},  # 只搜尋特定來源
27)

優點:開源、零設定、本地運行、支援 metadata filter
缺點:不適合超大規模(> 百萬筆)生產環境
定價:免費


Pinecone — 雲端託管首選

 1from pinecone import Pinecone, ServerlessSpec
 2
 3pc = Pinecone(api_key="your-api-key")
 4
 5# 建立 index
 6pc.create_index(
 7    name="my-rag-index",
 8    dimension=1536,           # text-embedding-3-small 的維度
 9    metric="cosine",
10    spec=ServerlessSpec(cloud="aws", region="us-east-1"),
11)
12
13index = pc.Index("my-rag-index")
14
15# 寫入(帶 metadata)
16index.upsert(vectors=[
17    {
18        "id": "chunk_001",
19        "values": embedding_vector,
20        "metadata": {"source": "annual_report.pdf", "page": 12, "topic": "finance"},
21    }
22])
23
24# 查詢
25results = index.query(
26    vector=query_embedding,
27    top_k=5,
28    filter={"topic": {"$eq": "finance"}},  # metadata filter
29    include_metadata=True,
30)

優點:全託管、自動擴展、高可用、filter 功能強大
缺點:有費用、資料在第三方雲端
定價:免費額度 + 按用量計費
適合:需要快速上線、不想維護基礎設施的團隊


Qdrant — 自託管生產首選

 1from qdrant_client import QdrantClient
 2from qdrant_client.models import (
 3    Distance, VectorParams, PointStruct, Filter, FieldCondition, MatchValue
 4)
 5
 6client = QdrantClient(url="http://localhost:6333")  # 或 QdrantClient(":memory:")
 7
 8# 建立 collection
 9client.create_collection(
10    collection_name="knowledge_base",
11    vectors_config=VectorParams(size=1536, distance=Distance.COSINE),
12)
13
14# 寫入
15client.upsert(
16    collection_name="knowledge_base",
17    points=[
18        PointStruct(
19            id=1,
20            vector=embedding_vector,
21            payload={"source": "doc.pdf", "page": 3, "category": "finance"},
22        )
23    ],
24)
25
26# 查詢(帶複雜 filter)
27results = client.search(
28    collection_name="knowledge_base",
29    query_vector=query_embedding,
30    query_filter=Filter(
31        must=[FieldCondition(key="category", match=MatchValue(value="finance"))]
32    ),
33    limit=5,
34)

優點:高效能、豐富 filter、支援多向量、可自託管或用 Qdrant Cloud
缺點:比 ChromaDB 設定稍複雜
適合:需要自託管且有複雜過濾需求的生產系統


pgvector — PostgreSQL 擴充

如果你的應用已經在用 PostgreSQL,pgvector 可以讓你不需要引入新的資料庫。

 1-- 安裝擴充
 2CREATE EXTENSION IF NOT EXISTS vector;
 3
 4-- 建立有向量欄位的表
 5CREATE TABLE documents (
 6    id SERIAL PRIMARY KEY,
 7    content TEXT,
 8    source VARCHAR(255),
 9    embedding vector(1536)  -- 向量維度
10);
11
12-- 建立 HNSW index(加速查詢)
13CREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops);
14
15-- 插入資料
16INSERT INTO documents (content, source, embedding)
17VALUES ('文件內容', 'doc.pdf', '[0.1, 0.2, ...]'::vector);
18
19-- 相似度搜尋
20SELECT content, source, 1 - (embedding <=> '[0.3, 0.1, ...]'::vector) AS similarity
21FROM documents
22ORDER BY embedding <=> '[0.3, 0.1, ...]'::vector
23LIMIT 5;
 1# Python 操作
 2import psycopg2
 3import numpy as np
 4
 5conn = psycopg2.connect("postgresql://user:password@localhost/mydb")
 6cur = conn.cursor()
 7
 8query_vec = np.array(get_embedding("你的查詢")).tolist()
 9
10cur.execute("""
11    SELECT content, source,
12           1 - (embedding <=> %s::vector) AS similarity
13    FROM documents
14    ORDER BY embedding <=> %s::vector
15    LIMIT 5
16""", (query_vec, query_vec))
17
18results = cur.fetchall()

優點:不增加新系統、可以跟業務資料做 JOIN、事務支援
缺點:效能不如專門的向量 DB(百萬級以上需要仔細調校)
適合:已有 PostgreSQL、規模中等(< 500 萬筆)的應用


選型決策樹

你的資料量有多大?
├── < 10 萬筆,主要是開發/測試
│   └── → ChromaDB(本地,零設定)
│
├── 10 萬 ~ 500 萬筆,生產環境
│   ├── 已有 PostgreSQL?
│   │   └── → pgvector(最省事)
│   ├── 想自託管?
│   │   └── → Qdrant(效能最好的開源選項)
│   └── 想雲端託管?
│       └── → Pinecone(最省維運)
│
└── > 500 萬筆,高並發
    ├── 自託管 → Qdrant / Milvus
    └── 雲端 → Pinecone / Weaviate Cloud

實作:帶 Metadata Filter 的完整 RAG

結合上述兩個概念,這裡展示一個帶有來源過濾的實用 RAG 系統:

 1import chromadb
 2import openai
 3from langchain_text_splitters import RecursiveCharacterTextSplitter
 4from pathlib import Path
 5
 6client = openai.OpenAI(api_key="your-api-key")
 7chroma = chromadb.PersistentClient(path="./rag_db")
 8collection = chroma.get_or_create_collection(
 9    "company_docs",
10    metadata={"hnsw:space": "cosine"},
11)
12
13def embed(text: str) -> list[float]:
14    return client.embeddings.create(
15        input=text, model="text-embedding-3-small"
16    ).data[0].embedding
17
18def index_file(filepath: str, category: str) -> int:
19    """索引單一檔案,帶來源 metadata"""
20    text = Path(filepath).read_text(encoding="utf-8")
21    splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
22    chunks = splitter.split_text(text)
23
24    collection.add(
25        documents=chunks,
26        embeddings=[embed(c) for c in chunks],
27        metadatas=[{"source": filepath, "category": category} for _ in chunks],
28        ids=[f"{filepath}_chunk_{i}" for i in range(len(chunks))],
29    )
30    return len(chunks)
31
32def rag_with_filter(query: str, category: str | None = None, top_k: int = 4) -> dict:
33    """支援 category filter 的 RAG 查詢"""
34    where = {"category": category} if category else None
35
36    results = collection.query(
37        query_embeddings=[embed(query)],
38        n_results=top_k,
39        where=where,
40        include=["documents", "metadatas", "distances"],
41    )
42
43    chunks    = results["documents"][0]
44    metadatas = results["metadatas"][0]
45    distances = results["distances"][0]
46
47    # 過濾低相關度(餘弦距離 > 0.5 表示相似度 < 0.5)
48    filtered = [
49        (c, m) for c, m, d in zip(chunks, metadatas, distances) if d < 0.5
50    ]
51
52    if not filtered:
53        return {"answer": "找不到相關資料。", "sources": []}
54
55    context = "\n\n---\n\n".join(c for c, _ in filtered)
56    sources  = list({m["source"] for _, m in filtered})
57
58    prompt = f"""根據以下參考資料回答問題。請在回答末尾標注資料來源。
59
60【參考資料】
61{context}
62
63【問題】{query}"""
64
65    answer = client.chat.completions.create(
66        model="gpt-4o-mini",
67        messages=[{"role": "user", "content": prompt}],
68        temperature=0,
69    ).choices[0].message.content
70
71    return {"answer": answer, "sources": sources}
72
73
74# 使用範例
75if __name__ == "__main__":
76    # 索引不同類別的文件
77    # index_file("hr_policy.md", category="hr")
78    # index_file("engineering_guide.md", category="engineering")
79
80    # 只在工程文件裡搜尋
81    result = rag_with_filter(
82        query="如何申請 production deploy?",
83        category="engineering",
84    )
85    print(result["answer"])
86    print("來源:", result["sources"])

小結

這篇涵蓋了:

  • 5 種 Chunking 策略的原理、優缺點與選擇時機
  • 4 種向量資料庫的特性比較與選型決策樹
  • 帶 Metadata Filter 的完整 RAG 實作

有了扎實的資料基礎,下一篇我們進入真正的進階技術:
混合搜尋、HyDE(假設性文件嵌入)、Multi-Query Retrieval、Reranker——
讓你的 RAG 在複雜問題上也能精準命中。


系列導覽

  • 第一篇:基礎概念與第一個 RAG 系統
  • 第二篇(本篇):Chunking 策略與向量資料庫選型
  • 第三篇:進階檢索技術(混合搜尋、HyDE、Multi-Query、Reranker)
  • 第四篇:查詢優化與 Context 壓縮
  • 第五篇:生產級 RAG 評估與 Agentic RAG