Skip to content

高可用調度:輪換、固定帳號、粘性會話與失敗重試

你把 Antigravity Tools 當成本地 AI 閘道用了一段時間後,基本都會撞到同一個問題:帳號越少越容易 429/401/invalid_grant,帳號越多越容易"哪個帳號在幹活"說不清,快取命中率也會掉。

這節課就把調度這塊講清楚:它到底怎麼選帳號、什麼叫"粘性會話"、什麼時候會強制輪換、以及如何用"固定帳號模式"把調度變成可控的。

學完你能做什麼

  • 看懂 Antigravity Tools 的 3 種調度模式在真實請求裡各做了什麼
  • 知道"會話指紋(session_id)"怎麼生成,以及它怎麼影響粘性調度
  • 在 GUI 裡啟用/關閉"固定帳號模式",並理解它會覆蓋哪些調度邏輯
  • 遇到 429/5xx/invalid_grant 時,知道系統會怎麼標記限流、怎麼重試、什麼時候輪換

你現在的困境

  • Claude Code 或 OpenAI SDK 跑著跑著突然 429,一重試就換帳號,快取命中率下滑
  • 多個客戶端並發跑任務,經常"互相踩掉"對方的帳號狀態
  • 你想排障,但不知道當前請求是哪個帳號在服務
  • 你只想用某個"最穩的帳號",但系統總在輪換

什麼時候用這一招

  • 你需要把"穩定性(少報錯)"和"快取命中(同帳號)"做權衡
  • 你想讓同一條對話盡量復用同一個帳號(減少 Prompt Caching 抖動)
  • 你要做灰度/排障,想把所有請求都固定到一個帳號

🎒 開始前的準備

  1. 至少準備 2 個可用帳號(帳號池越小,輪換空間越小)
  2. 反代服務已啟動(在「API Proxy」頁面能看到 Running 狀態)
  3. 你知道設定檔在哪裡(如果你需要手動改設定)

先把設定系統這課補上

如果你還不熟 gui_config.json 和哪些設定能熱更新,先看 設定全解:AppConfig/ProxyConfig、落盤位置與熱更新語意

核心思路:一次請求會經過哪幾層"調度"

調度不是一個"單獨的開關",而是幾層機制疊在一起:

  1. SessionManager 先給請求打一個會話指紋(session_id)
  2. Handlers 每次重試都會要求 TokenManager 強制輪換attempt > 0
  3. TokenManager 再根據:固定帳號 → 粘性會話 → 60s 視窗 → 輪詢 選出帳號
  4. 遇到 429/5xx 時會記錄限流資訊,後續選帳號會主動跳過限流帳號

什麼是"會話指紋(session_id)"?

會話指紋就是一個"盡量穩定的 Session Key",用來把同一段對話的多次請求綁在同一帳號上。

在 Claude 請求裡,優先級是:

  1. metadata.user_id(客戶端顯式傳入,且非空且不含 "session-" 前綴)
  2. 第一條"足夠長"的 user 訊息做 SHA256 雜湊,然後截斷成 sid-xxxxxxxxxxxxxxxx

對應實作:src-tauri/src/proxy/session_manager.rs(Claude/OpenAI/Gemini 都有各自的提取邏輯)。

小細節:為什麼只看第一條 user 訊息?

源碼裡明確寫了"只雜湊第一條使用者訊息內容,不混入模型名稱或時間戳",目標是讓同一對話的多輪請求盡量生成相同的 session_id,從而提高快取命中率。

TokenManager 選帳號的優先級

TokenManager 的核心入口是:

  • TokenManager::get_token(quota_group, force_rotate, session_id, target_model)

它做的事可以按優先級理解:

  1. 固定帳號模式(Fixed Account):如果在 GUI 中啟用了"固定帳號模式"(執行時設定),且該帳號沒被限流、也沒被配額保護,就直接用它。
  2. 粘性會話(Session Binding):如果有 session_id 且調度模式不是 PerformanceFirst,會優先復用該 session 綁定的帳號。
  3. 60s 全域視窗復用:如果沒傳 session_id(或還沒綁定成功),在非 PerformanceFirst 下會盡量在 60 秒內復用"上一次用過的帳號"。
  4. 輪詢(Round-robin):以上都不適用時,按一個全域自增索引輪詢選擇帳號。

此外還有兩條"隱形規則",很影響體感:

  • 帳號會先排序:ULTRA > PRO > FREE,同 tier 內優先剩餘配額高的帳號。
  • 失敗或限流會被跳過:已嘗試失敗的帳號會進入 attempted 集合;被限流標記的帳號會被跳過。

3 種調度模式到底差在哪

在設定裡你會看到:CacheFirst / Balance / PerformanceFirst

以"後端 TokenManager 真實分支"為準,它們的關鍵差異只有一個:是否啟用粘性會話 + 60s 視窗復用

  • PerformanceFirst:跳過粘性會話與 60s 視窗復用,直接走輪詢(並繼續跳過限流/配額保護帳號)。
  • CacheFirst / Balance:都會啟用粘性會話與 60s 視窗復用。

關於 max_wait_seconds

前端/設定結構裡有 max_wait_seconds,並且 UI 只在 CacheFirst 下允許調整。但目前後端調度邏輯只基於 mode 分支,並沒有讀取 max_wait_seconds

失敗重試與"強制輪換"怎麼聯動

在 OpenAI/Gemini/Claude 的 handler 裡,都會用類似下面的模式處理重試:

  • 第 1 次嘗試:force_rotate = false
  • 第 2 次及以後:force_rotate = trueattempt > 0),TokenManager 會跳過粘性復用,直接找下一個可用帳號

遇到 429/529/503/500 等錯誤時:

  • handler 會呼叫 token_manager.mark_rate_limited(...) 把這個帳號記錄為"限流/過載",後續調度會主動跳過它。
  • OpenAI 相容路徑還會嘗試從錯誤 JSON 裡解析 RetryInfo.retryDelayquotaResetDelay,等待一小段時間再繼續重試。

跟我做:把調度調到"可控"

第 1 步:先確認你真的有"帳號池"

為什麼 調度再高級,池子裡只有 1 個帳號也沒得選。很多"輪換不生效/粘性沒感覺"的根因都是帳號太少。

操作 打開「Accounts」頁面,確認至少有 2 個帳號處於可用狀態(不是 disabled / proxy disabled)。

你應該看到:至少 2 個帳號能正常刷新配額,並且反代啟動後 active_accounts 不為 0。

第 2 步:在 GUI 裡選擇調度模式

為什麼 調度模式決定了"同一段對話"到底是盡量復用同一帳號,還是每次都輪詢。

操作 進入「API Proxy」頁面,找到 "Account Scheduling & Rotation" 卡片,選擇其中一個模式:

  • Balance:推薦預設值。大多數情況下更穩(會話粘性 + 失敗時輪換)。
  • PerformanceFirst:並發高、任務短、你更在意吞吐而不是快取時選它。
  • CacheFirst:如果你希望"對話盡量固定帳號",可以選它(當前後端與 Balance 的行為差異很小)。

如果你要手動改設定,對應片段是:

json
{
  "proxy": {
    "scheduling": {
      "mode": "Balance",
      "max_wait_seconds": 60
    }
  }
}

你應該看到:切換模式後會立即寫入 gui_config.json,反代服務執行時直接生效(無需重啟)。

第 3 步:啟用"固定帳號模式"(把輪換關掉)

為什麼 排障、灰度、或者你想把某個帳號"釘死"給某個客戶端用時,固定帳號模式是最直接的手段。

操作 在同一張卡片裡打開 "Fixed Account Mode",然後在下拉框裡選擇帳號。

別忘了:這個開關只在反代服務 Running 時可用。

你應該看到:後續請求都會優先使用這個帳號;如果它被限流或被配額保護,會回退到輪詢。

固定帳號是執行時設定

固定帳號模式是執行時狀態(透過 GUI 或 API 動態設定),不會持久化到 gui_config.json。重啟反代服務後,固定帳號會恢復為空(回到輪詢模式)。

第 4 步:需要的時候清掉"會話綁定"

為什麼 粘性會話會把 session_id -> account_id 記在記憶體裡。如果你在同一台機器上做不同實驗(比如切換帳號池、切換模式),舊綁定可能會干擾你觀察。

操作 在 "Account Scheduling & Rotation" 卡片右上角點 "Clear bindings"。

你應該看到:舊會話會重新分配帳號(下一個請求會重新綁定)。

第 5 步:用回應標頭確認"到底是哪個帳號在服務"

為什麼 你想驗證調度是否符合預期,最可靠的辦法是拿到服務端返回的"當前帳號識別碼"。

操作 對 OpenAI 相容端點發一個非串流請求,然後觀察回應標頭裡的 X-Account-Email

bash
  # 例子:最小化的 OpenAI Chat Completions 請求
  # 注意:model 必須是你當前設定裡可用/可路由的模型名
curl -i "http://127.0.0.1:8045/v1/chat/completions" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer sk-REPLACE_ME" \
  -d '{
    "model": "gemini-3-pro-high",
    "stream": false,
    "messages": [{"role": "user", "content": "hello"}]
  }'

你應該看到:回應標頭裡出現類似下面的內容(示例):

text
X-Account-Email: [email protected]
X-Mapped-Model: gemini-3-pro-high

檢查點 ✅

  • 你能說清楚 fixed accountsticky sessionround-robin 三個機制誰覆蓋誰
  • 你知道 session_id 是怎麼來的(優先 metadata.user_id,否則雜湊第一條 user 訊息)
  • 你遇到 429/5xx 時能預期:系統會先記錄限流,再換帳號重試
  • 你能用 X-Account-Email 驗證當前請求到底是哪個帳號在服務

踩坑提醒

  1. 帳號池只有 1 個時,不要期待"輪換能救你" 輪換只是"換另一個帳號",池子裡沒第二個帳號時,429/invalid_grant 只會更頻繁暴露。

  2. CacheFirst 不是"永遠等到可用" 當前後端調度邏輯遇到限流會傾向於解綁並切換帳號,而不是長期阻塞等待。

  3. 固定帳號不是絕對強制 如果固定帳號被標記為限流、或被配額保護命中,系統會回退到輪詢。

本課小結

  • 調度鏈路:handler 提取 session_idTokenManager::get_token 選帳號 → 出錯時 attempt > 0 強制輪換
  • 你最常用的兩個開關:調度模式(是否啟用粘性/60s 復用)+ 固定帳號模式(直接指定帳號)
  • 429/5xx 會被記錄成"限流狀態",後續調度會跳過該帳號,直到鎖定時間過期

下一課預告

下一課我們看 模型路由:當你希望對外暴露"穩定的模型集合",以及想做萬用字元/預設策略時,該怎麼設定與排查。


附錄:源碼參考

點擊展開查看源碼位置

更新時間:2026-01-23

功能檔案路徑行號
調度模式與設定結構(StickySessionConfig)src-tauri/src/proxy/sticky_config.rs1-36
會話指紋生成(Claude/OpenAI/Gemini)src-tauri/src/proxy/session_manager.rs1-159
TokenManager:固定帳號模式欄位與初始化src-tauri/src/proxy/token_manager.rs27-50
TokenManager:選帳號核心邏輯(固定帳號/粘性會話/60s 視窗/輪詢/配額保護)src-tauri/src/proxy/token_manager.rs470-940
TokenManager:invalid_grant 自動停用並移出池src-tauri/src/proxy/token_manager.rs868-878
TokenManager:限流記錄與成功清理 APIsrc-tauri/src/proxy/token_manager.rs1087-1147
TokenManager:更新調度設定 / 清理會話綁定 / 固定帳號模式 settersrc-tauri/src/proxy/token_manager.rs1419-1461
ProxyConfig:scheduling 欄位定義與預設值src-tauri/src/proxy/config.rs174-257
反代啟動時同步 scheduling 設定src-tauri/src/commands/proxy.rs70-100
調度相關的 Tauri 命令(get/update/clear bindings/fixed account)src-tauri/src/commands/proxy.rs478-551
OpenAI handler:session_id + 重試時強制輪換src-tauri/src/proxy/handlers/openai.rs160-182
OpenAI handler:429/5xx 記錄限流 + 解析 retry delaysrc-tauri/src/proxy/handlers/openai.rs349-367
Gemini handler:session_id + 重試時強制輪換src-tauri/src/proxy/handlers/gemini.rs62-88
Gemini handler:429/5xx 記錄限流並輪換src-tauri/src/proxy/handlers/gemini.rs279-299
Claude handler:提取 session_id 並傳給 TokenManagersrc-tauri/src/proxy/handlers/claude.rs517-524
429 retry delay 解析(RetryInfo.retryDelay / quotaResetDelay)src-tauri/src/proxy/upstream/retry.rs37-66
限流原因識別與指數退避(RateLimitTracker)src-tauri/src/proxy/rate_limit.rs154-279

關鍵結構體

  • StickySessionConfig:調度模式與設定結構(mode, max_wait_seconds
  • TokenManager:帳號池、會話綁定、固定帳號模式、限流追蹤器
  • SessionManager:從請求中提取 session_id