给模型网关加一条备用线路:路由组与自动分叉
我的模型调用基本都走第三方池子:便宜、型号多,但单个账号会限流,也会抖。编辑器、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:看是哪个客户端在烧额度;
- 输入和输出:算钱,也看缓存有没有生效;
- 状态:区分成功、限流和超时,决定要不要换池子。
换线路的判断顺序也固定下来:
- 先看账本里错误码的分布,确认是限流还是上游真的挂了;
- 再看路由组的 order,是不是把最不稳的那条放在了最前面;
- 最后才动 autofork 的开关——它放大的请求量是要付钱的。
中间这一层的价值,是让上层永远只认一个 base_url;线路怎么变,是网关自己的事。
现在我改线路,不用动编辑器、不用改脚本,改一份配置、等一次健康检查。省下的时间不多,但足够让我在想换池子的时候,不用先回忆「上次那份 key 放在哪」。