Embedding:把語意變成向量
第 1 課說過,RAG 的流程是「把文件切塊 → 存進向量庫 → 使用者問問題時撈最相關的塊 → 丟給 LLM 生答案」。這條流程的樞紐在「撈最相關的塊」那一步:你怎麼知道哪塊最相關?
用關鍵字比對的話,使用者問「加班費怎麼算」,文件寫的是「延長工時工資標準」,就完全抓不到。用 BM25 這種傳統檢索也類似——它算的是詞頻,遇到換句話說就失靈。RAG 真正需要解決的是語意相關,不是字面相關。這件事的底層工具就是 embedding。
這堂學什麼
- Embedding 的白話解釋:語意座標是什麼、為什麼有效
- Cosine 相似度的直覺、公式、Python 純手工推導與套件寫法
- 2026 年主流模型評比:OpenAI text-embedding-3 系列、Voyage-4 系列的定價、維度與繁中表現
- 手把手實戰:用 Python 算一批句子的相似度矩陣,用 seaborn 熱力圖讓結果一目了然
- 維度數量對成本與精準度的影響,選型決策樹
觀念一:Embedding 是語意座標
你有沒有在地圖上查過「我附近的拉麵店」?地圖能找到是因為你的位置被記成座標,拉麵店也各有自己的座標,系統算的是座標之間的距離。
Embedding 做的事完全一樣,只是把「地理位置」換成「語意」。一個 embedding 模型把一段文字輸入,輸出一個向量——一組 1,000 多個浮點數,例如 [0.032, -0.187, 0.041, ...]。這組數字就是這段文字在「語意空間」裡的座標。模型在訓練時學會:語意相近的句子會落在空間裡相近的位置,語意無關的句子則相距甚遠。

實際上當然不是 2D,而是 1,536 維或更高——人類看不懂,但向量運算能精確捕捉語意的細微差異。重要觀念是:你不需要讀懂那些數字,只需要知道「近」就代表「語意像」。
觀念二:Cosine 相似度
有了向量座標,下一步是「怎麼量兩個向量有多像」。直覺上可以用歐式距離(兩點直線距離),但在高維空間裡歐式距離受向量長度影響很大,容易失準。RAG 系統一律用 cosine 相似度。

公式長這樣:
$$ \text{cosine}(A, B) = \frac{A \cdot B}{|A| \cdot |B|} $$
翻成白話:分子是兩個向量的點積(對應元素相乘後加總),分母是各自的長度相乘。結果在 -1 到 1 之間,實務上 embedding 向量通常已經 L2 normalize 過,這時點積就等於 cosine 相似度——所以你會看到很多向量庫直接用點積而不另外計算 cosine,結果是一樣的。
用 Python 從頭推導只要三行:
import numpy as np
def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)))
為什麼不用歐氏距離? 假設文件 A 和文件 B 語意一樣,但 B 只是 A 的摘要(更短),歐氏距離會因為向量長度不同而顯示「不相似」,但 cosine 只看方向,完全不受影響。這就是 RAG 場景首選 cosine 的原因。
觀念三:2026 主流模型怎麼選
2026 年市面上 embedding API 主要三個陣營:OpenAI、Voyage AI(已被 MongoDB 收購)、Google。以下只看最常進 RAG 系統的選項。

| 模型 | 維度 | 定價($/1M tokens) | 特點 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1,536 | $0.02 | 整合最廣、入門首選 |
| OpenAI text-embedding-3-large | 3,072 | $0.13 | 英文精準度最高 |
| Voyage-4-lite | 512 起 | $0.02 | 前 200M token 免費 |
| Voyage-4 | 1,024 起 | $0.06 | 多語言、支援 MoE 混搭 |
| Voyage-4-large | 2,048 起 | $0.12 | Voyage 旗艦、前 200M 免費 |
定價來源:OpenAI 官方 API 文件(2026-07)、Voyage AI 官方定價頁(2026-07)。API 價格會動,上線前務必再查一次官方。
繁中怎麼選? 這是許多台灣開發者的大哉問。實測和社群評測的結論如下:
text-embedding-3-small的中文能力在多語言 MTEB 子集上略遜於英文,但對大多數繁中企業文件場景已夠用,而且台灣開發者生態工具配套最齊。- Voyage-4 系列是目前 API 提供商裡多語言最強的之一,voyage-multilingual-2 和 voyage-4 在日、韓、中文的 MTEB 測試都優於 OpenAI-large。如果你的文件以中文為主、且需要極高精準度,Voyage-4 值得試。
- 如果你是第一次做 RAG:先用
text-embedding-3-small跑通整個流程,再根據實際 recall 品質決定要不要換模型。不要在還沒跑起來的時候就在模型選型上卡關。
兩個服務都提供 Matryoshka Representation Learning(MRL)支援:你可以要求輸出更低維度的向量(例如 256 維而不是 1,536 維),精準度只掉一點點,但儲存成本和查詢速度都大幅改善。
觀念四:維度與成本
「維度」就是向量裡有幾個數字。3,072 維的向量比 1,536 維的向量精準,但也多用兩倍儲存空間、查詢時計算量翻倍、帳單也翻倍。

實務決策原則:
- 10 萬個 chunk 以下、中文繁體文件:從
text-embedding-3-small(1,536 維)或voyage-4-lite(1,024 維)開始,成本幾乎可以忽略。 - 百萬 chunk 以上:換算儲存成本,1M 個 1,536 維 float32 向量約 6 GB,生產環境才需要開始認真考慮降維。
- Matryoshka truncation:兩家 API 都支援,指定
dimensions=512或dimensions=256,回傳的是截斷版,比重新 embed 低維版更省事。
手把手實戰
目標:取一批中英混合句子的 embedding,算出兩兩 cosine 相似度,用熱力圖讓你直覺看到哪些句子「語意在一起」。
環境需求
pip install openai numpy matplotlib seaborn python-dotenv
在專案根目錄建 .env:
OPENAI_API_KEY=sk-...
Step 1:取得一批句子的 embedding
# embed_demo.py
import os
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
MODEL = "text-embedding-3-small"
def get_embeddings(texts: list[str]) -> np.ndarray:
"""批次呼叫 API,回傳 shape (n, dim) 的 numpy 陣列。"""
response = client.embeddings.create(input=texts, model=MODEL)
# response.data 是 list,順序與 input 對應
vectors = [item.embedding for item in response.data]
return np.array(vectors, dtype=np.float32)
一次呼叫可以傳多個字串進 input(官方限制每次最多 2,048 個,單字串最多 8,191 token),API 回傳的順序和你傳入的順序保證一致。這樣做比逐條呼叫快很多,也省 API overhead。
Step 2:自己算 cosine 相似度矩陣
def cosine_matrix(embeddings: np.ndarray) -> np.ndarray:
"""
L2 normalize 後做矩陣乘法,得到 (n, n) 的 cosine 相似度矩陣。
對角線是 1.0(自己和自己最像),其餘 0~1。
"""
norms = np.linalg.norm(embeddings, axis=1, keepdims=True) # shape (n, 1)
normalized = embeddings / norms # L2 normalize
return normalized @ normalized.T # (n, n)
這段程式碼值得逐行看清楚:
np.linalg.norm(embeddings, axis=1, keepdims=True):對每一列(每個向量)算長度,保持維度方便廣播。embeddings / norms:每個向量除以自己的長度,讓所有向量長度變成 1(L2 normalize)。normalized @ normalized.T:矩陣乘法。normalize 後的點積 = cosine 相似度,這樣一次算出所有對。
Step 3:準備測試句子,跑完整流程
# 設計三個語意群:HR、技術、美食
sentences = [
# HR 群
"如何申請年假?",
"公司的請假規定是什麼?",
"員工特休計算方式",
# 技術群
"Python 怎麼解析 JSON?",
"用 json.loads() 把字串轉成字典",
"requests 套件發送 HTTP POST",
# 美食群
"台北哪裡有好吃的拉麵?",
"我今天想吃牛肉麵",
]
print(f"取得 {len(sentences)} 個句子的 embedding...")
embeddings = get_embeddings(sentences)
print(f"向量維度:{embeddings.shape[1]}") # 應輸出 1536
sim = cosine_matrix(embeddings)
print("Cosine 相似度矩陣(前 3x3 預覽):")
print(np.round(sim[:3, :3], 3))
執行結果你會看到前 3×3 的 HR 群相似度普遍在 0.85 以上,而 HR 和美食的相似度通常落在 0.5~0.6 左右。這就是模型「懂語意」的直接證據。
Step 4:用熱力圖視覺化
def plot_similarity_heatmap(sim: np.ndarray, labels: list[str], savepath: str = "heatmap.png"):
fig, ax = plt.subplots(figsize=(10, 8))
# 截短標籤避免版面爆掉
short_labels = [s[:10] + "…" if len(s) > 10 else s for s in labels]
sns.heatmap(
sim,
annot=True, # 在格子裡印數值
fmt=".2f",
cmap="YlOrRd", # 黃→橘→紅,越紅越像
xticklabels=short_labels,
yticklabels=short_labels,
ax=ax,
vmin=0.4, # 調低下限讓差異更明顯
vmax=1.0,
square=True,
linewidths=0.5,
)
ax.set_title("Cosine 相似度矩陣", fontsize=14, pad=12)
plt.xticks(rotation=30, ha="right", fontsize=9)
plt.yticks(rotation=0, fontsize=9)
plt.tight_layout()
plt.savefig(savepath, dpi=150)
print(f"圖片已存到 {savepath}")
plot_similarity_heatmap(sim, sentences)

執行完整腳本:
python embed_demo.py
你會在工作目錄得到 heatmap.png。圖上的三個紅色方塊正好對應三個語意群——HR、技術、美食——這個畫面讓你直覺理解 embedding 在做什麼,比讀任何解釋都直接。
Step 5(選做):換用 Voyage-4
如果你的文件主要是繁中,可以試試 Voyage-4:
pip install voyageai
import voyageai
import os
vo = voyageai.Client(api_key=os.environ["VOYAGE_API_KEY"])
def get_embeddings_voyage(texts: list[str], model: str = "voyage-4") -> np.ndarray:
"""
input_type 告訴模型這批文字是「文件」還是「查詢」。
文件端用 "document",查詢端用 "query"——
非對稱 embedding 的兩端要分開 embed,這是常見坑。
"""
result = vo.embed(texts, model=model, input_type="document")
return np.array(result.embeddings, dtype=np.float32)
前 200M token 免費,原型開發期間幾乎不花錢。
常見坑
坑 1:RateLimitError: Error code: 429 — you exceeded your current quota
剛申請的 OpenAI 帳號在還沒加付款方式之前,呼叫 embedding API 就會看到這個錯誤。解決方式:OpenAI 後台 → Billing → Add payment method,加完後等幾分鐘再試。另一個觸發原因是單次批次句子數太多、速度超過 tier 的 TPM 限制:把批次切小(每次 100 句以下),或在迴圈裡加 time.sleep(1)。
坑 2:混用不同模型的向量存進同一個向量庫
# 搜尋結果全錯,但程式沒有報錯
results = collection.query(query_embeddings=[query_vec], n_results=5)
# 傳回完全不相關的文件
症狀是查詢結果莫名其妙——明明問 HR 問題,撈回來都是技術文件。根本原因:你用 text-embedding-3-small embed 了文件,後來換成 voyage-4 embed 查詢,兩個模型的向量空間根本不同,座標毫無意義。解法:embed 文件和查詢必須用同一個模型。如果你決定換模型,就要把向量庫裡所有文件全部重新 embed 一遍。在程式裡把模型名稱設成常數(例如 EMBED_MODEL = "text-embedding-3-small"),整個系統只有一個地方宣告。
坑 3:對稱 vs 非對稱 embedding 的 input_type 填錯
Voyage AI 的 API 有 input_type 參數,可以填 "document" 或 "query"。如果你兩邊都填 "document",搜尋精準度會比應有的低一截,但不會報錯——這種無聲的效能退化最難發現。正確用法:embed 知識庫內容時填 "document",embed 使用者問題時填 "query"。OpenAI 的 API 不分這兩種,但設計上也鼓勵非對稱使用。
坑 4:中文字數估算 token 失準,帳單爆炸
英文大概 1 token = 4 個字元,但中文通常 1 個字就是 1 token,有時一個字還拆成 2 個 token(取決於 tokenizer)。你估算成本時如果用英文的規則,會嚴重低估。正確做法:用 OpenAI 的 tiktoken 套件實際數你的語料:
import tiktoken
enc = tiktoken.get_encoding("cl100k_base") # text-embedding-3 系列用這個 tokenizer
sample = "如何申請年假?員工特休的規定是什麼?"
token_count = len(enc.encode(sample))
print(f"字元數:{len(sample)},token 數:{token_count}")
# 輸出:字元數:20,token 數:21 ← 比字元多
算出平均每個 chunk 多少 token,乘上總 chunk 數和 API 單價,才是真實成本。
坑 5:向量沒有 L2 normalize 就直接存,之後查詢結果不穩定
很多向量庫(Chroma、pgvector)預設用 cosine 相似度時會在查詢時自動 normalize,但如果你直接存入未 normalize 的原始向量,然後用 IP(inner product)模式查詢,結果就是向量長度影響排序——長文件的向量長度通常比短文件大,排序會偏向長文件。最安全的做法:存之前先 normalize:
def l2_normalize(v: np.ndarray) -> np.ndarray:
return v / np.linalg.norm(v, axis=-1, keepdims=True)
embeddings_normalized = l2_normalize(embeddings)
# 存這個進向量庫,查詢時也 normalize query vector
作業
- 跑完本課的
embed_demo.py,產出一張熱力圖。換幾個句子玩:加入近義詞對(例如「休假」和「離職」),觀察相似度數值。 - 在上面的測試集裡加一個「語意模糊」的句子(例如「公司最近怎麼了?」),看它和三個群落的相似度落在哪個範圍,思考為什麼。
- 選做:把 OpenAI 換成 Voyage-4,跑同一批句子,比較兩組相似度矩陣有沒有差異。
- 進階:用
dimensions=256參數取得縮短的向量(text-embedding-3-small支援),和原始 1,536 維的結果比較,看精準度掉了多少。
下一課預告
現在你會把文字變成向量了。下一個問題是:幾千、幾萬個向量要存在哪裡、怎麼快速找到最相近的那幾個?
第 3 課《向量資料庫:選型與實作》會帶你看清楚 pgvector(PostgreSQL 外掛)、Chroma(本地輕量)、Pinecone/Qdrant(雲端托管)的差異——它們本質都是「超快的 k-NN 搜尋引擎」,但選型錯了你可能在幾萬 chunk 的規模就遇到速度瓶頸。我會帶你在本機把 Chroma 跑起來、塞一批向量進去、查詢,用真實程式碼感受一次完整的 embed → 存 → 查流程。