1. 单上游挂掉时请求为什么还在原地打转
多上游 API 接入做到一定规模后,最让人头疼的不是「连不上」,而是「连得上但一直在错的地方重试」。我见过不少团队把 New API 部署起来,渠道配了五六条,优先级权重填得整整齐齐,结果某条上游开始间歇性 429 或者 TTFB 从 600ms 飙到 8 秒,前端成功率面板还显示 99%,实际用户已经在骂了。原因不复杂:New API 原生的渠道选择逻辑是「分组 → 模型 → 优先级 → 权重 → 发请求 → 失败重试 → 次优先级」,优先级和权重都是静态参数,管理员主观上更信任谁就填谁,它并不知道此刻哪条上游真的健康。
这个场景下你要解决的核心问题,是让路由模块从「静态排序」变成「动态打分」,并且把健康检查、失败重试、故障切换三件事拆开配置。本文围绕 newapi 路由模块的高可用实现展开,面向多上游 API 接入场景,交付可复制的路由配置片段和故障注入验证步骤,目标很明确:单上游异常时请求自动转移,服务不中断。适合已经在跑 New API 或类似聚合网关、准备把可用性从「能用」提到「稳」的后端和运维同学。下面所有配置都以 TaoToken 统一通道作为上游接入层来演示,你可以把同样的思路套到自己的渠道池上。
先说清楚 New API 原生逻辑的两个坑。第一个坑是「重试到底」:当一个分组里的渠道全部重试完,请求会停在最后一个渠道上,极端情况分组只有一个渠道时,后续重试只会反复命中这同一个坏渠道,等于没切换。第二个坑是跨组重试的时机判断依赖当前组的重试状态,组与组之间来回试探非常浪费时间。所以真正的高可用不是「多重试几次」,而是「不要发生无意义的重试」——请求进来先建一个实时候选池,按健康度排序,选当前最优的那条发出去,失败立刻降级到次优,而不是在坏渠道上耗完重试次数。
我试过把健康检查做成被动统计 + 主动探测两层。被动层持续维护每条上游最近 50 次请求的结果分布:2xx、429、5xx、timeout、connection reset、upstream disconnect 分别计数,注意 429 和 timeout 不能简单归为同一类错误,前者是限流信号、后者是链路或上游过载信号,处理策略不同。主动层按固定间隔发轻量探测请求,拿到 TTFB 的 EWMA、p50、p95、p99,源码里那种简单平均延迟根本不够用,一个 p99 飙到 10 秒的渠道平均值可能还很漂亮。最后用Score = Health × Latency × Load × RateLimit × Cost × Policy算出渠道分数,优先级保留但降级为「管理员给的基础主观分」,真正决定发往哪里的是实时分数。
2. TaoToken 统一通道的前置准备
在动手改路由之前,先把上游接入层收敛好。多上游场景最怕的是每条上游一套鉴权、一套 Base URL、一套模型名映射,路由模块光做地址转换就够呛。我的做法是用 TaoToken 作为统一通道,所有上游渠道在它这一层完成模型对齐和 Key 管理,New API 这边只认一个稳定的 Base URL 和一组 Key,路由模块专注做健康度和切换决策,职责清晰很多。
前置准备分三步。第一步拿 Key:登录官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册后进控制台,在 API Keys 页面创建密钥,建议按环境分开建,测试和生产的 Key 不要混用,方便出问题时快速吊销。第二步确认 Base URL:统一走 https://taotoken.net/api,注意这个地址不带任何查询参数,配置时不要自作主张加斜杠或路径后缀,很多 404 就是这么来的。第三步确认模型 ID:在模型对话页面先把要用的模型跑通一次,记下准确的 Model ID 字符串,后面写进 New API 渠道配置和路由规则里必须一字不差。
这里有个容易忽略的点:New API 的渠道配置里,Base URL 填的是上游的根地址,实际请求路径由它自己拼接/v1/chat/completions这类后缀。所以你在 TaoToken 这边看到的完整调用地址是https://taotoken.net/api/v1/chat/completions,但填进渠道的 Base URL 只写https://taotoken.net/api。我踩过的坑就是多写了一段/v1,结果变成/v1/v1/chat/completions,报错信息还特别含糊,排查了半天。
如果你打算长期跑编码类或 Agent 类负载,建议顺手看一下 Coding Plan 的额度结构,它和按量计费的 Key 是分开管理的,路由层做成本维度打分时可以把这两类额度当成不同的 Cost 权重。接入文档在 doc 页面有完整的参数说明,配置前花十分钟过一遍能省掉后面大量试错。Key 管理入口在 api-keys,模型调试在模型对话,这几个页面建议都收藏,排障时会反复用到。
前置阶段还要做一件事:给你的上游渠道编号并记录元数据。比如upstream-a对应 TaoToken 通道下的某组模型、upstream-b对应另一组,每条记录里写清楚它的成本倍率、并发上限、是否允许承载生产流量。这份元数据后面会直接喂给路由模块的 Policy 和 Cost 因子,没有它,动态打分就是无源之水。
3. 可复制的路由与健康检查配置
这一节给可直接落地的配置片段。New API 的渠道和路由配置分散在管理后台和配置文件里,我把它整理成三段:渠道定义、健康检查参数、路由打分规则。先看渠道定义,用 JSON 描述,字段名和你后台看到的保持一致,路径按你实际部署调整。
{ "channels": [ { "id": 101, "name": "taotoken-primary", "type": "openai", "base_url": "https://taotoken.net/api", "key": "sk-your-primary-key", "models": ["gpt-4o", "claude-sonnet-4-20250514"], "group": "production", "priority": 10, "weight": 80, "meta": { "cost_factor": 1.0, "max_concurrency": 64, "prod_allowed": true } }, { "id": 102, "name": "taotoken-backup", "type": "openai", "base_url": "https://taotoken.net/api", "key": "sk-your-backup-key", "models": ["gpt-4o", "claude-sonnet-4-20250514"], "group": "production", "priority": 5, "weight": 20, "meta": { "cost_factor": 1.2, "max_concurrency": 32, "prod_allowed": true } } ] }注意两条渠道的 Base URL 和模型列表完全一致,区别只在 Key、优先级和成本因子。这样路由切换时不需要做任何模型名转换,请求体原样转发即可,这是统一通道带来的最大便利。Key 一定要用环境变量注入,别把明文写进版本库。
第二段是健康检查参数,用 TOML 写,放在网关的配置目录下,文件名按你的项目约定来,我这里叫router-health.toml:
[health_check] interval_ms = 5000 timeout_ms = 3000 window_size = 50 probe_model = "gpt-4o" probe_prompt = "ping" probe_max_tokens = 1 [health_check.thresholds] error_rate_5xx = 0.10 error_rate_429 = 0.20 ttfb_p95_ms = 4000 ttfb_p99_ms = 8000 consecutive_failures = 3 [retry] max_attempts = 2 backoff_ms = 200 retry_on = ["429", "5xx", "timeout", "connection_reset"] no_retry_on = ["400", "401", "403", "404"] [circuit_breaker] failure_threshold = 5 open_duration_ms = 30000 half_open_probes = 2几个参数值得展开。window_size = 50对应前面说的最近 50 次请求统计,窗口太小噪声大、太大反应慢,50 是个折中。consecutive_failures = 3是熔断触发条件之一,连续三次失败直接摘除,不等错误率统计。no_retry_on里放 400/401/403/404 很关键,这些是请求本身或鉴权的问题,重试一百次也没用,只会放大延迟。open_duration_ms = 30000表示熔断后 30 秒进入半开状态,放两个探测请求试探,成功就恢复,失败继续熔断。
第三段是路由打分规则,用 JSON 表达各因子的权重和归一化方式:
{ "scoring": { "formula": "health * latency * load * ratelimit * cost * policy", "weights": { "health": 0.35, "latency": 0.25, "load": 0.15, "ratelimit": 0.15, "cost": 0.05, "policy": 0.05 }, "latency_metric": "ewma_ttfb", "latency_penalty_p95_ms": 4000, "load_metric": "inflight_over_max_concurrency", "ratelimit_source": "response_headers", "fallback_on_all_unhealthy": "highest_priority" } }fallback_on_all_unhealthy这个字段是保命的:当所有渠道分数都低于阈值时,不要直接返回 503,而是退回最高优先级的渠道硬发一次,同时打告警。全挂的概率很低,但真遇到时宁可让请求慢一点也别直接拒绝。ratelimit_source设为response_headers表示从上游返回的x-ratelimit-remaining、x-ratelimit-reset里读限流状态,TaoToken 通道会透传这些头,路由层拿到后直接参与打分,remaining 接近 0 的渠道自动降权。
配置写完后重启网关服务,观察日志里健康检查的探测记录,确认每条渠道都在被周期性探测、分数在正常刷新。如果日志里只有一条渠道有探测记录,检查probe_model是否在该渠道的模型列表里,模型不存在会导致探测直接 404 被跳过。
4. 故障注入验证:请求真的自动转移了吗
配置写完不算完,必须做故障注入验证,否则你永远不知道切换逻辑是不是真的生效。验证分三个层次:单渠道故障、限流场景、全渠道降级。
第一层,单渠道故障。把taotoken-primary的 Key 临时改成一个无效值,模拟鉴权失败。预期行为是:健康检查探测返回 401,连续三次后熔断打开,路由分数归零,新请求全部走taotoken-backup。验证命令用 curl 打你的网关入口,连续发 10 个请求:
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} %{time_total}\n" \ -X POST http://localhost:3000/v1/chat/completions \ -H "Authorization: Bearer sk-your-gateway-key" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hi"}],"max_tokens":5}' done预期输出是 10 个 200,时间在几百毫秒到两秒之间波动,不应该出现 401 或 5xx。如果出现 401,说明网关把上游的鉴权错误透传给了客户端,检查no_retry_on是否生效、熔断是否真的打开了。同时看网关日志,应该能看到circuit_open channel=101和后续请求的routed_to=102记录。
第二层,限流场景。这个稍微麻烦一点,需要模拟上游返回 429。最直接的办法是在路由层加一个测试开关,让指定渠道的探测请求强制返回 429,或者用一个小脚本对上游快速打满并发触发真实限流。预期行为是:429 计数上升,error_rate_429超过 0.20 阈值后该渠道降权,请求开始向备用渠道分流,但不会完全摘除(因为 429 是临时性的,熔断策略应该比 5xx 更温和)。验证时观察两个指标:客户端侧的成功率应该保持 100%,网关侧的分流比例应该从 100:0 逐渐变成 60:40 左右。
第三层,全渠道降级。把两条渠道的 Key 都改坏,预期行为是fallback_on_all_unhealthy生效,请求仍然发往最高优先级渠道,客户端收到的是上游的真实错误(401),而不是网关自己编的 503。这一层验证的是「降级不掩盖问题」,全挂的时候要让调用方看到真实原因,方便快速定位。
三层验证都通过后,再做一次长时间稳定性观察:让流量正常跑 30 分钟,看健康检查的分数曲线是否平稳、有没有渠道在健康和不健康之间反复横跳。频繁抖动通常说明阈值设得太敏感,把error_rate_5xx从 0.10 放宽到 0.15、consecutive_failures从 3 调到 5 试试。验证过程中如果发现切换有延迟,重点看interval_ms和timeout_ms的比例,探测超时时间不能接近探测间隔,否则探测请求自己就会堆积。
5. 常见报错对照与排查路径
这一节按真实报错信息对照排查,都是我在多上游接入里反复遇到的。
401 Unauthorized出现在客户端侧,先分清是网关鉴权失败还是上游鉴权失败。看响应体里的 error message,如果是网关自己的 Key 问题,检查请求头Authorization格式;如果是上游透传的,说明熔断没拦住坏渠道,回去检查no_retry_on是否包含 401、熔断阈值是否被绕过。还有一种情况是 Key 本身有效但额度耗尽,TaoToken 控制台的 api-keys 页面能看到每个 Key 的余额和状态,先在那里确认。
local proxy failed或connection refused这类错误,基本是 Base URL 或网络层的问题。检查渠道配置里的base_url是不是https://taotoken.net/api,有没有多写路径、有没有写成 http。如果网关部署在内网,确认出站规则允许访问该域名。这个报错和路由逻辑无关,但经常被误判成「切换失败」,实际是压根没连上。
reading choices或invalid response format报错,说明上游返回了非预期结构,常见于模型 ID 写错、请求体里混入了上游不认识的字段。排查方法是用同样的请求体直接 curl 上游地址,对比返回。如果直连正常、走网关报错,检查路由层有没有对请求体做改写,特别是模型名映射那一步。Model ID 必须和模型对话页面里看到的完全一致,大小写、日期后缀都不能错。
OAuth相关报错一般出现在用 Claude Code 这类客户端接入时,认证方式没对齐。这类客户端走的是 Anthropic 兼容协议,Base URL 和 Key 的填法和 OpenAI 协议不同,需要单独配。如果你是用 Claude Code 接 TaoToken,参考 ClaudeCodeAnthropic 页面的说明,把 Base URL、Key、Model ID 三件套填全,缺一个都会报 OAuth 或鉴权错误。同理,用 CC Switch 或 Cline MCP 接入时,也是这三件套:Base URL 填https://taotoken.net/api,Key 填控制台生成的密钥,Model ID 填准确的模型字符串,三个都对上才能通。
429 Too Many Requests持续出现但渠道健康度显示正常,检查限流头的解析逻辑。有些上游的x-ratelimit-remaining单位是请求数、有些是 token 数,解析时如果单位搞混,打分就会失真。另外确认ratelimit_source配置的是response_headers而不是static,静态配置的限流值不会随实际用量变化。
timeout集中在某个渠道但错误率没到熔断阈值,看 TTFB 的 p95 和 p99。如果 p99 已经超过ttfb_p99_ms但平均值还好,说明是长尾问题,把latency_metric从ewma_ttfb临时改成p95_ttfb能让打分更敏感。这类问题在高峰期特别明显,建议在压测环境复现后再调参。
排查通用顺序:先看客户端收到的状态码和错误体,再看网关日志里的routed_to和circuit_state,最后直连上游对比。三步走完基本能定位到是配置、网络还是上游本身的问题。
6. 把路由层跑稳之后的下一步
路由模块配好、故障注入验证通过之后,这套东西的价值才刚开始体现。统一通道加动态打分的组合,让你可以在不改客户端任何代码的前提下,随时调整上游池的构成:新增一条渠道只需要在配置里加一段 JSON,健康检查会自动接管探测,分数达标后自然分流;下线一条渠道把权重调零、等存量请求跑完再删配置,全程无感。
如果你还在用单 Key 直连的方式接上游,建议先把接入层收敛到统一通道,再谈路由高可用。Key 管理和模型调试在模型对话和 api-keys 页面都能完成,接入细节看 doc。长期跑编码和 Agent 负载的话,Coding Plan 的额度模型和按量 Key 不同,路由层的 Cost 因子要单独给它设权重,别和普通请求混算。
最后留一个实操建议:把健康检查的分数和熔断状态接进你的监控面板,设两条告警——单渠道连续熔断超过 5 分钟、全渠道平均分数低于阈值。路由层做得再好,没有可观测性就是黑盒,出了问题只能靠猜。配置文件和验证脚本都进版本库,每次调参留 commit message,下次抖动时你能快速回滚到上一个稳定版本。