1. 为什么需要双轨大模型路由架构
1.1 端侧推理的吸引力与天花板
这两年做大模型应用,大家应该都有一个共同感受:模型能力提升很快,但真正把模型用起来、放进生产环境,难点根本不在模型本身,而在"怎么稳定地提供服务"。
我最早做AI应用时,习惯性把一切都丢给云端API,代码写起来确实省事,一行HTTP请求就能拿到结果。但上线跑了一段时间,问题陆续浮出水面:网络抖动导致超时、云端服务偶发限流、高峰期排队变慢、单个请求成本在长上下文场景下高得离谱。最麻烦的是,有些场景根本不适合把数据发到云端——内部文档分析、本地业务数据问答、隐私敏感的处理流程,数据出网这一关就过不去。
于是我开始认真考虑端侧推理。Ollama这类工具把本地部署大模型的门槛拉到了历史最低点,以前要配CUDA、编译依赖、折腾半天才能跑起来的模型,现在一条命令就能下载并启动,而且它默认暴露的API接口和OpenAI兼容,改两行base_url就能接进现有代码。端侧部署的直观好处是:零网络延迟、零API费用、数据不出设备、离线也能用。
但端侧推理也有明显的天花板。首先是硬件约束,公司配的办公笔记本、NUC小主机、树莓派这类设备,显存和内存都有限,跑不了太大的模型。其次是能力上限,端侧能流畅跑的7B、8B级别模型,在某些任务上——尤其是复杂推理、长文生成、代码生成——和云端的大参数模型差距是肉眼可见的。最后是稳定性,本地进程偶发崩溃、模型加载失败、内存不足,没有人给你做SLA保障。
1.2 云端API的依赖困境
云端API的优势恰恰补足了端侧的短板:模型能力天花板高、服务稳定性由平台方负责、算力弹性大。但云端依赖带来的问题也很现实,我在生产环境里踩过几次之后才意识到,单点依赖云端API是一件风险很高的事情。
举一个实际例子。有一次上午十点,业务方反馈所有AI功能都不可用,排查下来是云端API服务端发生了故障,报错持续了将近四十分钟。那四十分钟里,我们整个AI模块就像断了一条腿,所有请求全部失败。平时没人觉得API也会挂,但它真的会挂,而且你完全不可控。类似的还有:某个大版本模型灰度期间输出突然变慢、免费额度的吞吐被限流、夜深人静时偶发的500错误。这些不稳定因素叠加起来,让我意识到一件事——生产环境里的AI能力不能只有一条路。
1.3 双轨设计的核心目标
所谓双轨大模型路由,简单说就是让系统同时具备两条推理通道:一条是端侧Ollama服务作为"第一优先轨道",负责能搞定的请求;一条是云端大模型API作为"备份轨道",负责端侧搞不定的复杂请求,或者在端侧不可用时接管全部流量。
这个架构的核心目标不是用端侧替代云端,也不是用云端兜底端侧,而是在两者之间建立一套自动化的调度逻辑——根据请求类型、模型能力、硬件负载、网络状况、成本预算,动态决定每个请求走哪条路。当一条轨道出问题时,系统能自动切换,用户几乎无感知。
我见过很多团队的做法是手工切换:端侧挂了就手动改配置指向云端,云端限流了就再改回来。这种方案在演示环境没问题,但在生产环境是灾难。因为故障发生的时刻往往是你最忙乱的时候,手工切换的速度永远赶不上故障扩散的速度。所以双轨路由必须做成自动化,而且要带有容灾降级能力——不只是切换轨道,还要在极端情况下降低服务规格,保证核心功能不中断。
2. 端侧Ollama边缘推理的落地细节
2.1 Ollama部署与基础配置
Ollama的部署本身没什么难度,跨平台支持做得很好,Windows、macOS、Linux都有对应的安装方式。但我建议生产级使用还是部署在Linux环境,无论从稳定性还是从远程管理角度都更可控。安装完之后,有几个基础配置值得认真对待。
第一个是模型存放路径。Ollama默认把模型放在用户目录下,我见过不少人把系统盘撑爆的案例。合理的做法是把模型目录迁移到独立的数据盘。Linux下通过设置OLLAMA_MODELS环境变量实现,Windows下安装到D盘也是同样的思路。实际操作时,先创建目标目录,再把原有的模型文件迁移过去,最后配置环境变量并重启服务。这一步看起来不起眼,但能避免后面"磁盘满了导致模型加载失败"的尴尬。
第二个是服务监听配置。默认情况下Ollama只监听127.0.0.1,如果边缘节点是一个独立的服务器,需要允许局域网内的其他设备访问,就要设置OLLAMA_HOST=0.0.0.0。这里要特别提醒:暴露到局域网意味着如果没有访问控制,任何能触达这个端口的人都能调用你的模型服务。稳妥的做法是在前面加一层网关或防火墙规则,只允许特定内网IP访问。
第三个是并发参数。Ollama默认的并发能力受硬件限制,可以通过OLLAMA_NUM_PARALLEL控制并行请求数,OLLAMA_MAX_LOADED_MODELS控制同时加载的模型数量。这些参数需要根据实际硬件压测来定,不是越大越好。
2.2 模型选型:边缘侧到底该跑什么模型
端侧跑什么模型,是整个架构里最需要花心思的决策。我给的选型原则是三条:硬件跑得动、任务够得着、生态跟得上。
先说"硬件跑得动"。端侧设备千差万别,如果目标设备只有CPU没有独显,那就老老实实选2B~4B的小模型;如果有8GB左右的显存,7B~9B的量化模型比较合适;如果内存32GB以上但没独显,可以靠CPU推理配合大内存跑大一点的模型,但速度要做好心理准备。我的经验是:不要看模型参数量的绝对值,要看量化后的实际占用,目测占用不超过可用显存的80%才比较稳妥,否则并发一上来很容易OOM。
再说"任务够得着"。这一步要根据你的业务场景选。我维护的这一套系统主要做三类事情:短文本分类和信息抽取、基于知识库的检索问答、代码片段生成与解释。前两类用7B模型完全能胜任,第三类如果要求高质量,7B会吃力。所以我在边缘节点上同时准备了两个模型:一个3B级别的小模型处理简单任务,一个7B级别的模型处理复杂任务。两个模型按需加载,避免始终占用大模型的内存。
最后是"生态跟得上"。Ollama支持从官方模型库拉取模型,国内用户如果下载速度不理想,可以配置镜像源或使用一些加速通道。不过我更关注的是模型本身的授权协议和更新节奏,选择社区活跃、迭代频繁的模型,后面遇到问题有人排雷。我目前的主力选择是Qwen系列和Llama系列,前者对中文支持好,后者生态工具链完整。
2.3 端侧推理的性能调优
端侧推理的性能优化,核心是找到硬件利用率和响应速度的平衡点。
CPU推理场景,关键参数是线程数。Ollama底层用的推理引擎支持设置线程数,默认配置不一定是当前机器的最优值。我实测过一台12核的Linux服务器,默认线程数跑7B模型出token速度大概在8~10 token/s,手动把线程数调到物理核心数之后提升到12~14 token/s,收益明显。注意是物理核心数,不是逻辑线程数,超线程对这类计算密集型任务的加成有限。
GPU推理场景,关键参数是num_gpu和上下文长度。如果显存足够把模型全部加载进GPU,速度会非常理想;显存不足时,Ollama会把部分层放在CPU上跑,混合推理速度会明显下降。这时候有两个选择:换更小参数的模型,或者加载更低的量化精度。我个人倾向于保持模型精度不变,优先换小模型,因为量化精度下降带来的质量损失在语言任务上的表现很难预估。
上下文长度是另一个需要克制的地方。大上下文意味着KV Cache占用更大,显存消耗呈线性增长。很多人喜欢把上下文设到32K甚至更大,但在端侧硬件上这通常会挤占模型本身的加载空间,反而拖慢速度。我一般根据业务需求来定,纯短文本任务8K足够,带知识库检索的回答任务16K基本覆盖绝大多数场景。
实操中有个容易被忽略的细节:Ollama会把模型常驻在内存/显存中一段时间,如果模型长时间未被调用,下次请求时重新加载会明显变慢。对于生产环境,建议在路由层加一个预热机制——定期发送一个极小请求,让模型保持热状态。这个技巧很小,但对请求毛刺的改善非常有效。
3. 云端大模型接入与容灾设计
3.1 云端API的统一适配层
接入云端大模型API,第一步不是写业务代码,而是构建一个统一适配层。市面上的云端大模型服务接口风格接近OpenAI规范,但细节差异很多:鉴权方式、超时配置、错误码语义、请求体字段都有区别。如果业务代码直接调用具体厂商的SDK,后面切换或增加厂商时,改动面会非常大。
我的做法是定义一个统一的大模型调用接口,抽象出几个核心方法:文本生成、对话补全、流式调用、模型能力查询。每个云端厂商实现一套适配器,实现这些接口。业务层只跟统一接口打交道,不感知底层用的是哪家服务。这样带来的直接好处是,容灾切换时不需要改业务代码,只需要在适配层调整路由规则。
统一适配层还需要统一做的事包括:请求重试策略(哪些错误码值得重试,哪些重试没用)、超时管理(连接超时、读取超时要分开设置)、调用日志记录、成本统计。这些横切关注点放在适配层统一处理,比散落在业务代码里要清晰得多。
3.2 容灾降级的三种模式
双轨架构的容灾降级设计,我把它拆成三个层次,每一层解决不同的问题。
第一层是请求级降级。这是最细粒度的容灾策略。具体逻辑是:当一个请求发往首选轨道——无论是端侧还是云端——在超时或明确失败后,自动改发往备用轨道。举例来说,如果路由规则决定某个请求先走端侧Ollama,但端侧服务响应超过5秒未返回,系统自动把这个请求转发到云端API。这里的核心是超时阈值要设置合理,太短会导致大量请求被不必要地转发到云端,太长则影响用户体验。
第二层是服务级熔断。如果某个轨道的失败率在一段时间内持续超过阈值,就临时关闭这条轨道,让所有流量走另一条轨道。这和微服务架构里的熔断器思路一致。我采用的方法是滑动窗口统计,统计最近一分钟内请求的失败率,超过30%就触发熔断,熔断持续30秒后再试探性放行一小部分流量,看服务是否恢复。之所以要有这个机制,是因为请求级降级只能解决单个请求的失败,解决不了"整条轨道都挂了"的情况——如果端侧服务因为模型损坏而持续崩溃,每个请求都先触发一次端侧请求再降级,只会带来无意义的延迟。
第三层是能力级降级。当两个轨道都不可用或者资源极其紧张时,系统不能直接报错,而是降低服务规格。常见的做法是:从大模型降级为规则引擎或搜索匹配、从复杂任务降级为简单任务、返回缓存的历史答案而不是实时生成。这一层其实和业务强相关,需要根据你的实际场景设计。我在这套系统里做了一个简单的策略:端侧和云端都不可用时,自动切换到基于关键词的兜底问答,保证核心业务不白屏。
3.3 健康检查与定时巡检机制
容灾的另一个关键点是主动健康检查,不能等到请求失败才发现轨道挂了。我在双轨架构里加了一个巡检任务,每隔30秒做一次探活请求。
端侧探活的逻辑比较简单:请求Ollama的/api/tags接口,能正常返回模型列表就认为服务在线。这个接口开销极小,不像一次完整推理那样消耗资源。如果连续三次探活失败,就把端侧轨道标记为不可用。云端探活则通过适配层发送一个极小请求——类似"请回复OK"这种——设定较短的超时时间,用于判断API服务是否正常。
巡检结果会写入一个共享的状态存储,路由层每次决策前先读取这个状态,而不是每次都实时探测。这个设计避免了请求链路上引入额外延迟。状态如果异常,再触发一次实时探活做二次确认,防止因误判导致流量错误切换。
4. 双轨路由的代码实现与核心逻辑
4.1 路由判定流程设计
路由判定是整个系统的大脑,我把它设计成一条清晰的决策链:先判断模型是否可用,再判断请求应该走哪条轨道,最后执行调用并在必要时降级。
决策链的第一环是维护一个路由规则表。规则表里每条记录包含任务类型、目标模型、首选轨道、备用轨道、超时时间、失败处理策略。比如"短文本分类"这个任务,目标模型是端侧3B模型,首选端侧,备用云端小模型;"复杂代码生成"这个任务,首选云端大模型,备用端侧7B模型。把路由规则做成可配置项,而不是硬编码在代码里,后续调整不需要发版。
决策链的第二环是根据当前轨道状态过滤候选轨道。如果首选轨道被熔断或巡检标记不可用,直接跳过首选,使用备用轨道。如果两条轨道都不可用,进入能力级降级逻辑。
决策链的第三环是执行调用并记录结果。这一步的关键是全程监控,每次调用都要记录耗时、是否成功、走了哪条轨道、消耗了多少token。这些监控数据不仅是排障的依据,也是后续优化路由规则的数据来源。
4.2 关键代码实现
路由层的核心代码我写了一个简化的版本,方便说明设计思路。完整版还包含配置加载、监控上报等逻辑,这里聚焦核心部分。
class DualTrackRouter: def __init__(self, edge_client, cloud_client, state_store): self.edge = edge_client # Ollama客户端 self.cloud = cloud_client # 云端API适配客户端 self.state = state_store # 轨道状态存储 def route(self, task_type: str, messages: list, context: dict): rule = self.get_rule(task_type) # 根据路由规则和轨道状态,决定请求的主备顺序 channels = self.order_channels(rule) last_error = None for channel in channels: if not self.state.is_available(channel): continue try: result = self.invoke(channel, rule, messages, context) self.record_success(channel, result) return result except Exception as e: last_error = e self.record_failure(channel, e) self.maybe_trip_circuit_breaker(channel) # 两个轨道都失败,进入降级逻辑 return self.degrade(task_type, messages, context, last_error)这里有一个设计要点是order_channels方法,它负责根据任务类型动态决定主备顺序。我之前遇到过一种情况:某个任务平时走云端更合适,但端侧模型刚好升级了版本,能力有明显提升,我希望某些简单任务在端侧可用时优先走端侧。这个决策逻辑就写在order_channels里,通过一个可配置的优先级分值来排序。
invoke方法统一处理调用,内部包含超时控制和流式/非流式的适配。超时这里我要特别强调:Ollama本地推理和云端API的超时设置完全不同。本地推理如果模型已经加载且显存足够,响应速度很快,但首次加载模型时可能需要几秒甚至更久;云端API网络延迟虽然低,但生成长文本时耗时预计会比较久。所以超时阈值建议动态计算,根据任务预估输出长度设定合理的超时时间。
降级逻辑的代码实现如下:
def degrade(self, task_type, messages, context, last_error): # 策略一:尝试用备用模型重新生成(例如用1.5B小模型替代7B) if context.get("allow_smaller_model"): try: result = self.edge.invoke_fallback_model(messages) return self.with_degradation_note(result, "model_downgraded") except Exception: pass # 策略二:返回缓存结果 cache = self.cache.get(task_type, messages) if cache: return self.with_degradation_note(cache, "cache_hit") # 策略三:基于规则的兜底答案 fallback_answer = self.rule_engine.answer(task_type, messages) return self.with_degradation_note(fallback_answer, "rule_fallback")降级的核心原则是永远不直接抛异常给用户。我见过不少系统的做法是失败就直接返回错误信息,这在AI应用里体验非常差——用户输入了一长串问题,结果换来一句"服务异常"。哪怕降级后回答质量差一些,也比白屏强。
4.3 轨道状态存储与熔断器实现
轨道状态和熔断器是整个路由系统能不能可靠工作的基础设施。状态存储我用的是Redis,原因很简单:边缘节点和路由服务可能不在同一个进程里,需要共享状态;Redis的过期和原子操作能很方便地实现熔断状态管理。
熔断器的实现思路是:每个轨道维护一个计数器窗口,记录最近100次调用的成功/失败状态。失败率超过阈值就打开熔断开关。熔断打开后,请求直接跳过该轨道,不再实际发起调用,这样能避免在服务已经故障的情况下继续产生无意义的压力。熔断打开后进入半开状态,允许少量试探请求通过,成功率达到要求就关闭熔断。
这里有个很实际的细节:熔断的恢复周期不能太短。如果服务恢复需要几十秒,而你把熔断恢复设为5秒,那么在服务真正恢复前会有大量请求反复触发失败。我一般设为30秒到60秒,宁可多等一会儿,也不要频繁试探。
5. 常见问题与排查实录
5.1 问题速查表
双轨路由架构在实际运行中遇到的问题五花八门,我把高频问题整理成一张速查表,方便对照排查。
| 问题现象 | 可能原因 | 排查方法与解决方案 |
|---|---|---|
| 端侧请求响应极慢 | 模型冷启动,需要重新加载 | 先检查ollama ps确认加载状态,再配置预热机制 |
| 端侧推理速度突然下降 | 并发请求把CPU/GPU占满 | 查看nvidia-smi或htop确认资源占用,调整并发上限 |
| 云端API偶发超时 | 网络抖动或服务端限流 | 设置合理的重试策略,用指数退避重试,同时走端侧兜底 |
| 路由频繁切换轨道 | 故障阈值设置过低 | 检查熔断阈值配置,提高失败率阈值或延长统计窗口 |
| 降级后结果质量明显下降 | 模型能力或上下文信息不足 | 降级响应中加入标记字段,客户端可提示"当前为应急模式" |
| 模型加载导致内存溢出 | 多个大模型同时常驻 | 调整OLLAMA_MAX_LOADED_MODELS,按需加载模型 |
5.2 实测中的几个坑
第一个坑是"流式响应的超时判断"比非流式要复杂得多。非流式接口看总耗时就行,流式接口需要区分"首包延迟"和"包间间隔"。我踩过的具体问题是:端侧Ollama在首次加载模型时,客户端连上端口但迟迟不返回首个数据包,如果按总超时判断就会误判为失败,触发不必要的降级。解决方案是首包超时设宽松一些,比如30秒,包间超时设短一些,比如15秒没有新内容就中断并降级。判断逻辑要区分这两种超时。
第二个坑是模型文件的损坏问题。Ollama断电或磁盘异常后,模型文件可能损坏,服务启动正常但推理时报错。这类问题比较隐蔽,因为探活接口返回正常,只有实际推理时才暴露。我在巡检机制里特意加了一个周期性"深度探活"——每10次探活中抽1次做实际推理验证,而不是只请求tags接口。这个机制帮我抓到了三次模型损坏问题,每次都发生在故障切换前,如果没有深度探活,故障就会直接暴露在用户请求上。
第三个坑是云端API的错误码语义不统一。不同厂商对限流、欠费、服务不可用的表达方式不一样,有的返回429,有的返回500,有的返回自定义错误码。如果适配层没有把错误码做归一化处理,路由层的熔断判断就会混乱。我的做法是适配层统一抛出三类异常:TransientError(可重试)、PermanentError(不重试直接降级)、QuotaError(限流,走流量切换逻辑)。业务代码只需要捕获这三类异常,不用关心底层是什么错误。
第四个坑是成本统计容易被忽略。双轨架构上线后,如果不去统计每条轨道的调用量,你很难发现路由规则是否合理。我曾经发现某个任务明明端侧能力完全够用,但因为路由规则的优先级配置不当,大部分请求都走了云端,成本比预期高出好几倍。后来在监控面板上加上轨道流量占比的图表,一眼就能发现问题,调整规则后成本立刻降下来了。
6. 路由规则优化的经验心得
双轨路由架构上线运行几个月后,我发现最花精力的不是架构设计,而是路由规则的持续调优。这里分享几条实操中总结出来的经验。
第一条经验是"不要试图让路由规则一次到位"。我刚设计这套系统时,试图把所有任务的优先级都提前定好,结果上线后不停被打脸——有些认为端侧能搞定的任务实际效果很差,有些认为必须走云端的任务端侧反而表现不错。后来改成灰度调优的方式:路由规则先保守配置,通过线上数据观察实际效果,每周根据监控数据调整一次。这个节奏比较合理。
第二条经验是"让业务方参与到降级策略的设计中"。技术人员容易只关注"系统能不能降级",但忽略了"降级后的结果能不能接受"。比如同样是用小模型替代大模型,"短文本分类"降级后准确率下降3%用户无感,"代码生成"降级后代码质量下降用户立刻有意见。这些业务感知层面的差异,只有业务方最清楚。把每个任务的降级策略和业务方对齐,比技术层面单方面决策要稳妥得多。
第三条经验是"预留人工干预的接口"。自动化容灾不是万能的,有些故障场景自动化系统无法正确判断。比如某个任务在某个时间段流量异常增长,可能是有活动或突发新闻,这时候自动路由逻辑可能会误判为故障。我的做法是提供一个管理接口,允许人工手动指定某个轨道优先或禁用,并记录手动干预的原因和时间。自动化负责绝大多数场景,人工在关键决策点兜底,实际运行下来这套组合最可靠。
从整个项目复盘来看,双轨大模型路由架构解决的核心问题不再是"怎么调用模型",而是"怎么可靠地调用模型"。端侧Ollama提供了低成本的常驻推理能力,云端API提供了高上限的弹性能力,两者通过一套自动化的路由、容灾、降级机制组合起来,既照顾了成本和隐私,又保证了可用性和体验。如果你也在做类似的大模型应用,我建议先从一两个核心任务开始,把路由骨架和监控埋点搭好,再逐步扩展开来——架构本身不复杂,真正的复杂度都在那些没人提前告诉你的细节里。