前言
上一篇我們建立了一個最基本的 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 在複雜問題上也能精準命中。
系列導覽