iT邦幫忙

2025 iThome 鐵人賽

DAY 19
0
DevOps

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

Day19 - 掌握 LLM 應用可觀測性:監控延遲、Token 與成本(含工具選型)

  • 分享至 

  • xImage
  •  

🔹 前言

昨天我們完成了 API Gateway,讓前端可以透過一個統一的 Proxy 來存取 LLM。
但在真實運行環境裡,還有一個非常現實的問題:

👉 我的系統到底快不快?準不準?花了多少錢?

這就是今天要談的主題:LLM 應用的 Observability(可觀測性)

https://ithelp.ithome.com.tw/upload/images/20251003/20120069RvduQsRoVB.png


🔹 為什麼需要重視可觀測性?

一般在後端服務裡,我們會監控:

  • 延遲(Latency):API 請求花了多少時間?
  • 錯誤率(Error Rate):有沒有出現 5xx?
  • 資源使用(Resource Usage):CPU / Memory / Disk。

而在 LLM 應用裡,我們需要額外關心三件事:

  1. 延遲(Latency)

    • LLM 回應可能需要幾百 ms 到幾秒不等。
    • 在多步驟工作流(Chain / Agent)裡,每一步的延遲會累加。
  2. Token 使用量

    • 每次請求會消耗 Prompt Token + Completion Token。
    • Token 數直接影響成本。
  3. 成本(Cost)

    • OpenAI / Anthropic / Azure / AWS Bedrock 都是 依照 Token 計價
    • 如果不觀測,月底帳單可能嚇死人。

🔹 監控哪些指標?

以下是常見的監控指標(Metrics):

類別 指標 說明
效能 Latency 每次 API 請求的平均 / P95 / P99 延遲
成本 Token Usage Prompt / Completion / Total
成本 Cost 換算成美金(依照模型單價)
穩定性 Error Rate 請求失敗率,是否超時或 API 429
使用情境 User Query 分佈 哪些問題最常被問,幫助優化 Cache

🔹 如何實作?

1. Logging:紀錄每次請求

API Gateway(FastAPI)層,我們不只是單純攔截,而是在 /ask 端點裡直接記錄:

  • 延遲(latency)
  • Token 使用量(prompt / completion)
  • 成本(cost)
  • 模型名稱(model)
@app.post("/ask")
async def ask(request: Request):
    body = await request.json()
    model = body.get("model", DEFAULT_MODEL)
    messages = body.get("messages") or [{"role": "user", "content": "Say hello!"}]
    messages = [{"role": "system", "content": "請一律使用繁體中文回答。"}] + messages

    start = time.time()
    try:
        usage_prompt = usage_completion = 0
        answer_text = None

        # 呼叫 OpenAI API (Responses API 或 Chat Completions API)
        resp = client.responses.create(
            model=model,
            input=[{"role": m["role"], "content": m["content"]} for m in messages],
            temperature=body.get("temperature", 0.2),
        )
        answer_text = (resp.output_text or "").strip()
        if resp.usage:
            usage_prompt     = getattr(resp.usage, "input_tokens", 0) or 0
            usage_completion = getattr(resp.usage, "output_tokens", 0) or 0

        latency = time.time() - start
        cost = calc_cost(model, usage_prompt, usage_completion)

        # ===== Logging =====
        logger.info(
            f"LLM Request | model={model} latency={latency:.2f}s "
            f"prompt_tokens={usage_prompt} completion_tokens={usage_completion} cost={cost:.6f}"
        )

        return {
            "model": model,
            "latency_s": round(latency, 3),
            "prompt_tokens": usage_prompt,
            "completion_tokens": usage_completion,
            "cost_usd": round(cost, 6),
            "answer": answer_text,
        }

  • Latencytime.time() 量測請求時間
  • Token Usage:從 LLM 回傳的 usage 抓取
  • Cost:透過 calc_cost() 換算成美元
  • Logging:寫入檔案或 Console,方便後續輸出到 DB / ELK

⚠️ 為了符合隱私合規性:日誌不會存原始 user prompt;若需稽核改存 hash/抽樣。


2. Metrics:用 Prometheus + Grafana

Prometheus 提供 Counter / Histogram ,方便收集:

from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST

REQUEST_COUNT = Counter("llm_requests_total", "Total LLM requests", ["model"])
TOKEN_USAGE = Counter("llm_tokens_total", "Total tokens used", ["model", "type"])
LATENCY = Histogram("llm_request_latency_seconds", "LLM request latency", ["model"])
ERROR_COUNT = Counter("llm_errors_total", "Total LLM errors", ["model", "error_type"])
COST = Counter("llm_cost_usd_total", "Total cost (USD)", ["model"])

配合 Grafana Dashboard,我們就能看到:

  • 每天多少請求
  • Token 使用曲線
  • 成本趨勢

⚠️ 注意:single process(uvicorn --reload False)可直接使用上述寫法;若要在 multi-process(如 gunicorn -w 4 部署,請改用 prometheus_client.multiprocess.Registry(範例如下),避免指標重複。

import os
from prometheus_client import CollectorRegistry, multiprocess, generate_latest

if os.environ.get('PROMETHEUS_MULTIPROC_DIR'):
    # Multi-process mode
    registry = CollectorRegistry()
    multiprocess.MultiProcessCollector(registry)
else:
    # Single process mode (開發環境)
    from prometheus_client import REGISTRY as registry

3. 成本估算:換算 Token → $

⚠️ 定價來源與命名說明
下方 PRICING 僅作示例,請以官方定價頁為準;實際價格可能因地區、企業折扣、方案不同而變動。 文中不使用「未公布或已下架」的型號名稱,避免混淆。上線前請再次核對你實際用到的模型。(本文撰寫於 2025-10-03,表為當日定價)

以 OpenAI GPT-4o-mini 為例:

  • Input (Prompt): $0.15 / 1M tokens
  • Output (Completion): $0.60 / 1M tokens

其他的模型:

供應商 模型(建議名稱) Input / 1M Output / 1M 備註 / 來源
OpenAI GPT-4o $2.50 $10.00 官方模型頁(一般用於 Responses/Chat)(OpenAI Platform)
OpenAI GPT-4o-mini $0.15 $0.60 官方 API 定價頁(Realtime API—Text 區塊);若非 Realtime,仍以此頁為準核價。(OpenAI)
Anthropic Claude Sonnet 4.5 $3.00 $15.00 官方公告:「4.5 與 Sonnet 4 同價」(Anthropic)
Anthropic Claude 3.5 Haiku $0.80 $4.00 官方 Haiku 定價說明(3.5)(Anthropic)
Anthropic Claude Opus 4.1 $15.00 $75.00 官方 Opus 4.1 頁/定價文件 (Anthropic)

我們可以在 Logging 裡直接加一個小函式:

PRICING = {
    "gpt-4o":      {"prompt": 2.50/1_000_000, "completion": 10.00/1_000_000},
    "gpt-4o-mini": {"prompt": 0.15/1_000_000, "completion": 0.60/1_000_000},
    "gpt-4.1":     {"prompt": 2.00/1_000_000, "completion": 8.00/1_000_000},
    "gpt-4.1-mini":{"prompt": 0.40/1_000_000, "completion": 1.60/1_000_000},
}

def calc_cost(model: str, prompt_tokens: int, completion_tokens: int) -> float:
    p = PRICING.get(model)
    if not p:
        return 0.0
    return prompt_tokens * p["prompt"] + completion_tokens * p["completion"]

每次請求都記錄成本,最後就可以匯總成報表。


🔹 Demo:FastAPI + Prometheus + Grafana

今天的程式碼會放在 GitHub Repo,有興趣的讀者歡迎自行Fork研究

簡化版流程

https://ithelp.ithome.com.tw/upload/images/20251003/20120069KMl4QoRT0I.png

專案架構及說明

day19_observability/
├─ app/
│  ├─ app.py              # FastAPI + Prometheus exporter
│  ├─ requirements.txt
│  └─ Dockerfile
├─ tests/
│  └─ test_requests.py    # 測試腳本
├─ docker/
│  ├─ prometheus.yml
│  └─ grafana/
│     └─ provisioning/
│        ├─ dashboards/
│        │  ├─ dashboard.yml
│        │  └─ llm_observability.json
│        └─ datasources/
│           └─ datasource.yml
├─ environment.yml        # Conda 環境設定
├─ .env                   # 環境變數 (不要上傳)
├─ .env.example           # 環境變數範例
├─ docker-compose.yml
├─ README.md
└─ .dockerignore

這是一個最小可行的 LLM Observability 範例專案,透過 Flask API 包裝 OpenAI,並輸出 Prometheus Metrics,最後用 Grafana Dashboard 視覺化。我已經將 App 連帶 Prometheus 以及 Grafana 都打包了,只要透過 docker-compose 就可以全部啟動,請參考 GitHub Repo 裡的 README.md 啟動專案。

執行手動測試打 requests:

❯ curl -X POST http://localhost:8000/ask \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4.1","messages":[{"role":"user","content":"請簡短的介紹台灣?"}]}'
{"model": "gpt-4.1", "latency_s": 2.455, "prompt_tokens": 28, "completion_tokens": 77, "cost_usd": 0.000672, "answer": "台灣位於東亞,四面環海,首都是台北。擁有2300多萬人口,以高科技產業、豐富美食和多元文化聞名。自然景觀多樣,包括高山、溫泉和美麗海岸。台灣是亞洲重要的經濟體,也是民主社會。"}

❯ curl -X POST http://localhost:8000/ask \
  -H 'Content-Type: application/json' \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"用三點解釋 RAG"}]}'
{"model": "gpt-4o-mini", "latency_s": 6.012, "prompt_tokens": 28, "completion_tokens": 195, "cost_usd": 0.000121, "answer": "RAG(Retrieval-Augmented Generation)是一種結合檢索和生成的模型,主要用於自然語言處理。以下是三個要點解釋 RAG:\n\n1. **檢索與生成結合**:RAG 模型首先從外部資料庫中檢索相關信息,然後將這些信息與生成模型結合,生成更具上下文和準確性的回答。\n\n2. **提高準確性**:透過檢索最新或專業的資料,RAG 能夠提供比單純生成模型更準確和具體的回答,特別是在面對需要最新資訊的問題時。\n\n3. **應用廣泛**:RAG 可應用於各種任務,如問答系統、對話生成和內容創作等,特別適合需要大量背景知識的場景。"}

也可以執行腳本測試:

❯ python tests/test_requests.py
✅ gpt-4o-mini 回答:「成功不是終點,失敗不是致命的,重要的是繼續前行的勇氣。」... (tokens=29+25, cost=1.9e-05$)
✅ gpt-4o-mini 回答:RAG(Retrieval-Augmented Generation)是一種結合... (tokens=27+72, cost=4.7e-05$)
✅ gpt-4o 回答:在金融業中,RAG(紅、黃、綠)系統是一種常見的風險管理和績效評估工具。這種系統... (tokens=29+395, cost=0.004023$)

📊 Metrics 部分輸出:
# HELP llm_requests_total Total LLM requests
# TYPE llm_requests_total counter
llm_requests_total{model="gpt-4o-mini"} 9.0
llm_requests_total{model="gpt-4o"} 2.0
llm_requests_total{model="gpt-4.1"} 1.0
# HELP llm_tokens_total Total tokens used
# TYPE llm_tokens_total counter
llm_tokens_total{model="gpt-4o-mini",type="prompt"} 265.0
llm_tokens_total{model="gpt-4o-mini",type="completion"} 435.0
llm_tokens_total{model="gpt-4o",type="prompt"} 57.0
llm_tokens_total{model="gpt-4o",type="completion"} 641.0
llm_tokens_total{model="gpt-4.1",type="prompt"} 28.0
llm_tokens_total{model="gpt-4.1",type="completion"} 77.0
# HELP llm_cost_usd_total Total cost (USD)
# TYPE llm_cost_usd_total counter
llm_cost_usd_total{model="gpt-4o-mini"} 0.00030075
llm_cost_usd_total{model="gpt-4o"} 0.006552500000000001
llm_cost_usd_total{model="gpt-4.1"} 0.000672

執行 curl http://localhost:8000/metrics | head -50 可以得到部分 metrics:

# HELP python_gc_objects_collected_total Objects collected during gc
# TYPE python_gc_objects_collected_total counter
python_gc_objects_collected_total{generation="0"} 1885.0
python_gc_objects_collected_total{generation="1"} 249.0
python_gc_objects_collected_total{generation="2"} 0.0
# HELP python_gc_objects_uncollectable_total Uncollectable objects found during GC
# TYPE python_gc_objects_uncollectable_total counter
python_gc_objects_uncollectable_total{generation="0"} 0.0
python_gc_objects_uncollectable_total{generation="1"} 0.0
python_gc_objects_uncollectable_total{generation="2"} 0.0
# HELP python_gc_collections_total Number of times this generation was collected
# TYPE python_gc_collections_total counter
python_gc_collections_total{generation="0"} 227.0
python_gc_collections_total{generation="1"} 20.0
python_gc_collections_total{generation="2"} 1.0
# HELP python_info Python platform information
# TYPE python_info gauge
python_info{implementation="CPython",major="3",minor="11",patchlevel="13",version="3.11.13"} 1.0
# HELP process_virtual_memory_bytes Virtual memory size in bytes.
# TYPE process_virtual_memory_bytes gauge
process_virtual_memory_bytes 8.4549632e+07
# HELP process_resident_memory_bytes Resident memory size in bytes.
# TYPE process_resident_memory_bytes gauge
process_resident_memory_bytes 7.0643712e+07
# HELP process_start_time_seconds Start time of the process since unix epoch in seconds.
# TYPE process_start_time_seconds gauge
process_start_time_seconds 1.75759896776e+09

到瀏覽器輸入:http://localhost:9090/,可以看到 prometheus dashboard:

https://ithelp.ithome.com.tw/upload/images/20251003/20120069hwdbUq9Xju.png

到瀏覽器輸入:http://localhost:3000/,可以看到 Grafana dashboard:

https://ithelp.ithome.com.tw/upload/images/20251003/2012006946EnfYdKT8.png

log 寫到 logs/llm_requests.log,未來也能整合到 ELK / Loki / Datadog

2025-09-11 16:30:17,178 [INFO] LLM Request | model=gpt-4o-mini latency=1.89s prompt_tokens=21 completion_tokens=14 cost=0.000012
2025-09-11 16:30:17,180 [INFO] [HEALTH] ok
2025-09-11 16:30:27,243 [INFO] [HEALTH] ok

這邊列出了三個報表:

  • 總 requests 數
  • Requests Latency - p95: 第 95 百分位延遲。
    • 以上面的圖為例子:在 22:05 之後,p95 突然跳到 9 ~ 10 秒,代表新一批請求裡有部分變得更慢,最慢的 5% 大於9秒。
  • Token 成本:以使用的模型分開統計,並以美金(USD)計價。

🔹 實務觀測工具:這些方案可以直接用

快速選擇指南

類型 工具 適合誰 特色
基礎監控 Prometheus + Grafana DevOps / SRE 開源標配,metrics + dashboard
雲端套件 Datadog / New Relic 有錢的公司 一站式 APM,快速上線
LLM API 觀測 Helicone / Langfuse LLM 初創團隊 Token 成本、trace、feedback
實驗追蹤 Weights & Biases ML 團隊 Prompt / fine-tune 實驗管理
品質評估 Ragas / DeepEval RAG 開發者 自動化測試、回答品質驗證

詳細功能對比

功能面向 Prometheus Grafana Helicone Langfuse W&B Ragas DeepEval
Metrics ✅ ML 支援完整
Tracing OTel 整合
Token 成本
Prompt 管理 部分
品質評估 ✅ (Feedback)
開源 / SaaS 開源 開源 SaaS+開源 開源 SaaS 開源 開源
適用場景 Infra 可視化 API log/成本 LLM pipeline trace ML/LLM 實驗 RAG 評估 RAG+Agent評估

情境決策樹狀圖

https://ithelp.ithome.com.tw/upload/images/20251003/20120069m2Z79mCFRm.png


🎯 小結

今天我們學會了 如何監控 LLM 應用的運行狀態

  • Latency:使用者體驗的關鍵。
  • Token Usage:直接決定成本。
  • Cost Estimation:必須即時追蹤,避免超支。
  • Logging:紀錄每次請求(延遲、Token、成本、模型),方便後續送到 ELK / Loki。
  • Observability 工具:Prometheus + Grafana 是最常見組合。

有了這些基礎指標,我們在 Debug 問題控制成本 上就能更有信心。但還有一個更關鍵的問題:

🤔 模型回答得很快,但答案是錯的怎麼辦?

明天(Day 20),我們會深入「品質監控」,特別是 幻覺偵測(Hallucination Detection): 如何判斷模型是不是在胡說八道?

我們明天見!有興趣的話,歡迎追蹤系列文 ⭐

📚 引用 / 延伸閱讀


上一篇
Day18 - 用 FastAPI 實作 LLM API Gateway:驗證、限流、觀測與實務選型
下一篇
Day20 - LLM 回應品質監控:幻覺偵測與三層防護實作
系列文
30 天帶你實戰 LLMOps:從 RAG 到觀測與部署24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言