iT邦幫忙

2025 iThome 鐵人賽

DAY 24
1
AI & Data

從0開始的MLFLOW應用搭建系列 第 24

Day 24 – 使用 Postman 測試 FastAPI 與錯誤處理設計

  • 分享至 

  • xImage
  •  

🎯 目標

  1. 使用 Postman 測試 /recommend/log-ab-event 兩個端點。
  2. 為 API 加上 輸入驗證錯誤處理 (Error Handling)
  3. 提升整體 API 穩定性與除錯能力。

🔍 背景

在前兩天 (Day 22–23),我們:

  • 完成了推薦 API /recommend
  • 寫好了紀錄 API /log-ab-event,可以把使用者行為存進 workspace/logs/ab_events.csv

但目前這兩個 API 都屬於「prototype」版本:

  • 沒有驗證空輸入。
  • 模型名稱錯誤時會報 Python Traceback。
  • 硬碟寫入錯誤或缺少資料夾會直接閃退。

今天我們要讓它變得「像企業 API 一樣」穩健、可維護。


⚙️ 1. 更新 FastAPI 主程式

📂 修改 src/api/main.py 為以下內容(完整可用版):

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, ValidationError
import mlflow
import mlflow.pyfunc
import pandas as pd
import os, csv
from datetime import datetime

# === 初始化 FastAPI ===
app = FastAPI(
    title="Anime Recommender API",
    description="FastAPI + MLflow 企業級推薦系統",
    version="2.2.0"
)

# === Pydantic 輸入模型 ===
class RecommendRequest(BaseModel):
    anime_titles: list[str]

class ABEvent(BaseModel):
    user_id: str
    model_version: int
    recommended_title: str
    clicked: bool
    timestamp: datetime = datetime.utcnow()

# === 設定 MLflow ===
mlflow.set_tracking_uri("http://mlflow:5000")
model_cache = {}  # 模型快取,避免重複載入

def get_model(model_name: str):
    """依照模型名稱載入模型,若不存在則回傳 404"""
    if model_name not in model_cache:
        model_uri = f"models:/{model_name}/Staging"
        print(f"📦 Loading {model_uri} ...")
        try:
            model_cache[model_name] = mlflow.pyfunc.load_model(model_uri)
        except Exception:
            raise HTTPException(status_code=404, detail=f"Model '{model_name}' not found in Registry.")
    return model_cache[model_name]

# === 健康檢查 ===
@app.get("/health")
def health_check():
    return {"status": "ok", "message": "FastAPI is running 🚀"}

# === 推薦 API ===
@app.post("/recommend")
def recommend(request: RecommendRequest, model_name: str = Query("AnimeRecsysModel")):
    try:
        if not request.anime_titles:
            raise HTTPException(status_code=400, detail="anime_titles cannot be empty.")
        model = get_model(model_name)
        df = pd.DataFrame(request.anime_titles)
        result = model.predict(df)
        return {
            "model_name": model_name,
            "input": request.anime_titles,
            "recommendations": result[0]
        }
    except ValidationError as ve:
        raise HTTPException(status_code=422, detail=ve.errors())
    except HTTPException as he:
        raise he
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Internal Error: {str(e)}")

# === AB Test 記錄 API ===
@app.post("/log-ab-event")
def log_ab_event(event: ABEvent):
    LOG_DIR = "/usr/mlflow/workspace/logs"
    os.makedirs(LOG_DIR, exist_ok=True)
    log_path = os.path.join(LOG_DIR, "ab_events.csv")

    try:
        file_exists = os.path.isfile(log_path)
        with open(log_path, mode="a", newline="", encoding="utf-8") as f:
            writer = csv.writer(f)
            if not file_exists:
                writer.writerow(["timestamp", "user_id", "model_version", "recommended_title", "clicked"])
            writer.writerow([
                event.timestamp.isoformat(),
                event.user_id,
                event.model_version,
                event.recommended_title,
                event.clicked
            ])
        return {"message": "Event logged successfully ✅", "event": event.dict()}
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to write log: {e}")

🧠 新增與優化的地方

功能 說明
model_cache 避免每次都重新載入 MLflow 模型,提高效能。
HTTPException 統一錯誤格式,避免 Python Traceback。
驗證空輸入 防止空列表造成模型出錯。
模型不存在處理 找不到模型名稱時回傳 HTTP 404。
CSV 寫入錯誤處理 目錄不存在、磁碟滿等錯誤會回傳 HTTP 500。

🧪 2. 使用 Postman 測試

打開 Postman,設定環境變數:

base_url = http://127.0.0.1:8000

https://ithelp.ithome.com.tw/upload/images/20251008/20178626qPea5NjRwE.png


✅ 成功案例 1:推薦 API

Method: POST
URL: {{base_url}}/recommend?model_name=AnimeRecsysModel
Body (JSON):

{
  "anime_titles": ["Naruto", "Bleach"]
}

https://ithelp.ithome.com.tw/upload/images/20251008/20178626lmRDz3JSpY.png

https://ithelp.ithome.com.tw/upload/images/20251008/20178626xx4DVSUHHs.png


❌ 錯誤案例 1:空輸入

{
  "anime_titles": []
}

https://ithelp.ithome.com.tw/upload/images/20251008/20178626tqDKhLE1CZ.png


❌ 錯誤案例 2:模型名稱錯誤

POST /recommend?model_name=WrongModel

https://ithelp.ithome.com.tw/upload/images/20251008/20178626GiFnGwnXyF.png


✅ 成功案例 2:記錄 API

URL: {{base_url}}/log-ab-event
Body (JSON):

{
  "user_id": "u123",
  "model_version": 8,
  "recommended_title": "Steins;Gate",
  "clicked": true
}

https://ithelp.ithome.com.tw/upload/images/20251008/201786268tANkxNmMh.png


❌ 錯誤案例 3:少欄位

{
  "user_id": "u123"
}

https://ithelp.ithome.com.tw/upload/images/20251008/20178626HrQct3s8MP.png


🧭 Postman 測試清單建議

測試名稱 方法 URL 狀況 預期
推薦成功 POST /recommend 正常輸入 HTTP 200
模型不存在 POST /recommend?model=Wrong 錯誤模型名 HTTP 404
空輸入 POST /recommend 空清單 HTTP 400
紀錄成功 POST /log-ab-event 正確輸入 寫入 CSV
缺欄位 POST /log-ab-event 少欄位 HTTP 422

🧩 錯誤回傳格式範例(統一化)

所有錯誤現在都會以 FastAPI 預設結構回傳:

{
  "detail": "anime_titles cannot be empty."
}

或複雜格式:

{
  "detail": [
    {
      "loc": ["body", "model_version"],
      "msg": "field required",
      "type": "value_error.missing"
    }
  ]
}

這讓前端(Streamlit 或 Web App)能輕鬆解析錯誤狀態。


💡 延伸思考:自訂錯誤中介層 (Middleware)

若未來要在企業環境使用,可以加上:

  • @app.exception_handler(Exception)

  • 自訂統一格式,例如:

    {"status":"error","message":"Internal Server Error","code":500}
    
  • 並透過 Loguru 或 Sentry 收集 log。


✅ 重點總結

  • /recommend/log-ab-event 已具備完整錯誤處理。
  • 使用 Postman 可模擬前端操作、檢查錯誤情境。
  • 錯誤格式統一,方便整合 UI 或監控。
  • FastAPI API 現在足夠穩定,能部署到雲端或後續供 Streamlit使用。

🚀 下一步 (Day 25 預告)

Day 25 我們將進入最後一篇 FastAPI 部分:
建立第二個模型並實作 A/B 測試分流機制
透過時間戳記的不同,可以看到不同模型的推薦內容。


上一篇
Day 23 – 建立記錄 API /log-ab-event
下一篇
Day 25 – 建立第二個模型並實作 A/B 測試分流機制
系列文
從0開始的MLFLOW應用搭建29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言