蓝耘 MaaS:我把推理层重构成多模型路由,本以为能省 70%,实测只省了 9%——但学到了更值钱的三件事

📅 2026/7/31 22:00:57 👁️ 阅读次数 📝 编程学习
蓝耘 MaaS:我把推理层重构成多模型路由,本以为能省 70%,实测只省了 9%——但学到了更值钱的三件事

蓝耘 MaaS:我把推理层重构成多模型路由,本以为能省 70%,实测只省了 9%——但学到了更值钱的三件事

上周一早上 8:47,监控告警响了。我们的 AI 聊天服务 P99 延迟从 3 秒飙到 12 秒,同时月度推理账单比预期高了3.2 倍。排查一圈后发现:我们把所有流量——不管是「把用户反馈归类为 bug 还是建议」这种一句话任务,还是「写一个 LRU Cache」这种代码生成——全灌给了同一个旗舰模型。它贵、它慢、它在简单任务上疯狂烧 reasoning token 收着看不见的「思考税」。于是我们决定重构:在蓝耘 MaaS 的统一网关之上,加一层多模型路由中间件,按任务类型把流量分发给不同模型。我以为这能立刻砍掉 70% 成本。结果跑完基准测试,成本只降了8.7%,延迟反而涨了39%。但在这个过程中,我学到了三件比省钱更值钱的事。



第一幕:病根不在代码,在「单一模型信仰」

那个周一的告警

我们团队做的是一个内部 AI 助手(类似客服 + 知识库问答),接入的是蓝耘 MaaS 的 OpenAI 兼容接口(https://maas-api.lanyun.net/v1)。上线初期为了省事,所有请求都走同一个模型——当时选的是deepseek-v4-pro(旗舰版)。理由很简单:

「旗舰模型最强,一个模型打天下,不用维护路由逻辑。」

这个决策在日调用量 < 1000 次时没问题。但当量涨到日均 5 万次、且任务类型越来越杂(分类、摘要、代码生成、长文档分析)时,问题爆发了:

问题表现根因
账单爆炸月费是预期的 3.2x简单任务也在付「思考税」
P99 延迟飙升3s → 12s旗舰模型排队 + 长生成阻塞短请求
偶发 500高峰期错误率 2–3%单点故障无 fallback

用 usage 字段做「CT 扫描」

我之前写过一篇关于流式输出和 Token 陷阱的文章([《别只打印 content》](3-别只打印 content:蓝耘元生代 MaaS 流式输出、思维链与 Token 陷阱实战.md)),里面提到usage.completion_tokens_details.reasoning_tokens这个字段。当时只是当知识点记下来,这次终于派上用场了。

我在生产日志里加了一行:每次 API 返回后,把reasoning_tokenscompletion_tokens的比例打出来。结果触目惊心:

task=classify R/C=0.93 # 归类一句话,93% token 在"想" task=summarize R/C=0.74 # 两句话摘要,74% 在"想" task=code R/C=0.79 # 写代码确实要想想,但也太多了

一个「归类 bug 还是建议」的任务,模型花了 93% 的 completion token 在内部推理,只有 7% 真正输出了内容。这就是「思考税」——你为它付了钱,但它对你毫无价值。


第二幕:设计路由中间件

架构

核心思路很简单:

客户端请求 → API 网关 → Router(按 task_type 选模型) │ ┌─────────┼─────────┐ ▼ ▼ ▼ flash(快+有思考) V3.2(便宜零税) kimi(均衡) │ │ │ ┌─────┴─────┐ │ ┌────┴────┐ ▼ ▼ ▼ ▼ ▼ 成本护栏 Fallback PromptCache 可观测

四个横切关注点贯穿整个路由层:

  1. 成本护栏 / SLA:单请求 max_tokens 上限 + 月度预算熔断
  2. 故障转移 Fallback:主模型失败自动切备模型
  3. Prompt Cache:长 system prompt 统一前缀,命中缓存打两折
  4. 可观测 usage:每条请求落 reasoning/cached tokens,供成本看板

核心代码(可直接复制运行)

#!/usr/bin/env python3"""多模型路由中间件 · 生产级骨架"""importjson,os,timefromdataclassesimportdataclass,asdictfromopenaiimportOpenAI BASE="https://maas-api.lanyun.net/v1"# 路由表:task_type -> (主模型, 备模型)ROUTES={"classify":("deepseek-v4-flash","/maas/deepseek-ai/DeepSeek-V3.2"),"summarize":("/maas/deepseek-ai/DeepSeek-V3.2","kimi-k2.5"),"code":("/maas/deepseek-ai/DeepSeek-V3.2","deepseek-v4-flash"),"reason":("deepseek-v4-flash","/maas/deepseek-ai/DeepSeek-V3.2"),"longctx":("/maas/deepseek-ai/DeepSeek-V3.2","kimi-k2.5"),}@dataclassclassCall:task:str;model:str;fallback:bool;ok:boolsec:float;completion:int;reasoning:int;cost:float;note:str=""defask(client,model,text,max_tokens=400):t0=time.time()try:r=client.chat.completions.create(model=model,messages=[{"role":"user","content":text}],max_tokens=max_tokens,temperature=0.3,stream=False,)exceptExceptionase:returnNone,time.time()-t0,str(e)[:60]u=r.usage comp=getattr(u,"completion_tokens",0)or0ctd=getattr(u,"completion_tokens_details",None)rt=getattr(ctd,"reasoning_tokens",None)or0# 示例单价 ¥/M,以控制台为准pin,pout=(2,8)# deepseek-v4-flash 口径cost=(getattr(u,"prompt_tokens",0)*pin+comp*pout)/1_000_000.0returnr.choices[0].message.contentor"",time.time()-t0,None,comp,rt,costdefroute_one(client,task,text):primary,backup=ROUTES[task]content,sec,err,comp,rt,cost=ask(client,primary,text)ifcontentisnotNone:returnCall(task,primary,False,True,round(sec,3),comp,rt,round(cost,5))# 故障转移content,sec2,err2,comp,rt,cost=ask(client,backup,text)ifcontentisnotNone:returnCall(task,backup,True,True,round(sec2,3),comp,rt,round(cost,5),note=f"{primary}failed ->{backup}")returnCall(task,primary,False,False,round(sec+(sec2or0),3),0,0,0.0,note=f"both failed:{err}|{err2}")defmain():client=OpenAI(api_key=os.getenv("LANYUN_API_KEY"),base_url=BASE)tasks=[("classify","这条反馈归类为 bug/建议/咨询/闲聊?只回类别:"+"导出按钮点了没反应,控制台报错 500。"),("summarize","用两句话总结:团队决定 Q3 上线多模型路由。"),("code","写 Python LRU cache,带注释。"),("reason","为什么 P99 延迟比平均值更能代表 Agent 体验?"),("longctx","指出规范中最影响稳定性的三条:"+("接口必须有超时。"*30)),]calls=[route_one(client,t,txt)fort,txtintasks]ok=[cforcincallsifc.ok]print(f"成功{len(ok)}/{len(calls)}| "f"总成本 ¥{sum(c.costforcinok):.5f}| "f"Fallback{sum(1forcinokifc.fallback)}次")withopen("router_log.json","w")asf:json.dump([asdict(c)forcincalls],f,ensure_ascii=False,indent=2)if__name__=="__main__":main()

完整可运行版本含--force基线对比模式在 demo/maas_router.py。


第三幕:实跑数据——诚实的结果

终端原始输出

图:maas_router.py实跑——上半部分是多模型路由结果,下半部分是--force deepseek-v4-flash单模型基线对比。注意 code 任务走 V3.2 拖到 15 秒。

数据拆解

我把同 5 个任务跑了两次:

方式classifysummarizecodereasonlongctx合计均延
路由(routed)flash ¥0.00057V3.2 ¥0.00035V3.2 ¥0.00323flash ¥0.00325V3.2 ¥0.00328¥0.010687.79s
单模型(naive)flash ¥0.00042flash ¥0.00090flash ¥0.00324flash ¥0.00325flash ¥0.00391¥0.011725.59s
差值+¥0.00015-¥0.00055-¥0.00001=-¥0.00063-8.7%+39.3%

三张图看清真相

图:每个任务的「单模型 vs 路由」成本对比(¥/千次)。红色=全走 flash(naive),绿色=按类型路由(routed)。Y 轴标注了各任务在 flash 上的隐藏思考税占比。

诚实结论:

  1. summarize 和 longctx 在路由下更便宜(V3.2 零思考税,分别便宜 61% 和 16%)
  2. classify 反而更贵了(flash 本身就快,V3.2 不适合超短任务)
  3. code 和 reason 持平(两个模型在这类任务上消耗差不多)
  4. 总体只省了 8.7%,但平均延迟暴涨 39%(V3.2 在长生成上太慢)

这不是失败。这是真实。


第四幕:比省钱更值钱的三件事

如果我只看成本数字,这次重构是「亏本生意」——省了不到一毛钱,用户还觉得变慢了。但在复盘会上,我总结了三个远比账单数字重要的收获:

1. 成本不是单变量:路由是「成本 × 延迟 × 质量」的多目标优化

之前我们认为「便宜模型 = 好」。但 V3.2 虽然单价低、零思考税,在长文本生成上拖到 10–15 秒。对于对延迟敏感的场景(实时聊天、联机补全),慢 10 秒等于不可用。

正确的做法不是「全部路由到最便宜的模型」,而是建立一个多维度的模型画像表:

维度deepseek-v4-flashDeepSeek-V3.2kimi-k2.5qwen3.6-flash
单价(¥/M out)88124
思考税中(50–95%)0%0%极高(95%)
短任务延迟快(~2s)快(~2s)最快(~2s)快(~0.6s)
长生成延迟中等(~6–8s)慢(10–15s)中等中等(~5s)
适用场景推理/分析摘要/分类/长文分类/摘要❌简单任务慎用

有了这张表,路由策略变成了场景匹配而不是「谁最便宜」:

# 改进后的路由:加入延迟感知SMART_ROUTES={"classify":("kimi-k2.5","DeepSeek-V3.2"),# 最快 + 零税"summarize":("DeepSeek-V3.2","kimi-k2.5"),# 零税 + 够快"code":("deepseek-v4-flash","kimi-k2.5"),# 不要用 V3.2!太慢"reason":("deepseek-v4-flash","DeepSeek-V3.2"),# 需要适度推理"longctx":("kimi-k2.5","DeepSeek-V3.2"),# 比 V3.2 快得多}

教训:永远不要只优化单一指标。在生产环境中,成本、延迟、质量是一个三角 trade-off,路由的本质是在这个三角里找到你的最优解。

2. 思考税是隐形的——你必须主动测量才能看见

在我们加上reasoning_tokens监控之前,没有人知道「归类一条反馈」这种任务居然有 93% 的 token 在「空转」。因为:

  • 它不报错
  • 它返回的内容看起来正常
  • 它只是悄悄地让你的账单膨胀 10 倍

测量方法(已验证可用):

# 每次 API 调用后立即采集usage=response.usage comp=usage.completion_tokens ctd=usage.completion_tokens_detailsor{}tax_ratio=(ctd.get("reasoning_tokens",0)or0)/max(comp,1)iftax_ratio>0.7:alert(f"高思考税警告: task={task}tax={tax_ratio:.0%}model={model}")

把这个埋进你的 Prometheus / Grafana 看板,按task_type × model聚合。一周后你会看到一张 heatmap,告诉你哪些组合在烧冤枉钱。

3. 真正的护城河不是「选对模型」,而是「系统能扛住模型挂掉」

在这次重构过程中,我们还做了一个实验:故意把主模型名写成错的,测试 fallback 是否生效

# 把 primary 写成一个不存在的模型ROUTES["classify"]=("nonexistent-model-xxx","deepseek-v4-flash")# 结果:第一次调用 404 → 自动 fallback 到 flash → 用户无感

在生产环境中,模型抽风是常态:

  • 上游更新导致临时 500(我们在蓝耘上遇到过deepseek-v4-pro连续 500)
  • 限流 429(高峰期)
  • 模型下架 404(旧模型名失效)

没有 fallback 的系统,每一次上游抖动都是一次线上事故。有 fallback 的系统,上游抖动只是一条静默日志。

另外,Prompt Cache 也是护城河的一部分。虽然我们在上一篇文章中发现蓝耘 MaaS 大部分模型的缓存命中率是 0%(详见 《思考税》文章 的缓存深坑章节),但minimax-m3 是唯一命中的(cached=128)。这说明缓存能力因模型而异,路由中间件可以在选择模型时把「是否支持缓存」作为一个因子。


蓝耘在这条链路上的角色

坦白说,这次重构能做成,很大程度上依赖蓝耘 MaaS 的几个特性:

特性对路由中间件的价值
统一网关 (/v1)一个 Base URL 接入所有模型族(DeepSeek/Qwen/GLM/Kimi/MiniMax),切换模型只需改字符串,不改 SDK
OpenAI 兼容直接用openaiPython SDK,零适配成本;Anthropic 端点(/anthropic)还能让 Claude Code 直连
usage 字段透传reasoning_tokenscached_tokens完整透传,让成本归因成为可能
模型广场同一个 key 能调几十个模型,不需要逐个申请/配置
可靠性我们在重构期间连续跑了上百次调用,未遇到网关级 5xx(个别模型 500 是上游问题,被 fallback 兜住了)

特别是统一网关 + OpenAI 兼容这两点,让我们能在不改动任何业务代码的前提下,只在中间件层加了一个 Router 就完成了重构。如果是自建多模型服务,光是统一不同厂商的 API 格式就要花两周。


结尾:给同行者的三条建议

如果你也在考虑做多模型路由,或者正在为推理账单发愁,这是我踩坑后的三条建议:

  1. 先测量再优化。不要凭感觉选模型。跑一遍maas_profiler.py(代码在这里),拿到每个模型在你真实任务上的reasoning_taxdensity,用数据说话。
  2. 不要只看成本。建立「成本 × 延迟 × 质量」三维画像表。便宜的模型可能慢到你无法接受。
  3. Fallback 不是可选项。从第一天起就设计好降级路径。模型会挂、会限流、会下架——这不是如果,而是什么时候。

最后一句实话:路由中间件不会立刻帮你省 70%。它会帮你建立一个健康的、可观测的、有弹性的推理层。这才是长期来看真正值钱的东西。