一条水管从多个水箱依次接过来,第一个水箱见底处亮着一只开关,管道上装着几只各管一个水箱的阀门

我们见过的多数模型路由代码,切换条件写得很粗:请求失败就换下一个。这个写法在真实环境里会出两类事故——额度用完之后一路疯狂重试,或者网络抖一下就把流量悄悄切到更贵的通道上。有一份开源语音输入工具的实现把这件事做得少见地干净:它把多个识别模型串成一条链,只在一种明确的返回码上切换,并给耗尽的模型设冷却期,全部耗尽就提示用户,同时把实时模式降级成分段模式。规则一共四条,但它给的是一个完整样本:配额可以当成路由信号来用,前提是你先把它和故障分开。

一、先把”用完了”和”坏了”拆成两件事

按该项目的做法,它接的是一个模型服务平台,控制台侧有两步前置:创建密钥,以及在额度页面打开”免费额度用完即停”。第二步容易被当成计费设置跳过去,但它其实是路由逻辑能不能工作的前提——不打开,超出部分直接开始计费,程序永远收不到一个可以辨识的”耗尽”信号,后面写的切换分支形同虚设。

这个设计值得琢磨的地方是:它把”配额”从一个账务概念变成一个可观测的事件。事件要有触发条件、有后果、有恢复时间。触发条件是明确的返回内容,后果是从候选链上摘掉这个模型,恢复时间是冷却期结束。三件事齐了,路由才有确定性;缺一条,就退化成”看到报错就乱撞”。

反过来讲,也不该把所有异常都升级成路由事件。该项目明确写了不触发切换的三种情况:网络错误、密钥无效、限流。这三类各有正确处置:等一等、停下来告警、按退避重试。把它们统统当”额度用完”,就会发生一件最贵的事——把本该重试的请求换到别的通道上跑,账单涨、问题还在。

二、核心交付物之一:五类错误的处置策略表

我们在自己项目里把这张表当成路由模块的输入契约。分类不是为了打标,是为了每一类对应一个不同的控制分支和一种不同的用户可见结果。

错误类别 识别依据 控制分支 是否计入切换次数 用户侧表现
额度耗尽 返回中明确的耗尽标识或对应错误码 摘掉该候选,进冷却期,取下一个候选 计入 说明已换通道,必要时说明能力变化
限流 频率类返回码与重试建议头 原地退避重试,不换候选 不计入 短暂等待提示,不改模式
鉴权失败 密钥类返回码 停止整条链,告警并要求人工处理 不计入 明确报错,不许静默换密钥
网络超时 连接层错误、无响应 按链路降级或重试,不换候选 不计入 说明当前不可用,给重进方式
模型不可用 服务侧下线或该请求形态不支持 跳过该候选,取下一个,记待清理 计入 不解释内部候选,只说结果可用与否

表格之外还有一条纪律:分类要在客户端做,不要等返回文本给人读。错误码与耗尽标识要写成常量并集中维护,供应商改了返回结构时,改动只落在一处。

三、候选链的编排规则

据该项目文档,串联规则四点:同一供应商的候选数量上限为 8 个模型;只有明确返回额度耗尽才切换;耗尽的模型进入 24 小时冷却;全部耗尽则向用户提示额度已用完。

这四点的工程含量比看起来高。

  • 数量上限是对”链太长”的保护。候选越多,一次失败请求要试的分支越多,最坏情况下用户等到的是串行的多次超时。上限也逼着人做取舍:链上只放行为可预期的模型。
  • 冷却期是对”反复唤醒”的保护。没有冷却,请求量少时会发生一件事:刚耗尽的模型被下一个请求再撞一次,撞完又失败,链上时间全花在无效试探上。冷却期内不许被唤醒,这一点必须写成硬规则,而不是”优先级降一降”。
  • 全部耗尽要有终态。很多实现写到”取下一个候选”就停了,没写”没有下一个怎么办”。该项目给了终态:提示用户。终态要能操作,不能只是一个错误码。
  • 跨供应商排序要留探测窗口。多个来源时,判断一个候选是否可用不能靠单次请求,也不能靠无限重试;要定义”探测几次、间隔多久、多久判死”。

另有一处容易漏:候选模型的行为并不等价。该项目按模型名把能力分成三类——边录边出的实时识别、有单次时长上限的分段识别(据该文档约四分钟)、以及只能做文件转写的类型,后者直接从听写列表里隐藏。把不等价的模型放在同一条链上时,链的排序要和能力类别绑定,不能只按额度剩余排。

四、降级是能力变化,不是失败,必须让用户看见

该项目四条规则里最见功夫的是最后一条的副作用处理:当实时模型都耗尽、链降到分段类型时,当前这一次会话会被降级为分段模式,实时预览不再可用。

这件事的界面含义经常被写成一句话报错。正确的做法是把”现在是什么模式”当成一个可见状态:

  1. 界面显示当前运行形态,而不是只显示”正在识别”。
  2. 说清为什么变了:是额度耗尽,还是能力本身不支持连续输入。
  3. 说清什么时候恢复:跟随候选的冷却与补充,不许承诺没有依据的时间。
  4. 不许静默改变行为。同一段话在实时形态和分段形态下结果不同,用户不知道形态变了,就会把差异当成质量不稳定。

我们把它写成一条通用交付规则:任何由配额或错误分类触发的能力变化,都要在界面上留下可读的痕迹;只在日志里记的那次叫故障,界面上记了的那次才叫降级。

五、密钥、发送范围与对账

一条链跑起来之后,第二个容易被忽略的面是安全与计量。

密钥侧,该项目的做法是存进操作系统的安全存储(凭据管理器、钥匙串一类),而不是配置文件。这一点在客户端工具上尤其重要:配置文件会随项目一起被拷走、进版本库、被打包。另外接口地址按业务空间与区域分不同域名,说明”密钥”与”接入点”是两件事,换区域时两者都要改,只改一处会出现看起来像鉴权失败的错误。

发送范围侧,该项目记录了两条值得抄的纪律:润色环节只送识别出的文本而不送音频,并且对部分模型默认关闭思考模式以避免延迟;同时它会一起发送当前应用名,而这个项可以在设置里关掉。”额外发送什么”要有一份能关闭、能查证的清单,日志里则不许出现密钥与应用名这类可识别上下文。

对账侧,我们的做法是把每个候选的用量、单位成本、耗尽时刻与切换次数按候选维度落表。月底要能回答两个问题:这条链替我们避开了多少超额计费,以及哪几个候选从来没被耗尽过(那是可以下掉的冗余)。该项目也记录了几个已知限制,比如需要按住组合键触发、未签名安装包会被系统拦截、没有移动端版本,这些不影响路由规则本身,但影响这条规则能被部署到哪些环境。

六、上线前的三条验收

这三条是我认为配额路由唯一值得过的验收,缺一条就别上线:

验收项 制造方式 通过判据
只切该通道 人为把一个候选跑到耗尽状态 只有该候选进冷却,其余候选不受影响,计费不越界
不误判为耗尽 断开网络、用无效密钥各试一次 不进切换分支,分别走重试与停止告警
全耗尽有终态 把链上候选全部置为耗尽 给出可读提示与恢复说明,界面模式标注同步变化

配额这件事在工程上还有一种更长期的风险:额度政策会变、价格会变、单个模型会下线。这三种变更下的退出顺序要提前写在配置里——先摘掉行为不再符合预期的候选,再调整冷却与排序,最后才是重新评测。洹拓在给客户做类似链路时,会把这三步写成变更单,而不是运维当场拍。因为一旦允许”随手摘一个模型”,这条链的可观测性就没了,月底对账时谁也对不上为什么这笔用量出现在这里。