大多數工程師看到 RAG 就直接
pip install langchain,把 PDF 切成 512 token,塞進 ChromaDB,呼叫 GPT-4o,然後跟老闆說「系統做好了」。 真正的答案是:RAG 是一個系統,不是一個腳本。你需要解析管線、混合索引、重排序、評估框架,以及一套讓你在生產環境裡活下去的可觀測性架構。 腳本在 demo 時可以運作。系統在三個月後的週一早上凌晨兩點還能運作。 這篇文章記錄的是後者。
面試情境
面試官:「假設你加入一家 500 人的科技公司,負責從零打造內部知識庫問答系統。有 5 萬份文件(PDF、Word、HTML 混雜),200 位同時在線用戶,SLA 要求 P95 < 3 秒,預算每月 $3,000 以內。你的第一個月怎麼規劃?第 4 週的架構長什麼樣子?最大的技術風險在哪裡?」
一、專案目標:企業知識庫問答系統的真實需求
1.1 為什麼這個題目值得深挖
這不是一個玩具問題。企業內部知識庫是 RAG 應用最高頻、也最容易出錯的場景。文件格式雜亂、安全等級各異、查詢意圖模糊、幻覺率要求嚴格——每一個細節都可以把一個「能動的 demo」變成「生產事故」。
我在 2025 年底實際交付了一個類似規模的系統(為了保密,以下數字略有調整,但技術決策完全真實)。這篇文章是那次交付的技術後記。
1.2 業務需求清單
| 維度 | 需求 |
|---|---|
| 文件規模 | 5 萬份(PDF 60%、Word 25%、HTML 15%) |
| 並發用戶 | 200 人同時在線,峰值 QPS 約 40 |
| 延遲 SLA | P95 < 3 秒(端對端,含 LLM 生成) |
| 幻覺率上限 | < 8%(由法務部門要求,涉及合規文件) |
| 多租戶隔離 | 3 個部門,各自的文件不可互相查詢 |
| 語言 | 繁體中文為主,英文文件占 30% |
| 預算 | 每月 $3,000(含 LLM API、向量資料庫、運算) |
| 安全等級 | L1(公開)/ L2(內部)/ L3(機密)三級 |
1.3 最容易踩的三個坑
坑一:把「能查到」等同於「查得好」 早期版本 Context Recall 只有 61%,意思是有 39% 的正確答案根本沒被撈上來。用戶的感受是「明明文件裡有,系統卻說不知道」。這比幻覺更難被察覺,因為系統不會報錯。
坑二:忽略文件解析的複雜度
第一週用 pdfminer 直接解析,結果掃描版 PDF 全部變成亂碼,帶表格的財務報告欄位全混在一起。解析管線的工程量遠超預期,最終占了整個專案 30% 的工時。
坑三:評估框架後置 「先做完功能再寫測試」在 RAG 系統裡是慢性自殺。沒有 golden QA dataset 就不知道改動是否讓系統變好或變壞。第 2 週才補 RAGAS 評估,導致第 1 週的很多「優化」其實是方向錯的。
二、三個演進階段(Week 1 / Week 2–3 / Week 4)
2.1 Week 1:POC — 先讓它動起來
目標:在 3 天內有一個可以 demo 的版本,驗證基本可行性。
┌──────────────────────────────────────────────────────────────┐
│ Week 1 POC 架構 │
├──────────────────────────────────────────────────────────────┤
│ │
│ PDF 文件 ┌─────────────┐ ┌──────────────────┐ │
│ (本地資料夾)──▶ │ pdfminer │──▶ │ 固定 512 token │ │
│ │ 簡單解析 │ │ 切分(無重疊) │ │
│ └─────────────┘ └────────┬─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ChromaDB(本地 SQLite) │ │
│ │ OpenAI text-embedding-3-small │ │
│ └──────────────────────────┬───────────────────────────┘ │
│ │ │
│ 用戶提問 ──▶ 向量查詢 ──▶ │ Top-5 chunks │
│ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ GPT-4o (直接呼叫,無快取,無串流) │ │
│ │ 簡單 prompt:「根據以下內容回答:{context}」 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
新增元件:pdfminer、ChromaDB、text-embedding-3-small、GPT-4o 直呼
成本:
- 運算:本地 MacBook,$0
- LLM:每次查詢約 3K tokens,$0.045/次,200 次/天 = $9/天
- 向量 DB:本地,$0
- 總計:約 $270/月
解決了什麼:
- 驗證了基本 RAG 流程可行
- 有東西可以 demo 給老闆看
還沒解決:
- 掃描版 PDF 完全失效
- Word / HTML 格式不支援
- 沒有多租戶隔離
- 幻覺率約 34%(手動測試 50 題)
- 查詢延遲 P95 8.2 秒(主要是 LLM 冷啟動 + 無串流)
- ChromaDB 沒有生產級可用性保證
2.2 Week 2–3:MVP — 讓它在生產環境活下去
┌──────────────────────────────────────────────────────────────────┐
│ Week 2-3 MVP 架構 │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────┐ ┌─────────────────────────────────────────┐ │
│ │ 文件上傳 │──▶│ Document Parser Pipeline │ │
│ │ API │ │ PDF(OCR) │ Word │ HTML │ 其他 │ │
│ └────────────┘ └──────────────────┬──────────────────────┘ │
│ │ │
│ ┌─────────────▼──────────────┐ │
│ │ Semantic Chunker │ │
│ │ Parent(1024) + Child(256) │ │
│ │ + Metadata 注入 │ │
│ └─────────────┬───────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────┐ │
│ │ Qdrant Cloud │ │
│ │ Dense vector(1536-dim)+ BM25 sparse │ │
│ │ Payload: dept, security_level, doc_type │ │
│ └──────────────────┬────────────────────────────┘ │
│ │ │
│ 查詢流程: │ │
│ 問題 ──▶ ┌──────────────────────────────────────┐ │
│ │ Parallel Search │ │
│ │ BM25(8ms)+ Vector(45ms) │ │
│ └──────────────────┬───────────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ RRF Fusion │ │
│ │ alpha = 0.7 │ │
│ └──────────┬────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Cross-encoder Rerank │ │
│ │ ms-marco(120ms) │ │
│ └──────────┬────────────┘ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ GPT-4o + Citation │ │
│ │ Prompt(1.8s avg) │ │
│ └───────────────────────┘ │
│ │
│ + RAGAS 評估框架(200 golden QA pairs) │
│ │
└──────────────────────────────────────────────────────────────────┘
新增元件:
unstructured.io(PDF OCR + Word + HTML 解析)- Qdrant Cloud(替換 ChromaDB,原生 hybrid search 支援)
- BM25 sparse vector(Qdrant 原生支援)
- RRF fusion layer
cross-encoder/ms-marco-MiniLM-L-6-v2(re-ranking)- RAGAS 評估框架 + 200 golden QA 資料集
- Metadata payload filter(部門 + 安全等級)
成本:
- Qdrant Cloud(1 node, 4GB):$70/月
- LLM(加了 prompt cache,節省 40%):$350/月
- OCR 處理(一次性):$50
- 運算(2 vCPU FastAPI server):$60/月
- 總計:約 $530/月
解決了什麼:
- 幻覺率從 34% 降至 11%
- Context Recall 從 61% 升至 83%
- P95 延遲從 8.2s 降至 3.8s(未達 SLA,繼續優化)
- 支援 PDF/Word/HTML 三種格式
- 多租戶 payload filter 實現部門隔離
還沒解決:
- P95 還差 0.8 秒(SLA 要求 3s)
- 文件上傳是同步的,上傳大型 PDF 會 block
- 沒有監控 dashboard
- token 成本在文件量增加後會線性成長
2.3 Week 4:Production — 讓它在三個月後的凌晨兩點還能活著
┌──────────────────────────────────────────────────────────────────────┐
│ Week 4 Production 架構 │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────────┐ ┌───────────────────────────┐ │
│ │ API GW │──▶│ FastAPI │──▶│ Redis(查詢結果快取) │ │
│ │ (rate │ │ (async) │ │ TTL: 1hr,hit rate ~45% │ │
│ │ limit) │ └──────┬───────┘ └───────────────────────────┘ │
│ └──────────┘ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Celery Worker │◀── Redis Broker │
│ │ (非同步 Ingestion)│ │
│ └──────────┬──────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────┐ │
│ │ Document Parser Pipeline │ │
│ │ ┌──────────────────────────────────────┐ │ │
│ │ │ Tika Server(PDF/Word) │ │ │
│ │ │ + OCRmyPDF(掃描版) │ │ │
│ │ │ + BeautifulSoup(HTML) │ │ │
│ │ └──────────────────────────────────────┘ │ │
│ │ Throughput: 12 pages/sec(4 workers) │ │
│ └────────────────────┬──────────────────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────┐ │
│ │ Qdrant Cluster(3 nodes) │ │
│ │ Collection per tenant(3 個部門) │ │
│ │ Quantization: int8(節省 50% 記憶體) │ │
│ └────────────────────┬──────────────────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────┐ │
│ │ Query Pipeline(全 async) │ │
│ │ BM25 ‖ Vector → RRF → Rerank → GPT-4o │ │
│ │ + Streaming response(TTFB < 800ms) │ │
│ └────────────────────┬──────────────────────┘ │
│ │ │
│ ┌────────────────────▼──────────────────────┐ │
│ │ 可觀測性 Stack │ │
│ │ Prometheus + Grafana(延遲、幻覺率趨勢) │ │
│ │ LangSmith(Trace 每一次 LLM 呼叫) │ │
│ │ RAGAS 定期跑 regression(每日 02:00) │ │
│ └───────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘
新增元件:
- Celery + Redis(非同步 ingestion queue)
- Qdrant 3-node cluster(HA + int8 quantization)
- Redis query cache(TTL 1 小時)
- Streaming response(SSE)
- LangSmith trace integration
- Grafana dashboard(15 個 panel)
- RAGAS nightly regression(GitHub Actions cron)
成本:
- Qdrant Cluster(3 nodes):$210/月
- LLM(加了 gpt-4o-mini fallback for simple queries):$280/月
- Redis(ElastiCache t3.micro):$25/月
- Celery workers(2 vCPU × 4):$120/月
- 監控(Grafana Cloud free tier):$0
- 總計:約 $635/月(遠低於 $3,000 預算)
解決了什麼:
- P95 降至 2.9 秒(SLA 達標)
- Streaming TTFB 800ms,用戶感受大幅改善
- 幻覺率降至 6%
- 文件上傳非阻塞,上傳 100MB PDF 後立即回應
- 完整 trace,每次 LLM 呼叫可追溯
三、文件解析管線:PDF/Word/HTML → 結構化 Chunks
3.1 為什麼解析比想像中難
在開始之前,我以為文件解析是「三行程式碼的問題」。實際上,5 萬份企業文件有以下特徵:
- 掃描版 PDF 占 23%:完全沒有文字層,需要 OCR
- 有表格的 PDF 占 41%:
pdfminer把表格內容按閱讀順序拉平,欄位混在一起 - 有頁眉頁腳的 Word 占 78%:頁碼、公司名稱、保密聲明反覆出現,污染 chunk
- HTML 含大量導覽列 / 廣告元素:需要正文萃取
┌───────────────────────────────────────────────────────────────┐
│ Document Parser Pipeline │
├───────────────────────────────────────────────────────────────┤
│ │
│ 原始檔案 │
│ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ PDF │ │ Word │ │ HTML │ │
│ └───┬────┘ └───┬────┘ └───┬────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Type Detection │ │
│ │ pdfinfo → has_text? 是否為掃描版 │ │
│ └──────────────────┬─────────────────────┘ │
│ ┌──────────┴──────────┐ │
│ ▼ ▼ │
│ ┌──────────────┐ ┌───────────────────┐ │
│ │ 文字版 PDF │ │ 掃描版 PDF │ │
│ │ pdfplumber │ │ OCRmyPDF │ │
│ │ (表格萃取) │ │ (Tesseract zh-TW) │ │
│ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │
│ └─────────┬──────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Post-Processing │ │
│ │ • 頁眉頁腳偵測(regex + 位置分析) │ │
│ │ • 表格 → Markdown table 格式 │ │
│ │ • 標題層級標記(H1/H2/H3) │ │
│ │ • 語言偵測(zh-TW / en) │ │
│ └────────────────────┬───────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Semantic Chunker │ │
│ │ 按標題邊界切分,不切斷句子 │ │
│ │ Parent chunk: 1024 tokens │ │
│ │ Child chunk: 256 tokens(50 重疊) │ │
│ └────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────────────────┘
3.2 實際性能數字
| 文件類型 | 解析速度 | 記憶體峰值 | 主要瓶頸 |
|---|---|---|---|
| 文字版 PDF | 18 pages/sec | 250MB | pdfplumber 表格解析 |
| 掃描版 PDF | 2.1 pages/sec | 1.2GB | OCR(Tesseract) |
| Word (.docx) | 45 pages/sec | 120MB | 圖片萃取 |
| HTML | 200 pages/sec | 80MB | CSS selector 解析 |
教訓:掃描版 PDF 的處理速度是文字版的 8.6 倍慢。初期低估了 OCR 工作量,最終用了 4 個 Celery worker 並行跑 OCR,整體 ingestion throughput 約 12 pages/sec。
3.3 語意邊界切分 vs 固定切分
固定切分(512 token,無重疊)的問題:
# 原始段落(跨越 chunk 邊界)
...董事會於 2024 年 3 月 15 日決議,授權管理層就 A 案進行
[CHUNK 1 結束]
談判,授權金額上限為新台幣一億元,有效期至 2025 年底...
[CHUNK 2 開始]
查詢「A 案談判授權金額」時,Chunk 1 和 Chunk 2 各自都只有半個答案,兩個都進不了 Top-5 就直接答錯了。
語意邊界切分的策略:
- 優先在標題(H1/H2/H3)處切分
- 次優先在段落(雙換行)處切分
- 最後才考慮句尾標點
- Parent chunk(1024 token)用於 re-ranking,Child chunk(256 token)用於向量索引
四、索引策略:多粒度 Chunking + 混合索引設計
4.1 Parent-Child Chunking
1# Payload schema(每個 child chunk 都帶完整 metadata)
2{
3 "id": "doc_001_chunk_042",
4 "parent_id": "doc_001_parent_008",
5 "text": "...(256 tokens)...",
6 "parent_text": "...(1024 tokens)...",
7 "metadata": {
8 "doc_id": "doc_001",
9 "doc_title": "2024 年度採購規範",
10 "doc_type": "policy",
11 "department": "procurement",
12 "security_level": 2,
13 "created_date": "2024-03-15",
14 "page_number": 12,
15 "section_title": "第三章 授權層級",
16 "language": "zh-TW",
17 "chunk_index": 42,
18 "total_chunks": 67
19 },
20 "dense_vector": [...], # 1536-dim, text-embedding-3-small
21 "sparse_vector": {...} # BM25 sparse
22}
設計決策:
- Child chunk 用來向量搜索(短文字 embedding 更精準)
- Parent chunk 送給 LLM(更多上下文,減少幻覺)
- Metadata 全部放進 payload,不做二次查詢
4.2 索引規模
| 指標 | 數值 |
|---|---|
| 文件總數 | 50,000 |
| 平均 chunks per doc | 24 |
| 總 child chunks | 1,200,000 |
| Dense vector 索引大小 | 7.3GB(原始 float32) |
| 量化後(int8) | 3.6GB |
| BM25 sparse index | 1.2GB |
| Payload + metadata | 2.1GB |
| Qdrant 總磁碟 | 約 12GB |
4.3 查詢延遲(各策略對比)
| 策略 | P50 | P95 | Recall@5 |
|---|---|---|---|
| 純向量(float32) | 38ms | 62ms | 71% |
| 純向量(int8 量化) | 31ms | 45ms | 69%(差 2%) |
| 純 BM25 | 6ms | 11ms | 54% |
| Hybrid RRF(int8) | 52ms | 78ms | 83% |
| Hybrid + Rerank | 178ms | 210ms | 89% |
int8 量化讓延遲降 27%,Recall 只差 2%,CP 值極高。
五、查詢系統:Hybrid Search + Re-ranking 完整實作
5.1 端對端查詢流程
┌────────────────────────────────────────────────────────────────────┐
│ Query Pipeline │
├────────────────────────────────────────────────────────────────────┤
│ │
│ 用戶提問 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Query Pre-processing │ │
│ │ • 語言偵測 → 切換 embedding model(zh vs en) │ │
│ │ • 查詢展開(HyDE):讓 LLM 生成假設性答案再做向量搜索 │ │
│ │ • Redis key = hash(query + dept + security_level) │ │
│ │ • Cache hit? → 直接回傳(P95 28ms) │ │
│ └─────────────────────────┬───────────────────────────────────┘ │
│ │ Cache miss │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Parallel Search(asyncio.gather) │ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌──────────────────────────┐ │ │
│ │ │ BM25 Search │ │ Dense Vector Search │ │ │
│ │ │ Qdrant sparse │ │ Qdrant HNSW │ │ │
│ │ │ Top-20,8ms │ │ Top-20,45ms │ │ │
│ │ │ + payload filter │ │ + payload filter │ │ │
│ │ └──────────┬──────────┘ └───────────┬──────────────┘ │ │
│ │ └──────────────┬────────────┘ │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────┐ │ │
│ │ │ RRF Fusion │ │ │
│ │ │ score = 1/(k + rank_bm25)│ │ │
│ │ │ + α/(k + rank_vec) │ │ │
│ │ │ k=60, α=0.7, Top-10 │ │ │
│ │ └──────────────┬────────────┘ │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Cross-encoder Re-ranking │ │
│ │ model: ms-marco-MiniLM-L-6-v2(本地部署,4 vCPU) │ │
│ │ input: (query, parent_chunk) × 10 pairs │ │
│ │ output: relevance score,取 Top-3 │ │
│ │ latency: 120ms P95 │ │
│ └──────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ Confidence Gate │ │
│ │ 最高 rerank score < 0.35 → 回傳「找不到相關資料」 │ │
│ │ (避免幻覺式回答) │ │
│ └──────────────────────────┬─────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ LLM Generation(Streaming) │ │
│ │ GPT-4o(長問題)/ GPT-4o-mini(短問題,< 50 chars) │ │
│ │ Citation-grounded prompt │ │
│ │ Context window: Top-3 parent chunks(約 3072 tokens) │ │
│ │ TTFB: 800ms,total: 1.8s avg │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 總延遲(P95):8ms(cache) + 45ms(vec) + 120ms(rerank) │
│ + 1800ms(LLM) + 網路 ≈ 2.9s ✓ │
│ │
└────────────────────────────────────────────────────────────────────┘
5.2 RRF 公式與 alpha 調參
Reciprocal Rank Fusion 公式:
RRF_score(chunk) = 1 / (k + rank_BM25) + α × 1 / (k + rank_vector)
其中:
k = 60(平滑常數,防止 rank=1 的分數過高)
α = 0.7(向量搜索的權重係數)
為什麼 α = 0.7?
我們跑了 grid search(α 從 0.3 到 0.9,step=0.1),在 200 個 golden QA 上測試:
| α 值 | Context Recall | Precision@3 |
|---|---|---|
| 0.3 | 80% | 0.71 |
| 0.5 | 85% | 0.76 |
| 0.7 | 89% | 0.82 |
| 0.9 | 87% | 0.79 |
α = 0.7 在繁體中文語料上表現最好。英文語料則 α = 0.6 略勝。
flip condition:如果查詢以精確術語為主(如法規條號、產品型號),α 應調低至 0.4,讓 BM25 主導。
5.3 常見失敗診斷(Symptom → Diagnosis)
| 症狀(Traces/Metrics 中觀察到) | 根因 | 修復方式 |
|---|---|---|
| Rerank P95 > 400ms | Cross-encoder OOM,開始用 swap | 降低 batch size 或增加記憶體 |
| Cache hit rate < 20% | 查詢多樣性高,TTL 設定合理但沒用 | 加 Query Normalization(去停用詞) |
| LLM 回傳「找不到」但文件明確存在 | Confidence gate threshold 太高(0.35) | 調降至 0.28 並觀察幻覺率 |
| BM25 score 全部為 0 | Sparse index 未更新(Celery worker crash) | 檢查 worker heartbeat,加 dead letter queue |
| 向量搜索召回率突然下降 10% | Embedding model 被悄悄更新(API 端) | Pin text-embedding-3-small 版本,加 embedding drift 監控 |
六、LLM 整合:提示工程與幻覺控制
6.1 Citation-grounded Prompting
你是企業知識庫助手。請根據以下文件片段回答問題。
回答時必須:
1. 只使用下方文件中的資訊,不要添加外部知識
2. 引用來源格式:[文件標題,第 N 頁]
3. 如果文件中沒有足夠資訊回答問題,請明確說「根據現有資料無法確定」
4. 不要猜測或推斷超出文件範圍的內容
--- 文件片段 ---
[1] 來源:{doc_title_1},第 {page} 頁
{chunk_text_1}
[2] 來源:{doc_title_2},第 {page} 頁
{chunk_text_2}
[3] 來源:{doc_title_3},第 {page} 頁
{chunk_text_3}
---
問題:{user_question}
請用繁體中文回答,並在答案末尾列出引用來源。
這個 prompt 模式的關鍵:
- 「不要添加外部知識」:明確限制 LLM 不要用訓練資料補充
- 引用格式強制:讓用戶可以點回原始文件驗證
- 明確的「不知道」語意:比讓 LLM 猜測好 100 倍
6.2 Confidence Scoring
1def should_answer(rerank_scores: list[float], threshold: float = 0.28) -> bool:
2 """
3 如果最高 rerank score 低於 threshold,拒絕回答
4 threshold 的選擇基於 ROC curve:FPR=5% 時對應的 TPR
5 """
6 return max(rerank_scores) >= threshold
7
8# 實際數字(在 200 golden QA 上校準):
9# threshold=0.35: precision=98%, recall=71%(太保守,漏掉太多)
10# threshold=0.28: precision=94%, recall=87%(選這個)
11# threshold=0.20: precision=85%, recall=95%(幻覺率上升)
6.3 Token 成本優化
| 策略 | 節省比例 | 實作方式 |
|---|---|---|
| Prompt caching | 40% | GPT-4o system prompt 不變,啟用 cache_control |
| GPT-4o-mini for simple queries | 35% | 問題長度 < 50 字且 rerank score > 0.7 時降級 |
| Context window 壓縮 | 15% | 把 3 個 parent chunk 截短至 900 tokens 各 |
| 串流式回應 | 0%(成本不變,體驗提升) | SSE streaming |
實際成本:
- 優化前:$0.18/次(3K tokens input × $0.005 + 500 tokens output × $0.015)
- 優化後:$0.04/次(prompt cache + mini fallback + 壓縮)
- 月成本(40 QPS × 3600s × 8hr × 30天 ÷ 10 = 約 345,600 次):實際約 $13,800 次 × $0.04 = $552/月
七、評估框架:RAGAS 指標 + 人工評測 + 持續監控
7.1 RAGAS 四大指標(實際基準)
| 指標 | 定義 | Week 1 POC | Week 4 Production | 達標門檻 |
|---|---|---|---|---|
| Faithfulness | 答案內容是否有 context 支撐 | 0.66 | 0.94 | ≥ 0.90 |
| Answer Relevancy | 答案是否切題 | 0.72 | 0.91 | ≥ 0.85 |
| Context Recall | 正確答案的 context 有沒有被撈到 | 0.61 | 0.89 | ≥ 0.85 |
| Context Precision | 撈到的 context 有多少是有用的 | 0.58 | 0.83 | ≥ 0.80 |
計算方式:RAGAS 本身用 LLM 當 judge(GPT-4o),每次評估 200 題約花 $1.2。
7.2 Golden QA Dataset 建立
第一步:從真實用戶查詢記錄中抽取 500 個問題(系統上線後第一週的 log)。 第二步:人工標記每題的「正確答案」和「應包含哪些 source chunks」。 第三步:清洗掉模糊問題(主觀判斷、多解答案),保留 200 題。 第四步:加入 edge cases:跨文件問題(30 題)、否定語義問題(20 題)、超出範圍問題(50 題)。
最終 200 題分佈:
- 事實查詢:80 題
- 流程說明:50 題
- 跨文件整合:30 題
- 超出知識庫範圍(預期回答「不知道」):40 題
7.3 Embedding Drift 偵測
1# 每週跑一次,比較新文件的 embedding 分佈
2def detect_embedding_drift(new_vectors, reference_vectors, threshold=0.05):
3 """
4 計算新 batch 的 embedding centroid 與 reference 的 cosine distance
5 如果 > threshold,發出 Slack 告警(可能是 embedding model 被更新)
6 """
7 new_centroid = np.mean(new_vectors, axis=0)
8 ref_centroid = np.mean(reference_vectors, axis=0)
9 drift = 1 - cosine_similarity([new_centroid], [ref_centroid])[0][0]
10 return drift
告警閾值:
- drift > 0.03:黃色告警(觀察)
- drift > 0.05:紅色告警(停止 ingestion,人工確認 embedding model 是否變更)
7.4 生產環境告警矩陣
| 指標 | 黃色告警 | 紅色告警 | 對應 Runbook |
|---|---|---|---|
| P95 延遲 | > 3.5s | > 5s | 檢查 Celery queue 深度 + Qdrant 節點狀態 |
| Faithfulness | < 0.88 | < 0.80 | 查 LangSmith trace,看是否 top chunks 品質下降 |
| Cache hit rate | < 30% | < 15% | 檢查 Redis 記憶體使用量,考慮升級 |
| Error rate | > 1% | > 3% | 查 Celery dead letter queue + API GW logs |
| Embedding drift | > 0.03 | > 0.05 | 確認 OpenAI embedding 版本 |
八、為什麼選 X 不選 Y
決策一:向量資料庫選擇
| 選擇 | 選 Qdrant 的理由 | 不選的理由 |
|---|---|---|
| Qdrant ✓ | 原生 hybrid search(dense+sparse),int8 量化開箱即用,self-hosted 選項,$70/月 starter | — |
| Pinecone | 全託管,zero ops | 不支援 BM25 sparse(需自行實作),$0.096/1M vectors = 在 1.2M chunks 下 $115/月,多租戶需要 namespace,查詢需帶 namespace 參數,易出錯 |
| Weaviate | 功能豐富,有 BM25 | 記憶體消耗比 Qdrant 高 40%,中文分詞需要額外設定,文件不如 Qdrant 清晰 |
Flip condition:如果完全不想管基礎設施,或規模超過 1 億 chunks,換 Pinecone 的 serverless 方案。
決策二:BM25+Vector Hybrid vs 純向量
| 選擇 | 選 Hybrid 的理由 | 不選純向量的理由 |
|---|---|---|
| Hybrid(BM25+Vector) ✓ | Context Recall 83% vs 71%(+12%),精確術語查詢(產品編號、法規條文)BM25 表現遠勝向量 | 實作複雜度略高 |
| 純向量 | 實作簡單,延遲低 8ms | Recall 只有 71%;精確關鍵字匹配失效;繁中語料的 embedding 語意漂移問題 |
Flip condition:如果所有查詢都是語意性問題(如「這份合約的精神是什麼」),純向量已足夠。精確術語查詢占 > 20% 時,hybrid 必選。
決策三:Cross-encoder Reranking vs ColBERT
| 選擇 | 選 Cross-encoder 的理由 | 不選 ColBERT 的理由 |
|---|---|---|
| Cross-encoder ✓ | 精度更高(MRR@3 比 ColBERT 高 6%),模型小(66MB),本地部署成本低($30/月 CPU instance) | — |
| ColBERT | 延遲更低(預計算 passage vectors) | Token-level interaction 需要 GPU 才能達到 < 100ms,在 CPU 上每次 rerank 450ms 超出預算;部署複雜度高 |
Flip condition:如果有 GPU instance 且每日查詢量 > 500K,ColBERT 的延遲優勢開始顯現。
決策四:Async Queue (Celery) vs 同步 Ingestion
| 選擇 | 選 Celery 的理由 | 不選同步的理由 |
|---|---|---|
| Celery + Redis ✓ | 上傳 100MB PDF 立即回應(TTFB < 200ms),失敗可自動 retry,worker 數量可彈性調整,任務狀態可查詢 | — |
| 同步 Ingestion | 實作最簡單 | 上傳 100MB PDF 等待 45 秒,timeout 風險高;OCR 失敗整個 request 失敗,沒有重試機制;無法水平擴展 |
Flip condition:文件數 < 1000 份且單個文件 < 5MB 時,同步 ingestion 已足夠,不值得引入 Celery 的運維複雜度。
決策五:RAGAS vs 自製評估框架
| 選擇 | 選 RAGAS 的理由 | 不選自製的理由 |
|---|---|---|
| RAGAS ✓ | 開箱即用的 4 個指標,社群活躍,與 LangSmith 整合好,1 週內上線 | — |
| 自製 Eval | 完全掌控,不依賴外部 LLM judge | 從零實作 Faithfulness judge 需要 3-4 週;RAGAS 有論文支撐,指標定義清晰;自製框架難以與業界對比 |
Flip condition:需要高度客製化指標(如特定領域的合規檢查),或 RAGAS 的 LLM judge 成本過高(> $10/次評估),才值得自製。
決策六:Parent-Child Chunking vs 固定大小 Chunking
| 選擇 | 選 Parent-Child 的理由 | 不選固定大小的理由 |
|---|---|---|
| Parent-Child ✓ | Child(256)精準定位,Parent(1024)提供充足上下文;跨句子的語意完整;實測幻覺率比固定切分低 8% | — |
| 固定大小(512) | 實作最簡單 | 語意邊界被隨機切斷;同一個答案分散在多個 chunk;重疊(overlap)雖然緩解問題但增加 50% 的 chunks 數量 |
Flip condition:文件結構高度統一(如資料庫 schema 文件、API 文件),固定大小 + overlap 已足夠,不值得 parent-child 的索引複雜度。
九、系統效應(量化改進表)
| 指標 | Week 1 POC | Week 4 Production | 改進幅度 | 主要驅動因素 |
|---|---|---|---|---|
| 幻覺率 | 34% | 6% | -82% | Citation prompt + Confidence gate + Reranking |
| Context Recall | 61% | 89% | +46% | Hybrid search(BM25+Vector)+ Parent-child chunking |
| Answer Relevancy | 0.72 | 0.91 | +26% | Reranking 篩掉不相關 chunks |
| Faithfulness | 0.66 | 0.94 | +42% | Citation-grounded prompt + Context Precision 提升 |
| P95 延遲 | 8.2s | 2.9s | -65% | 串流輸出(-3s) + int8量化(-0.4s) + Redis cache(-1.5s on cache hits) |
| Cost/query | $0.18 | $0.04 | -78% | Prompt caching + mini fallback + context 壓縮 |
| OCR 支援 | 無 | 有(2.1 pps) | N/A | OCRmyPDF + Celery async |
| 可用性 | 無保證 | 99.5%(3-node Qdrant) | N/A | Cluster HA |
| Index size | — | 12GB(50K docs) | — | int8 量化節省 50% |
| 查詢快取 hit rate | 0% | 45% | — | Redis + 1hr TTL + query normalization |
9.1 幻覺率 34% → 6% 的分解
| 改進項目 | 幻覺率降幅 |
|---|---|
| Citation-grounded prompt | -10% |
| Confidence gate(低分時拒答) | -8% |
| Cross-encoder reranking(排掉不相關 chunks) | -6% |
| Parent chunk(更多上下文,LLM 不需要猜測) | -4% |
9.2 P95 8.2s → 2.9s 的分解
| 優化項目 | 節省時間 |
|---|---|
| SSE Streaming(主觀感受,TTFB 800ms) | -3.0s(感知) |
| Redis cache(45% hit,28ms 直接回傳) | -1.5s(加權平均) |
| int8 量化(向量搜索 62ms → 45ms) | -0.17s |
| asyncio parallel search(BM25 ‖ Vector) | -0.38s |
| GPT-4o-mini fallback(35% 查詢) | -0.5s(加權) |
十、面試答題要點
「我會把這個系統的演進分成三週。第一週做 POC,用最簡單的工具驗證可行性,這時幻覺率 34%、延遲 8.2 秒都可以接受,目標只是讓老闆看到東西能動。第二三週做 MVP,核心轉型是引入 Hybrid Search(BM25 + 向量),加上 Cross-encoder Re-ranking——這個組合把 Context Recall 從 61% 拉到 83%,是後續所有品質提升的基礎。同步,我會在第二週就建立 200 題的 golden QA dataset 跑 RAGAS,沒有評估框架你根本不知道自己在往哪個方向走。第四週衝生產:Celery 異步 ingestion 解決上傳 blocking 問題,int8 量化 + Redis cache 把 P95 壓到 2.9 秒達成 SLA,Prompt caching + mini fallback 把成本從 $0.18/次壓到 $0.04/次。最大的學習是:系統的品質瓶頸通常不在 LLM,而在 Retrieval——Hybrid Search 和 Re-ranking 兩個改動,比換一個更強的 LLM 對幻覺率的改善大 3 倍。」
十一、系列導航
← Phase 18 Part 2:LLM Observability — Tracing、Evaluation 與生產監控 | Phase 19 Part 2:Capstone — 多模態 RAG 與影像文件處理 →
本文是「AI 工程從零開始」系列的第 41 篇。所有數字均來自實際生產環境(部分細節為保護客戶隱私而調整)。如有任何問題,歡迎在評論區留言。
