多模型AI网关的容灾与智能路由实践
多模型网关的容灾、熔断与智能路由实践给一个错题拍照三秒内拿到分步解析、知识点拆解和薄弱环节诊断——这听起来像一个调一下 OpenAI API 就能做出来的功能。但当模型供应商开始限流、超时、返回脏数据当一个班级的学生在同一秒按下拍照按钮调一下 API和一个可用的产品之间隔着一整套工程实践。本文聊一聊我们在一个 AI 教育产品里是如何把调 API这件事做成一个生产级 AI 网关的。一、问题的起点单点模型 单点故障最早的版本很朴素前端拍照 → 后端转发给某家视觉大模型 → 拿到 JSON → 渲染。上线第一周就踩了所有该踩的坑限流与超时高峰期模型方 429、504 是家常便饭前端只能转圈圈到天荒地老。JSON 不稳定模型偶尔任性把 JSON 包在 markdown 代码块里或者中途截断前端解析直接崩。成本失控所有题目不管难易都走最贵的 pro 模型简单题也在烧钱。迁移成本高换一家供应商要改一遍代码迁移期间还得双跑对齐。这些问题的共性是把模型当成了一个可信、稳定、廉价的函数调用。现实里它更像一个偶尔会抽风的同事——能力强但你不能把整个业务押在他一个人身上。于是我们做了一个决定在后端和真实模型之间架一层多模型 AI 网关。它对外暴露统一的接口对内负责多供应商路由、容灾、熔断、结果修复。本文讲的就是这一层的设计。二、核心抽象能力Capability与提供方Provider网关要管理的第一件事是谁能为某个请求服务。我们把这件事拆成两个概念能力Capability业务上的一类需求比如multimodal多模态视觉理解、format-json把脏文本修成合法 JSON、erase-handwriting擦除试卷上的手写笔迹。提供方Provider某个具体的供应商实现比如 volcengine、deepseek它属于某个能力有自己的优先级。一个 provider 可以同时归属多个能力一个能力下挂多个 provider。这样一个二维表就是路由的全部基础CapabilityProviders[multimodal] [ {Name: deepseek-v4-flash,Priority: 1, Role: flash}, {Name: volcengine, Priority: 2, Role: flash}, ... ]启动时从一份providers.json配置里读出所有 provider用一个工厂函数按type字段openai-compatible/volcengine/textin-erase…创建对应的服务实例注册进两张表一张按 name 索引直接指定时用一张按 capability priority 排序自动路由时用。同一批对象两个视角。这个抽象的收益立竿见影新增一家供应商只需要在配置文件里加一条 JSON零代码改动。三、容灾的本质一次按优先级试一遍的循环容灾的实现出乎意料地直白。一次auto模式的请求核心就两步第一步构建一张待执行函数表。遍历该能力下所有 provider为每个 provider 生成一个闭包此时不执行只是把如何调用它封装起来fnMap[prov.Name]func()(interface{},error){returnr.callMethod(prov.Service,method,args...)}这里有个 Go 闭包的经典坑循环变量捕获。如果在range里直接用循环变量p所有闭包会共享最后一个p。必须用prov : p拷一份局部变量否则最后所有 fn 都会去调用同一个 provider。这种 bug 不会在低并发下暴露一旦上线就诡异得很。第二步按优先级逐个尝试谁先成功就用谁for_,provider:rangeproviders{fn:fnMap[provider.Name]result,err:fn()iferr!nil{pm.MarkFailure(provider.Name,err)// 记录失败continue// 继续下一个}pm.MarkSuccess(provider.Name)// 重置失败计数returnresult,provider.Name,nil// 成功立即返回}就这么几行但每一个细节都值得抠短路返回第一个成功的就 return后面的 provider 完全不会被调用省 token、省时间。失败可区分我们定义了一种ContentEmptyError表示模型正常响应了但说这题它不会——这是语义上的空结果不是服务故障不应该触发熔断计数。把服务挂了和题目太难区分开是这种系统里很容易忽略的一点。降级排除调用方可以传一个excludeNames列表告诉 Execute这些 provider 我已经试过且失败了别再试。这在智能路由的重试循环里很有用避免重复尝试已知会失败的 provider。四、熔断连续失败三次请你去休息光有试下一个还不够。如果一个 provider 已经在持续报错比如 API Key 过期、额度用尽每次请求都先去试它、等它超时、再 fallback是对用户体验和成本的双重浪费。于是引入熔断每个 provider 维护一个连续失败计数FailCount。失败一次 1成功一次归零。FailCount 3时标记Disabled true后续请求在选择阶段就直接跳过它不再发起真实调用。熔断状态会持久化到磁盘data/provider-status.json服务重启后保持熔断需要人工确认恢复POST /api/admin/providers/:name/reset。为什么熔断后不自动恢复而是要人工介入因为连续失败三次通常意味着配置级别的硬故障Key 失效、套餐到期自动半开重试只会再次拖慢线上请求。把恢复的决定权交给运维配合一套管理 API查看状态、手动禁用、动态调优先级反而更稳。熔断触发时还会发一条告警短信让你在用户投诉之前就知道哪家供应商又出幺蛾子了。一个性能细节防抖持久化熔断状态每次变更都要写文件吗高并发下一个 provider 在 100ms 内可能失败十几次每次都写文件会有磁盘 I/O 尖峰。我们用了一个**防抖debounce**机制状态变更后2 秒内若再有变更则取消上一次的写、重新计时。最终一次写文件落下最新状态。这里还有一个并发安全的坑定时器在触发前可能被新的定时器替换触发时要校验map 里存的还是不是我这个 timer否则旧 timer 可能在被 Stop 之后仍触发一次写操作Go 的Timer.Stop()不保证已入队的回调不执行。这种竞态在压测时才会暴露。五、智能路由让简单题走便宜模型容灾解决的是可用性智能路由解决的是成本与延迟的平衡。一个观察很多错题其实是简单题难度 1-3用 flash 模型又快又便宜只有难题、带几何图的题才需要 pro 模型仔细分析。如果一刀切用 pro成本扛不住一刀切用 flash难题解析质量又不够。于是我们在ai-analyze拍题分析这个核心接口里插了一个两阶段流水线拍题图片 │ ▼ [阶段一难度评估] 用一个 flash 模型快速看一眼图片 │ 输出{ level: 1-10, hasImage, handwriting, subject, question, questionCount } │ ▼ [路由决策] 根据难度和是否有题图决定阶段二用 pro 还是 flash │ level 4 且无辅助图 → flash │ 其他难题 / 带图 → pro │ ▼ [阶段二深度分析] 用选中的模型配合对应 prompt输出完整解析几个值得一提的设计题目计数校验。评估阶段顺便数了图片里有几道题。questionCount 1返回 1005提示用户一次只拍一道题 0返回 1006。在模型分析之前就把无效请求挡掉省一次昂贵的 pro 调用。手写感知。评估阶段还检测图片里有没有学生的手写过程。如果有阶段二自动切换到一个含薄弱环节分析的专用 prompt除了给标准答案还会分析学生在哪一步思路断了——这是错题本产品最有价值的部分不是给答案而是诊断为什么错。预提取回填。评估阶段的 flash 模型其实已经顺手把subject和question抽出来了。阶段二就不必再重复抽取这两项prompt 可以更精简lite 模式既省 token 又更快。这种一次评估多处复用的思路是把成本压下来的关键。角色匹配 降级。路由决策时先在角色匹配要 pro 就找 pro的 provider 里选最高优先级的如果一个匹配的都没有降级到任意可用 provider。可用性优先于成本优化——找不到便宜的贵的也得顶上不能让用户拿不到结果。六、和脏数据搏斗LLM JSON 修复流水线LLM 返回的 JSON 是不可靠的。我们见过的情况包括但不限于把 JSON 包在 json 代码块里、字段名拼错、中文引号、中途被 max_tokens 截断、把数学公式写成不合法的转义……前端直接JSON.parse必崩。我们的做法是多层防御鲁棒解析。先用一个宽容的解析器剥代码块、修引号、补尾逗号、尝试提取最大的合法 JSON 片段。这一层能解决 80% 的问题。LLM 自修复。如果本地解析失败再调用一个format-json能力的模型DeepSeek 或千问走同样的容灾把原始脏文本 上次的错误信息 上次的失败输出喂给它让它修。最多重试 3 轮每轮把上一轮的错误反馈回去。这种用模型修模型的输出是真实业务里非常实用的兜底。空结果降级。对于知识点薄弱分析这类接口热力图部分是纯本地统计不依赖 LLMLLM 只负责生成根因分析/学习路径/策略建议等增值内容。LLM 挂了的时候热力图照常返回其余板块降级为status: insufficient。核心数据永远在AI 锦上添花——这是面对模型不稳定时的正确姿势。七、流式输出SSE 与客户端断开解题过程如果一口气返回用户要盯着 loading 看 5-10 秒流式输出SSE让答案一个字一个字地打出来体感快得多。但流式有个麻烦没法做故障转移。非流式请求失败了可以换下一个 provider 重试流式一旦开始往外吐字节中途失败就只能告诉用户出错了没法默默换一家继续已经发出去的 chunk 收不回来。所以我们的流式接口走的是另一套策略开始前选一家。优先选 flash低延迟、首字快按角色和优先级挑一家可用的 provider。监听客户端断开。用context监听连接关闭用户关了页面就立刻取消对模型方的请求不浪费上游额度。流式数学公式标准化。模型输出的 LaTeX 可能是\(...\)或\[...\]前端要的是$...$和$$...$$。在流式场景下这事更棘手你不能等到全部输出完再替换那样就失去流式意义了但\(和\[又是两个字符可能跨 chunk。我们实现了一个流式标准化器维护一个状态机缓冲未决的反斜杠逐 chunk 处理。八、一些非功能性但救命的设计共享 HTTP 连接池。所有 provider 共用一个http.Transport200 连接池和两个http.Client一个非流式带 300s 超时一个流式不带超时自己控制。避免每个 provider 各起一套连接减少句柄和 TLS 握手开销。并发限制。中间件层用原子计数器限制全局并发请求数比如 50超出直接返回 429。防止高峰期模型方还没熔断自己的服务先被内存撑爆。HMAC 签名认证。业务接口用HMAC-SHA256做请求签名防止接口被刷。可配置开关调试期间可以关掉。优雅关闭。监听 SIGINT/SIGTERM等待正在处理的请求完成超时时间为出站超时 30s再退出。AI 请求动辄十几秒硬杀会丢一堆正在跑的推理。结构化日志 滚动。用 Go 1.21 的slog做结构化日志配合lumberjack按大小/时间轮转。线上排错靠的是日志里的 trace不是println。九、回到前端把模型能力渲染成产品后端把脏活累活干完了前端的任务是把那些结构化结果变成学生能看懂的东西。这部分我们用了 Vue 3 Vite Element Plus Tailwind 的组合技术选型平平无奇但有几个点值得说KaTeX 渲染数学。解析步骤里全是公式KaTeX 比 MathJax 快一个量级首屏渲染不掉帧。配合后端的数学公式标准化前后端约定统一的 LaTeX 分隔符。Mermaid 流程图。后端会基于解题步骤用一个 format 模型生成 Mermaid 流程图代码前端直接渲染成可视化流程图。学生看到的是第一步→第二步→得出答案的图而不是一坨文字。ECharts 薄弱星图。知识点薄弱分析返回的热力图数据前端用 ECharts 画成薄弱星图——重灾区的知识点又红又大一眼就知道该补哪。学习路径的依赖关系也用 Mermaid 有向图渲染知识点按严重程度核心重灾区/重点关注/需巩固着色。这些都是把 AI 的结构化输出翻译成学生能用的产品形态。后端再聪明如果前端只是一坨 JSON 文本价值就少了一半。十、写在最后把调 API做成网关值吗回头看我们从直接调模型演进到自建 AI 网关多写了几千行 Go 代码多了熔断、路由、修复、持久化一堆机制。值吗对一个玩具 demo 不值对一个真实在跑、有付费用户、有高峰流量的产品值。因为可用性。任何一家供应商挂了用户感知不到——网关自动切到下一家。我们做过统计某个高峰日里有一家 provider 触发了熔断但因为容灾当天的失败率依然接近零。成本。智能路由让简单题走 flash 模型整体 token 成本降了相当可观的一个比例。可演进性。换模型、加模型、调权重改配置 重启即可不需要发版改代码。在这个模型迭代飞快的年代这个灵活性是真金白银。如果你也在做一个依赖外部 AI 模型的产品强烈建议在后端和模型之间留出这么一层抽象。它不一定一开始就要这么完备但能力 / 提供方的二维抽象、优先级 容灾循环、JSON 修复兜底这三件事越早做后面越省心。猫头鹰AI错题本https://www.aicuoti.cn/大家可以去多试试。这篇就先到这儿。