AI 工程從零開始|Phase 19 Part 1:Capstone — 企業級 RAG 知識庫系統端對端實作

大多數工程師看到 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
延遲 SLAP95 < 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 實際性能數字

文件類型解析速度記憶體峰值主要瓶頸
文字版 PDF18 pages/sec250MBpdfplumber 表格解析
掃描版 PDF2.1 pages/sec1.2GBOCR(Tesseract)
Word (.docx)45 pages/sec120MB圖片萃取
HTML200 pages/sec80MBCSS 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 就直接答錯了。

語意邊界切分的策略:

  1. 優先在標題(H1/H2/H3)處切分
  2. 次優先在段落(雙換行)處切分
  3. 最後才考慮句尾標點
  4. 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 doc24
總 child chunks1,200,000
Dense vector 索引大小7.3GB(原始 float32)
量化後(int8)3.6GB
BM25 sparse index1.2GB
Payload + metadata2.1GB
Qdrant 總磁碟約 12GB

4.3 查詢延遲(各策略對比)

策略P50P95Recall@5
純向量(float32)38ms62ms71%
純向量(int8 量化)31ms45ms69%(差 2%)
純 BM256ms11ms54%
Hybrid RRF(int8)52ms78ms83%
Hybrid + Rerank178ms210ms89%

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 RecallPrecision@3
0.380%0.71
0.585%0.76
0.789%0.82
0.987%0.79

α = 0.7 在繁體中文語料上表現最好。英文語料則 α = 0.6 略勝。

flip condition:如果查詢以精確術語為主(如法規條號、產品型號),α 應調低至 0.4,讓 BM25 主導。

5.3 常見失敗診斷(Symptom → Diagnosis)

症狀(Traces/Metrics 中觀察到)根因修復方式
Rerank P95 > 400msCross-encoder OOM,開始用 swap降低 batch size 或增加記憶體
Cache hit rate < 20%查詢多樣性高,TTL 設定合理但沒用加 Query Normalization(去停用詞)
LLM 回傳「找不到」但文件明確存在Confidence gate threshold 太高(0.35)調降至 0.28 並觀察幻覺率
BM25 score 全部為 0Sparse 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 caching40%GPT-4o system prompt 不變,啟用 cache_control
GPT-4o-mini for simple queries35%問題長度 < 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 POCWeek 4 Production達標門檻
Faithfulness答案內容是否有 context 支撐0.660.94≥ 0.90
Answer Relevancy答案是否切題0.720.91≥ 0.85
Context Recall正確答案的 context 有沒有被撈到0.610.89≥ 0.85
Context Precision撈到的 context 有多少是有用的0.580.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 表現遠勝向量實作複雜度略高
純向量實作簡單,延遲低 8msRecall 只有 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-ChildChild(256)精準定位,Parent(1024)提供充足上下文;跨句子的語意完整;實測幻覺率比固定切分低 8%
固定大小(512)實作最簡單語意邊界被隨機切斷;同一個答案分散在多個 chunk;重疊(overlap)雖然緩解問題但增加 50% 的 chunks 數量

Flip condition:文件結構高度統一(如資料庫 schema 文件、API 文件),固定大小 + overlap 已足夠,不值得 parent-child 的索引複雜度。


九、系統效應(量化改進表)

指標Week 1 POCWeek 4 Production改進幅度主要驅動因素
幻覺率34%6%-82%Citation prompt + Confidence gate + Reranking
Context Recall61%89%+46%Hybrid search(BM25+Vector)+ Parent-child chunking
Answer Relevancy0.720.91+26%Reranking 篩掉不相關 chunks
Faithfulness0.660.94+42%Citation-grounded prompt + Context Precision 提升
P95 延遲8.2s2.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/AOCRmyPDF + Celery async
可用性無保證99.5%(3-node Qdrant)N/ACluster HA
Index size12GB(50K docs)int8 量化節省 50%
查詢快取 hit rate0%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 篇。所有數字均來自實際生產環境(部分細節為保護客戶隱私而調整)。如有任何問題,歡迎在評論區留言。

Yen

Yen

Yen