HTTP 請求的超時通常是 30~60 秒。
你的 Agent 需要 30~60 分鐘。
這不只是「把 timeout 調大」的問題——
這是一個需要重新設計請求/回應模型的架構問題。
面試情境
面試官: 「客戶想打造一個自動化市場競品分析 Agent。當用戶輸入指令,Agent 需要搜尋 50 個網頁、調用大數據分析工具、撰寫 20 頁報告。整個工作流需要 30 分鐘到 1 小時。你如何設計後端分散式架構?如果執行到第 25 分鐘時某個節點崩潰,如何確保不從頭來過?」
一、核心問題:同步 HTTP 模型的三個致命限制
同步模型(不可行):
用戶發出請求
│
▼
HTTP Request ───────────────────────────────── 等待 60 分鐘?
│
HTTP Response ← 60 分鐘後 ← 如果連線斷了呢?
如果手機鎖屏了呢?
如果用戶換了瀏覽器分頁呢?
三個根本限制:
限制 1:HTTP 超時
└─ 大多數 Load Balancer、API Gateway 的 timeout 是 30~300 秒
Agent 跑 60 分鐘,連線早就被中斷
限制 2:無法容錯
└─ 如果 Worker 在第 25 分鐘崩潰
用戶必須從頭開始,浪費 25 分鐘的 Token 成本
限制 3:無法水平擴展
└─ 一個請求佔用一個 Thread 60 分鐘
100 個並發用戶 → 需要 100 個長期佔用的 Thread
→ 資源利用率極低
二、解決方案:解耦架構(Decoupled Architecture)
核心設計原則:
請求接收 和 任務執行 完全解耦
用戶 和 任務結果 透過 異步機制溝通
┌──────────────────────────────────────────────────────────────┐
│ 完整系統架構 │
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 用戶端(Frontend) │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ 1. 提交任務 │ │ │
│ │ │ POST /tasks → { task_id: "task_abc123" } │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ │ ┌──────────────────────────────────────────────────┐ │ │
│ │ │ 2. 輪詢進度 / WebSocket 訂閱 │ │ │
│ │ │ GET /tasks/task_abc123/status │ │ │
│ │ │ → { status: "running", progress: 45% } │ │ │
│ │ └──────────────────────────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ ↑ │
│ ▼ │ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ API Gateway Layer │ │
│ │ ├── 接收請求 → 立即回傳 task_id(< 100ms) │ │
│ │ └── 將任務寫入 Cloud Pub/Sub │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Task Queue(Cloud Pub/Sub) │ │
│ │ ├── 持久化的消息佇列 │ │
│ │ ├── 保證至少傳遞一次(at-least-once delivery) │ │
│ │ └── Worker 崩潰後,消息重新分配 │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ Agent Worker Pool(GKE) │ │
│ │ │ │
│ │ Worker 1 Worker 2 Worker 3 ... Worker N │ │
│ │ (每個 Worker 處理一個長任務) │ │
│ └────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ State Store(Checkpoint) │ │
│ │ ┌─────────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Redis(熱狀態) │ │ Firestore(完整歷史) │ │ │
│ │ │ 當前執行步驟 │ │ 任務定義 + 所有 CP │ │ │
│ │ └─────────────────────┘ └─────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
三、Checkpoint 斷點續傳設計
任務執行流程(含 Checkpoint):
任務開始
│
▼
[CP 0] 寫入 Firestore:任務定義、輸入參數、開始時間
│
▼
Step 1:爬取第 1~10 個網頁
│
▼
[CP 1] 寫入:{ step: "web_crawl_1-10", results: [...], status: "done" }
│
▼
Step 2:爬取第 11~30 個網頁
│
▼
[CP 2] 寫入:{ step: "web_crawl_11-30", results: [...], status: "done" }
│
▼
Step 3:爬取第 31~50 個網頁
│ ← Worker 在這裡崩潰!
X (CRASH at minute 25)
故障恢復流程:
Cloud Tasks / Redis 偵測到 Worker 心跳停止(30 秒內)
│
▼
將 task_abc123 重新放回 Queue
│
▼
新的 Worker 領取任務
│
▼
讀取 Firestore:最後成功的 Checkpoint = CP 2
│
▼
從 Step 3(爬取第 31~50 個網頁)繼續執行
│
▼
跳過已完成的 Step 1 和 Step 2(不重複執行,不重複消耗 Token)
Checkpoint 的粒度設計:
粒度太粗(每個大階段存一次):
├── 優點:儲存頻率低,開銷小
└── 缺點:崩潰後要從上個大階段重新跑,可能浪費很多 Token
粒度太細(每個 LLM 呼叫都存):
├── 優點:幾乎零重複執行
└── 缺點:Firestore 寫入成本高,可能影響執行速度
推薦粒度:
└── 每個「有意義的獨立步驟」完成後存一次
例:每爬完一批網頁、每個 Agent 子任務完成、每次 LLM 生成重要中間結果
四、進度回報的三種設計
方案一:Polling(輪詢)
用戶每 N 秒問一次「任務完成了嗎?」
優點:實作簡單,前後端都容易
缺點:有延遲(用戶最多等一個 polling interval),
輪詢請求對 API 有額外負載
適用:進度不需要即時更新,或非瀏覽器環境(CLI、排程系統)
方案二:WebSocket
前端建立 WebSocket 連線,後端主動推送進度更新
優點:即時(用戶立刻看到進度變化)
缺點:需要維護持久連線,有 connection limit
適用:用戶需要即時進度回饋的 Web App
方案三:Server-Sent Events(SSE)
前端建立 SSE 連線,後端單向串流進度事件
優點:比 WebSocket 輕量,只需要單向推送
缺點:只能後端 → 前端,不能雙向
適用:進度顯示為主,不需要用戶中途干預的場景
┌──────────────────────────────────────────────────┐
│ 選型建議: │
│ └── 一般企業 Agent:Polling(簡單,夠用) │
│ └── 用戶體驗重要:SSE(輕量即時) │
│ └── 需要雙向互動(如 HITL 確認):WebSocket │
└──────────────────────────────────────────────────┘
五、Worker 的彈性擴縮設計
GKE 的彈性擴縮架構:
正常時段(低流量):
┌────────────────────────────────────────┐
│ Pub/Sub Queue: 5 個等待中的任務 │
│ Worker Pod: 5 個(各處理 1 個任務) │
└────────────────────────────────────────┘
尖峰時段(高流量,例如用戶同時提交大量分析任務):
┌────────────────────────────────────────────────────────────┐
│ Pub/Sub Queue: 200 個等待中的任務 │
│ │ │
│ ▼ │
│ Horizontal Pod Autoscaler 偵測到 Queue 深度增加 │
│ │ │
│ ▼ │
│ 自動擴展 Worker Pod:5 → 50 個 │
│ (GKE Autopilot 自動管理節點資源) │
└────────────────────────────────────────────────────────────┘
擴縮觸發指標(HPA Custom Metrics):
└── Pub/Sub 未讀消息數 > 10 → 擴展更多 Worker
└── Pub/Sub 未讀消息數 < 2 → 縮減 Worker(避免資源浪費)
六、Edge Case 設計:哪些情況需要特別處理
情況 1:任務永遠不結束
└─ 加硬性 TTL:任務最長執行時間(例如 2 小時)
超過 TTL → 強制終止,回傳 partial result 或 timeout error
情況 2:同一個任務被多個 Worker 同時領取(at-least-once 的副作用)
└─ 使用 Redis 的 Distributed Lock(SETNX)
Worker 開始前嘗試 Lock,Lock 成功才執行
避免任務被重複執行
情況 3:Gemini API 限流(429 Too Many Requests)
└─ Worker 內部加 Exponential Backoff Retry
重試 3 次後,將任務放回 Queue(延遲 5 分鐘後再試)
情況 4:任務結果太大(20 頁報告 = 大量 Token)
└─ 報告存入 Cloud Storage(GCS),Firestore 只存 GCS 的 URI
用戶透過 Signed URL 直接從 GCS 下載結果
七、完整的任務狀態機
任務狀態流轉:
PENDING(等待中)
│ Worker 領取任務
▼
RUNNING(執行中)
│ │
│ │ Worker 崩潰 / 主動放棄
│ ▼
│ FAILED(失敗)
│ │ 重試次數 < 3
│ └── → 回到 PENDING
│
│ 全部步驟完成
▼
COMPLETED(完成)
│
▼
用戶可下載結果(Signed URL)
特殊狀態:
CANCELLED → 用戶主動取消,釋放 Worker
TIMEOUT → 超過最長執行時間,回傳 partial result
八、面試答題要點
「長任務不能用同步 HTTP 模型,因為 30~60 分鐘遠超過任何 Load Balancer 的 timeout,且無法容錯。
我的設計是解耦架構:API Gateway 收到請求後立即寫入 Cloud Pub/Sub 並回傳 task_id(< 100ms)。GKE 上的 Worker Pool 異步消費任務,可以根據 Queue 深度自動水平擴縮。
容錯靠 Checkpoint:每完成一個有意義的步驟(爬完一批網頁、Agent 子任務完成),就把當前狀態寫入 Redis(熱狀態)和 Firestore(持久化)。Worker 崩潰後,排程器重新分配任務,新 Worker 從最後的 Checkpoint 繼續,不重複消耗 Token。
進度回報用 Polling 或 SSE(依用戶體驗需求選擇)。結果存 GCS,用戶透過 Signed URL 下載。
邊界情況:用 Redis Distributed Lock 避免重複執行;TTL 硬上限防止任務永遠不結束;429 限流用 Exponential Backoff + 任務重排。」
系列導覽:
← (二十)RKK 實戰:間接 Prompt Injection 與 Dual-LLM 防禦架構
→ (二十二)RKK 實戰:動態並行 Tool-Calling 與依賴解析引擎
