高可用調度:輪換、固定帳號、粘性會話與失敗重試
你把 Antigravity Tools 當成本地 AI 閘道用了一段時間後,基本都會撞到同一個問題:帳號越少越容易 429/401/invalid_grant,帳號越多越容易"哪個帳號在幹活"說不清,快取命中率也會掉。
這節課就把調度這塊講清楚:它到底怎麼選帳號、什麼叫"粘性會話"、什麼時候會強制輪換、以及如何用"固定帳號模式"把調度變成可控的。
學完你能做什麼
- 看懂 Antigravity Tools 的 3 種調度模式在真實請求裡各做了什麼
- 知道"會話指紋(session_id)"怎麼生成,以及它怎麼影響粘性調度
- 在 GUI 裡啟用/關閉"固定帳號模式",並理解它會覆蓋哪些調度邏輯
- 遇到 429/5xx/invalid_grant 時,知道系統會怎麼標記限流、怎麼重試、什麼時候輪換
你現在的困境
- Claude Code 或 OpenAI SDK 跑著跑著突然 429,一重試就換帳號,快取命中率下滑
- 多個客戶端並發跑任務,經常"互相踩掉"對方的帳號狀態
- 你想排障,但不知道當前請求是哪個帳號在服務
- 你只想用某個"最穩的帳號",但系統總在輪換
什麼時候用這一招
- 你需要把"穩定性(少報錯)"和"快取命中(同帳號)"做權衡
- 你想讓同一條對話盡量復用同一個帳號(減少 Prompt Caching 抖動)
- 你要做灰度/排障,想把所有請求都固定到一個帳號
🎒 開始前的準備
- 至少準備 2 個可用帳號(帳號池越小,輪換空間越小)
- 反代服務已啟動(在「API Proxy」頁面能看到 Running 狀態)
- 你知道設定檔在哪裡(如果你需要手動改設定)
先把設定系統這課補上
如果你還不熟 gui_config.json 和哪些設定能熱更新,先看 設定全解:AppConfig/ProxyConfig、落盤位置與熱更新語意。
核心思路:一次請求會經過哪幾層"調度"
調度不是一個"單獨的開關",而是幾層機制疊在一起:
- SessionManager 先給請求打一個會話指紋(session_id)
- Handlers 每次重試都會要求 TokenManager 強制輪換(
attempt > 0) - TokenManager 再根據:固定帳號 → 粘性會話 → 60s 視窗 → 輪詢 選出帳號
- 遇到 429/5xx 時會記錄限流資訊,後續選帳號會主動跳過限流帳號
什麼是"會話指紋(session_id)"?
會話指紋就是一個"盡量穩定的 Session Key",用來把同一段對話的多次請求綁在同一帳號上。
在 Claude 請求裡,優先級是:
metadata.user_id(客戶端顯式傳入,且非空且不含"session-"前綴)- 第一條"足夠長"的 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)
它做的事可以按優先級理解:
- 固定帳號模式(Fixed Account):如果在 GUI 中啟用了"固定帳號模式"(執行時設定),且該帳號沒被限流、也沒被配額保護,就直接用它。
- 粘性會話(Session Binding):如果有
session_id且調度模式不是PerformanceFirst,會優先復用該 session 綁定的帳號。 - 60s 全域視窗復用:如果沒傳
session_id(或還沒綁定成功),在非PerformanceFirst下會盡量在 60 秒內復用"上一次用過的帳號"。 - 輪詢(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 = true(attempt > 0),TokenManager 會跳過粘性復用,直接找下一個可用帳號
遇到 429/529/503/500 等錯誤時:
- handler 會呼叫
token_manager.mark_rate_limited(...)把這個帳號記錄為"限流/過載",後續調度會主動跳過它。 - OpenAI 相容路徑還會嘗試從錯誤 JSON 裡解析
RetryInfo.retryDelay或quotaResetDelay,等待一小段時間再繼續重試。
跟我做:把調度調到"可控"
第 1 步:先確認你真的有"帳號池"
為什麼 調度再高級,池子裡只有 1 個帳號也沒得選。很多"輪換不生效/粘性沒感覺"的根因都是帳號太少。
操作 打開「Accounts」頁面,確認至少有 2 個帳號處於可用狀態(不是 disabled / proxy disabled)。
你應該看到:至少 2 個帳號能正常刷新配額,並且反代啟動後 active_accounts 不為 0。
第 2 步:在 GUI 裡選擇調度模式
為什麼 調度模式決定了"同一段對話"到底是盡量復用同一帳號,還是每次都輪詢。
操作 進入「API Proxy」頁面,找到 "Account Scheduling & Rotation" 卡片,選擇其中一個模式:
Balance:推薦預設值。大多數情況下更穩(會話粘性 + 失敗時輪換)。PerformanceFirst:並發高、任務短、你更在意吞吐而不是快取時選它。CacheFirst:如果你希望"對話盡量固定帳號",可以選它(當前後端與Balance的行為差異很小)。
如果你要手動改設定,對應片段是:
{
"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。
# 例子:最小化的 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"}]
}'你應該看到:回應標頭裡出現類似下面的內容(示例):
X-Account-Email: example@gmail.com
X-Mapped-Model: gemini-3-pro-high檢查點 ✅
- 你能說清楚
fixed account、sticky session、round-robin三個機制誰覆蓋誰 - 你知道
session_id是怎麼來的(優先metadata.user_id,否則雜湊第一條 user 訊息) - 你遇到 429/5xx 時能預期:系統會先記錄限流,再換帳號重試
- 你能用
X-Account-Email驗證當前請求到底是哪個帳號在服務
踩坑提醒
帳號池只有 1 個時,不要期待"輪換能救你" 輪換只是"換另一個帳號",池子裡沒第二個帳號時,429/invalid_grant 只會更頻繁暴露。
CacheFirst不是"永遠等到可用" 當前後端調度邏輯遇到限流會傾向於解綁並切換帳號,而不是長期阻塞等待。固定帳號不是絕對強制 如果固定帳號被標記為限流、或被配額保護命中,系統會回退到輪詢。
本課小結
- 調度鏈路:handler 提取
session_id→TokenManager::get_token選帳號 → 出錯時attempt > 0強制輪換 - 你最常用的兩個開關:調度模式(是否啟用粘性/60s 復用)+ 固定帳號模式(直接指定帳號)
- 429/5xx 會被記錄成"限流狀態",後續調度會跳過該帳號,直到鎖定時間過期
下一課預告
下一課我們看 模型路由:當你希望對外暴露"穩定的模型集合",以及想做萬用字元/預設策略時,該怎麼設定與排查。
附錄:源碼參考
點擊展開查看源碼位置
更新時間:2026-01-23
| 功能 | 檔案路徑 | 行號 |
|---|---|---|
| 調度模式與設定結構(StickySessionConfig) | src-tauri/src/proxy/sticky_config.rs | 1-36 |
| 會話指紋生成(Claude/OpenAI/Gemini) | src-tauri/src/proxy/session_manager.rs | 1-159 |
| TokenManager:固定帳號模式欄位與初始化 | src-tauri/src/proxy/token_manager.rs | 27-50 |
| TokenManager:選帳號核心邏輯(固定帳號/粘性會話/60s 視窗/輪詢/配額保護) | src-tauri/src/proxy/token_manager.rs | 470-940 |
| TokenManager:invalid_grant 自動停用並移出池 | src-tauri/src/proxy/token_manager.rs | 868-878 |
| TokenManager:限流記錄與成功清理 API | src-tauri/src/proxy/token_manager.rs | 1087-1147 |
| TokenManager:更新調度設定 / 清理會話綁定 / 固定帳號模式 setter | src-tauri/src/proxy/token_manager.rs | 1419-1461 |
| ProxyConfig:scheduling 欄位定義與預設值 | src-tauri/src/proxy/config.rs | 174-257 |
| 反代啟動時同步 scheduling 設定 | src-tauri/src/commands/proxy.rs | 70-100 |
| 調度相關的 Tauri 命令(get/update/clear bindings/fixed account) | src-tauri/src/commands/proxy.rs | 478-551 |
| OpenAI handler:session_id + 重試時強制輪換 | src-tauri/src/proxy/handlers/openai.rs | 160-182 |
| OpenAI handler:429/5xx 記錄限流 + 解析 retry delay | src-tauri/src/proxy/handlers/openai.rs | 349-367 |
| Gemini handler:session_id + 重試時強制輪換 | src-tauri/src/proxy/handlers/gemini.rs | 62-88 |
| Gemini handler:429/5xx 記錄限流並輪換 | src-tauri/src/proxy/handlers/gemini.rs | 279-299 |
| Claude handler:提取 session_id 並傳給 TokenManager | src-tauri/src/proxy/handlers/claude.rs | 517-524 |
| 429 retry delay 解析(RetryInfo.retryDelay / quotaResetDelay) | src-tauri/src/proxy/upstream/retry.rs | 37-66 |
| 限流原因識別與指數退避(RateLimitTracker) | src-tauri/src/proxy/rate_limit.rs | 154-279 |
關鍵結構體:
StickySessionConfig:調度模式與設定結構(mode,max_wait_seconds)TokenManager:帳號池、會話綁定、固定帳號模式、限流追蹤器SessionManager:從請求中提取session_id