iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
DevOps

30 天帶你實戰 LLMOps:從 RAG 到觀測與部署系列 第 27

Day27 - RAG FAQ Chatbot 實戰案例 I:功能驗收全紀錄(檢索 × 快取 × 安全 × 監控)

  • 分享至 

  • xImage
  •  

🔹 前言

經過前 26 天對於 RAG 以及對於 LLMOps 的各面向的學習,終於來到實作以及驗收這個 RAG FAQ Chatbot 的階段。今天的目標就是在本機環境先驗收 FAQ Chatbot 的功能與效能,確保系統達到「可用」水準。

⚠️ 注意:今天的功能驗收是在 開發/本地環境 完成,真正的 產線環境配置將在 Day28 展示。


🔹專案介紹

今天的專案一樣會放在 GitHub 上面,歡迎有興趣的人開 Issues、Fork ,或是直接在文章友善提問。

在 Day21–26,我們已經逐步完成了快取、Registry、安全檢查、Routing、觀測等模組。
今天(Day27) 的「開發」不是從零開始,而是把這些模組整合進同一個 API 服務,並驗證它們能共同運作

功能模組 實作方式 驗收對應
API 層 FastAPI Router (/ask, /healthz, /metrics) 基本可用性、健康檢查
安全防護 輸入驗證、Rate Limiter 錯誤阻擋、壓測時限流
檢索與重排序 FAISS + CrossEncoder Reranker 查詢正確性
LLM 回答 GPT-4o-mini 答案合理性
結果快取 In-memory / Redis 快取命中率
觀測性 Prometheus 指標 延遲、QPS、Token 成本

下圖展示了整個系統的架構:

https://ithelp.ithome.com.tw/upload/images/20251011/20120069lla6GgBpQg.png
Day27 RAG FAQ Bot 系統架構圖

有了上面的整合清單與架構圖,我們可以確認:
Day27 的開發已經把前面所有模組(快取、安全、檢索、觀測…)都串接到一個完整的 FAQ Chatbot 中。

接下來的重點,就是要透過一系列 功能驗收與效能測試,確保這個系統不只是「寫出來」,而是真的能達到 可用水準


🔹 驗收準備

📝 驗收環境

  • 硬體環境:MacBook Air M3,24GB RAM,本地 docker-compose 啟動
  • 嵌入模型 (Embedding)text-embedding-3-small(OpenAI,低成本向量化)
  • 重排序模型 (Rerank)BAAI/bge-reranker-v2-m3(可切換其他的線上模型,此驗收使用前者),取前 Top-3 片段
  • 聊天模型 (LLM 回答)gpt-4o-mini(回答生成)
  • 向量資料庫:FAISS in-memory index
  • 測試工具
    • ApacheBench (ab):單點延遲壓測
    • Locust:模擬多使用者併發
    • pytest:功能 & 安全測試

📝 驗收方法及驗收標準

詳見 Day02 - 系列專案介紹:企業知識庫 QA Chatbot

項目 方法 / 工具 驗收標準
延遲測試 ApacheBench / Locust;隨機選取 50 筆查詢,記錄 p50/p95 延遲 API + Rerank:p95 ≤ 3s本機 Rerank:p95 ≤ 5s
吞吐量測試 Locust;模擬 10 位使用者並發查詢,持續 1 分鐘 平均 QPS ≥ 3,且 HTTP 5xx < 1%
正確性測試 pytest + 人工 spot check;FAQ 測資(三類問題:流程 / 客服 / 技術) Top-1 準確率 ≥ 70%
安全測試 測資:空查詢、SQL injection、SSRF / 內網 URL;檢查 metadata 所有攻擊必須被阻擋,並在 metadata 記錄
成本測試 送出 100 筆 ≤300 tokens 查詢,統計 token 使用與成本 平均成本 < NT$0.2 / 次
可用性測試 本機模擬 crash(kill API process);觀察 systemd/docker-compose 冷啟動恢復服務 < 30s
觀測驗證 Prometheus + Grafana 或瀏覽器 /metrics 能看到延遲、QPS、快取命中率、Token/Cost、錯誤率

🔹 功能驗收結果

先打開前端介面,確認人類使用友善 👍

https://ithelp.ithome.com.tw/upload/images/20251011/20120069nG667jb5jL.png

1. 📂 文件處理

驗證方法:

  • 準備 PDF / DOCX / HTML / TXT / md 五種不同種類的文件並且放到 /data/raw/,舉例來說,manual.pdf 裡面大概長這樣:

https://ithelp.ithome.com.tw/upload/images/20251011/20120069hX9v3MMjQ6.png

❯ tree ./

./
├── architecture.md
├── deploy.docx
├── faq.html
├── intro.txt
└── manual.pdf
  • 執行 python -m app.ingest.cli_ingest --input tests/data/docs --output tests/output
❯ python -m app.ingest.cli_ingest \
  --input ./data/raw \
  --out ./data/clean/chunks.jsonl
[ingest] CWD=/Users/hazel/Documents/github/rag-qa-bot/backend
[ingest] input_dir=/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw (exists=True)
[ingest] output=/Users/hazel/Documents/github/rag-qa-bot/backend/data/clean/chunks.jsonl (mode=file)
[ingest] loader found 5 docs by ext: {'.md': 1, '.txt': 1, '.docx': 1, '.pdf': 1, '.html': 1}
Ingest: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████| 5/5 [00:00<00:00, 3569.01it/s]
✅ Ingest 完成:檔案模式(直寫 .jsonl),共 5 個 chunks → /Users/hazel/Documents/github/rag-qa-bot/backend/data/clean/chunks.jsonl
  • 檢查輸出是否產生 index.faissdocstore.jsonl
❯ python -m app.build_index \
  --chunks ./data/clean/chunks.jsonl \
  --docstore ./data/docs/docstore.jsonl \
  --index ./data/docs/index.faiss
✅ build_index: docstore=data/docs/docstore.jsonl index=data/docs/index.faiss chunks=5 openai=yes faiss=yes
❯ ls -al ./data/docs
total 80
drwxr-xr-x@ 4 hazel  staff    128 Sep 24 17:31 .
drwxr-xr-x@ 6 hazel  staff    192 Sep 24 17:32 ..
-rw-r--r--@ 1 hazel  staff   4553 Sep 25 14:50 docstore.jsonl
-rw-r--r--@ 1 hazel  staff  30765 Sep 25 14:50 index.faiss
  • 開啟 docstore.jsonl,確認內容有被清洗 & 切片
❯ cat  ./data/docs/docstore.jsonl | jq .
{
  "id": "/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/architecture.md::chunk-0",
  "title": "architecture.md",
  "text": "# FAQ Bot 系統架構設計 ## 整體流程 1. 使用者透過前端輸入問題 2. 問題經過向量化(Embedding) 3. 系統到向量資料庫檢索相似片段 4. 檢索到的內容會被拼接進 Prompt 5. 大語言模型 (LLM) 生成回答 6. 回答回傳給使用者 ## 元件細節 ### 前端 (Frontend) - 提供使用者輸入框與回覆區域 - 可內嵌在內部 Portal - 與 Backend API 溝通 ### 後端 (Backend FastAPI) - 提供 `/ask` API - 健康檢查端點 `/healthz` - 指標端點 `/metrics` - 整合快取、檢索與 LLM ### 向量資料庫 (FAISS) - 採用 L2 正規化後的內積檢索 - 儲存文件清洗與切片後的片段 - 提供 Top-k 查詢 ### 大語言模型 (LLM) - 使用 OpenAI GPT-4o-mini - 控制回答長度與溫度參數 - 回答中可引述來源 ### 觀測 (Observability) - 使用 Prometheus - 指標包含: - llm_requests_total - llm_request_latency_seconds - llm_tokens_total - llm_cost_total_usd",
  "source": "/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/architecture.md"
}
...
...
...

驗收結果:

  • 四種格式文件均能解析 ✅
  • FAISS in-memory index 成功建立,檢索能回傳相關 chunk ✅

2. 🤖 問答功能

驗證方法:

呼叫 /ask API,輸入典型問題(流程 / 客服 / 技術)

流程類(操作步驟 / 使用流程)提問:

❯ curl -s -X POST http://127.0.0.1:8000/ask \
 -H "Content-Type: application/json" \
 -d '{"query": "如何啟動 FAQ Bot 服務?"}' | jq .
{
 "answer": "啟動 FAQ Bot 服務的方法是執行指令「uvicorn app.main:app」。",
 "meta": {

客服類(常見 FAQ / 使用者疑問)提問:

❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "這個系統能回答什麼?"}' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1712  100  1669  100    43    760     19  0:00:02  0:00:02 --:--:--   779
{
  "answer": "系統可以回答與公司內部知識庫相關的問題,例如系統架構、部署方式、操作手冊。",
  "meta": {
    "cached": false,
    "context_preview": 
    ...

技術類(部署、環境、開發相關)提問:

❯ curl -s -X POST http://127.0.0.1:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "建置 FAQ Bot 需要什麼環境?"}' | jq .
{
  "answer": "建置 FAQ Bot 需要的環境包括能夠運行 uvicorn 的伺服器、能夠處理 HTTP 請求的 API 介面、以及用於監控的 Prometheus 和 Grafana。",
  "meta": {

重複相同問題,觀察快取是否命中。可以看到如果問了重複的問題的話,cached 會變成 true

❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "這個系統能回答什麼?"}' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1711  100  1668  100    43   800k  21140 --:--:-- --:--:-- --:--:--  835k
{
  "answer": "系統可以回答與公司內部知識庫相關的問題,例如系統架構、部署方式、操作手冊。",
  "meta": {
    "cached": true,
    "context_preview": "[faq.html] FAQ Bot 常見問題 常見問題集 Q1: 這個系統能回答什麼? A1: 系統可以回答與公司內部知識庫相關的問題,例如系統架構、部署方式、操作手冊。 Q2: 資料來源是什麼? A2: 所有答案都基於公司內部文件,例如 PDF 手冊、Word 檔案、HTML 文件。 Q3: 模型會記錄我的提問嗎? A3: 不會,系統只會暫存快取 300 秒,並不會長期保存提問。 Q4: 支援多語言嗎? A4: 是的,FAQ Bot 能同時
  • 觀察 Reranker 的排序結果,檢查 meta 裡的 scores,應該是由大到小排序,並且取前 3 個 chunks 丟進 LLM。
❯ curl -s -X POST http://127.0.0.1:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "LLM 相關指標包含哪些?"}' | jq .
{
  "answer": "LLM 相關指標包含:llm_requests_total、llm_request_latency_seconds、llm_tokens_total、llm_cost_total_usd。",
  "meta": {
    "cached": true,
    "context_preview": 
    ....
    ,
    "sources": [
      {
        "id": "/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/architecture.md::chunk-0",
        "title": "architecture.md",
        "source": "/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/architecture.md",
        "score": 0.2665708065032959,
        "reranker_score": 6.509603977203369. // <---- reranker_score
      },
	...,
	...
    ],
    "usage": {
      "input_tokens": 970,
      "output_tokens": 31,
      "total_tokens": 1001
    },
    "cost_usd": 0.0001641,
    "rerank_status": "ok",
    "rerank_model": "cross-encoder/ms-marco-MiniLM-L-6-v2",
    "top_k": 3,
    "policy_version": "2025-09-22-1700",
    "request_id": "ca4c173e-0749-4afe-90a7-5ec138394944"
  }
}

如果檢索不到內容,會 fallback 並且回答「找不到相關資料」:

❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "公司的客服聯絡方式是什麼?"}' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1515  100  1463  100    52   559k  20368 --:--:-- --:--:-- --:--:--  739k
{
  "answer": "資訊不足。",
  "meta": {
    "cached": true,
    "context_preview": "[manual.pdf] 使用手冊 - FAQ Bot 第 1 頁 共 3 頁 功能特色: - 自動回答常見問題 - 節省客服人力 - 提升回覆速度 1 系統操作說明 第 2 頁 共 3 頁 1. 啟動服務:uvicorn app.main:app 2. 發送問題:curl -X POST /ask 3. 查詢指標:curl -X GET /metrics 2 疑難排解 第 3 頁 共 3 頁 - 如果無法連線,請檢查防火牆設定。 - 如果回覆品質下降,請檢查文件是否更新索引",
    "sources": [

驗收結果:

  • API 正常返回回答與 metadata ✅
  • 相同查詢快取命中,延遲下降 ✅
  • Reranker 依分數排序正確,fallback 生效 ✅

3. ✅ 測試案例覆蓋

  • pytest 已涵蓋 retrieval / rerank / routes / security / cache 等核心模組
❯ tree ./app/tests -L 1

./app/tests
├── __pycache__
├── conftest.py
├── data
├── test_cache.py
├── test_e2e.py
├── test_eval_small.py
├── test_health.py
├── test_ingest.py
├── test_input_limits.py
├── test_loaders.py
├── test_perf_mocked.py
├── test_rate_limit.py
├── test_reranker.py
├── test_retrieval.py
└── test_routes.py
  • ✅ 全部測試通過(4 個跳過的測試是 e2e 測試,會實際打到 OpenAI
❯ pytest -m "not e2e and not perf and not eval"
============================================================== test session starts ===============================================================
platform darwin -- Python 3.11.13, pytest-8.4.2, pluggy-1.6.0
rootdir: /Users/hazel/Documents/github/rag-qa-bot/backend
configfile: pytest.ini
testpaths: app/tests
plugins: mock-3.15.1, anyio-4.10.0
collected 37 items / 5 deselected / 32 selected

app/tests/test_cache.py ...                                                                                                                [  9%]
app/tests/test_health.py ...                                                                                                               [ 18%]
app/tests/test_ingest.py .                                                                                                                 [ 21%]
app/tests/test_input_limits.py .....                                                                                                       [ 37%]
app/tests/test_loaders.py .ssss                                                                                                            [ 53%]
app/tests/test_rate_limit.py ..                                                                                                            [ 59%]
app/tests/test_reranker.py .....                                                                                                           [ 75%]
app/tests/test_retrieval.py ..                                                                                                             [ 81%]
app/tests/test_routes.py ......                                                                                                            [100%]

================================================================ warnings summary ================================================================
app/tests/test_ingest.py:7
  /Users/hazel/Documents/github/rag-qa-bot/backend/app/tests/test_ingest.py:7: PytestUnknownMarkWarning: Unknown pytest.mark.ingest - is this a typo?  You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/how-to/mark.html
    @pytest.mark.ingest

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
============================================= 28 passed, 4 skipped, 5 deselected, 1 warning in 4.25s =============================================


🔹 安全性驗收

目標:

所有攻擊必須被阻擋,並在 metadata 記錄。

驗證方法:

  • 空查詢:送出 { "query": "" }
❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": ""}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   204  100   191  100    13  65143   4433 --:--:-- --:--:-- --:--:--   99k
{
  "error": "invalid_input",
  "message": "Input rejected by policy.",
  "meta": {
    "type": "length",
    "redacted": true,
    "policy_version": "2025-09-22-1700",
    "request_id": "cd044400-3b73-4c2d-8e76-950dc42078d3"
  }
}
  • SQL injection:送出 { "query": "1 OR 1=1" }
❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "1 OR 1=1"}' | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   213  100   192  100    21  59590   6517 --:--:-- --:--:-- --:--:-- 71000
{
  "error": "invalid_input",
  "message": "Input rejected by policy.",
  "meta": {
    "type": "keyword",
    "redacted": true,
    "policy_version": "2025-09-22-1700",
    "request_id": "0e3b8b4c-8387-4fad-a28c-1534d345b5c0"
  }
}
  • SSRF 內網探測:送出 { "query": "http://127.0.0.1:8000/secret" }
❯ curl -X POST http://localhost:8000/ask \
  -H "Content-Type: application/json" \
  -d '{"query": "http://127.0.0.1:8000/secret"}' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   229  100   188  100    41  82203  17927 --:--:-- --:--:-- --:--:--  111k
{
  "error": "invalid_input",
  "message": "Input rejected by policy.",
  "meta": {
    "type": "url",
    "redacted": true,
    "policy_version": "2025-09-22-1700",
    "request_id": "c08e1d3a-d871-4251-a6dd-791d216f2450"
  }
}

驗收結果:

  • 空查詢返回 400 ✅
  • SQLi 被阻擋,並回覆 400 ✅
  • 127.0.0.1 URL 被拒絕,並回覆 400 ✅
  • 拒絕事件有在 meta 記錄(含 policy type / policy_version / request_id 等)✅

🔹 正確性驗收

目標:

人工評估 Top-1 準確率 ≥ 70%。

驗證方法:

設定16個測試資料集(標準應該是要 50 條以上,但我們的原始資料不夠多):

{"question":"這個專案的願景是什麼?","answer":"協助企業將知識數位化,並透過 AI 技術提升內部資訊檢索的效率。","keywords":["知識數位化","AI","檢索效率"],"gold_source_id":"/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/intro.txt::chunk-0"}

{"question":"系統整體流程的前兩步是什麼?","answer":"1. 使用者透過前端輸入問題 2. 問題經過向量化(Embedding)","keywords":["輸入問題","向量化","Embedding"],"gold_source_id":"/Users/hazel/Documents/github/rag-qa-bot/backend/data/raw/architecture.md::chunk-0"}
...
...
總共十六條測試資料,涵蓋不同問題。

執行 AutoEval 回答品質測試(Day30 會詳細說明):

❯ pytest -m eval -s -vv
[cache] Using in-memory TTLCache (ttl=3600s)
============================================================== test session starts ===============================================================
platform darwin -- Python 3.11.13, pytest-8.4.2, pluggy-1.6.0 -- /opt/homebrew/Caskroom/miniforge/base/envs/rag_qa_bot/bin/python
cachedir: .pytest_cache
rootdir: /Users/hazel/Documents/github/rag-qa-bot/backend
configfile: pytest.ini
testpaths: app/tests
plugins: mock-3.15.1, anyio-4.10.0
collected 38 items / 37 deselected / 1 selected

app/tests/test_eval_small.py::test_eval_on_small_dataset
[eval] Top-1 source accuracy = 1.00 (16/16) | compare_level=doc
[eval] Text pass rate (Jaccard>=0.60) = 0.62 (10/16)
PASSED

================================================= 1 passed, 37 deselected, 2 warnings in 47.00s ==================================================

驗收結果:

Top-1 來源回答正確性=100%(16/16)文字相似度通過率=62%。✅


🔹 延遲驗收

目標:

  • API + Rerank:p95 ≤ 3s
  • 本機 Rerank:p95 ≤ 5s

驗證方法:

用現有的 eval_small 問題隨機抽 50 筆問題,並且允許重複:

❯ # 在 backend/
jq -r '.question' app/tests/data/eval_small.jsonl | shuf -r -n 50 > bench_queries.txt

❯ cat  bench_queries.txt
向量資料庫採用什麼檢索方式並儲存什麼內容?
這個系統可以回答哪類問題?
...

寫一隻腳本逐筆做延遲測試,每一筆都不會用到快取,這個驗證主要是要看看加入 rerank 會不會讓延遲增加:

❯ sh ./benchmark/bench_latency.sh
== [Local Rerank] warm-up x3 ==
== [Local Rerank] sequential 50 requests ==

[01/50] sending...
progress: 1/50 (last=2.004729s code=200)
[02/50] sending...
progress: 2/50 (last=2.343764s code=200)
...
...
[49/50] sending...
progress: 49/50 (last=2.566958s code=200)
[50/50] sending...
progress: 50/50 (last=2.228147s code=200)
== [Local Rerank] stats (n=50) ==
n=50 ok=50 bad=0
p50=2.1235s  p95=2.8477s

驗收結果:

API+本機 Rerank(BAAI/bge-reranker-v2-m3)p95 ≈ 2.8477s,達標(≤ 3s)。✅


🔹 成本驗收

目標:

平均成本 < NT$0.2 / 次

驗證方法:

用現有的 eval_small 問題隨機抽 100 筆問題,允許重複測試且不吃快取。
目的是為了驗證 最壞狀況 下沒有吃到快取的成本仍然在可以接受的範圍(NTD$ 0.2 / 一次查詢)內:

❯ ./benchmark/cost_bench.sh
[gen] wrote 100 queries to benchmark/cost_queries.txt
== [Cost Bench] sequential 100 requests (nocache) ==

[001/100] sending...
progress: 1/100 (code=200)
[002/100] sending...
...
...
progress: 98/100 (code=200)
[099/100] sending...
progress: 99/100 (code=200)
[100/100] sending...
progress: 100/100 (code=200)
[done] ok=100 bad=0 -> benchmark/cost_result.jsonl
==== Cost Summary ====
queries_file    : benchmark/cost_queries.txt
results_file    : benchmark/cost_result.jsonl
n/ok/bad        : 100/100/0
fx (NTD/USD)    : 32.0
total_usd       : 0.015570
avg_usd         : 0.000156
avg_ntd         : 0.005
avg_tokens      : 966.8
ACCEPT(avg_ntd<0.2): PASS
pct(total_tokens ≤ 300): 0.0%

這隻腳本的匯率會用 NTD:USD = 32:1 的匯率去換算。

驗收結果:

成本:avg_ntd = NT$0.005/次(fx=32),✅ 小於 0.2。
token:目前 avg_tokens ≈ 967(❌ 未達 ≤300),不過這算是設計假設,所以以成本的角度而言仍然是通過驗證條件的。

後續可以採取的行動:採用「token 預算式 Context 組裝」+「RERANK_TOP_K=2」+「System Prompt精簡」,預期把 avg_tokens 拉到 ≤ 300,成本將進一步降到 ≈ NT$0.002–0.003/次。


🔹 可用性驗收

目標:

  • 服務發生 crash 時,系統需能自動重啟
  • 冷啟動時間(從 crash → /healthz 200)必須 < 30 秒

驗證方法:

# 啟動 app
❯ uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1

# 在另外一個終端機 Shell 確認健康檢查通過
❯ curl -sf http://127.0.0.1:8000/healthz && echo READY

{"status":"ok"}READY

# 開一個新的 shell,找到 uvicorn process PID:
❯ ps aux | grep "uvicorn app.main:app" | grep -v grep
hazel            12158  13.0  1.2 416578112 309328 s014  U+    6:45PM   0:02.93 /opt/homebrew/Caskroom/miniforge/base/envs/rag_qa_bot/bin/python /opt/homebrew/Caskroom/miniforge/base/envs/rag_qa_bot/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 1

# 記錄下上面的 PID - 12158,執行另一支腳本紀錄從 kill process 到回復的時間
❯ sh ./benchmark/crash_bench.sh
[cache] Using in-memory TTLCache (ttl=3600s)
INFO:     Started server process [12253]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:59625 - "GET /healthz HTTP/1.1" 200 OK
cold start = 6s

驗收結果:

  • 模擬 crash → 重新啟動 uvicorn → /healthz 回 200
  • 冷啟動時間 = 6s < 30s

🔹 吞吐量驗收

目標:

平均 QPS ≥ 3,且 HTTP 5xx < 1%

驗證方法:

  • 工具:Locust

  • 模擬條件:

    • 10 位使用者並發查詢(-u 10,每秒啟動 1 位 -r 1
    • 持續時間:1 分鐘
    • 查詢集:以 快取命中率高 的問題集為主,模擬真實使用情境
  • 指令:

❯ locust -f benchmark/locustfile_cache.py \
  --headless -u 10 -r 1 --run-time 1m \
  --host http://127.0.0.1:8000 \
  --csv benchmark/locust_cache --only-summary
[2025-09-25 19:24:58,768] hazel/INFO/locust.main: Starting Locust 2.40.5
[2025-09-25 19:24:58,769] hazel/INFO/locust.main: Run time limit set to 60 seconds
[2025-09-25 19:24:58,770] hazel/INFO/locust.runners: Ramping to 10 users at a rate of 1.00 per second
[2025-09-25 19:25:07,797] hazel/INFO/locust.runners: All users spawned: {"RAGUser": 10} (10 total users)
[2025-09-25 19:25:58,622] hazel/INFO/locust.main: --run-time limit reached, shutting down
[2025-09-25 19:25:58,671] hazel/INFO/locust.main: Shutting down (exit code 0)
Type     Name                                                            # reqs      # fails |    Avg     Min     Max    Med |   req/s  failures/s
--------|--------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
POST     /ask (cached)                                                     1868     0(0.00%) |    171       0   14761      3 |   31.21        0.00
--------|--------------------------------------------------------------|-------|-------------|-------|-------|-------|-------|--------|-----------
         Aggregated                                                        1868     0(0.00%) |    171       0   14761      3 |   31.21        0.00

Response time percentiles (approximated)
Type     Name                                                                    50%    66%    75%    80%    90%    95%    98%    99%  99.9% 99.99%   100% # reqs
--------|------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
POST     /ask (cached)                                                             3      4      5      5      6      8   3300   5400  14000  15000  15000   1868
--------|------------------------------------------------------------------|--------|------|------|------|------|------|------|------|------|------|------|------
         Aggregated                                                                3      4      5      5      6      8   3300   5400  14000  15000  15000   1868

驗收結果:

指標 數值 驗收標準 結論
總請求數 1868 - -
測試時間 (s) 60 - -
平均 QPS 31.2 ≥ 3
失敗率 (5xx / 429) 0.00% < 1%
p50 延遲 3 ms - -
p95 延遲 8 ms - -
最長延遲 (max) 15,000 ms -(僅極少數 outlier) ⚠️ 少數長尾
  • 系統在快取命中情境下表現穩定,QPS ≈ 31.2,遠高於目標值。
  • 失敗率 0.00%,符合驗收標準。
  • 大部分請求延遲極低(p95 僅 8 ms),只有少數長尾請求(可能為快取未命中或後端 LLM 慢)影響 max 值,屬可接受範圍。
  • 本測試已 停用 rate limit,確保量測到的是系統真實吞吐量。

吞吐量驗收 ✅


🔹 觀測性驗收

目標

  • 所有核心指標均有上報並可於 Grafana 可視化:
    延遲、QPS、快取命中率、Token/Cost、錯誤率
  • 確保能支援 SLA 驗證與後續調整改善。

驗證方法

  • 指標來源

    • /metrics 端點,Prometheus pull。
    • Metrics 定義於 app/observability.py
  • 工具

    • Grafana Dashboard 匯入 JSON - observability/dashboards/faq-bot-overview_v2.json
    • Explore 模式下手動查詢 PromQL 驗證
    • 用壓測工具(Locust / ab)產生流量

https://ithelp.ithome.com.tw/upload/images/20251011/20120069XkcwNBCgd4.png

1) 延遲 (Latency)

  • Metrics:

    • llm_request_latency_seconds(Histogram, label=route
    • rag_embedding_latency_seconds(Histogram, label=stage
    • rag_retrieval_latency_seconds(Histogram)
    • rag_rerank_latency_seconds(Histogram)
    • rag_llm_latency_seconds(Histogram, label=model

https://ithelp.ithome.com.tw/upload/images/20251011/20120069fl4fqQH3X4.png

2) QPS (吞吐量)

  • Metrics:
    • llm_requests_total(Counter, labels=route,status

https://ithelp.ithome.com.tw/upload/images/20251011/20120069dx17IAQ6dD.png

3) 快取命中率 (Cache Hit Ratio)

  • Metrics:
    • rag_cache_requests_total(Counter, label=route
    • rag_cache_results_total(Counter, labels=route,result

https://ithelp.ithome.com.tw/upload/images/20251011/20120069UorWsxqtgs.png

4) Token / Cost

  • Metrics:
    • llm_tokens_total(Counter, labels=kind,model
    • llm_cost_total_usd(Counter, label=model

https://ithelp.ithome.com.tw/upload/images/20251011/20120069V8iss8GroA.png

5) 錯誤率 (Error Rate)

  • Metrics:
    • rag_errors_total(Counter, label=stage
    • llm_requests_total{status="error"}(Counter)

https://ithelp.ithome.com.tw/upload/images/20251011/20120069Al6MWKME5f.png

驗收結果:

  • 指標覆蓋完整,延遲 / QPS / 快取命中率 / Token+Cost / 錯誤率 皆可在 Grafana 面板清楚觀察。
  • 驗收標準:PASS ✅

後續可以做的加強:

  1. 可補上 p99 延遲每請求成本直方圖(Histogram bucket 1e-5~1e-2)。
  2. 對「錯誤率」再分 5xx / 4xx,方便區分限流 vs 系統異常。
  3. REQUEST_COUNT 細分 success / client_4xx / server_5xx,並在面板新增「Error Rate by class」對比圖

🔹 驗收結果與 P0 改善計畫

項目 方法 / 依據 驗收標準 結果
延遲(序列 50 筆、不中快取) bench_latency.sh p95 ≤ 3s(API+Rerank)/ ≤ 5s(本機 Rerank) p95≈2.85s(本機 BAAI/bge-reranker-v2-m3)PASS
吞吐量(1 分鐘、10 並發) Locust(命中快取場景;rate limit 關閉) 平均 QPS ≥ 3 且 5xx < 1% QPS≈31.2、失敗率 0.00% PASS
成本(100 筆、≤300 tokens 目標) cost_bench.sh avg_ntd < 0.2/次 avg_ntd≈0.005(fx=32)PASSavg_tokens≈967(>300,僅成本達標)
安全性(空查詢/SQLi/SSRF) 直接打 /ask 全擋、含 metadata 全擋,400/metadata 完整 PASS
可用性(crash→恢復) crash_bench.sh(uvicorn) 冷啟動 < 30s cold start=6s PASS
觀測(Grafana/PromQL) 匯入儀表板 + Explore 可見 延遲/QPS/快取/Token/Cost/錯誤 面板完整可視化 PASS
正確性(子集評估) pytest -m eval Top-1 ≥ 70% Top-1=100%(16/16);文字相似度通過率 62%

📌 P0(最高優先級)改善計畫 & Next Steps

類別 工作項目 動作 實作要點 驗收方式
Tokens/成本 Prompt 精簡 移除冗語、限制輸出長度 系統提示改條列,回答 ≤300 tokens 100 筆 eval 平均 tokens ≤300
Tokens/成本 Context 預算器 動態計算 query+prompt token,依 budget 截斷 context 保留高分片段,優先含標題 eval 不超過 budget,正確率不降
吞吐量/效能 關閉快取壓測 Locust 測試非快取情境 10 並發 1 分鐘,報告 QPS、p95 QPS ≥3(無快取基準)
吞吐量/效能 Rate limit On/Off 壓測 開關限流各跑一輪 產出兩組報告(標註 On/Off) 區分 429 與真實效能
可靠性/錯誤率 REQUEST_COUNT 細分 status 拆成 success / 4xx / 5xx Counter 新增三類標籤 /metrics 有效區分
可靠性/錯誤率 rag_errors_total 全覆蓋 cache/embedding/retrieval/rerank/llm/route 全部分支 inc 模擬錯誤驗證每個 stage 觀測到不同標籤錯誤
觀測告警 p95 延遲告警 加入規則:/ask p95>3s(5m) Grafana Alert 規則 模擬高延遲能觸發
觀測告警 快取命中率告警 hit ratio<80%(5m) 人工製造 miss 驗證 觸發告警
觀測告警 5xx 告警 5xx>0(1m) 模擬 LLM API 失敗 觸發告警
可用性 自動重啟 systemd/docker-compose restart: always kill 進程後自動拉起 服務恢復 ≤30s
可用性 /healthz 細化 回報 cache/index/LLM/rerank 狀態 探針失敗時 503 + 詳細 meta K8s/ALB 能正確摘除

🔹 小結

Day27 完成驗收後,FAQ Bot 已具備「功能可用性」。延遲、吞吐量、快取命中率、成本與可用性均達標,最小可行系統(MVP)已建立。

整體來說,FAQ Bot 能在本地環境穩定運作並通過驗收。
接下來的挑戰,不只是「能不能用」,而是「如何在更真實的環境中維持穩定與安全」。
這就是我們在 Day28 要面對的課題。


📚 參考資料


上一篇
Day26 - LLM 應用成本改善:如何採用 MVP 三步驟節省 60% 成本?
下一篇
Day28 - RAG FAQ Chatbot 實戰案例 II:Cloudflare + AWS 低成本架構與完整試算
系列文
30 天帶你實戰 LLMOps:從 RAG 到觀測與部署28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言