给模型网关加一条备用线路:路由组与自动分叉

我的模型调用基本都走第三方池子:便宜、型号多,但单个账号会限流,也会抖。编辑器、harness、几个脚本各自直连上游的时候,一次 429 就要手动改配置。后来我在中间放了一个 OpenAI 兼容网关,上层只认一个 base_url,换线路的事留给网关自己做。

为什么要在中间挡一层

不是为了架构好看。直连最大的问题是配置散在客户端:编辑器里一份 key,harness 里一份,脚本里再一份,上游一换全要找出来改。中间加一层之后,上层只认一个地址、一个 key;哪个池子便宜、哪个账号今天被限流,都在网关里调。

代价是多了一跳:延迟多几毫秒,故障时多一个要排查的环节。这一层自己也会挂,所以它必须足够简单——只做协议转换、路由和记账,不做业务。

路由组:同名模型按 order 排队

最常用的配置是路由组:一个对外暴露的模型名,后面挂多个上游,按 order 排优先级。请求先打第一个,遇到 429、超时或者 5xx,就落到下一个。

场景网关做什么上层看到什么
主线路 429换下一个上游重试一次成功的响应,慢一点
主线路超时按 order 继续往下走同上
所有线路都失败返回最后一次错误一个明确的错误码

这里有个取舍:重试会把请求放大。一次调用最坏情况下打满所有上游,如果上游按请求数计费,账单会比预期高。我的做法是只对「还没开始计费」的错误重试——限流、超时、过载、额度不足,这些通常发生在请求被接受之前。

自动分叉:没吐字才换

路由组解决的是「换一个上游」,自动分叉解决的是「同时问两个」。规则很简单:主模型在吐出第一个字之前就报 RATE_LIMIT、TIMEOUT、OVERLOADED、QUOTA 这类错误时,同一次请求会分叉给备用路由,谁先答完用谁;一旦主线路已经开始吐字,就不再换。

后面这半句是重点。输出了一半再换线路,用户看到的会是两段不同模型的文字拼在一起,语气、格式、结论都可能打架。半句话比慢三秒难看得多。

分叉不是免费的

一次分叉意味着两边都在算 token,只有一边的结果被留下。所以这个开关我不会全量打开:值不值得,要看那条线路最近的失败率,以及这个模型答一半被打断有多难看。失败率低、回答又长的模型,宁可让它慢慢重试。

配置本身很短:

model    = 对外模型名
routing  = order
members  = 上游甲, 上游乙, 上游丙
autofork = on

运维:每天凌晨自己更新

网关跑在容器里。每天凌晨有个定时任务检查有没有新版本,有就拉镜像、重启、做健康检查;检查不过就回滚到上一个镜像。

记账比更新重要。网关把每次调用写成一行 usage jsonl:时间、agent、上游、模型、输入、输出、状态。报表都从这份账本里算,不从面板截图里算——截图会过期,账本可以重算。

账本里每一列都有用处:

  • 时间加上游:看某条线路今天是不是一直在抖;
  • agent:看是哪个客户端在烧额度;
  • 输入和输出:算钱,也看缓存有没有生效;
  • 状态:区分成功、限流和超时,决定要不要换池子。

换线路的判断顺序也固定下来:

  1. 先看账本里错误码的分布,确认是限流还是上游真的挂了;
  2. 再看路由组的 order,是不是把最不稳的那条放在了最前面;
  3. 最后才动 autofork 的开关——它放大的请求量是要付钱的。

中间这一层的价值,是让上层永远只认一个 base_url;线路怎么变,是网关自己的事。

现在我改线路,不用动编辑器、不用改脚本,改一份配置、等一次健康检查。省下的时间不多,但足够让我在想换池子的时候,不用先回忆「上次那份 key 放在哪」。