做AI应用的同学最近多半被同一个问题缠着:云端大模型的费用像流水一样往外走,可完全换成开源小模型,端到端效果又差了那么一截。我年初把一个工具链从纯云端API迁移到了“Ollama本地推理 + 云端API兜底”的双轨结构,跑了快半年,踩了不少坑,也把整套容灾降级机制磨得比较顺了。今天把这条链路从头拆开讲,从Ollama部署、模型下载加速、双轨路由层的设计,到故障怎么自动切换、Dify和Claude Code这些工具怎么接进来,一次说清楚。
这套方案适合谁?适合有三种情况的人:一是日常调用云端大模型API且账单压力明显的;二是本地有GPU(哪怕是消费级显卡)想跑开源模型,又不想放弃云端能力的;三是正在折腾Dify、Continue、Claude Code这类工具,想把Ollama作为统一推理后端塞进去的。下面所有内容都基于我这半年的实际运行经验,不是抄文档,是实打实跑过线上请求的。
1. 为什么需要双轨路由:单轨方案的三个死穴
先讲清楚“单轨”的问题,否则你理解不了为什么要在中间加一层路由。所谓单轨,就是所有请求要么全走云端,要么全走端侧。这两种极端方案我都试过,各有各的痛。
1.1 成本黑洞与配额焦虑
纯云端方案的痛点再明显不过。按token计费的模型,一旦接入到业务逻辑里,尤其是对话、Agent、批量处理这类场景,费用会以你完全想不到的速度膨胀。我有个项目原本用云端模型做日志摘要,一天大约处理10万条日志,每条约300 token,一天的token量就是3000万,算下来一个月光是摘要费用就是一笔不小的开支。
更难受的是配额和限流。云端厂商是按账号维度做的并发控制,你并发一上去,429限流立刻就来。为了不触发限流,你还得在代码里写熔断、退避、重试,这些逻辑不是说不能写,而是写完之后整个服务链路的复杂度上去了,排查问题的时候多一个维度。
1.2 数据出域与隐私敏感场景
这个点在很多团队里会被忽视,直到出问题才追悔莫及。文档分析、用户聊天记录、业务内部资料的摘要提取……这些数据一旦发给云端API,就等于出了你的安全边界。哪怕和厂商签了保密协议,很多行业(医疗、金融、政务)的合规要求也不允许这么干。
我不是在这儿唱高调,是真遇到过一个做企业内部知识库的朋友,他起初把全部文档问答都接到云端API,结果被安全团队叫停,整个项目推倒重来。后来他换成了本地推理方案,虽然模型能力弱一档,但数据从物理层面没出去过,合规这边直接过关。
1.3 可用性风险不只是“云端挂了”这一种
还有一层大家经常忽略:单轨上任何一个环节故障,你的业务就全停了。云端挂了、网络拥塞、密钥失效、余额不足——任何一个点都能让你的服务变成一个“正在旋转的等待图标”。
端侧单轨也一样,甚至更脆。本地服务进程崩溃、显存被其他任务抢占、模型被误删,这些故障往往比云端API的故障更难察觉,因为你得自己去盯进程、盯显存、盯磁盘空间。我遇到过Ollama服务端口被某个开发工具占用,本地推理默默失效半个小时,业务方已经开始告警,我这边还没反应过来。
所以“双轨”不是花活儿,而是把两条不完美、互有优劣的路径,通过路由层组合成一个高可用系统。核心逻辑就八个字:各取所长,互相兜底。
2. 端侧底座搭建:Ollama从下载到API可用的完整链路
要想搞双轨,第一步是把端侧这一轨立起来。Ollama是所有环节里最省心的一个,但省心不等于没有坑,下面按完整链路走一遍。
2.1 安装与目录规划:把模型装到D盘的正确姿势
Ollama的安装本身不难,官网下载对应平台的安装包即可。但如果你用的是Windows,默认安装会把模型存放在C盘的用户目录C:\Users\用户名\.ollama\models,这些模型动辄5GB、10GB,C盘空间吃紧是迟早的事。
我的建议是装完第一件事就是改模型目录,方法有两种。
方法一:设置环境变量。Windows下在“系统属性 > 环境变量”里新建一个系统变量OLLAMA_MODELS,值直接指定为你想要的模型目录,比如D:\ollama\models。设置完一定记得重启终端,否则不生效。macOS/Linux下则在~/.zshrc或~/.bashrc里加上export OLLAMA_MODELS=/data/ollama/models。
方法二:直接整体迁移。如果你已经装好并且拉过模型,C盘里已经有一堆文件了,最省事的做法是把整个.ollama目录剪切到D盘,然后设置环境变量指向新位置。注意剪切而不是复制,复制过程中如果文件被占用会损坏模型文件。剪切完之后启动ollama list,如果模型列表还在,就说明迁移成功了。
2.2 模型下载与加速策略:不要死磕官方渠道
ollama pull从官方仓库拉模型,体验时好时坏,速度不稳定是常态。我自己的经验是:与其等官方仓库慢慢拖,不如直接用社区镜像源。这属于常规软件工程手段,不是旁门左道。
在Ollama里启用镜像源,同样是设置环境变量。拉取时Ollama读取OLLAMA_HOST作为API服务地址,读取注册表里的镜像配置作为拉取源。实操层面对普通用户最方便的方式是设置:
# Windows 在系统环境变量里添加,Linux/macOS 在 shell 配置里添加 export OLLAMA_MODELS=/data/ollama/models export OLLAMA_HOST=0.0.0.0:11434镜像源的使用方式也很直接:找一个国内可访问的镜像站,在拉取时显式指定基础URL。Ollama支持通过OLLAMA_ORIGINS和hosts方式做请求路由,但更省心的是直接修改配置文件或使用环境变量指向镜像。具体镜像地址有很多公开可用的,选延迟稳定的即可。
还有一种更“笨”但更可靠的做法:直接从HuggingFace或ModelScope下载GGUF格式的模型文件,然后通过Ollama的Modelfile导入。这个过程稍微费点事,但胜在一次到位,不用反复重试。
# Modelfile 示例 FROM ./qwen2.5-7b-instruct-q4_k_m.gguf # 设置对话模板(重要,不设置模板对话效果会很差) TEMPLATE """{{- if .System }} <|im_start|>system {{ .System }}<|im_end|> {{- end }} <|im_start|>user {{ .Prompt }}<|im_end|> <|im_start|>assistant """ # 设置参数 PARAMETER temperature 0.7 PARAMETER top_p 0.8然后在Modelfile同目录下执行:
ollama create qwen2.5-7b -f Modelfile这样拉模型就完全不受官方源速度影响了,文件从哪个渠道下载快就用哪个渠道。
2.3 启动服务与API联通:先过这一关再说
安装好并拉完模型后,启动服务是第一个容易出问题的环节。在Windows上Ollama默认是开机自启的,服务端口固定为11434。如果遇到服务没起来,手动执行:
ollama serve看到类似listening on 127.0.0.1:11434的日志输出就说明服务就绪了。这里有两个细节值得注意。
第一,如果你想在局域网内让其他机器访问这台电脑上的Ollama服务(比如Dify部署在另一台服务器上),必须把OLLAMA_HOST设置为0.0.0.0:11434,同时Windows防火墙要放行11434端口。默认绑定的127.0.0.1只允许本机访问,这是很多“连不上Ollama”问题的根因。
第二,首次调用某个模型时会有明显的延迟,因为Ollama需要把模型加载进内存,这个过程可能持续几十秒。如果路由层没做超时控制,第一次请求大概率直接超时。解决方法是预先加载模型:
# 预先拉取模型到内存并保持常驻(keep_alive 设为 -1 表示不自动释放) curl http://127.0.0.1:11434/api/generate -d '{ "model": "qwen2.5:7b", "prompt": "ping", "keep_alive": -1 }'2.4 模型管理与API调用:日常操作一览
模型列表:
ollama list # 查看机器上有哪些模型 ollama ps # 查看哪些模型正在内存中运行 ollama stop qwen2.5 # 手动释放模型内存 ollama rm qwen2.5 # 删除模型API调用是双轨路由的关键。Ollama原生API和OpenAI兼容API我都用上了,两者的调用姿势差异不多,但要分清:
原生API长这样:
curl http://127.0.0.1:11434/api/chat -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好"}], "stream": false }'OpenAI兼容API则把地址指向/v1/chat/completions:
curl http://127.0.0.1:11434/v1/chat/completions -d '{ "model": "qwen2.5:7b", "messages": [{"role": "user", "content": "你好"}], "stream": false }'实测下来,Ollama的OpenAI兼容接口在/v1/models、/v1/chat/completions、/v1/embeddings这些核心路径上和OpenAI原版的响应结构基本一致,这为路由层做统一转发省了很大的事。后面讲路由实现的时候,你会看到我们几乎不用做响应结构转换。
3. 双轨路由层:规则、健康检查与转发逻辑
双轨架构的核心,是中间这层“路由网关”。它做三件事:判断请求应该走哪条路,实时探测两条路是否健康,然后把请求转发出去并把结果回传。我用的是Python FastAPI写的一个轻量网关,代码量不大,但每条规则都是实际需求逼出来的。
3.1 路由规则设计:不是简单的“二选一”
很多人一听“双轨路由”,以为就是一个if判断:能用本地就用本地,否则用云端。真这么干,很快就会被业务方骂死。路由规则必须结合业务语义来设计。
我实际在用的规则有这么几条,优先级从高到低排列。
一是模型名路由。请求方指定要用什么模型,网关根据模型名映射表转发。比如请求模型local/qwen2.5就走Ollama,请求cloud/gpt-5就走云端API。这是最硬性的规则,必须放最前面。
MODEL_ROUTES = { "local/qwen2.5": "ollama", "cloud/gpt-5": "openai", "local/embedding": "ollama", "cloud/embedding": "openai", }二是任务类型路由。适合走端侧的任务主要有几类:短文本分类、摘要、信息抽取、关键词提取、意图识别、embedding生成。这些任务对模型的能力要求不算高,但请求量大、实时性要求高,交给端侧能省下大量API费用。而代码生成、长文推理、复杂Agent规划这类任务,本地7B模型的能力确实不够,必须走云端。
三是上下文长度路由。本地模型有明确的上下文窗口限制(比如Qwen 2.5 7B是32K),如果用户的上下文超过这个长度,就算本地能跑,效果也会明显变差。这一条我会在路由开始时检查:如果context_length > 28000,直接走云端。
四是成本预算路由。这个比较进阶。我会给云端API设一个每日预算,当当天消耗达到80%时,原本走云端的请求降级转发到端侧。这样即使某天业务量暴增,云端费用也不会失控。实现方案是每小时统计云端消耗,更新内存中的降级标记位。
3.2 健康检查机制:两条轨道都要实时探活
既然叫“容灾降级”,前提就是你得能及时发现故障。我的方案是两条轨各自独立做健康检查,结果都写进一个共享状态里。
端侧Ollama的健康检查最简单,每10秒请求一次/api/tags,能返回模型列表就说明服务活着。注意这里有个细节:健康检查用的是/api/tags而不是/api/ps,因为/api/ps返回的是已加载到内存的模型列表,如果一段时间不用,Ollama会自动把模型释放,/api/ps会返回空列表,但这不代表服务不可用,用/api/tags能避免误判。
云端AP I的健康检查则复杂一点。不能每次都真实调一次大模型(费钱且慢),我的做法是每30秒探测一次模型列表接口,同时在上一次真实请求返回后记录耗时。如果连续3次探测失败,或者真实请求的失败率达到阈值,就把云端标记为降级状态。
class HealthStatus: def __init__(self): self.ollama_healthy = True self.cloud_healthy = True self.ollama_last_check = 0 self.cloud_last_check = 0 def check_ollama(self): try: r = requests.get("http://127.0.0.1:11434/api/tags", timeout=3) self.ollama_healthy = r.status_code == 200 except Exception: self.ollama_healthy = False self.ollama_last_check = time.time() def check_cloud(self): try: r = requests.get( "https://api.openai.com/v1/models", headers={"Authorization": "Bearer " + CLOUD_API_KEY}, timeout=5, ) self.cloud_healthy = r.status_code == 200 except Exception: self.cloud_healthy = False self.cloud_last_check = time.time()这里有一个值得仔细体会的点:健康检查和服务探活是两码事。健康检查探的是“服务进程或者API是否可达”,服务探活还要进一步确认“真实推理任务能不能跑通”。只做前者的话,Ollama进程活着但显存已满导致推理报错的情况,路由层是感知不到的。所以真实请求返回后,我还会把响应状态码同步更新到健康状态里,一旦连续报错就立即降级,不用等下一轮健康检查。
3.3 转发实现:保持OpenAI接口兼容的前提下做透传
网关对外暴露的接口直接模仿OpenAI的/v1/chat/completions和/v1/embeddings。这样上游所有基于OpenAI SDK开发的模块,只需要改一下base_url和api_key就能接入双轨路由,业务代码一行不用动。
@app.post("/v1/chat/completions") async def chat_completions(request: Request): body = await request.json() target = route_request(body) if target == "ollama": return await forward_to_ollama(body) else: return await forward_to_cloud(body)转发时特别注意两点。
第一,请求体里如果带了model字段,转发到Ollama时要替换成Ollama里真实存在的模型名。前面说的local/qwen2.5这种别名,必须在网关层完成映射。
第二,流式响应(stream)的透传。很多对话场景需要流式输出,如果网关把流式响应包装成了整体返回,用户会明显感觉到首字延迟变高。我的做法是用httpx.AsyncClient的stream方法把上游响应流原样转发给下游,代码大约这样实现:
async def forward_to_ollama(body): ollama_body = {**body, "model": resolve_model_name(body.get("model", ""))} # stream 模式直接透传流 if body.get("stream"): async with httpx.AsyncClient(timeout=600) as client: async with client.stream("POST", OLLAMA_BASE + "/v1/chat/completions", json=ollama_body) as resp: async for chunk in resp.aiter_bytes(): yield chunk else: async with httpx.AsyncClient(timeout=120) as client: resp = await client.post(OLLAMA_BASE + "/v1/chat/completions", json=ollama_body) return JSONResponse(content=resp.json(), status_code=resp.status_code)路由层的超时设置是根据实测调的。Ollama冷启动(首次加载模型)最长遇到过50秒,所以端侧转发的超时时间至少要给到120秒。云端API虽然一般10秒内返回,但高峰期不稳定,给60秒是安全的。如果统一用OpenAI SDK默认的60秒,端侧冷启动那一次大概率会超时。这类小细节往往决定路由层是“稳定运行”还是“频繁报警”。
4. 容灾降级:故障检测、自动切换与状态管理
路由写好了,下一步是让系统具备鲁棒性。降级不是简单地在if里加个else,一套完整的降级方案需要考虑故障类型、切换时机、降级后的恢复策略。我踩过的坑全都集中在这一节,值得认真看看。
4.1 端侧故障:Ollama进程崩溃与显存OOM
端侧故障有两个最常见的形态,一是进程挂了,二是推理时报OOM(显存不足)。
进程挂掉的情况,健康检查能探出来。路由层检测到ollama_healthy为False之后,所有原本走Ollama的请求自动转到云端。这个逻辑很直接,但有一个关键问题:要不要自动重启Ollama?我的建议是不在路由层做自动重启,而是让进程守护工具(systemd、Supervisor、Windows计划任务)去管。路由层只负责流量调度,不负责拉起进程,职责单一才能避免故障时刻的连锁反应。
显存OOM的情况要更隐蔽。Ollama进程正常,/api/tags也正常返回,但一旦推理大一点的模型,就返回类似CUDA out of memory的报错。这时健康检查完全感知不到,必须在真实请求的响应里捕捉错误码。我的路由层会判断Ollama返回的状态码和报错消息,如果是OOM或显存类错误,立即把Ollama标记为不健康,后续请求全部降级到云端,并发出告警通知我来人工处理。
def parse_ollama_error(resp_json: dict) -> Optional[str]: if "error" not in resp_json: return None err = resp_json["error"].lower() if "out of memory" in err or "oom" in err or "cuda" in err: return "oom" if "model not found" in err: return "model_not_found" return None4.2 云端故障:限流429与超时
云端的故障形态跟端侧完全不同。端侧挂了是脆断,云端挂了往往先是“变慢”,然后才是拒绝服务。
遇到429限流,最简单粗暴的策略是退避重试。第一次重试等待1秒,第二次等2秒,第三次等4秒,最多重试3次。超过3次还失败,就把这个请求降级到端侧。这里有个注意点:重试要只在幂等场景里做。如果业务是生成一条日志摘要或者翻译一段话,重试没问题;如果业务是扣款或者下单,重试会造成重复扣费,必须由上游业务层来决定是否重试。
云端的超时降级我用的是“熔断器”思路:连续N次云端请求的耗时超过阈值或失败,就把云端标记为降级状态,接下来一段时间内所有请求直接走端侧,不去碰大概率已经故障的云端。这个降级状态的持续时间是均匀分布随机值(比如3到5分钟),避免所有实例同时恢复同时对云端发起洪水式探测。
4.3 降级标记的恢复策略与降级透明度
降级之后怎么恢复,往往比怎么降级更考验架构水平。
我的做法是,降级状态永远带一个过期时间。假设云端被标记为降级,5分钟之后自动重新探活。探活成功了就恢复流量,探活失败就再续一个降级周期。这种“自动续期”的实现比手工恢复省心得多。有段时间云端API老不稳定,一天里反复降级恢复了几十次,全靠这个机制撑着,我一次都没有手动介入。
还要考虑降级透明度。端侧模型和云端模型的能力有差距,同一个请求在不同轨道上跑出的结果质量不同。如果产品对输出质量有要求,降级时要在返回结果里带上一个标志位,比如x-degraded: true,让业务方能感知到当前是兜底模式。比如在用户界面上显示“当前响应由本地模型生成,可能存在质量波动”,好过用户发现结果变差之后自己猜来猜去。
HTTP/1.1 200 OK Content-Type: application/json x-routed-to: ollama x-degraded: true { ... }5. 常见工具接入:Dify、Claude Code与VS Code插件
架构搭好了,最终要落到工具链里用。这半年里我被问得最多的就是“Ollama怎么接到Dify”“Claude Code怎么用本地模型”,这里把几种常见工具的接入方式统一讲一遍。
5.1 Dify接入Ollama本地模型
Dify现在很多团队在用,它在“设置 > 模型供应商”里原生支持Ollama类型。配置时填三样东西:
- API地址:如果Dify和Ollama在同一台机器上,填
http://localhost:11434;如果Dify跑在Docker容器里,要填http://host.docker.internal:11434,这是Docker容器访问宿主机服务的专用地址,新手最容易在这一步卡住。 - API Key:Ollama本身不做鉴权,可以随便填一个占位符,比如
ollama。 - 模型名称:必须是Ollama里实际存在的模型名,比如
qwen2.5:7b。
配完之后在Dify里建一个应用,模型选择里就能看到Ollama的模型了。Dify调用Ollama走的是Ollama的原生/api/chat接口,不是OpenAI兼容接口,所以模型能力取决于你本地模型本身的水平。
我在Dify里跑的是知识库问答,本地模型直接做embedding(文本向量化)和大模型回复两步。embedding模型我选了nomic-embed-text或bge-m3,占显存小,速度很快。注意embedding模型和对话模型不要混用,一个是向量生成,一个是文本生成,混用了输出会很奇怪。
5.2 Claude Code + CC Switch + Ollama
CC Switch是一个模型供应商配置切换工具,能让你在Claude Code里配置不同的模型后端。把Ollama配置成Claude Code的推理后端,这一步的本质是让Claude Code的API请求指向本地Ollama的OpenAI兼容接口。
配置逻辑很简单:CC Switch里新建一个供应商配置,API地址指向Ollama的OpenAI兼容接口http://localhost:11434/v1,API Key占位,模型名填你本地已经拉取好的模型。但这里必须说句实话:Claude Code这种编程助手,原生是面向Claude模型的工具,换成7B级别的本地小模型之后,代码理解和生成能力会有明显落差,做点简单的代码补全勉强能用,复杂的重构和Agent多文件操作就不要太指望了。适合的场景是网络受限、数据敏感的开发环境里做基础问答和简单代码解释。
5.3 VS Code插件和ComfyUI的接入方式
VS Code里接Ollama主要是通过Continue、Codex扩展。Continue插件在设置里选择“Ollama”作为模型提供方,配置本地模型名即可。这里值得提的是,画图工作流工具ComfyUI也经常要接Ollama——不是用Ollama来画图,而是用Ollama上跑的LLM来做提示词理解、标签整理,或者给生成结果做总结。ComfyUI里的各类LLM插件节点,通常在配置项里填http://127.0.0.1:11434和模型名就能连通。这种跨工具链的统一接入,体验确实比每个工具单独找API方案顺手得多。
6. 真实踩坑实录:排查链路与优化建议
最后这部分我按“问题表象、排查链路、根因、修复”的顺序,复盘几个高频踩坑点。
6.1 “could not connect to ollama server”完整排查链路
这个错误几乎所有用过Ollama的人都见过,但90%的人只会在网上搜到“执行ollama serve”这句话,执行完发现没用。我梳理一条完整的排查顺序:
第一步,确认服务进程是否在跑。Windows下看任务管理器里有没有ollama进程,或直接执行ollama serve看日志输出。如果日志显示listen tcp 127.0.0.1:11434: bind: Only one usage of each socket address,说明端口被占。常见的占端口程序是Windows自带的Hyper-V或其它开发工具,在命令行执行netstat -ano | findstr 11434查PID,然后去任务管理器里核对占用进程。
第二步,确认服务绑定的IP。如果你用OLLAMA_HOST=127.0.0.1:11434启动,那局域网内其他机器调用http://本机IP:11434必然失败。需要改成OLLAMA_HOST=0.0.0.0:11434。注意修改完要重启服务。
第三步,确认防火墙。Windows默认会拦外部访问,在“Windows Defender防火墙 > 允许应用通过防火墙”里把Ollama加入放行列表,或者放行TCP 11434端口入站。
第四步,确认是不是跨容器访问。Dify跑在Docker里访问宿主机Ollama,用localhost是访问不到宿主机服务的,要用host.docker.internal或宿主机局域网IP。
第五步,确认是不是请求方用了HTTPS。本地Ollama只监听HTTP,如果你把base_url配置成了https://localhost:11434,握手会直接失败。很多SDK默认会用HTTPS,一定要在配置里显式改成http://。
6.2 镜像源与下载速度的实际经验
Ollama官方仓库拉取模型慢的问题,核心解决思路是换镜像源或调整下载工具。我前面讲的“直接下载GGUF再导入”虽然看起来笨,但win7、老系统、内网环境之类特殊场景下反而是最稳的。普通情况下,配置好镜像源之后,拉一个7B模型基本能在几分钟内完成,比官方源快一个量级。注意拉取前先把模型目录配好,不然等模型下完再想挪位置,移动几个GB的文件又是折腾。
6.3 资源占用、性能调优与多模型并发运维建议
Ollama默认的资源管理策略会根据模型大小和显存动态加载,一台电脑上同时加载两个大模型,显存很容易爆掉。我的建议是设置环境变量限制并发加载的模型数量:
# 同时最多加载 2 个模型 export OLLAMA_MAX_LOADED_MODELS=2 # 每个模型默认在内存中驻留的时间(秒),超时后被释放 export OLLAMA_KEEP_ALIVE=300这两个参数能显著降低OOM概率。另外,Ollama服务的并发能力受CPU、内存和磁盘I/O影响很大。如果多人在线使用,同一个模型反复热加载,磁盘I/O会很高。我给Ollama所在机器加了更快的模型目录盘(NVMe),实测并发请求的P99延迟降低了接近一半。预算允许的话,这一步非常值得做。
还有一条运维经验,适合上了规模的环境:Ollama本身没有完善的监控指标(没有prometheus端点),要盯住它的运行状态,最直接的方式是定时做API探测,并把健康状态上报给监控系统。我自己写了一个小脚本,每10秒探测/api/tags,同时抓取ollama ps的显存占用,超过阈值就往群里推告警。跑了大半年,这条监控帮我提前发现了3次显存泄漏和2次服务假死。
这套双轨架构稳定跑了几个月之后,我现在已经没有“选本地还是选云端”的纠结了。默认走本地,本地撑不住再上云端,云端也不稳就降级回本地——两条路互为备份,反而比单一依赖某一边更让人踏实。近期我还在做一层自动学习的能力:把历史上路由到云端的请求记录下来,统计它们长什么样(请求长度、任务类型、模型名),沉淀出一份动态路由画像,让以后的路由判断更聪明。这篇文章里的细节是我踩了无数个坑换来的,希望对正在折腾Ollama和双轨架构的你有点帮助。