FDE 面試準備指南(二十一):RKK 實戰——長任務 Agent 的異步分散式架構

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 與依賴解析引擎

Yen

Yen

Yen