大多數 RAG 示範在正式環境中都會失敗。它們在 10 題示範簡報上運作正常,但使用者一提出新問題就崩潰。本指南介紹能夠實際維持的架構與模式:尊重語意邊界的分塊、同時捕捉模糊與字面查詢的混合檢索、防止幻覺的提示結構,以及在擴展時維持帳單平穩的成本控管。
五個重要決策
- 分塊策略——比模型更能影響檢索品質
- 嵌入模型——決定召回率上限
- 檢索策略——僅使用向量並不足夠
- 提示結構——決定模型是否產生幻覺
- 產生模型 + 快取——決定單位經濟效益
1. 分塊——樸實勝過聰明
不要想得太複雜。使用每個分塊 1000-1500 個字元、重疊 100-200 個字元的遞迴字元分割器。它會優先嘗試在段落邊界,其次在句子,再其次在單字處切分。在不同領域都很穩定:
# Recursive character splitter with overlap — the boring choice that wins
from langchain_text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=1200,
chunk_overlap=200,
separators=["\n\n", "\n", ". ", " ", ""],
)
chunks = splitter.split_text(document)
# 1200 chars ≈ 300-400 tokens — fits 5+ chunks in a Sonnet context window
# with room for system prompt + question + answer看起來更聰明但很少有幫助的方法:具備句子感知能力的模型、支援 Markdown 的分割器、搭配嵌入的滑動視窗。它們並不差——只是投入大量工作後略好一些。把這些投入用在檢索上。
2. 嵌入模型——text-embedding-3-large 是預設選擇
Kunavo 不提供嵌入服務——此步驟請直接呼叫嵌入供應商。 /v1/embeddings 是已實作的線路格式,但其後沒有啟用任何模型,因此對它的請求會失敗;GET /v1/models 始終是判斷哪些項目可呼叫的權威來源。實務上,這不會讓 RAG 管線增加任何費用,只有多一把金鑰:嵌入呼叫和產生呼叫本來就是獨立請求,因此可針對 OpenAI、Voyage 或 Cohere 執行嵌入,再以 Kunavo 產生內容。
text-embedding-3-large 在多數正式環境使用情境中,兼具多語言能力與準確度的平衡,價格依 OpenAI 自行公布的費率計算——請在其定價頁面查看,而不是在這裡查看,因為我們不轉售該服務,也不應在此引用其價格。變更分塊策略或模型本身時,請重新產生嵌入;內容更新時不必重新產生(只要加入新的分塊)。
較小嵌入適用的例外情況:純英文短文字檢索(FAQ、產品卡片)。對這些內容,text-embedding-3-small 的價格為 large 的四分之一,品質損失可忽略。對多語言或技術內容,請繼續使用 large。
3. 檢索——純向量不如混合檢索
向量相似度很擅長模糊的語意比對(「如何取消」→「訂閱取消政策」)。但它非常不擅長處理字面詞彙(「SKU-A92837」或「訂單 #4729」)。解決方法是混合檢索:平行執行 BM25 關鍵字搜尋,然後使用 reciprocal rank fusion:
# Hybrid retrieval: vector similarity + BM25 keyword scoring
# Pure vector misses literal keywords (product SKUs, IDs, dates)
def retrieve(question: str, k: int = 5) -> list[dict]:
q_embed = embed([question])[0]
vector_hits = vector_db.similarity_search(q_embed, k=k * 2)
keyword_hits = bm25_search(question, k=k * 2)
# Reciprocal rank fusion — simple, robust
scores: dict[str, float] = {}
for rank, hit in enumerate(vector_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
for rank, hit in enumerate(keyword_hits):
scores[hit["id"]] = scores.get(hit["id"], 0) + 1 / (rank + 60)
top_ids = sorted(scores, key=scores.get, reverse=True)[:k]
return [chunk_by_id[i] for i in top_ids]在選擇品質尚可的嵌入模型之後,這是最大的單一品質提升。在保留的 100 題評估集上追蹤 recall@5——如果加入 BM25 後從 70% 提高到 90%,就等於移除了三分之一的「我不知道」失敗情況。
4. 提示結構——防止幻覺的三種模式
# Final RAG prompt structure — three sections, citations enforced
SYSTEM_PROMPT = """You answer based exclusively on the supplied Context.
- Cite the [doc_id] for each factual claim.
- If the context doesn't answer the question, say "I don't have that
information" — do not extrapolate or use general knowledge.
- Be concise. No throat-clearing."""
def answer(question: str) -> dict:
chunks = retrieve(question, k=5)
context = "\n\n---\n\n".join(
f"[doc:{c['id']}] {c['text']}" for c in chunks
)
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=[
{"role": "system", "content": [{
"type": "text",
"text": SYSTEM_PROMPT,
"cache_control": {"type": "ephemeral"},
}]},
{"role": "user", "content": f"# Context\n{context}\n\n# Question\n{question}"},
],
max_tokens=600,
)
return {
"text": resp.choices[0].message.content,
"sources": [c["id"] for c in chunks],
}系統提示中有三項不可妥協的要求:
- 引用來源——要求模型在每項聲明附上
[doc:42]。如果引用的 ID 不存在,就表示你捕捉到了一次幻覺 - 明確拒答——「如果內容沒有回答,請說我沒有相關資訊。」沒有這項要求時,模型會使用世界知識補完回答
- 簡潔輸出——簡短回答與準確回答相關。最多 600 個 token 是很好的正式環境預設值
5. 產生模型 + 快取——成本層
Claude Sonnet 4.6 是預設選擇。如果成本受限,Claude Haiku 4.5 是便宜的備援選項。始終將系統提示包在 cache_control 中——系統提示每次呼叫都相同,因此快取可將這部分的成本降至輸入費率的 10%。
正式環境規模下的實際每次查詢成本(5K 內容、500 個輸出、啟用快取):
| 模型 | 每次查詢成本 | 每天 10,000 次查詢時 | 相較於 Sonnet 的品質 |
|---|---|---|---|
claude-sonnet-4-6 | ~$0.007 | ~$70/天 | 基準 |
claude-haiku-4-5 | ~$0.001 | ~$10/天 | ~85%,便宜 4 倍 |
每天 10,000 次查詢 → Sonnet $70/天,Haiku $10/天。高風險使用情境(面向客戶、法律、醫療)選擇 Sonnet;內部/大量處理選擇 Haiku。
正式環境中會出現的問題(以及如何捕捉)
- 分布漂移:訓練語料不再符合真實使用者問題。每週抽樣 100 次查詢,手動檢查 recall@5
- 過時的嵌入:來源文件已更新,但嵌入未重新整理。每週追蹤索引大小與來源大小
- 虛假引用:模型捏造看似真實的文件 ID。在呈現前,針對實際檢索到的 ID 清單驗證每個
[doc:N] - 延遲陡升:向量資料庫在超過一百萬個向量後效能暴跌。使用 HNSW 索引;如果是多租戶,請按租戶分區
4 週正式環境路線圖
- 第 1 週:使用 100 份文件、10 個測試問題建立原型,進行手動評估
- 第 2 週:擴展至完整語料庫,建立混合檢索,撰寫 100 題評估集
- 第 3 週:根據評估反覆調整分塊 + 系統提示,發布給內部使用者
- 第 4 週:監控、成本預算、搭配引用來源 UI 的公開發布
常見問題
正式環境的 RAG 系統應使用多大的分塊?
每個分塊使用 1,000–1,500 個字元,重疊 100–200 個字元;使用遞迴字元分割器,優先在段落邊界,其次在句子,再其次在單字處切分。具備句子感知能力的模型、支援 Markdown 的分割器和滑動視窗,在投入高得多的工作量後會略好一些——但這些投入用在檢索上,回報更高。
RAG 管線應使用哪個嵌入模型?
text-embedding-3-large 是多語言和技術內容的預設選擇;對於 FAQ 和產品卡片等純英文短文字檢索,text-embedding-3-small 的價格約為前者的四分之一,而品質損失可忽略。請注意,Kunavo 不提供嵌入服務:/v1/embeddings 是已實作的資料傳輸格式,但其後沒有啟用任何模型,因此嵌入步驟會直接呼叫 OpenAI、Voyage 或 Cohere,而產生步驟則呼叫 Kunavo。這不會讓管線增加任何費用,只有多一把金鑰,因為無論如何兩者都是獨立請求。
僅使用向量搜尋足以完成 RAG 檢索嗎?
不夠。向量相似度很適合處理模糊的語意比對,但無法處理 SKU 或訂單編號等字面詞彙。平行執行 BM25 關鍵字搜尋,再使用 reciprocal rank fusion 合併結果,通常能將 recall@5 從約 70% 提高到約 90%——這是在選擇品質尚可的嵌入模型之後,最大的單一品質提升。
如何阻止 RAG 系統產生幻覺?
在系統提示中加入三種模式:要求每個事實性聲明都附上 [doc:N] 引用;指示模型在內容沒有回答時說明它沒有相關資訊;並讓回答保持簡短——最多 600 個輸出 token 是很好的正式環境預設值。接著,在呈現前驗證每個引用的 id 是否存在於檢索結果集合中;不存在的 id 就是已被捕捉到的幻覺。
正式環境中的 RAG 查詢成本是多少?
在 5K 上下文、500 個輸出 token 且啟用提示快取的情況下:Claude Sonnet 4.6 每次查詢約 $0.007,Claude Haiku 4.5 約 $0.001(便宜 4 倍,品質約為 85%)。每天 10,000 次查詢時,Sonnet 約為 $70/天,Haiku 約為 $10/天。