1. 为什么 Agent 一上生产,账单就先失控
多模型调度这件事,很多团队都是被账单逼着做的。我见过一个内部数据查询 Agent,上线第一个月账单三千多,拆开看模型维度才发现,接近八成的调用花在了根本不需要大模型的请求上——用户只是问「帮我查一下 8821 这条记录的状态」,Agent 却把六千字的背景材料整段喂给了最贵的那个模型,让它在里面找一行状态字段。
这就像让博士生下楼取快递。不是他取不了,是你付的价钱不对。更亏的是延迟:旗舰模型的首字延迟普遍比小模型高一个量级,用户为了一个查状态的请求多等两三秒,成本高、延迟高、体验差,三头都亏。而这一切的根源只有一个——我们把所有请求都当成了难题。
多模型调度要解决的不是「怎么把模型换成更便宜的」,而是怎么让每个请求都落到「刚好够用」的那个模型上。它包含两件事:模型路由(正常情况选谁)和降级链(主模型超时或限流时自动切到备用模型)。前者省钱,后者保命,缺一不可。
单模型策略在生产环境会稳定产生三重代价。第一重是成本上的大材小用:真实流量分布极度偏斜,客服或数据查询类 Agent 里六到八成是短上下文、单步、答案封闭的简单任务,只有一到两成需要真正的推理能力,用统一的大模型处理全部流量,等于按最高单价给全部请求计费。第二重是延迟上的陪跑:简单任务本来 300 毫秒能出结果,走大模型就是 1.5 秒起步,首字延迟从 800ms 涨到 2500ms,用户的主观感受就是「这东西卡」。第三重最容易被忽略——能力上的错配:有些强格式约束的任务,小模型反而更准。把非结构化文本抽成固定 JSON、按模板改写、中文数字转阿拉伯数字,大模型「太聪明了」,容易自作主张帮你优化格式,或者在数值上做它认为合理的推断;小模型指令遵循更机械,输出反而稳定。我见过一个订单抽取 Agent,从大模型换到小模型后字段准确率从 94% 涨到 98%,因为不再出现「模型贴心补全了不存在的字段」这类问题。
把这三重代价放一起,结论很清楚:单一模型不是简单省心,而是把成本、延迟、准确率三个指标同时锁死在一条不优的线上。多模型调度让你能对每一类流量单独做取舍。
实现路径大致五类:单模型固定写死(PoC 阶段正确,生产阶段代价大)、人工分场景硬编码(粗粒度,适合边界清晰的初期)、规则路由(配置规则命中,可解释性很高,中生产起步首选)、分类器或学习型路由(用小模型判难度再分发,适合有评测集的成熟期)、托管多模型平台(省心但决策黑盒,成本优化失去抓手)。本文主张以「规则路由为主干 + 分类器做增强 + 降级链兜底」,理由有三个:可解释,规则命中与否一眼能说清,出事能审计;零训练成本,不用先攒标注数据集才能上线;渐进式,先用规则吃掉最容易的那部分流量,通常就能省下四五成成本,等攒够日志和评测集,再把分类器加上去啃剩下的硬骨头。
2. TaoToken 统一通道:多模型调度的前置准备
多模型调度最烦的从来不是路由逻辑,而是「接模型」这件事本身。三个 provider 就是三套凭据、三条网络链路、三份超时配置、三种错误码语义。你还没开始写路由规则,光是把三个模型都调通、都埋上指标,一周就过去了。更麻烦的是,每加一个模型就要重来一遍,路由规则还没写,维护成本已经上去了。
TaoToken 在这里的价值是把它变成一个统一通道:一个 Base URL、一个 Key,就能访问多个模型。对多模型调度来说,这一点比「便宜」重要得多——因为路由和降级链的前提是「切换成本足够低」。如果每换一个模型都要改代码、改配置、重新测连通性,那你根本不会去做降级链,出问题只会干等主模型恢复。
具体怎么理解这个统一通道。你可以把它类比成一个「模型插座」:你的 Agent 只认一种协议(OpenAI 兼容),TaoToken 负责把请求转给背后不同的模型。你的路由配置里写的是模型名,换模型只改一个字符串,不用动业务代码、不用换 SDK、不用重新配网络。这对降级链尤其关键——降级动作本质上就是「换一个模型名重发一次」,如果换模型名要动基础设施,降级链就形同虚设。
接入前你需要准备三样东西。第一,一个可用的 Key,在控制台的 API Keys 页面创建,建议按用途分开建(比如 light/mid/heavy 各一个),这样将来要下线某一档,吊销对应一个 Key 就行,不影响其他链路。第二,确认你要用的模型 ID,在模型列表或文档里查,注意模型名变动频繁,上线前以控制台当期为准。第三,一个能发 HTTP 请求的环境,curl 就够,先手工验证连通性再写进配置。
这里有个我踩过的坑值得先说:很多人一上来就把三个模型全配进路由,结果一个模型名写错,降级链每次触发都在这一跳失败,日志里全是「看起来像上游故障」的记录,排查方向被带偏。正确顺序是先把每个模型单独调通、单独验证,再放进路由配置。下面这段是最小验证命令,三个模型各跑一次,确认都能返回再往下走:
# 验证统一通道连通性:把 MODEL_ID 换成你要用的模型名 # 为什么先单独验证:路由和降级链都建立在「每个模型本身可用」之上, # 如果某个模型名写错,降级链会在这一跳反复失败,日志会误导排查方向。 curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "MODEL_ID", "messages": [{"role": "user", "content": "只回复两个字:正常"}], "max_tokens": 8 }'预期输出是一段标准 OpenAI 格式的 JSON,choices[0].message.content里是「正常」。三个模型都跑通之后,你才算真正具备了做多模型调度的前提。这一步看起来笨,但它把后面所有「降级失败」的可能性提前排除掉了——降级链的可靠性,取决于每一跳本身是否可靠。
还有一件事必须一开始就做对:密钥管理。多模型意味着多套密钥,而密钥写进 YAML 是最常见的翻车点。配置里只放环境变量名,真实值由运行环境注入,并且给预算装一个上限。没有预算上限,一个写错的路由规则能在半天内烧掉半个月预算;有了它,最坏情况是「降档服务、质量下降」,而不是「账单失控」。这是运维视角和开发视角的重要区别。
3. 可复制的路由配置与降级链规则
这一节是全文的核心,给出一套可以直接抄的配置。整体分三步:先把多个模型注册进配置,再写规则路由,最后配降级链。每一步都附上「为什么这么写」,因为照抄而不理解,出事时你改不动。
3.1 第一步:把多个模型注册成具名 provider
第一步只做一件事:让网关认识三个模型,但先不启用路由,全部走中档。这个中间态很重要,它给你一个基线,后面才能对比出路由到底省了多少。
# 配置片段:provider 注册 # 为什么这么写:把三档模型定义成三个具名 provider, # 路由器只需引用名字(light/mid/heavy),将来换具体模型时 # 只改这里一处,规则文件一行都不用动——这是「决策与实现分离」。 providers: light: type: openai-compatible base_url: "https://taotoken.net/api/v1" api_key: "${TAOTOKEN_LIGHT_KEY}" model: "LIGHT_MODEL_ID" timeout_ms: 5000 max_context: 8192 mid: type: openai-compatible base_url: "https://taotoken.net/api/v1" api_key: "${TAOTOKEN_MID_KEY}" model: "MID_MODEL_ID" timeout_ms: 6000 max_context: 65536 heavy: type: openai-compatible base_url: "https://taotoken.net/api/v1" api_key: "${TAOTOKEN_HEAVY_KEY}" model: "HEAVY_MODEL_ID" timeout_ms: 8000 max_context: 200000为什么每个 provider 都要单独设timeout_ms和max_context:这两个参数是路由规则的输入。max_context决定长上下文请求能不能交给它——如果你把 8k 上下文的请求路由给了上限 8k 的模型,结果是直接报错而不是优雅降级;timeout_ms决定降级触发的时机,旗舰模型给 8s、轻档给 5s,是因为它们的正常耗时本来就不同,用统一的超时值要么误杀快模型、要么让慢模型磨蹭太久。
3.2 第二步:写第一版规则路由
规则路由的价值在于用最低成本拿走最大收益。我第一版只写了四条规则,就把近六成流量从重档拉了下来。
# 配置片段:规则路由 # 为什么这么写:规则按优先级从上往下匹配,命中即停。 # 把「确定性最高、最不会错」的规则放最前面, # 把「兜底」规则放最后。规则顺序本身就是你的策略表达。 routing: enabled: true default: mid # 未命中任何规则时的默认档,先保守 rules: - name: "不可逆操作强制重档" priority: 20 # 优先级最高,先于成本考虑 match: writes_data: true irreversible: true route_to: heavy - name: "强格式抽取走轻档" priority: 10 match: intent_in: ["extract_json", "format_convert", "classify"] max_context: 4096 route_to: light - name: "只读状态查询走轻档" priority: 9 match: keywords_any: ["查一下", "状态", "单号", "记录编号", "是否已"] tools_only_readonly: true max_turns: 3 route_to: light - name: "长上下文走重档" priority: 8 match: min_context: 24000 route_to: heavy为什么「不可逆操作」要给最高优先级:因为它不是成本优化项,而是风控项。写库、发消息、改配置这类操作一旦判断失误,代价远超那点 Token 钱。把风控规则放在成本规则之前,是这套配置里最重要的一条顺序原则。很多团队把成本规则放最前,结果「删除记录」这种高风险请求被判成简单任务走了小模型——省了几分钱,赔上了一次事故。
为什么max_context要作为硬条件:它是防止路由发出一个注定失败的请求。上下文超过模型上限时,调用会直接报错,等于既花了钱又没拿到结果。把它当硬约束放在规则里,路由层就变成了一个可行性检查器。
3.3 第三步:配降级链,区分「能降的错」和「不能降的错」
正常路径配完,生产环境的坑都在异常路径上。这一步把降级链配好,并做一件很多人忽略的事:按错误类型分流。
# 配置片段:降级链与错误分流 # 为什么这么写:failover 里的 on_errors 白名单决定了哪些错误才配降级。 # 429/5xx 是「模型侧暂时不行」,降级合理; # 400/401/403 是「我们的请求或凭据有问题」,降级无用且白花钱,必须 fail fast。 failover: enabled: true chain: [heavy, mid, light] on_errors: ["429", "500", "502", "503", "timeout"] # 只对这些错误降级 fail_fast_on: ["400", "401", "403", "404", "422"] # 这些直接报错,不降级 max_hops: 3 # 链长上限,防止无限下探 retry_same_model: 1 # 同一模型最多重试 1 次(仅对 timeout) health_check: interval_s: 30 failure_threshold: 3 # 连续 3 次失败 -> 熔断该 provider 5 分钟 cooldown_s: 300 budget_guard: daily_limit_env: "TAOTOKEN_DAILY_BUDGET_CNY" on_exceed: "downgrade_to_mid" # 预算耗尽时主动降档,而不是把请求打失败 alert_at_percent: 80为什么 400 类错误绝不能降级:这是降级链里最容易踩的坑。请求体格式错、密钥过期、模型名写错——这三类问题换任何一个模型都还是失败。如果把它们也纳入降级条件,一次配置错误会消耗三次调用(重档失败 → 中档失败 → 轻档失败),账单变成三倍,日志里还留下三条「看起来像模型故障」的记录,误导排查方向。降级只该治「对方的问题」,不该治「自己的问题」。
为什么熔断冷却期要设 5 分钟:模型侧的限流和故障通常不是瞬时的。第一次 429 之后的 3 秒内重试,大概率还是 429。给一个 300 秒的冷却窗口,让这段时间的流量直接走下一档,既避免了无意义的重试风暴,也给了上游恢复的时间。这个逻辑和微服务里的熔断器完全同源——Agent 系统的健壮性设计,本质上是在复用后端领域已经成熟的模式。
降级链的形状也有讲究。串行下探(重档失败才试中档)成本最低、只有必要时才花钱,代价是故障时用户要多等一跳;并行竞速(同时发两个模型取先返回的)能压住延迟,但成本翻倍。我的做法是默认串行,只对交互式场景开启「重档 + 中档并行」,并给并行设一个极短的等待窗口(比如 800ms 内重档没返回就先采用中档结果)。判据很简单:用户的等待时间贵,还是 Token 贵——内部批处理选前者,面向终端用户的实时对话选后者。
4. 验证请求与故障切换实测
配置写完不算完,必须验证。这一节做两件事:验证正常路由是否按预期命中,以及手动制造一次故障,看降级链是否真的切过去了。
4.1 验证正常路由
先发一条简单查询,看日志里命中了哪条规则:
# 发一条只读状态查询,预期命中「只读状态查询走轻档」 curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "MID_MODEL_ID", "messages": [{"role": "user", "content": "帮我查一下 8821 的状态"}], "max_tokens": 64 }'预期日志里出现routing: matched rule "只读状态查询走轻档" -> light。再发一段两万字的会议记录要求总结,日志应显示命中「长上下文走重档」-> heavy。这两条验证通过,说明规则匹配和优先级顺序都对了。
4.2 手动制造一次故障切换
这是本文最该动手做的一步。把 mid 的密钥故意改错,触发 401,观察降级链的反应:
# 故意用错误的 Key 触发 401,验证 fail_fast 是否生效 # 为什么这么测:401 属于「自己的问题」,降级链不应该去试其他模型, # 否则一次密钥过期会连试三个模型,账单三倍、日志误导。 curl https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-wrong-key-for-test" \ -H "Content-Type: application/json" \ -d '{"model": "MID_MODEL_ID", "messages": [{"role": "user", "content": "test"}]}'预期日志显示fail_fast: 401, no failover,而不是连试三个模型。这一步验证的是「不该降的错没降」。
接着验证「该降的错降了」:把 heavy 的 base_url 改成一个不存在的域名,触发 timeout,日志应显示timeout -> failover to mid,且业务侧仍拿到正常回复。这一步验证的是降级链真的在工作。
4.3 用指标做闭环验证
路由配完不代表结束,没有验证的优化等于没做。用指标做一次闭环:
# 按模型拆分单次请求成本 # 为什么这么写:输入与输出单价不同(输出通常贵数倍), # 所以要分开乘系数再加总,否则「上下文变短但输出变长」会被算反。 sum by (model) ( rate(llm_input_tokens_total[1h]) * on() group_left() model_input_price + rate(llm_output_tokens_total[1h]) * on() group_left() model_output_price ) # 轻档命中率:衡量路由规则的实际效果(目标 >55%) sum(rate(llm_requests_total{route="light"}[1h])) / sum(rate(llm_requests_total[1h])) # 降级率:衡量模型侧稳定性(持续 >5% 说明上游有问题) sum(rate(llm_failover_total[1h])) / sum(rate(llm_requests_total[1h]))预期结果:一周后轻档命中率从 0 涨到 50%~65%,单次平均成本下降到原来的 30%~45%,降级率保持在 5% 以内。如果降级率超过 5%,说明降级链在频繁触发,此时应该先修稳定性,再谈省钱——频繁降级意味着你的主模型链路本身有问题,降级只是掩盖了它。
我实测下来,这套配置在自己的数据查询 Agent 上跑两周,月账单降到原来的约 32%,首字延迟 P50 从 1780ms 降到 620ms,评测集准确率从 97.0% 掉到 96.4%。这 0.6 个百分点是一次真实的取舍,不是纯粹的胜利。如果你的业务对准确率极度敏感,应该把 default 从 mid 改回 heavy,只保留「确定安全」的那几条规则。多模型调度不是越省越好,而是在你能承受的质量底线之上尽量省。
5. 常见报错排查:401、local proxy failed、reading choices、OAuth
配置跑起来之后,报错会集中在几个地方。这一节按真实报错逐条对照,给出定位思路。
5.1 401 Unauthorized
最常见,也最容易被误判成「模型故障」。401 的根因永远是凭据问题:Key 写错、Key 过期、Key 没有对应模型的权限、环境变量没注入成功。定位顺序是:先确认环境变量在运行进程里真的存在(echo $TAOTOKEN_MID_KEY,注意有些进程管理器不会继承 shell 的环境变量),再用 curl 单独验证这个 Key,最后才怀疑配置。
关键点:401 必须配在fail_fast_on里,绝不能降级。我见过一次密钥过期,因为没配 fail_fast,降级链连试三个模型,账单三倍,日志里三条「模型调用失败」把真正的根因埋了。
5.2 local proxy failed
这个报错通常出现在本地模型(轻档走 Ollama)这一跳。含义是网关连不上本地推理服务。排查三步:确认本地服务在跑(curl http://localhost:11434/api/tags)、确认端口和配置里的base_url一致、确认网关进程和本地服务在同一网络命名空间(容器化部署时最容易出这个问题,网关在容器里,localhost指向的是容器自己而不是宿主机)。
这个报错对降级链的意义是:本地模型是链上最不可靠的一跳,它的故障率通常高于云服务。所以本地模型也必须纳入健康检查,连续失败要熔断,否则每次降级到它都失败,链走到头就是明确报错。
5.3 reading choices 相关报错
这类报错通常长这样:cannot read properties of undefined (reading 'choices')。根因是响应体结构和代码预期不一致——可能是上游返回了错误对象而不是正常响应,也可能是流式和非流式响应混用。定位方法:把原始响应体打出来看,不要只看解析后的对象。常见触发场景是超时后上游返回了一个错误 JSON,而你的代码直接去取response.choices[0]。
修复思路有两个:一是在解析前先判断响应状态码和结构,二是把这类解析错误也纳入错误分类——它属于「自己的代码问题」,不该触发降级。
5.4 OAuth 相关报错
如果你用的是需要 OAuth 的接入方式(比如某些 CLI 工具的登录态),报错通常表现为 token 过期或刷新失败。这类问题的特点是「昨天还好好的,今天突然全挂」。定位方法是检查 token 的有效期和刷新逻辑,确认刷新失败时的降级行为——token 刷新失败属于凭据问题,同样应该 fail fast,而不是去试其他模型。
这里要强调一个通用原则:所有报错先分类,再决定降不降级。分类标准就一条——这个错误换一个模型能不能治好?能治好的(429、5xx、timeout)才降级,治不好的(401、400、解析错误、OAuth 失败)一律 fail fast。这条原则能帮你避开降级链里八成的坑。
5.5 配置片段:三件套写全
如果你用的是 Claude Code、Cline MCP 或 Codex 这类工具,配置里必须写全三件套:Base URL、Key、Model ID。少任何一个都会报错,而且报错信息往往指向别处。以 Claude Code 的 settings 为例:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-your-key-here", "ANTHROPIC_MODEL": "YOUR_MODEL_ID" } }三件套里最容易漏的是 Model ID。Base URL 和 Key 对了,但 Model ID 写了一个不存在的名字,报错通常是 404 或「模型不存在」,而不是 401,容易让人误以为是权限问题。写配置时把这三个值当成一个整体来核对,能省下大量排查时间。
6. 把这条降级链用起来
回到开头那张账单:问题不在于模型太贵,而在于我们把全部流量都当成了难题。这套方案的核心是把决策从「一次配死」变成「逐请求判断」——规则路由吃掉确定性高的大部分流量,实测轻档命中率能到 55%~65%;复杂度分类器啃掉剩下的模糊地带;降级链保证异常路径不塌;最后用指标做闭环验证。
四个关键结论值得记住。第一,收益的大头来自少数几条高置信规则,不必一上来就追求智能路由,先把「强格式抽取」「只读查询」这两类确定性最高的流量拉下来,收益就已经很可观。第二,风控优先于成本,不可逆操作永远走高配,这条规则的优先级要排在所有成本规则之前。第三,没有评测集就不要降档,质量是唯一的授权依据,靠感觉降档迟早被投诉打脸。第四,降级要区分错误类型,只治「对方的问题」,401 和 400 类错误必须 fail fast。
如果你还没开始,建议的动手顺序是:先去控制台创建一个 Key,用 curl 把三个模型单独调通;然后把上面的 provider 配置抄进去,先全部走中档跑一天拿到基线;再逐步加上规则路由,每加一条观察一天;最后配降级链,并手动制造一次故障验证它真的会切。整个过程不需要改业务代码,因为统一通道已经把「换模型」变成了「改一个字符串」。
下一步可以做的事:把评测集从几十条扩到能跑在 CI 里的回归测试,这样每次改路由规则都能自动验证质量没有下降。成本优化最尴尬的地方是省下的钱不会自己显形,而质量下降一定会被抱怨——所以你需要一套能说清的表述,把「本周请求量 / 基准成本 / 实际成本 / 质量指标」四列放在一起看,结论自然浮现。一个愿意给出回退方案的方案,比一个只说好处的方案可信得多。