
先说个背景。上个月我帮朋友做企业客服知识库的时候被云端大模型API的费用和稳定性折腾得够呛月初账单直接涨了三倍接口还时不时来个5秒超时稍微碰上业务高峰就限流。后来我搭了一条“端侧Ollama兜底”的旁路把简单问答、私域知识检索这类请求先甩给本地小模型只有复杂推理、长文总结才走云端API。运行了两周成本降了一大半服务的可用性反而上去了。这篇文章就是把这套“双轨”方案从思路到落地的完整拆解包含路由策略、容灾降级、以及我在实际项目中踩过的坑。适合正在用API做业务、想降本控费或对端侧大模型部署感兴趣的同学参考。1. 双轨路由的由来与核心设计思路1.1 端侧推理与云端大模型到底差在哪里先别急着写代码把两边的“脾气”摸清楚方案才不会跑偏。端侧推理指的是把模型部署在本地服务器、工控机甚至笔记本电脑上通过Ollama这类推理框架对外提供服务。云端大模型则指商用API比如你在各大模型平台上开通的接口。端侧和云端不是“谁更好”的关系而是“谁更适合什么场景”的关系。我整理了一张对比表是我选型时一直在参考的维度端侧Ollama云端大模型API单次调用成本几乎为零只耗电按tokens计费量大后非常可观推理延迟本地网络通常几十毫秒到几百毫秒受公网影响常出现1~5秒波动隐私与数据合规数据不出内网安全可控数据需发送至云端有合规风险模型能力上限受限于显存/内存通常用7B~32B模型可达上百B参数推理、长文能力更强离线可用性完全离线可用断网即不可用运维复杂度需要自己管理模型、显存、存储几乎零运维但需关注配额与限流从这个表能看出来端侧的核心价值是“可控、便宜、隐私”云端核心价值是“聪明、全面、省心”。如果你只依赖其中一边都会遇到问题。只上云成本和质量依赖不可控只做端侧复杂任务效果又不够。我在第一个版本就犯了“只上云”的错后来转为“端侧优先、云端兜底”效果立刻不一样。1.2 双轨路由不是“二选一”而是“分级处理”双轨路由的思路简单讲就是给每个请求做一次“体检报告”然后决定交给哪条轨处理。不需要在所有场景都用最强模型也不需要为了省钱牺牲效果分级处理是性价比最高的方案。具体来说我会把请求分为三类A类简单确定性任务例如FAQ问答、意图识别、实体抽取、格式化输出。这类任务用端侧7B~14B模型就足够返回快、成本低。B类中等复杂度任务例如多轮对话、需要引用私域知识的问答。这类任务对模型能力有一定要求但端侧14B~32B模型也能完成可以优先尝试端侧。C类复杂推理任务例如长文档总结、代码生成、数学推理、多步工具调用。这类任务直接走云端大模型不浪费端侧重试时间。双轨路由的本质是对“效果”和“成本”做一次动态平衡。我不会强制某类请求一定走某条轨而是会结合最新状态动态调整比如端侧负载过高时把部分B类请求临时升级到云端云端故障时把C类请求降级到端侧保证核心服务不中断。这就是标题里“边缘推理”和“容灾降级”两个关键词的真正意思边缘推理解决成本与隐私问题容灾降级解决可用性问题。2. 端侧基石Ollama部署与性能调校2.1 快速部署Ollama安装、下载与模型导入Ollama是目前把本地模型推理做到“开箱即用”的最佳工具之一没有复杂依赖一条命令就能起服务而且天然提供OpenAI兼容接口后面和云端API对接时会省掉很多适配工作。在Linux服务器上部署其实就三步# 1. 安装Ollama curl -fsSL https://ollama.com/install.sh | sh # 2. 启动服务 systemctl start ollama systemctl enable ollama # 3. 拉取模型以qwen2.5:7b为例 ollama pull qwen2.5:7b如果你对默认安装路径不满意想装到数据盘可以设置OLLAMA_MODELS环境变量指向大容量磁盘再重启服务。我当时就是把模型目录挂到独立数据盘上这样就算系统盘重装模型也不需要重新下载。这里补一个很多人踩的坑如果直接ollama pull速度很慢通常是外网连接不稳定导致的。解决办法有两个一是手动下载GGUF格式的模型文件再通过Modelfile方式导入二是配置镜像源后拉取。手动导入的方式更稳妥具体做法是# 手动导入GGUF模型 ollama create my-model -f ModelfileModelfile里只需要指定基础模型路径例如FROM ./qwen2.5-7b-instruct-q4_k_m.gguf这样就不需要依赖外网下载链路适合内网环境。2.2 模型选型与量化等级怎么定选模型不能只看参数数量还要看你的硬件到底能扛住什么量级的推理。显存决定你能否顺利跑起模型内存决定上下文长度和并发能力。参考选型逻辑如下8GB显存选择7B~8B模型量化等级Q4_K_M上下文长度4096~8192适合简单问答。16GB显存选择14B模型量化等级Q4_K_M或Q5_K_M上下文长度8192可以覆盖多数业务场景。24GB显存及以上选择32B模型或同时加载两个7B模型做多模型路由覆盖中高复杂度任务。量化等级是个必须理解的概念。简单说量化就是让模型“减肥”把原本用16位或32位浮点数表示的参数压缩到8位或4位整数表示。Q4_K_M代表4位量化模型体积小、速度快但推理效果会有轻微损失Q8_0代表8位量化效果更好但需要更大显存。我在生产环境中测试下来7B模型用Q4_K_M精度在中文意图识别和FAQ问答上的效果与未量化版本几乎无差但推理速度提升近一倍、显存占用减少约60%。所以在端侧场景下我推荐优先用Q4_K_M只有当模型回答质量明显下降时才升到Q5或Q8。2.3 并发、保活与显存占用Ollama关键参数实战Ollama默认配置比较保守直接上生产会遇到“并发一高就排队”的情况。它有几个环境变量需要重点调整我列一下最实用的组合环境变量作用我的推荐值OLLAMA_NUM_PARALLEL单个模型并发处理请求数4或8OLLAMA_MAX_LOADED_MODELS同时最多加载几个模型2OLLAMA_KEEP_ALIVE模型在内存中的保活时间5m或-1OLLAMA_CONTEXT_LENGTH默认上下文长度8192设置方式是在systemd服务文件中加入Environment字段然后重启服务sudo systemctl edit ollama加入[Service] EnvironmentOLLAMA_NUM_PARALLEL4 EnvironmentOLLAMA_MAX_LOADED_MODELS2 EnvironmentOLLAMA_KEEP_ALIVE5m EnvironmentOLLAMA_CONTEXT_LENGTH8192保存后重启Ollama就会生效。这里需要特别提醒OLLAMA_KEEP_ALIVE这个参数。它表示模型处理完请求后在内存中保留的时间。设为-1表示永久驻留好处是每次请求都是“热启动”秒级响应坏处是显存和内存一直被占着如果同时加载多个模型就会把显存撑爆。但设为过短比如30s又会让模型频繁加载冷启动时间非常痛苦。我的做法如果业务高峰期集中在白天就设为10m让模型在两次请求之间不频繁卸载如果7x24小时都有流量就设为-1让主模型常驻。但只有一个主模型常驻不要所有模型都常驻。还有个细节Ollama加载模型后显存占用并不会因为请求结束而立刻释放。如果想要主动释放可以通过API调一次卸载。另外显存不足时建议立刻看两件事一是用ollama ps查看当前加载了哪些模型二是降低并发数。由于显存是共享资源并发过高很容易触发OOM表现为请求直接报错或模型自动重载响应时间剧烈抖动。先降并发再考虑换更小量化等级的模型这是基本排查顺序。3. 云端接入与容灾降级让服务“挂而不死”3.1 统一API约定为什么所有模型都要走OpenAI兼容格式双轨架构的一个关键决策是让端侧和云端使用同一种API协议。这是整个路由网关能跑通的前提。Ollama本身提供/v1/chat/completions接口格式与OpenAI一致云端大模型API也普遍兼容OpenAI格式。这意味着我在网关层只需要写一套调用逻辑通过配置切换base_url和api_key就能在端侧和云端之间自由切换。下面是我的统一调用抽象层上层业务不用关心模型跑在哪里from openai import OpenAI client_edge OpenAI(base_urlhttp://127.0.0.1:11434/v1, api_keyollama) client_cloud OpenAI(base_urlhttps://api.example.com/v1, api_keysk-xxx) def chat(model: str, messages: list, use_edge: bool True, temperature: float 0.7): client client_edge if use_edge else client_cloud resp client.chat.completions.create( modelmodel, messagesmessages, temperaturetemperature, ) return resp.choices[0].message.content有了这一层后面做重试、熔断、降级都会变得非常简单。因为这个统一接口只依赖HTTP不管你端侧跑的是Qwen还是Llama云端接的是哪家API对上层调用方来说完全透明。3.2 重试与熔断云端故障的第一道防线云端API不是100%稳定429限流、5xx错误、网络超时是家常便饭。我在网关层实现了“重试熔断降级”三层防护这是整套容灾架构的核心。第一层重试。不是所有失败都需要重试。4xx业务类错误如参数错误重试没有意义网络超时和5xx才值得重试。重试要带指数退避避免在API已经过载时继续加重压力。import time import random def call_with_retry(func, max_retries3): for attempt in range(max_retries): try: return func() except Exception as e: if attempt max_retries - 1: raise wait_time 2 ** attempt random.uniform(0, 1) time.sleep(wait_time)这里2 ** attempt是指数退避的核心逻辑第一次失败等约1秒第二次约2秒第三次约4秒。加随机抖动是为了防止多个请求同时重试造成“重试风暴”。第二层熔断。重试解决的是单次偶发故障但如果云端已经整体不可用重试再多次也没用反而会占用大量资源。熔断器要检测连续失败次数或失败率超过阈值就“打开”在一段时间内直接跳过云端调用而不是每次都傻等超时。class CircuitBreaker: def __init__(self, failure_threshold5, cooldown_seconds60): self.failure_threshold failure_threshold self.cooldown_seconds cooldown_seconds self.failures 0 self.opened_at None def allow_request(self): if self.opened_at and time.time() - self.opened_at self.cooldown_seconds: self.reset() return not (self.opened_at and time.time() - self.opened_at self.cooldown_seconds) def record_success(self): self.failures 0 self.opened_at None def record_failure(self): self.failures 1 if self.failures self.failure_threshold: self.opened_at time.time()熔断器打开后后续请求在一段时间内走降级路径不再往云上打。冷却期结束后熔断器自动合上放少量流量试探云端是否恢复这就是所谓“半开状态”。3.3 降级策略从云端平滑切到端侧的三种姿势当云端不可用或者费用告警我需要把请求降级到端侧。降级不是简单地把同一个请求换一个模型而是要提前想清楚不同场景的处理方式。三种降级姿势供参考全量降级云端完全不可用所有请求切到端侧。最粗暴但能保底。适合内部系统对响应质量要求不高的场景。按任务降级只降级A类、B类简单任务C类复杂任务直接返回提示“当前服务繁忙请稍后再试”。这适合对外业务至少保证基础体验。质量降级端侧模型能力不够但也可以“先给一版答案再打上标记”。比如返回答案时附带modelllama3.2:3b告诉调用方这是降级版本必要时展示“当前为精简模型回答可能存在偏差”的提示。我自己常用的是“按任务降级质量标记”的组合。云端故障时简单问题照常回答复杂问题回答质量会下降但至少服务没有中断。这比直接报错要友好得多。4. 双轨路由策略的实现与代码落地4.1 路由决策的三种模式规则、分类与动态评分路由策略绝不是“if else”那么简单需要根据业务场景选择不同模式。我总结了三类规则路由最直接根据请求来源、用户ID、任务类型等硬规则分流。例如日志分析类请求全走端侧代码生成类请求全走云端。优点是简单可控缺点是规则需要人工维护。模型分类路由用一个小的端侧模型判断请求属于哪类任务再决定走哪条轨。这相当于多了一个“调度员”准确率尚可但会增加一次模型调用延迟。动态评分路由综合请求复杂度、端侧负载、云端健康状态计算一个评分再决定路由。这是最灵活的方式也是我目前的生产方案。我的思路是“规则为主、动态调整为辅”。规则保证不会走飞动态评分让策略自适应变化。两者结合既稳定又灵活。4.2 一个可运行的路由器完整代码示例下面是一个简化但可运行的路由器核心逻辑整合了规则与动态评分import time import threading class Router: def __init__(self, edge_modelqwen2.5:7b, cloud_modelgpt-4o-mini): self.edge_model edge_model self.cloud_model cloud_model self.edge_load 0 # 端侧当前并发数 self.edge_error_rate 0.0 # 端侧近期错误率 self.cloud_breaker CircuitBreaker() self.lock threading.Lock() def route(self, messages, task_typegeneral): # 规则判断敏感任务不送云端 if self._is_sensitive(messages): return edge, self.edge_model # 规则判断复杂任务且云端健康走云端 if task_type in (complex_reasoning, long_doc): if self.cloud_breaker.allow_request(): return cloud, self.cloud_model return edge, self.edge_model # 动态评分根据端侧负载与错误率判断 score self._score_edge() if score 60 and self.edge_load 4: return edge, self.edge_model return cloud, self.cloud_model def _score_edge(self): # 打分逻辑负载越低、错误率越低分数越高 load_score max(0, 100 - self.edge_load * 20) error_score max(0, 100 - self.edge_error_rate * 100) return min(load_score, error_score) def _is_sensitive(self, messages): # 简单关键词规则生产可换成熟算法 sensitive_keywords [身份证, 手机号, 地址, 合同] for msg in messages: content msg.get(content, ) for kw in sensitive_keywords: if kw in content: return True return False这段代码展示了一个核心思想路由不是单纯猜而是有业务规则做底盘、有状态指标做动态调节。真实环境中edge_load可以由Ollama监控接口获取错误率可以从日志中统计。你可以按需扩展打分函数把更多指标加进去。4.3 让路由无感统一模型网关的接口设计路由器解决了“请求往哪发”的问题但调用方不希望感知这些。所以我通常会封装一层统一的模型网关提供与业务完全无关的接口对外看起来就是一个标准LLM服务内部自动处理路由、重试、熔断、降级。我的接口设计如下POST /v1/chat/completions入参model逻辑模型名如default、strong、messages、其他OpenAI标准参数出参OpenAI兼容格式的响应路由在这层按逻辑模型名做策略映射MODEL_ROUTE_MAP { default: {strategy: auto}, strong: {strategy: cloud_only}, cheap: {strategy: edge_only}, }这样业务方只需要关心“我需要多强的模型”而不用关心具体是谁在处理。以后即便我把Ollama换成其他推理框架或增加新的云端供应商都不会影响上层业务。这个统一网关是整条链路的“胶水”也是运维和迭代最值得下功夫的地方。5. 实战踩坑与问题排查手册5.1 Ollama端侧的高频坑显存、冷启动、中文效果先讲显存。Ollama并发数调高后很容易触发OOM。你看到的现象是请求忽然变慢然后Ollama出现模型自动卸载界面里表现为model reload。排查方式是先看ollama ps确认是不是多个模型同时驻留再确认并发数是不是超过了显存承载能力。不要只调并发数不观察显存这两者必须联动。冷启动问题也很烦。第一轮请求等十几秒后续请求秒回这就是模型冷加载。我推荐用“预热请求”解决服务启动后后台立刻发一个最小请求给Ollama让模型常驻再加上OLLAMA_KEEP_ALIVE-1基本可以消除冷启动。中文效果差的问题通常出在模型选择上。有些英文为主的模型在中文任务上表现不行。我在中文生产环境里优先推荐Qwen2.5系列如果一定要用Llama3.2这类英文模型建议在Modelfile里显式指定SYSTEM提示词提前说明“你是一个中文助手请用中文回答”能改善一部分效果。5.2 云端容灾的隐藏坑限流、超时与费用失控云端限流不是“偶发”而是“必然”。大模型API的限流策略通常按TPM每分钟token数和RPM每分钟请求数双维度控制。你只在单点重试还不够还要在网关层实现全局的并发控制。排查限流时注意看清响应头里的Retry-After字段有些API会明确告诉你要等多久。我的代码里会优先读取这个字段而不是用固定退避策略。超时设置也有讲究。生成类任务一般需要流式响应如果你用非流式接口必须把超时时间放宽到60秒甚至更长。但超时时间过长又会影响容灾判断所以最稳妥的方案是流式请求配合“首包超时检测”首包迟迟不来就判超时之后持续有数据就不中断。这样既不会因为长生成误杀正常请求也不会因为云端吞包而无限等待。费用失控是另一个隐藏危险。一旦路由策略写错或者云端重试逻辑不当token消耗会成倍上涨。我的对策是在网关层加“每日预算”和“单请求最大token”双重限制超了就自动把请求全部降级到端侧。5.3 路由层的复杂场景会话保持、上下文传递与观测路由层最难处理的其实是多轮会话。同一个用户的连续对话如果第一轮走了云端、第二轮走了端侧模型上下文就断了体验会很分裂。我的解决策略是“会话级路由粘滞”在网关层根据conversation_id做哈希让同一个会话尽量固定在同一条轨上。除非对应轨道熔断或不可用否则不切换。上下文传递也不能忽略。端侧模型的上下文窗口通常比云端小如果对话历史太长直接传给端侧会导致模型忽略早期信息甚至报错。我通常会做一个“上下文裁剪器”按字符数和消息数双阈值截断超出的部分用摘要替代。例如每轮结束后用端侧模型把前面对话压缩成200字以内的摘要下一轮再带摘要和新问题。最后强烈建议把每个请求的路由决策日志下来至少包含时间、请求ID、路由结果、模型名、耗时、失败重试次数。我靠着这些日志才定位出“某类错误率升高是因为路由策略把简单任务送去了云端”这类问题。没有观测就没有优化。写在最后一个实操上的建议如果你正准备落地这套双轨架构我个人建议不要一上来就追求“完美的动态路由”。先把固定规则跑通再逐步引入评分和熔断。我在早期版本中甚至连路由打分都没写只做了“云端群故障时切端侧”这一条规则就已经把可用性从90%拉到了99.5%。踩过几次坑之后我的体会是双轨路由的价值不在于“每一条请求都被最优分配”而在于“系统在任何极端情况下都有退路”。先保住退路再谈最优调度这个顺序不要搞反。另外可以留一个手动开关事故时人工干预计费路由永远比纯自动更可靠。