完整 RAG flow
從 chunk 到 generate 的 5 步驟 pipeline——一個極簡 in-memory 範例,順便講 vector DB 怎麼選。
TL;DR
- 5 步:chunk → embed → store(preprocess 階段)→ retrieve → augment → generate(query 階段)
- top-k 通常 3–10;太多會 dilute context、太少會漏答案
- Retrieval 的 query 不一定要用 user 原文——可以先讓 model 改寫成 search query
一個情境:把上一篇兜起來
延續 FAQ bot 例子。我們有:
- 一份
report.md,按##header 切成 12 個 chunk - Voyage embedding API
- 一個 user 問題:「軟體工程部去年做了什麼?」
目標:retrieve 最相關的 2 個 chunk,丟給 Claude 回答。
5 個步驟
前 3 步是一次性的(除非文件變動再重跑);後 3 步每次 user 問問題都跑。
極簡 in-memory 實作
不用真的 vector DB,先用 numpy 把概念跑出來:
import numpy as np
import voyageai
vo = voyageai.Client()
def embed(texts, input_type):
return vo.embed(texts, model="voyage-3-large",
input_type=input_type).embeddings
# Step 1-2: chunk + embed (preprocess)
chunks = chunk_by_section(open("report.md").read())
vectors = np.array(embed(chunks, input_type="document"))
# Step 3: 「store」就是把 vectors 放在記憶體裡(in-memory 版)
# Step 4: embed user query
q = "What did the software engineering dept do last year?"
qv = np.array(embed([q], input_type="query")[0])
# Step 5: cosine similarity → top-k
sims = vectors @ qv # 假設都已經 normalize 過,dot product = cosine
top_k = np.argsort(-sims)[:3]
retrieved = [chunks[i] for i in top_k]
關鍵點:
vectors @ qv算的是 cosine similarity(因為 Voyage 預設 normalize)argsort(-sims)[:3]取相似度最高的 3 個 chunkretrieved就是要塞進 prompt 的 context
Step 6:augment + generate
把 retrieved chunks 包進 prompt。包法很重要:用 XML tag 把 context 跟 user query 分開:
context = "\n\n".join(f"<doc>\n{c}\n</doc>" for c in retrieved)
prompt = f"""根據下面文件回答 user 的問題。如果文件沒有答案,明確說不知道。
{context}
User 問題:{q}"""
res = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
print(res.content[0].text)
<doc>...</doc> 不是必要語法,但 Claude 對 XML tag 特別熟(pretraining 看了很多)。把多段 context 用 tag 框起來,Claude 比較不會把「指令」跟「資料」混淆。
Cosine similarity 在做什麼
Vector DB 比對 query vector 跟所有 chunk vector 的「角度」:
| 角度 | cosine similarity | 意義 |
|---|---|---|
| 0° | 1.0 | 兩段文字幾乎同義 |
| 90° | 0.0 | 完全無關 |
| 180° | -1.0 | 反義(很少見) |
文件中常看到「cosine distance」——就是 1 - cosine similarity。distance 越小越相似(很多 vector DB API 用這個慣例)。
Top-k 怎麼選
| 場景 | k 建議 |
|---|---|
| 問題明確、答案集中(FAQ、查 incident ID) | 3 |
| 一般文件問答 | 5 |
| 探索式、總結式問題 | 8–10 |
| 法律 / 醫療等高 recall 場景 | 10–20,後面再 rerank |
k 不是越大越好:每個多塞一段就多一份 input token,而且不相關的 chunk 會干擾 model 判斷。先用 k=5 起手,看 retrieval 準確率再調。
Vector DB:production 怎麼選
In-memory numpy 跑 prototype 很方便,但 chunk 上萬就要正式的 vector DB:
| DB | 適合什麼 | 特性 |
|---|---|---|
| pgvector(Postgres extension) | 已經在用 Postgres、不想多開服務 | 跟現有 RDB 同一份、SQL filter + vector 一起查 |
| Qdrant | 中型 production、self-host 或 cloud | open source、有強 filter、API 乾淨 |
| Pinecone | 不想自己管 infra、量很大 | 全 managed、scale 容易、貴 |
| Weaviate / Milvus / Chroma | 各有特色 | 看團隊偏好 |
選擇 rule:
- 已經用 Postgres → pgvector 八成夠
- 量超過 10M vectors / 要 multi-tenant → Pinecone 或 Qdrant cloud
- 一切重 self-host → Qdrant
不要花太多時間糾結,底層搬遷不難(API 都是 add(vec, meta) + search(qv, k))。先用方便的跑通整條 pipeline,再看瓶頸換。
預告:retrieval 還可以怎麼進化
5 步流程跑起來了,但 production 會撞到的事:
- 純 vector 對精確關鍵字(API name、錯誤碼、product SKU)很爛 — 下一篇講 hybrid 怎麼解
- top-k 順序不夠準 — reranker(Voyage rerank-2 / Cohere rerank)
- 用 Claude 回答時想知道引用了哪段 — Section 6 的 citations 跟 RAG 是一對親戚,做完這段一定要去看
- 每次都重 embed 整本書浪費錢 — preprocessing 結果存好就好
接下來
下一篇處理 #1:multi-index pipeline——semantic search 補上 BM25 lexical search,再用 reranker 把結果合併。production 的 RAG 八成長這樣。

