Embeddings 與 chunking
把文字變成可比較的座標,把長文切成適合 retrieve 的單位——RAG 的兩塊基石。
TL;DR
- Embedding = 「文字的座標」,意義近的座標就近——這是 semantic search 的數學基礎
- Chunking 不是切均等大小——句子 / 段落 / semantic boundary 通常更好
- Chunk 太小:失去 context;太大:embedding 不準。實務 300–800 token 起手
一個情境:相同關鍵字 ≠ 相關
User 問:「員工的 bug 修了多少?」
文件裡兩段都有 "bug" 這個字:
- A 段 — 醫療研究:「今年我們發現一種新的病毒 'bug',XDR-47……」
- B 段 — 軟體工程:「本季 engineering 修了 47 個 P1 issue……」(沒有 "bug" 字)
純關鍵字搜尋會抽 A 段(有 "bug");但人類一看就知道 B 段才是答案。這就是為什麼需要 embedding:它比對的是「意思」不是「字面」。
Embedding 是什麼
把一段文字餵給 embedding model,會吐出一個數字 list(典型 512 / 1024 / 1536 維)。每個數字大致在 -1 到 +1 之間,整個 vector 代表這段文字的「語意座標」。
先裝 SDK,去 voyageai.com 拿一把 API key 設成 VOYAGE_API_KEY 環境變數:
pip install voyageai
import voyageai
vo = voyageai.Client() # 讀 VOYAGE_API_KEY
def embed(text: str, input_type: str = "document"):
res = vo.embed([text], model="voyage-3-large", input_type=input_type)
return res.embeddings[0] # list[float],長度 1024
vec = embed("今年軟體工程部修了 47 個 bug")
# [0.013, -0.041, 0.082, ...] 共 1024 個數字
關鍵性質:意義相近的兩段文字,embedding 之間的角度(cosine)會很小。這讓我們可以「用數學算相似度」而不是「字串比對」。
每個 dimension 代表什麼?沒人知道,是訓練出來的,不可解釋。你可以想像第一維代表「這段話多醫療」、第二維「多技術」之類的(只是助記用,實際不是)。
為什麼要 chunk
Embedding model 有 input 上限(Voyage voyage-3-large 是 32K token),但更重要的是:整份文件 embed 成一個 vector 沒意義。你問「軟體工程部做了什麼」,整本年報的 vector 跟你的 query 算 cosine 一定不準。
所以要切 chunk,每個 chunk 各自 embed、各自存。chunk 才是 retrieve 的最小單位。
三種 chunking 策略
| 策略 | 怎麼切 | 優點 | 缺點 |
|---|---|---|---|
| Size-based | 固定字元數(例:500 字一段,overlap 50) | 任何文件都能用、好實作 | 切到句子中間、失去結構 |
| Structure-based | 按 markdown header / paragraph 切 | chunk 邊界自然 | 需要文件本身有結構(PDF、純文字不適用) |
| Semantic | 切句後用 NLP 判斷句子相關性,把相關的句子合併 | 品質最高 | 慢、實作複雜 |
實務取捨:
- Markdown / 規範文件 / FAQ:structure-based 最自然
- PDF 抽出來的純文字 / 客服紀錄:sentence-based 或 size-based + overlap
- 不知道用哪個:size-based 800 chars + 100 overlap 起手——production 八成都這樣
import re
def chunk_by_section(text: str) -> list[str]:
# markdown 用 ## 切
return re.split(r"\n## ", text)
def chunk_by_size(text: str, size=800, overlap=100) -> list[str]:
chunks, i = [], 0
while i < len(text):
chunks.append(text[i:i + size])
i += size - overlap
return chunks
Chunk 大小的取捨
| Chunk 太小(< 200 token) | Chunk 太大(> 1500 token) |
|---|---|
| 失去周邊 context | embedding 被「平均化」,特定主題訊號被稀釋 |
| 一個概念被切兩段 | retrieve 回來會塞爆 prompt |
| retrieve 出 5 個都只看到半句話 | top-k 的「k」實質變小 |
起手 rule of thumb:300–800 token 的 chunk + 50–100 token 的 overlap。Overlap 是為了避免「答案剛好被切在邊界」。
# 估 token 數的偷吃步:英文約 4 字元 / token,中文約 1.5 字元 / token
# size=800 chars 的英文 chunk ≈ 200 token;中文 ≈ 530 token
一個常被忽略的細節:input_type
很多 embedding API(Voyage、Cohere)允許指定 input_type:
document:embed 你存進 DB 的 chunkquery:embed user 的問題
兩邊用同一個 model,但內部會用稍微不同的方式對待——這讓「短 query」跟「長 chunk」的 vector 在同一個空間更可比。漏掉這個 flag 不會壞,但 retrieval 品質會差一截。
chunk_vec = embed(chunk, input_type="document") # 存進 DB 的
query_vec = embed(user_question, input_type="query") # 來查的
接下來
有了 embedding 跟 chunking,下一篇把整個 RAG flow 串起來——chunk → embed → store → retrieve → augment → generate,5 個步驟一個極簡 in-memory 範例 + 怎麼選 vector DB。

