在 AI 中转站或模型网关中,我们通常会同时接入多个上游渠道,并通过优先级、权重、分组、模型映射等策略做调度。但在开启 NewAPI 的“渠道亲和性”之后,你可能会遇到一个看似反直觉的问题:

某个渠道已经失败,甚至已经被关闭了,但请求仍然不断命中旧渠道;新启用的高优先级、高权重渠道没有马上生效。

这并不一定是调度系统失效,而是“渠道亲和性”本身的设计结果:它会优先复用上一次成功的渠道,从而提升同一会话、同一缓存键、同一请求来源的稳定性。本文会解释这个机制的工作方式,并给出一套脱敏后的推荐配置,帮助你在稳定性和故障切换之间取得平衡。


1. 什么是渠道亲和性

渠道亲和性可以理解为一种“会话级渠道记忆”。

当某个请求第一次成功命中某个渠道后,NewAPI 会根据请求中的某个标识生成缓存键,例如:

  • OpenAI Responses 的 prompt_cache_key
  • Anthropic Messages 的 metadata.user_id
  • OpenAI Chat Completions 的 user
  • 请求头里的 X-Request-Id
  • 平台内部的 token_id

之后,只要请求再次携带相同的标识,并且模型、分组、规则等条件匹配,NewAPI 就会优先使用上一次成功的渠道,而不是重新按照优先级和权重选择渠道。

这可以让同一个用户、同一个会话、同一个 Agent 任务更稳定地落到同一个上游,减少多渠道之间响应差异带来的问题。


2. 为什么高优先级渠道没有马上生效

开启渠道亲和性后,请求选择渠道的大致顺序是:

  1. 判断当前请求是否命中渠道亲和性规则。
  2. 如果命中规则,生成亲和缓存键。
  3. 如果缓存键已有对应渠道,则优先尝试该渠道。
  4. 如果该渠道仍然可用,就直接使用它。
  5. 如果没有可用亲和渠道,才进入普通的优先级、权重调度。
  6. 请求成功后,再把成功渠道写回亲和缓存。

所以,当你新增一个高权重、高优先级渠道时,如果某些请求已经有旧的亲和缓存,它们不会马上切换到新渠道。

这是预期行为。

如果希望新调度策略立刻生效,需要清理渠道亲和性缓存,或者降低 TTL,让旧缓存更快过期。


3. 失败渠道为什么还会“粘住”

常见原因有三个。

3.1 TTL 过长

如果默认 TTL 是 3600 秒,那么旧渠道最多可能被保留 1 小时。

如果你的渠道经常调整,建议把 TTL 改成:

300 秒

或:

600 秒

这样既保留一定亲和性,又不会让旧渠道长期影响调度。

3.2 skip_retry_on_failure 配置不合理

渠道亲和性规则里有一个关键字段:

"skip_retry_on_failure": false

如果设置成 true,命中的亲和渠道失败后,系统可能不会按你预期继续尝试其他渠道并更新亲和缓存。

如果你的目标是:

亲和渠道失败后,自动切换到其他可用渠道,并用新的成功渠道覆盖旧缓存

那么建议设置为:

"skip_retry_on_failure": false

3.3 禁用渠道不会主动全局删除所有相关缓存

当你关闭某个渠道时,NewAPI 通常不会立即扫描并删除所有指向该渠道的亲和缓存。

更常见的处理方式是:

  • 下次请求命中该缓存;
  • 系统发现缓存中的渠道不可用;
  • 当前请求重新走普通调度;
  • 成功后写入新的亲和渠道。

如果你希望关闭渠道后立即生效,最直接的方法是:

后台手动清空渠道亲和性缓存

4. 推荐后台配置

如果你希望渠道亲和性更适合生产环境中的故障切换,可以参考以下策略:

启用:开启
成功后切换亲和:开启
默认 TTL:300 或 600
最大条目数:按平台规模设置,例如 100000
渠道关闭后保留亲和:关闭

推荐理解如下:

配置项 推荐值 说明
启用 开启 允许同一会话复用成功渠道
成功后切换亲和 开启 失败后如果新渠道成功,可更新缓存
默认 TTL 300 / 600 避免旧渠道粘太久
最大条目数 100000 根据业务规模调整
渠道关闭后保留亲和 关闭 禁用渠道后尽快回到普通调度

5. 推荐规则 JSON 示例

以下配置已脱敏,模型名、请求头和规则名称均为示例。可以根据自己的平台模型命名进行调整。

[
  {
    "name": "openai-responses-affinity",
    "model_regex": [
      "^gpt-.*$",
      "^o[0-9].*$"
    ],
    "path_regex": [
      "^/v1/responses$",
      "^/responses$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "prompt_cache_key"
      },
      {
        "type": "request_header",
        "key": "X-Session-Id"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-Request-Id",
            "X-Session-Id"
          ]
        }
      ]
    }
  },
  {
    "name": "openai-chat-completions-affinity",
    "model_regex": [
      "^gpt-.*$",
      "^o[0-9].*$",
      "^example-openai-.*$"
    ],
    "path_regex": [
      "^/v1/chat/completions$",
      "^/chat/completions$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "user"
      },
      {
        "type": "request_header",
        "key": "X-Request-Id"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-Request-Id"
          ]
        }
      ]
    }
  },
  {
    "name": "anthropic-messages-affinity",
    "model_regex": [
      "^claude-.*$",
      "^example-anthropic-.*$"
    ],
    "path_regex": [
      "^/v1/messages$",
      "^/messages$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "metadata.user_id"
      },
      {
        "type": "gjson",
        "path": "container"
      },
      {
        "type": "request_header",
        "key": "X-App"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-App",
            "Anthropic-Version",
            "Anthropic-Beta"
          ]
        }
      ]
    }
  },
  {
    "name": "deepseek-anthropic-affinity",
    "model_regex": [
      "^deepseek-.*$",
      "^example-deepseek-.*$"
    ],
    "path_regex": [
      "^/v1/messages$",
      "^/messages$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "metadata.user_id"
      },
      {
        "type": "gjson",
        "path": "container"
      },
      {
        "type": "request_header",
        "key": "X-App"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-App",
            "Anthropic-Version",
            "Anthropic-Beta"
          ]
        }
      ]
    }
  },
  {
    "name": "deepseek-openai-chat-affinity",
    "model_regex": [
      "^deepseek-.*$",
      "^example-deepseek-.*$"
    ],
    "path_regex": [
      "^/v1/chat/completions$",
      "^/chat/completions$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "user"
      },
      {
        "type": "request_header",
        "key": "X-Request-Id"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-Request-Id"
          ]
        }
      ]
    }
  },
  {
    "name": "gemini-generate-content-affinity",
    "model_regex": [
      "^gemini-.*$",
      "^models/gemini-.*$"
    ],
    "path_regex": [
      "^/v1beta/models/.+:generateContent$",
      "^/v1beta/models/.+:streamGenerateContent$",
      "^/v1/models/.+:generateContent$",
      "^/v1/models/.+:streamGenerateContent$"
    ],
    "key_sources": [
      {
        "type": "gjson",
        "path": "user"
      },
      {
        "type": "request_header",
        "key": "X-Request-Id"
      },
      {
        "type": "context_int",
        "key": "token_id"
      }
    ],
    "value_regex": "",
    "ttl_seconds": 300,
    "include_using_group": true,
    "include_model_name": true,
    "include_rule_name": true,
    "skip_retry_on_failure": false,
    "param_override_template": {
      "operations": [
        {
          "keep_origin": true,
          "mode": "pass_headers",
          "value": [
            "User-Agent",
            "X-Request-Id"
          ]
        }
      ]
    }
  }
]

6. 调整渠道后的操作建议

当你执行以下操作时:

  • 关闭某个渠道;
  • 修改渠道优先级;
  • 修改渠道权重;
  • 新增高优先级渠道;
  • 删除故障渠道;
  • 修改模型映射;
  • 修改分组可用渠道;

建议同步执行:

清空渠道亲和性缓存

否则旧请求可能仍会继续命中旧的亲和渠道,直到 TTL 到期或请求触发重新选择。


7. 推荐排查方式

如果你怀疑某个请求仍然命中旧渠道,可以查看请求日志里的 channel_affinity 信息。

重点关注这些字段:

{
  "rule_name": "openai-responses-affinity",
  "channel_id": 1001,
  "model": "example-model",
  "request_path": "/v1/responses",
  "key_source": "gjson",
  "key_path": "prompt_cache_key",
  "using_group": "default"
}

如果日志里持续出现同一个 key_fp 和同一个 channel_id,说明它确实被亲和缓存固定到了该渠道。

如果你已经调整过优先级和权重,但请求仍然命中旧渠道,优先检查:

  1. 该规则的 TTL 是否太长。
  2. 是否清空过亲和缓存。
  3. skip_retry_on_failure 是否为 false
  4. 旧渠道是否仍然处于可用状态。
  5. 该请求是否一直携带相同的亲和 key。

8. 总结

渠道亲和性不是普通的负载均衡规则,而是一层“成功渠道记忆”。

它的优势是稳定:

  • 同一会话更容易落到同一渠道;
  • Agent、长上下文、缓存类请求更稳定;
  • 多渠道响应差异更小。

它的代价是调度变化不会立刻生效:

  • 高优先级渠道不一定马上接管;
  • 老渠道可能在 TTL 内继续被命中;
  • 禁用渠道后最好主动清空缓存;
  • 失败切换依赖 skip_retry_on_failure 和重试策略。

生产环境推荐采用:

短 TTL + 成功后切换亲和 + 失败允许重试 + 调整渠道后清空缓存

这样既能保留渠道亲和性的稳定性,又能在渠道失败、关闭或调度策略变化时尽快切换到新的可用渠道。

标签: NewAPI, AI网关

添加新评论