在 Day 18 完成 X-Request-ID 全域追蹤與結構化 JSON 日誌等可觀測性建設後,PrescriptionVLM 後端服務已能掌握每一次 API 請求的細部耗時與運作狀態。
然而,在實際部署與長輩使用情境中,我們發現了兩個容易導致成本增加的問題:
今天我們將為 PrescriptionVLM 系統導入 Redis 記憶體快取與限流機制。完成項目包含:
在結合視覺大語言模型(VLM)的系統架構中,導入快取與限流有以下好處:
requirements.txt)請開啟專案根目錄的 requirements.txt,加入 Redis 與 Flask-Limiter 相關套件:
redis==5.0.3
Flask-Limiter==3.6.0
app.py)開啟 app.py,導入 Redis 與 Flask-Limiter,並寫入圖片快取機制。
1. 匯入必要模組與初始化 Redis / Limiter
在 app.py 頂部加入:
import hashlib
import redis
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
# 初始化 Redis 連線(由環境變數讀取 Redis Host)
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
redis_client = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=0, decode_responses=True)
# 初始化 Flask-Limiter 限流器
limiter = Limiter(
key_func=get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"],
storage_uri=f"redis://{REDIS_HOST}:{REDIS_PORT}"
)
def compute_image_hash(image_bytes: bytes) -> str:
"""計算圖片二進位內容之 SHA-256 雜湊值"""
return hashlib.sha256(image_bytes).hexdigest()
2. 在 /analyze-prescription API 導入快取與限流裝飾器
修改 /analyze-prescription 路由,加上每分鐘限制 10 次的限流規則,並在呼叫 Gemini 前先查詢 Redis 快取:
@app.route("/analyze-prescription", methods=["POST"])
@limiter.limit("10 per minute") # 限制單一 IP 每分鐘最多請求 10 次
def analyze_prescription():
req_id = getattr(g, "request_id", "N/A")
logger.info(f"收到藥單解析請求 [ReqID: {req_id}]")
if "image" not in request.files:
return jsonify({"error": "未提供圖片檔案"}), 400
image_file = request.files["image"]
image_bytes = image_file.read()
image_file.seek(0) # 重設檔案指標
# ── 階段 1: 計算圖片 SHA-256 雜湊值 ──
img_hash = compute_image_hash(image_bytes)
cache_key = f"prescription_cache:{img_hash}"
# ── 階段 2: 檢查 Redis 快取命中 (Cache Hit) ──
cached_data = redis_client.get(cache_key)
if cached_data:
logger.info(f"⚡ 命中 Redis 圖片快取![Hash: {img_hash[:10]}...]")
result_data = json.loads(cached_data)
# 快取命中時依然推播 LINE 通知(確保使用者獲得即時反饋)
send_line_notification(
summary=result_data.get("spoken_summary", ""),
medicines_count=len(result_data.get("medicines", [])),
safety_warnings=result_data.get("safety_warnings", []),
audio_url=""
)
return jsonify({
"status": "success",
"cached": True,
"request_id": req_id,
"data": result_data
}), 200
# ── 階段 3: 未命中快取 (Cache Miss),呼叫 Gemini VLM 模型 ──
logger.info(f"快取未命中,開始呼叫 Gemini VLM 模型...")
vlm_start = time.time()
# ... (執行原本 Gemini 解析與 Pydantic 驗證邏輯) ...
vlm_latency = round((time.time() - vlm_start) * 1000, 2)
# ── 階段 4: 將解析成果寫入 Redis 快取 (設定生存時間 TTL 為 24 小時) ──
redis_client.setex(cache_key, 86400, json.dumps(result_data, ensure_ascii=False))
logger.info(f"已將解析成果寫入 Redis 快取,TTL: 24 小時")
# 推播 LINE 訊息
send_line_notification(
summary=result_data.get("spoken_summary", ""),
medicines_count=len(result_data.get("medicines", [])),
safety_warnings=result_data.get("safety_warnings", []),
audio_url=""
)
return jsonify({
"status": "success",
"cached": False,
"request_id": req_id,
"metrics": {"vlm_latency_ms": vlm_latency},
"data": result_data
}), 200
docker-compose.yml)為了確保 Flask API 與 Redis 能一鍵編排啟動,請在專案根目錄建立 docker-compose.yml:
version: '3.8'
services:
redis:
image: redis:7.2-alpine
container_name: prescription_redis
ports:
- "6379:6379"
restart: always
web:
build: .
container_name: prescription_service
ports:
- "5000:5000"
env_file:
- .env
environment:
- REDIS_HOST=redis
- REDIS_PORT=6379
depends_on:
- redis
restart: always
1. 透過 Docker Compose 一鍵啟動所有服務
在 Terminal 執行:
docker-compose up --build -d
2. 第一次上傳測試 (Cache Miss - 快取未命中)
在 Terminal 執行:
curl -i -X POST http://127.0.0.1:5000/analyze-prescription \
-F "image=@test_rx.jpg"
"cached": false,耗時約 2~3 秒,Terminal 印出 快取未命中,開始呼叫 Gemini VLM。3. 第二次上傳相同圖片 (Cache Hit - 快取命中)
再次執行完全相同的命令:
curl -i -X POST http://127.0.0.1:5000/analyze-prescription \
-F "image=@test_rx.jpg"
"cached": true,回應時間在 10ms 內完成。Terminal 印出 ⚡ 命中 Redis 圖片快取!。測試通過後,將 Day 19 的修改提交至 GitHub:
git add .
git commit -m "保留雙引號 改填寫自己要記錄的標記 ex.鐵人賽第十九天"
git push
今天我們透過 Redis SHA-256 雜湊快取 與 Flask-Limiter 限流機制,將重複圖片的解析時間從 3 秒縮短至 10ms,同時大幅減少 Gemini API 的呼叫次數,節省成本並防止過度的流量。
明天(Day 20),我們將進入系統的架構維護重點,實作 SQLite / PostgreSQL 資料庫自動備份與備援機制,確保長輩的用藥歷史紀錄永久安全留存!