RAG 是什麼:長 context 不是萬靈丹
假設你幫公司導入了 Claude API,老闆第一個需求是:「讓 AI 能回答我們內部文件的問題。」你打開 Playground,貼進一份 PDF,它回答得很好。你很開心——直到你發現文件有 400 頁,單次輸入的 token 費用讓報銷系統差點當機,而且每次查詢都要把所有文件塞給模型,慢到難以接受。更根本的問題:公司文件每週都在更新,而模型的訓練資料有截止日期。你沒辦法每隔幾週就重新訓練一個模型,也沒辦法每次都把整間公司的知識庫塞進一次請求。
這就是 RAG 存在的原因。
這堂學什麼
- RAG 解決的三個核心問題:私有資料、即時性、成本
- 長 context 模型的真實局限——以及它何時確實比 RAG 更好(誠實分析,不是一面倒)
- 索引管線(Indexing Pipeline)與查詢管線(Query Pipeline)的完整架構
- 2026 年主流工具生態一覽:Embedding 模型、向量資料庫、Rerank 服務
- 課程路線圖:7 堂課各自負責什麼,彼此如何接起來
RAG 解決的三個真實問題
RAG 全名 Retrieval-Augmented Generation(檢索增強生成)。核心思路只有一句話:不要把所有資料都塞給模型,而是先找出最相關的幾段,再把這幾段加進 prompt 讓模型回答。就像考試開書:你不需要把整本書背起來,只要在需要的時候翻到對的頁面。
但為什麼需要這樣做?有三個很具體的理由。
理由一:你的資料模型根本沒看過
GPT-4o、Claude Sonnet 的訓練資料有截止日期,而且根本不包含你公司的內部文件、你的產品說明書、你客戶的合約。有人會問:「那就微調(fine-tuning)吧?」微調適合調整模型的風格與格式,但不適合灌入大量知識——微調成本高、需要持續重跑,最重要的是模型學了之後無法精確說「這個答案出自第幾頁的哪段話」,幻覺(hallucination)問題依然存在。
RAG 的做法是:知識存在你的資料庫,模型在回答時現場去查。知識永遠是最新的,也永遠可以附上「此答案引用自 HR 政策手冊 v2.3 第 5 頁」的來源。
理由二:即時性
假設你的客服系統要回答「這款產品現在還有貨嗎?」庫存是每分鐘都在變動的資料,不可能訓練到模型裡。但如果你把最新庫存即時同步到向量資料庫,RAG 系統就能在查詢時拿到最新數字。這是長 context 無論如何辦不到的事——長 context 是「一次性輸入」,不是「持續更新的資料庫」。
理由三:規模與成本
一間公司的知識庫可能有幾萬份文件。如果每次問問題都要把全部文件塞進 context,費用和延遲都不可控。而 RAG 只把真正相關的 3~10 段文字放進 prompt,每次查詢的 token 量是固定的、可預測的。

長 context 不是萬靈丹——但有時它才是正確答案
這堂課的標題說「長 context 不是萬靈丹」,但我要先說一句反話:如果你的文件夠少,長 context 可能比 RAG 更省事。2026 年的主流模型 context window 都在 128K 到 1M token 之間,大約 200,000 tokens 可以放一本 300 頁的書。
什麼時候直接用長 context 更好
- 文件總量不超過 100~150K tokens(一份合約、一份技術規格書)
- 資料是靜態的,幾乎不更新
- 你需要模型同時理解整份文件的上下文:例如跨章節推理、分析整本合約裡的矛盾條款、找出前後不一致的地方
- 你在做原型開發,不想先花時間建向量管線——先驗證需求再建基礎設施
在這些情境下,「直接把文件塞進去」不只更簡單,有時回答品質還更好,因為模型看到了完整的上下文。
長 context 的真實局限
問題一:「迷失在中間」(Lost in the Middle)。多份研究顯示,把重要資訊放在長 context 的中段,模型的召回率會顯著下降——開頭和結尾記得最清楚。文件量一大、資訊散落各處,準確率就開始不穩定。
問題二:費用隨文件量線性成長。每次查詢都要付整份文件庫的 token 費用,沒辦法「只付用到的那幾段」。文件從 50 頁增加到 500 頁,費用就乘以十。
問題三:延遲高。塞 100K tokens 進去,首 token 延遲(TTFT)明顯比只塞 2K 慢。對需要快速回應的 API 產品來說,這個差距很明顯。
問題四:無法即時更新。每次文件改版,要重新把整份文件塞進 context。動態資料根本不適用。
決策框架
資料總量 < 50K tokens 且靜態 → 先試長 context,省得建管線,驗證需求後再評估要不要換 RAG。
超過 50K、或資料動態更新、或需要精確引用來源 → 建 RAG 管線。這堂課接下來的內容就是為這個場景而設計的。
2026 年最佳實踐:高品質的企業 AI 系統通常兩者都用——小範圍的精確文件用長 context 做深度理解,大規模知識庫用 RAG 做快速檢索,再合併結果。但這是進階架構,課程第 7 課會提到思路。現在先把 RAG 建好。

RAG 的完整架構
RAG 分兩條管線:索引管線(Indexing Pipeline)只跑一次或定期跑;查詢管線(Query Pipeline)則是每次使用者問問題時都執行。這兩條管線跑的時間點不同,是最多人看架構圖時搞混的地方。
索引管線:把資料變得可查詢
原始文件(PDF / Word / HTML / 資料庫)
↓ 文件解析(Parser)
純文字 + metadata(頁碼、日期、標題…)
↓ 切塊(Chunking)
多個文字片段(chunk 1、chunk 2… chunk N)
↓ Embedding 模型
每個 chunk 的向量(一串 float 數字)
↓ 寫入向量資料庫
可以做相似度搜尋的索引
這條管線的工作是「把資料準備好」。每次有新文件或文件更新,就重跑這條管線——它是離線(offline)的,不影響使用者體驗。
關鍵決策點在中間兩步:切塊的方式決定了「可以找到的最小資訊單位」是什麼,Embedding 模型決定了「語意相似」的定義是什麼。這兩個選擇錯了,後面再怎麼調都救不了。第 2 課和第 4 課會深入討論。
查詢管線:即時找到最相關的資訊
使用者的問題
↓ 用同一個 Embedding 模型轉成向量
查詢向量(query embedding)
↓ 向量相似度搜尋(ANN Search)
Top-K 個最相近的 chunks(例如前 20 個)
↓ (選用)Rerank 精排
最終 3~5 個最相關的 chunks
↓ 組合成 prompt + 送給 LLM
有來源引用的回答
查詢管線把使用者的問題也轉成向量,用這個向量在資料庫裡找「語意最相似」的幾段文字,把這幾段加進 prompt 讓 LLM 生成有根據的回答。整個流程通常在 300~800ms 內完成。
重要注意:索引和查詢必須用同一個 Embedding 模型。用不同模型產生的向量放在同一個空間裡比較相似度,結果完全沒意義——就像用公尺量的距離跟用英尺量的距離直接比大小一樣。把模型名稱定義成常數,讓兩條管線都引用同一個值。

2026 生態盤點
選工具之前先看全局,避免到第 3 課才發現選錯架構。
Embedding 模型
Embedding 模型把文字轉成向量。向量的品質決定了「語意相似」找得準不準。
| 模型 | 維度 | 定價(每百萬 tokens) | 特點 |
|---|---|---|---|
| OpenAI text-embedding-3-small | 1,536 | $0.02(批次 $0.01) | 最便宜、用途廣,主力首選 |
| OpenAI text-embedding-3-large | 3,072 | $0.13(批次 $0.065) | 精度高,適合法律/學術場景 |
| Voyage voyage-4-large | 可調 | $0.18* | 2026/01 發布,MoE 架構,MTEB 頂端 |
| Voyage voyage-4-lite | 可調 | $0.06* | 平衡精度與速度,中文效果佳 |
| Voyage voyage-4-nano | 256~1024 | 開源免費 | Apache 2.0,可完全本機跑 |
*Voyage 4 系列:每帳號前 2 億 tokens 免費。Voyage AI 由 Anthropic 投資,於 2026 年 1 月推出第四代模型,採用混合專家(MoE)架構,voyage-4-large 的服務成本比同等精度的稠密模型低約 40%;另提供業界首個「共用向量空間」:不同大小的 voyage-4 模型產生的向量可以互相比較,讓你在查詢端用小模型壓低延遲、文件端用大模型衝精度。
本課程選擇:實戰章節統一用 text-embedding-3-small(最易取得、成本最低、文件完整)。想衝精度的讀者可直接換成 Voyage——兩者 API 介面相近,切換只需改幾行程式碼和一個常數。
向量資料庫
向量資料庫負責儲存向量並快速做相似度搜尋(Approximate Nearest Neighbor, ANN)。
| 工具 | 類型 | 最適場景 |
|---|---|---|
| Chroma | 輕量 Python 函式庫 | 本機原型、學習、快速驗證 |
| pgvector | PostgreSQL 擴充套件 | 已有 Postgres,向量量 < 5000 萬 |
| Qdrant | 獨立服務(Rust) | 生產環境,速度與功能並重 |
| Pinecone | 全託管 SaaS | 零 ops 優先,2GB 免費額度 |
2026 年社群共識:
- 原型 / 學習:Chroma,
pip install chromadb兩行就能跑,不用架任何服務 - 現有 Postgres 系統:pgvector 自 0.5.0 支援 HNSW 索引後,百萬向量以下效能不輸專門的向量庫
- 生產環境 > 100 萬向量且速度敏感:Qdrant(Rust 撰寫,10M 向量 P99 延遲約 12ms,優於同量級競品的 16ms)
- 不想管 infra:Pinecone serverless 2GB 永久免費,適合小型產品
本課程第 3 課會詳細實作 Chroma(開發環境)和 pgvector(生產環境)兩條路線。
Rerank 服務
初始向量搜尋用餘弦相似度,速度快但不精確——它只看「向量距離」,不理解「問題和文件段落的真實語意關係」。Rerank 模型是第二道篩選:把前 2050 個候選結果用交叉編碼器(cross-encoder)重新打分排序,取最精準的 35 個送給 LLM。
- Cohere Rerank v3.5:約 $2 / 1,000 次搜尋(1 次搜尋 = 1 個查詢 + 最多 100 份文件),業界廣泛採用,支援 VPC 部署
- Voyage Rerank:與 Voyage Embedding 同生態,共享 2 億 tokens 免費額度
- Cross-encoder 本機跑:HuggingFace 有開源選項(如
cross-encoder/ms-marco-MiniLM-L-6-v2),適合資料不能出境的場景
Rerank 是第 5 課的重點。這裡先知道它的存在:如果你的 RAG 準確度不夠好,加 Rerank 通常是 ROI 最高的優化手段。

手把手實戰:建一個可以跑通的最小 RAG
理論夠了,動手做一個能跑的最小版本。用 Chroma(持久化模式)和 OpenAI Embedding——不用架任何伺服器,在本機就能完整跑通索引管線和查詢管線。
安裝依賴,準備環境
pip install chromadb openai python-dotenv
在專案根目錄建一個 .env 檔:
OPENAI_API_KEY=sk-...
確認 .gitignore 裡有 .env 這一行。金鑰不能進版本控制——第 6 課會再強調這一點。
準備測試文件
建 documents.py,用三段模擬的公司文件當測試資料:
# documents.py
docs = [
{
"id": "doc-001",
"text": (
"公司的遠端工作政策:員工每週最多可在家工作三天,"
"但每週四必須到辦公室出席。出差期間不計入遠端工作天數。"
),
"source": "HR 政策手冊 v2.3",
},
{
"id": "doc-002",
"text": (
"產假與陪產假:主要照顧者享有 16 週全薪產假,"
"次要照顧者享有 5 天全薪陪產假。需提前 8 週書面申請。"
),
"source": "HR 政策手冊 v2.3",
},
{
"id": "doc-003",
"text": (
"2026 年 Q2 業績:台灣區營收 NTD 1.2 億,"
"較去年同期成長 18%。主要成長來源為企業授權方案,佔總營收 62%。"
),
"source": "Q2 財報摘要",
},
]
建立索引管線
建 index.py,把文件轉成向量並存入 Chroma:
# index.py
from dotenv import load_dotenv
import chromadb
from openai import OpenAI
from documents import docs
load_dotenv()
# 索引和查詢要用同一個模型,定義成常數避免兩邊各自寫死
EMBED_MODEL = "text-embedding-3-small"
openai_client = OpenAI()
# PersistentClient:資料寫進磁碟,重啟後不消失
chroma_client = chromadb.PersistentClient(path="./chroma_data")
collection = chroma_client.get_or_create_collection(
name="company_docs",
metadata={"hnsw:space": "cosine"}, # 使用 cosine 相似度
)
def embed_texts(texts: list[str]) -> list[list[float]]:
"""批次呼叫 OpenAI Embedding API"""
response = openai_client.embeddings.create(
model=EMBED_MODEL,
input=texts,
)
return [item.embedding for item in response.data]
# 批次 embed 所有文件文字,再寫進向量庫
vectors = embed_texts([doc["text"] for doc in docs])
collection.add(
ids=[doc["id"] for doc in docs],
embeddings=vectors,
documents=[doc["text"] for doc in docs],
metadatas=[{"source": doc["source"]} for doc in docs],
)
print(f"索引完成,共 {collection.count()} 筆文件")
執行:
python index.py
# 索引完成,共 3 筆文件
執行後你會看到專案目錄多了一個 chroma_data/ 資料夾——向量就存在這裡。
建立查詢管線
建 query.py,實作完整的「問題→向量搜尋→組 prompt→LLM 回答」流程:
# query.py
from dotenv import load_dotenv
import chromadb
from openai import OpenAI
load_dotenv()
EMBED_MODEL = "text-embedding-3-small" # 必須和 index.py 一致
SIMILARITY_THRESHOLD = 0.60 # 低於這個相似度的 chunk 不送給 LLM
openai_client = OpenAI()
chroma_client = chromadb.PersistentClient(path="./chroma_data")
collection = chroma_client.get_collection("company_docs")
def query_rag(question: str, top_k: int = 5) -> str:
# 步驟 1:把問題也轉成向量
q_embedding = openai_client.embeddings.create(
model=EMBED_MODEL,
input=[question],
).data[0].embedding
# 步驟 2:向量相似度搜尋
results = collection.query(
query_embeddings=[q_embedding],
n_results=top_k,
include=["documents", "metadatas", "distances"],
)
chunks = results["documents"][0]
sources = [m["source"] for m in results["metadatas"][0]]
# Chroma cosine 模式回傳「距離」,1 - distance = similarity
similarities = [round(1 - d, 4) for d in results["distances"][0]]
# 步驟 3:過濾掉相似度太低的 chunk
filtered = [
(chunk, source, sim)
for chunk, source, sim in zip(chunks, sources, similarities)
if sim >= SIMILARITY_THRESHOLD
]
if not filtered:
return "根據目前的文件庫,找不到與這個問題相關的資訊。"
# 步驟 4:組合 context
context_parts = [
f"[來源: {source} | 相似度: {sim}]\n{chunk}"
for chunk, source, sim in filtered
]
context = "\n\n---\n\n".join(context_parts)
# 步驟 5:送給 LLM 生成回答
system_prompt = (
"你是公司內部知識庫助理。根據提供的文件內容回答問題。"
"如果文件裡找不到答案,請直接說「這份文件沒有包含這個資訊」,"
"不要猜測或補充訓練資料的知識。回答末尾請列出引用的來源名稱。"
)
user_prompt = f"文件內容:\n{context}\n\n問題:{question}"
response = openai_client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
],
temperature=0, # 設 0 讓回答盡量穩定、不亂發揮
)
return response.choices[0].message.content
if __name__ == "__main__":
questions = [
"每週可以在家上班幾天?",
"Q2 台灣區營收是多少?",
"公司有沒有提供健身房補貼?", # 文件裡沒有這個答案
]
for q in questions:
print(f"\n問題:{q}")
print(f"回答:{query_rag(q)}")
print("=" * 60)
預期輸出(節錄):
問題:每週可以在家上班幾天?
回答:根據公司遠端工作政策,員工每週最多可在家工作三天,
但每週四必須到辦公室出席。[來源: HR 政策手冊 v2.3]
問題:公司有沒有提供健身房補貼?
回答:這份文件沒有包含這個資訊。
注意第三個問題——模型正確承認「文件裡沒有」,不是亂編答案。這是 RAG 比直接問 LLM 更可靠的地方:你提供的 context 決定了答案的邊界。
加一層診斷:看清楚向量搜尋在幹嘛
加個小工具了解相似度分佈,這對後面除錯非常有用:
# debug_retrieval.py
from dotenv import load_dotenv
import chromadb
from openai import OpenAI
load_dotenv()
EMBED_MODEL = "text-embedding-3-small"
openai_client = OpenAI()
chroma_client = chromadb.PersistentClient(path="./chroma_data")
collection = chroma_client.get_collection("company_docs")
def inspect_retrieval(question: str, top_k: int = 3):
q_vec = openai_client.embeddings.create(
model=EMBED_MODEL,
input=[question],
).data[0].embedding
results = collection.query(
query_embeddings=[q_vec],
n_results=top_k,
include=["documents", "metadatas", "distances"],
)
print(f"\n查詢:「{question}」\n")
for i, (doc, meta, dist) in enumerate(zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0],
)):
sim = round(1 - dist, 4)
bar = "█" * int(sim * 20)
print(f"排名 {i+1} 相似度: {sim} {bar}")
print(f" 來源: {meta['source']}")
print(f" 內容: {doc[:55]}...")
print()
if __name__ == "__main__":
inspect_retrieval("產假有幾週?")
inspect_retrieval("今年業績如何?")
輸出大致如下:
查詢:「產假有幾週?」
排名 1 相似度: 0.8512 █████████████████
來源: HR 政策手冊 v2.3
內容: 產假與陪產假:主要照顧者享有 16 週全薪產假...
排名 2 相似度: 0.6204 ████████████
來源: HR 政策手冊 v2.3
內容: 公司的遠端工作政策:員工每週最多...
排名 3 相似度: 0.4831 █████████
來源: Q2 財報摘要
內容: 2026 年 Q2 業績:台灣區營收...
排名 1 的相似度 0.85 明顯高於其他,代表向量搜尋抓對了。在真實系統裡,如果 top-1 相似度低於 0.55,通常代表文件庫裡真的沒有相關資料——可以設門檻直接回「找不到」,不必強行送給 LLM 瞎猜。

常見坑
坑 1:兩個腳本用了不同的 Chroma 客戶端模式
症狀:chromadb.errors.InvalidCollectionException: Collection company_docs does not exist——明明跑了 index.py,query.py 還是說找不到。
原因:chromadb.Client()(不帶參數)是記憶體模式。每個 Python 行程各自維護獨立的 in-memory 狀態,index.py 建的 collection 根本不存在於 query.py 的行程裡。
解法:兩個腳本都用 chromadb.PersistentClient(path="./chroma_data")——本文範例程式已經這樣做了。如果你是照網路上的舊教學複製,請特別檢查這一行。
# 錯的:每個行程各自一份記憶體,沒辦法共享
chroma_client = chromadb.Client()
# 對的:資料寫進磁碟,兩個腳本都能存取
chroma_client = chromadb.PersistentClient(path="./chroma_data")
坑 2:索引和查詢用了不同的 Embedding 模型
症狀:查詢結果很奇怪——完全相關的文件排最後,不相關的反而排第一。用 debug_retrieval.py 看,所有相似度都在 0.45~0.55 之間,沒有明顯高峰。
原因:索引時用 text-embedding-3-large(3,072 維),查詢時改用 text-embedding-3-small(1,536 維),維度不同,cosine 相似度計算出來沒有意義。
更隱藏的版本:兩次都用 text-embedding-3-small,但索引時加了 dimensions=512 截短向量、查詢時沒加——維度都是 512,但值域分佈不同,結果一樣爛。
根本原則:把模型名稱定義成一個常數(EMBED_MODEL),讓索引和查詢都引用同一個變數,絕對不要在兩個地方各自寫死字串。
坑 3:Context 塞了太多低相關 chunk,LLM 開始混淆
症狀:回答超長、答非所問,或明明文件裡有答案,模型卻夾雜了不相關的資訊。
原因:你設了 top_k=10,向量搜尋忠實地回傳了 10 個結果——但排名 6~10 的相似度只有 0.45,跟問題關係不大。LLM 拿到這些雜訊就開始混淆。
解法:加相似度門檻,範例程式已實作(SIMILARITY_THRESHOLD = 0.60)。實際合適的門檻值取決於你的 embedding 模型和文件類型,從 0.6 開始調,用 debug_retrieval.py 觀察真實分佈。補充手段是第 5 課的 Rerank——它從 20~50 個候選裡選出真正的 Top 5,比用固定門檻更精準。
坑 4:文件沒有切塊直接 embed,觸發 token 上限錯誤
症狀:openai.BadRequestError: This model's maximum context length is 8191 tokens
原因:你把一整份 PDF 當成單一文字送去 embed。text-embedding-3-small 最多吃 8,191 tokens,大約 10~15 頁的文件就可能超標。這是非常常見的新手錯誤,因為「先切塊」這件事在最小範例裡不明顯。
暫時解法(僅用於測試):
# 粗暴截斷——只用於快速測試,正式環境絕對不能這樣
text = text[:3000]
正確解法在第 4 課(切塊策略)。文件在 embed 之前必須先切塊,這是整個 RAG 管線裡最影響效果的環節。
作業
- 把範例程式完整跑通,替換
documents.py裡的三段文字,換成你自己的資料(個人筆記、產品說明、某份文件的摘錄都可以)。跑debug_retrieval.py觀察相似度分佈。 - 試「文件裡沒有答案的問題」:提三個你確定文件沒有涵蓋的問題,看看模型有沒有正確回「這份文件沒有包含這個資訊」,還是開始幻覺出一個聽起來合理的假答案。這個觀察是第 6 課(防幻覺)的前置感受。
- 進階選做:把
SIMILARITY_THRESHOLD從 0.60 改成 0.80、再改成 0.40,觀察三種設定下回答品質和「找不到」的頻率如何變化。思考你的應用情境更在意「不漏答」還是「不亂答」。
下一課預告
你現在知道了 RAG 是什麼,也跑通了一個最小版本。但你有沒有注意到一個問題:我們一直在說「把文字轉成向量」,但這個向量到底是什麼?為什麼「產假有幾週?」和「產假與陪產假」在向量空間裡彼此接近,但和「Q2 財報」就很遠?如果換另一個 Embedding 模型,這個關係還成立嗎?
第 2 課深入 Embedding:它的數學直覺(不需要線代背景)、不同模型的 MTEB 評測怎麼看、同一段中文在 OpenAI 和 Voyage 模型下的向量有什麼差異,以及一個你在建 RAG 之前就必須做的決定——查詢向量和文件向量要不要用同一個模型、有沒有更好的非對稱 embedding 策略。