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