双轨大模型路由架构:Ollama边缘推理与云端容灾降级实践
做AI应用落地这几年我踩过最大的坑就是高估了单一模型通道的稳定性。你把宝全押在云端API上白天好好的晚上高峰批量任务一上来限流、超时、报错轮着来把宝押在本机跑Ollama吧7B小模型碰上复杂推理任务回答质量又撑不住。后来我把两套通道都留着用一层路由在中间做调度和降级效果立竿见影。这篇文章就把这套双轨大模型路由架构从端侧Ollama边缘推理到云端大模型容灾降级的完整思路拆给你看适合正在做AI应用后端、私有化部署或者想给团队搭一条低成本又扛得住故障的推理通道的同学参考。1. 双轨路由到底解决什么问题1.1 单一模型通道的四个致命问题先说结论不管是纯本地还是纯云端单通道方案在真实生产环境里都撑不住这不是模型能力问题而是工程问题。第一个是可用性问题。云端大模型API本质上是公网服务你的业务请求要经过DNS解析、公网链路、云厂商网关中间任何一环抖动表现在业务侧就是超时、5xx、请求排队。我见过最离谱的一次云厂商某个节点升级导致大面积限流我们线上知识库问答直接挂了三个小时而那时候本地Ollama明明还闲着一半的算力。第二个是成本问题。云端token计费看着单价不高一旦业务量上来批量总结、批量审核、批量打标这类任务一个月账单能让你重新审视整个架构。而本地推理的电费几乎可以忽略不计GPU服务器是一次性投入。第三个是隐私合规问题。很多企业内部文档、用户数据、研发代码根本不允许明文出域。云端API即使承诺不用于训练法务和合规那边也很难点头。数据留在本机边界就清晰得多。第四个是质量问题。本地小模型能力天花板明显复杂推理、长文本生成、代码生成这类任务和云端旗舰模型差距肉眼可见。你说全用本地吧用户觉得你产品智商不在线全用云端吧成本和风险又压不住。所以问题的本质不是选哪个而是怎么让两条通道协同工作。1.2 双轨架构的基本形态这套架构的核心就一句话端侧跑Ollama做边缘推理云端接大模型API做高能力兜底中间加一层路由负责调度、熔断、降级。端侧这一轨Ollama负责管理本地模型的生命周期你只需要准备一台带GPU的机器或者一台内存足够大的Mac就能跑起来。它是你的低成本、低延迟、高隐私通道。云端这一轨接的是各种大模型API能力和稳定性由云厂商保障你只需要关注密钥管理和配额。它是你的高质量、高可用、高成本通道。路由层则是整个架构的大脑它不关心模型内部怎么实现只关心三件事当前请求该走哪条通道、目标通道是否健康、挂了之后怎么降级。分层之后两条通道的替换成本都很低今天用Ollama跑Qwen明天换Llama路由层完全无感。1.3 适用场景清单这套架构不是万金油但以下几个场景特别适合。企业内部知识库问答文档不许外传同时希望回答质量有保障批量数据处理任务比如每日几万条工单自动分类对延迟不敏感但对成本敏感对外API服务要求高可用不能因为上游抖动就整体不可用还有混合办公场景网络不稳定的分支节点需要本地兜底。判断你需不需要这套架构就看一个问题你的业务能不能接受模型通道长时间不可用如果能接受单通道够用如果不能双轨路由就是刚需。2. 端侧Ollama边缘推理落地要点2.1 Ollama部署与基础配置Ollama的安装本身不复杂Linux上一句curl脚本就能完成但生产环境使用有几个细节必须提前处理。第一是模型存储目录。Ollama默认把模型放在用户目录下如果你的系统盘不大装两个大模型就满了。建议在安装后立刻设置OLLAMA_MODELS环境变量把模型仓库指到大容量数据盘上这相当于你给模型文件单独划了一个仓库后续管理起来也清爽。第二是服务监听地址。默认Ollama只监听127.0.0.1本机调用没问题但如果你打算让多台机器共享这组GPU资源就需要设置OLLAMA_HOST0.0.0.0让服务暴露到内网。注意这里一定要放在内网环境别直接暴露到公网Ollama没有内置鉴权裸奔出去等于给别人送算力。第三是系统资源限制。Ollama默认会尽量用完所有显存和内存这在多人共用的机器上容易互相挤兑。建议用Systemd的CPUQuota和MemoryLimit做硬性限制或者在同一台机器上只跑Ollama一个服务避免和其他业务抢资源。2.2 模型选型与量化模型选型是边缘推理最关键的一步选大了显存放不下选小了效果不好。我自己常用的几个组合供你参考。通用对话场景Qwen2.5系列是首选7B和14B两个尺寸性价比很高代码场景推荐Qwen2.5-Coder或者CodeLlama系列如果机器实在弱考虑更小尺寸的量化版本比如Qwen2.5-3B虽然能力弱一些但处理简单分类、抽取任务完全够用。显存估算有个粗略公式模型参数量乘以每参数字节数。FP16精度下7B模型大概占14GB显存配合量化到INT4之后能压到5GB左右。所以一张24GB显存的消费级显卡跑7B量化模型还能留出上下文和并发的余量两张卡或者48GB的卡可以尝试14B模型。这里给个非常实际的建议边缘节点优先跑量化模型牺牲一点质量换取吞吐和显存容量因为你的路由层本来就是按能力分流的高难度问题可以丢给云端本地通道先把大量简单请求接住就算完成任务。2.3 边缘推理的调用方式Ollama的API设计得很简洁REST风格不需要SDK也能直接调。核心就两个接口一个是生成接口 /api/generate适用于纯文本补全另一个是对话接口 /api/chat适用于多轮对话场景内部会帮你管理上下文。实际调用时有几个参数值得注意。keep_alive参数控制模型在显存中的驻留时间默认5分钟如果你的业务是突发性的建议调大到30分钟以上避免每次请求都重新加载模型如果机器要跑多个模型又需要把keep_alive设小让显存及时释放。并发这块Ollama默认支持一定的并行请求但要注意并行数越高单请求延迟越大因为算力是共享的。生产环境建议在路由层控制并发把Ollama的并行度设置在比默认值更保守的水平优先保证单请求延迟稳定。3. 云端大模型接入与统一抽象层3.1 云端API的接入形态云端这一轨的接入核心是兼容OpenAI接口规范。现在主流的云厂商大模型API基本都提供了OpenAI兼容的HTTP接口这意味着你可以用同一套客户端代码只改base_url和api_key就能切换供应商。实际操作中我建议不要直接调厂商SDK而是用OpenAI官方SDK配合自定义base_url。这样做的好处是依赖面最小切换厂商时不用重新编译代码改个配置就行。模型名称在云端和本地可能会重名比如都叫qwen2.5但你需要在配置里把它们当成两个独立的endpoint对待因为它们的性能和成本完全不一样。云端通道还需要处理配额和限流问题。每个API Key都有每分钟请求数和token数的限制超了就会返回429。路由层要针对云端通道单独做一层速率限制避免并发峰值直接把配额打爆触发长时间的封禁。3.2 为什么需要统一抽象层很多团队一开始图省事代码里直接写两个客户端一个调Ollama一个调云端APIif else判断走哪条。这个方案在只有两三个调用点的时候能用但只要调用点一多你会发现问题很大。第一是重复代码。每个调用点都要写一遍超时设置、错误处理、重试逻辑改一处漏三处。第二是策略散落。路由决策散落在各个业务模块里今天这个模块想优先本地明天那个模块想优先云端各写各的维护成本爆炸。第三是无法统一观测。两条通道的耗时、成功率、成本散落在不同日志里想做统计报表非常困难。所以统一抽象层的价值不是显得专业而是把通道差异、路由策略、可观测性集中到一个地方管理。业务代码只面对一个模型客户端接口底层是本地还是云端业务根本不用关心。3.3 配置管理与密钥安全双轨架构的配置项比单通道多不少包括两套通道的endpoint地址、模型名称映射、超时时间、路由权重、熔断阈值等等。这些配置一定要集中管理不要散落在代码里。密钥管理是重灾区。很多同学图方便把云端API Key直接写在配置文件里甚至提交到代码仓库这是非常危险的。建议用环境变量注入或者接入公司的密钥管理系统至少也要做到配置文件和代码分离并在版本库里把敏感信息占位符化。我踩过的坑是之前把密钥写在一个公共配置中心里结果同事调试时直接打日志把整个配置打出来了密钥跟着日志进了ELK后来不得不轮换密钥还得排查有没有被第三方读取。密钥安全这件事出了事故才后悔就晚了。4. 双轨路由与容灾降级核心实现4.1 路由决策因子设计路由层最核心的就是决策逻辑。我这里的决策因子按优先级排序满足高优先级条件就锁定通道否则继续往下判断。第一优先是硬性路由条件。比如数据合规要求某个请求涉及敏感数据强制走本地或者某个请求明确要求使用特定模型能力强制走云端。这类条件是业务强约束不能被其他因子覆盖。第二优先是可用性状态。如果目标通道处于熔断或降级状态直接跳过不用纠结这条通道质量多好。两条通道都挂了怎么办后面单讲兜底策略。第三优先是质量要求。业务方可以在请求里带一个quality参数高就强制走云端低就优先走本地。这个参数让业务方有话语权同时把决策权集中在路由层。第四优先是成本和延迟。对质量不敏感的批量任务计算token成本云端成本乘以预估token数如果超出预算阈值就走本地对延迟敏感的用户交互类请求如果本地空闲优先本地因为边缘推理省去了公网往返。最后是权重兜底。如果前面所有条件都不明确就用加权随机或者轮询的方式分配流量让两条通道都有健康流量避免空跑导致的假死。4.2 健康检查与熔断机制路由层必须实时掌握两条通道的健康状态这是容灾的基础。健康检查分两个层面。第一个层面是主动探活。每隔一段时间比如10秒路由层向Ollama发起一个极简的探测请求比如一个5token的生成任务云端通道则调用一个轻量的模型接口。连续探活失败达到阈值就标记通道不可用。第二个层面是被动熔断也就是熔断器模式。请求失败时计数连续失败达到阈值比如5次熔断器打开后续请求直接快速失败不再发起真实调用。熔断器打开后进入冷却期冷却期结束后进入半开状态放少量试探流量成功率达到要求就关闭熔断否则继续保持打开。这里有个容易踩的坑健康检查请求本身也会消耗资源如果探测频率太高Ollama的小模型一直在忙推理真正业务请求反而排队。我的经验是探测请求用单独的小模型比如一个几百MB的embedding模型探测目标是通道服务活着不是模型推理能力正常这样成本最低。4.3 降级链路设计与兜底策略降级链路的设计原则是从高能力通道往低能力通道降从高成本通道往低成本通道降同时预留业务降级出口。正常链路是这样优先按路由决策走云端和本地都健康时按质量要求分流。云端熔断时原来走云端的请求直接降到本地同时把降级事件记入日志和监控指标。本地熔断时原来走本地的请求升到云端代价是成本上升但至少服务不中断。两条通道都不可用时就是终极兜底。我的方案是请求排队和缓存复用。简单请求读取历史相似问答的缓存结果不依赖实时模型复杂请求进入本地队列等通道恢复后补跑同时给调用方返回一个明确的降级提示。这个兜底的核心价值是保证系统不雪崩哪怕回答延迟也比直接报错强。4.4 路由核心代码骨架下面给一个Python版本的路由核心骨架不依赖特定框架你用FastAPI、Flask还是纯函数都能套。import time import threading from dataclasses import dataclass from enum import Enum class Channel(Enum): EDGE edge # 本地Ollama CLOUD cloud # 云端大模型API dataclass class ChannelStatus: available: bool True consecutive_failures: int 0 circuit_open_until: float 0.0 class DualTrackRouter: def __init__(self, edge_client, cloud_client, fail_threshold5, cooldown30): self.edge edge_client self.cloud cloud_client self.edge_status ChannelStatus() self.cloud_status ChannelStatus() self.fail_threshold fail_threshold self.cooldown cooldown self.lock threading.Lock() def _record_success(self, channel: Channel): with self.lock: st self.edge_status if channel Channel.EDGE else self.cloud_status st.consecutive_failures 0 st.available True def _record_failure(self, channel: Channel): with self.lock: st self.edge_status if channel Channel.EDGE else self.cloud_status st.consecutive_failures 1 if st.consecutive_failures self.fail_threshold: st.available False st.circuit_open_until time.time() self.cooldown def _maybe_recover(self, st: ChannelStatus): # 冷却期结束进入半开状态放试探流量 if not st.available and time.time() st.circuit_open_until: st.available True def _decide(self, request) - Channel: # 1. 硬性约束 if request.get(force_edge): return Channel.EDGE if request.get(force_cloud): return Channel.CLOUD # 2. 可用性过滤 candidates [] with self.lock: self._maybe_recover(self.edge_status) self._maybe_recover(self.cloud_status) if self.edge_status.available: candidates.append(Channel.EDGE) if self.cloud_status.available: candidates.append(Channel.CLOUD) if not candidates: raise RuntimeError(all channels unavailable) # 3. 质量要求 if request.get(quality) high: return Channel.CLOUD if Channel.CLOUD in candidates else Channel.EDGE if request.get(quality) low: return Channel.EDGE if Channel.EDGE in candidates else Channel.CLOUD # 4. 成本/延迟策略这里简化为优先边缘 return Channel.EDGE if Channel.EDGE in candidates else Channel.CLOUD def chat(self, messages, **kwargs): channel self._decide(kwargs) try: if channel Channel.EDGE: resp self.edge.chat(messages, **kwargs) else: resp self.cloud.chat(messages, **kwargs) self._record_success(channel) return resp except Exception: self._record_failure(channel) # 降级当前通道失败换另一条通道再试一次 fallback Channel.CLOUD if channel Channel.EDGE else Channel.EDGE try: if fallback Channel.EDGE: resp self.edge.chat(messages, **kwargs) else: resp self.cloud.chat(messages, **kwargs) self._record_success(fallback) return resp except Exception: self._record_failure(fallback) raise这段骨架已经把决策、熔断、降级的主干逻辑写清楚了生产环境你还需要加超时控制、重试上限、链路追踪trace_id、以及降级事件的监控埋点。特别注意熔断恢复的逻辑半开状态只放少量请求试探不要恢复瞬间把全部流量压回去否则通道刚恢复又被压垮形成抖动循环。5. 常见问题与排查技巧实录5.1 Ollama下载模型太慢怎么办这是本地化部署最普遍的问题尤其在国内网络环境下拉取模型仓库经常卡住不动。很多人的第一反应是反复重试但结果往往是治标不治本。我实际验证过比较稳的方案有两个。一个是配置镜像源国内有一些社区维护的模型镜像站把OLLAMA的模型源指过去速度提升非常明显另一个是离线导入在有条件的下载环境下先把模型文件完整拉下来然后通过Ollama的导入命令手动加载到目标机器。这里还要提醒一个细节下载中断后Ollama会保留部分下载文件但有时会占用磁盘空间不释放。排查的时候可以看看模型目录下的临时文件确认有无残留。5.2 并发一上来Ollama就卡死本地推理最怕并发突增。表现是请求排长队甚至Ollama进程CPU打满后无响应。很多同学以为是机器性能不够其实大部分情况是并发配置和模型加载策略不对。排查顺序建议这样来。先看Ollama的并发参数不同版本支持的并发策略不一样需要确认num_parallel是否被限制为1如果是改为合适的值再看是否加载了多个模型每切换一次模型都要换进换出显存这在并发场景下是致命的最后看显存是否被打满用监控命令查看显存占用如果接近上限就把keep_alive调短或者减少并行数。我采用的兜底办法是在路由层加信号量限制同时打到本地的请求数比如只允许4个并发剩下的在路由层排队。宁可排队也不要让Ollama在重压下彻底假死因为假死后的恢复时间比排队时间长得多。5.3 本地和云端回答风格不一致双轨架构上线后业务方最常吐槽的问题就是同一个问题有时候回答很专业有时候回答像实习生。这不是模型坏了而是两条通道的模型能力和风格差异。解决思路是尽量抹平差异。首先是系统提示词完全一致写死成公共配置其次是生成参数保持一致temperature、top_p、max_tokens等参数必须在路由层统一设置不能云端用一套本地用一套最后是接受能力差异通过质量路由把复杂问题固定到云端简单问题固定到本地避免同一类问题在两条通道间随机跳变。实践下来即使做了这些用户还是可能感受到差异所以产品层面可以显示回答来源比如角标显示本机模型或云端模型让用户对回答质量有预期这比被无征兆地精分体验好得多。5.4 熔断误判与恢复抖动熔断机制如果阈值设得太低云端偶发的一次超时就会触发熔断导致大量本可以正常处理的请求被降级到本地质量下降。如果阈值设得太高通道已经挂了影响面就大了。这个度需要根据线上数据持续调。我前端时间就被误判坑过一次。某云厂商在凌晨做例行升级个别请求耗时突然从1秒涨到40秒我们当时熔断阈值是连续3次失败结果4点多的批量任务把云端通道熔断了。等云厂商恢复本地小模型已经在扛大流量回答质量明显下降早上用户投诉量直接翻倍。后来我的调整方案是熔断失败阈值提高到连续5次且加入错误码白名单只有5xx和超时才计数4xx这种客户端错误不参与熔断冷却时间设置为30秒以上并且熔断恢复后不立即全量放量而是先放10%的流量观察2分钟稳定后再逐步放开。写在最后的经验这套双轨路由架构我前后迭代了差不多四个版本从最开始的if else硬编码到现在的独立路由服务最大的体会是架构的价值不在方案多炫酷而在故障发生时系统还能不能撑住。本地和云端不是二选一而是互补的左右手路由层就是那个根据情况决定出哪只手的调度员。如果你正好也在搭类似的混合推理架构建议从最小闭环开始先让路由层跑通再逐步加熔断、加监控、加自动恢复不要一上来就追求功能完备。最后再分享一个小技巧给每条通道的每一次请求都打上channel标签和trace_id后续不管是排障、做成本核算还是调路由权重你都会感谢现在的这个决定。