1. 为什么我决定不再把核心业务逻辑交给云端 API
1.1 从一次线上事故说起
去年冬天的一个凌晨,我负责的一个内部知识问答系统突然大面积超时。排查了半小时才发现,是上游大模型 API 的调用配额在高峰期被限流了。那一刻我意识到一个问题:当你的产品核心能力完全依赖第三方 API 时,你其实是在给 API 打工。对方调整一次限流策略、改一次计费规则、甚至只是机房抖动一下,你的业务就得跟着抖三抖。
这不是个例。做 AI 应用开发的同行应该都有类似体会:调用量小的时候一切安好,一旦业务量上来,成本曲线陡得吓人,而且响应延迟完全不可控。更别提数据合规层面的顾虑——很多企业内部文档、客户资料,从原则上就不应该离开自己的服务器。
所以从去年下半年开始,我陆续把几套系统的推理层做了重构,核心思路就一句话:本地优先,云端兜底。日常请求走本地部署的模型,遇到本地扛不住的高复杂度任务,再自动降级到云端 API。这套架构跑了大半年,稳定性和成本都达到了我的预期,今天把完整的搭建思路和踩坑记录整理出来。
1.2 这套平台到底解决了什么问题
先把定位说清楚。我搭的这套东西,本质是一个私有 AI 中台,它要同时满足四个诉求:
- 数据不出内网:敏感文档、内部知识库的检索增强生成(RAG)全流程在本地完成,向量化、检索、生成都不经过外部网络。
- 成本可控:高频、低难度的请求(比如意图分类、简单问答、文本格式化)全部由本地小模型处理,只有真正需要强推理的任务才走云端。
- 可用性兜底:本地服务挂了或者模型加载失败时,能自动切换到云端,业务不中断。
- 可编排:不是简单调个接口,而是要有工作流编排能力,能把知识库、变量处理、条件分支、多模型路由串起来。
技术选型上,我用Dify做编排层和应用层,Ollama做本地模型运行时,DeepSeek作为云端兜底的主力模型,底层用Docker统一管理。这套组合的好处是每一层都可以独立替换——哪天想换本地推理引擎,或者想加一个新的云端供应商,改动都很小。
1.3 适合谁来参考这套方案
如果你符合下面任意一条,这篇内容应该对你有用:
- 手里有内部文档、知识库,想搭一个能问答的系统,但不想把数据传出去;
- 已经在用云端 API,但被成本和限流折腾得够呛,想找个折中方案;
- 团队里有一台带独立显卡的机器(甚至没有独显也行),想把它利用起来;
- 对 Dify、Ollama 这些工具有耳闻,但一直没跑通完整链路。
需要说明的是,这套方案不是"零成本",本地那台机器的电费和硬件折旧是实打实的。但相比按 token 计费的云端 API,只要你的调用量到一定规模,本地化的边际成本优势会非常明显。
2. 三层架构的拆解:编排、推理、兜底各司其职
2.1 Dify 在架构里扮演的角色
很多人第一次接触 Dify,会以为它只是个"聊天界面生成器",这其实低估它了。在我的架构里,Dify 承担的是大脑皮层的角色——它不负责具体的推理计算,但负责决定"谁来算、怎么算、算完怎么处理"。
具体来说,Dify 提供了几个关键能力:
- 工作流编排:可以把"接收问题 → 检索知识库 → 判断复杂度 → 选择模型 → 生成回答 → 后处理"这一整条链路可视化地串起来,每个节点都能单独配置。
- 模型供应商管理:它支持同时接入多个模型供应商,包括本地的 Ollama 和云端的 DeepSeek,并且可以在工作流里按条件动态选择。
- 知识库流水线:文档上传、分段、向量化、检索这一套是内置的,省去了自己写 RAG 管道的功夫。
- 变量聚合与条件分支:这是做"本地优先、云端兜底"逻辑的关键,后面会详细讲。
我选择 Dify 而不是自己从零写一套编排逻辑,核心原因是它把 RAG 和模型路由这两件最繁琐的事做成了开箱即用。自己写当然更灵活,但开发周期和后期维护成本会高出一个量级。
2.2 Ollama 为什么适合做本地推理层
本地推理引擎的选择其实不少,我最终选 Ollama,主要看中三点:
第一是部署简单。Ollama 基本是"下载即用",一条命令就能拉起一个模型服务,对外暴露一个兼容 OpenAI 格式的接口。这意味着 Dify 接入它几乎零成本——直接当成一个 OpenAI 兼容的供应商填进去就行。
第二是模型管理省心。它内置了模型拉取、版本管理、显存调度。你不需要自己去折腾模型格式转换、量化、加载脚本这些底层细节。对于我这种更关注应用层而不是推理内核的人来说,这省了大量时间。
第三是资源占用可控。Ollama 支持按需加载模型,不用的模型会自动卸载释放显存。在一台显存有限的机器上,这个特性很关键——你可以同时准备多个模型,但同一时刻只加载正在用的那个。
当然它也有短板,比如并发能力不如专业推理框架,高并发场景下需要额外做队列控制。但对中小规模的内部系统来说,完全够用。
2.3 DeepSeek 作为云端兜底的定位
云端兜底我选 DeepSeek,理由很直接:在同等能力水平下,它的调用成本相对友好,而且接口兼容性好。它提供标准的 OpenAI 兼容接口,接入 Dify 只需要填 API Key 和 Base URL。
但要注意,兜底不等于"随便用"。我在架构里给它设了明确的触发条件,只有满足以下情况才会走云端:
- 本地模型判断该问题复杂度超过阈值(比如需要长链条推理);
- 本地服务健康检查失败,临时不可用;
- 用户显式选择了"高质量模式"。
这样设计的好处是,云端调用量被压到最低,成本自然就下来了。我实测下来,在合理配置触发条件后,云端调用占比能控制在总请求量的 15% 以内。
2.4 三层之间的数据流与健康检查
把三层串起来看,一次完整的请求是这样流动的:
- 用户请求进入 Dify 的应用入口;
- Dify 先做意图识别和复杂度判断(这一步用本地小模型);
- 根据判断结果,路由到本地 Ollama 或云端 DeepSeek;
- 如果走本地,先做知识库检索,把检索结果拼进提示词;
- 模型生成回答,Dify 做后处理和格式化;
- 返回给用户。
健康检查这块我单独做了一个定时任务,每隔一段时间 ping 一次 Ollama 的服务端口。如果连续失败,就在 Dify 的工作流里把路由开关切到云端。这个逻辑用 Dify 的条件分支节点就能实现,不需要额外写代码。
| 层级 | 组件 | 核心职责 | 故障时的表现 |
|---|---|---|---|
| 编排层 | Dify | 工作流、路由、知识库 | 整体不可用,需优先保障 |
| 本地推理层 | Ollama | 高频请求推理 | 自动降级到云端 |
| 云端兜底层 | DeepSeek | 复杂任务推理 | 提示用户稍后重试 |
3. 从零把环境跑起来:Docker 编排的实操细节
3.1 Docker 环境准备中最容易忽略的几件事
Dify 官方推荐用 Docker Compose 部署,这一步看起来简单,但坑不少。我先把环境准备的关键点列出来:
第一,Docker Desktop 的资源分配要提前调。默认配置下,Docker Desktop 给容器的内存往往只有 2GB 左右,而 Dify 加上它的依赖(PostgreSQL、Redis、向量数据库)跑起来,2GB 根本不够,会频繁出现容器被 OOM Kill 的情况。我的建议是至少给到 8GB,如果本地还要跑 Ollama,机器总内存最好 32GB 起步。
第二,端口冲突要提前排查。Dify 默认会占用 80、5432、6379 等端口。如果你机器上已经装了本地的 PostgreSQL 或 Redis,启动时就会报端口占用。解决办法要么改 Dify 的端口映射,要么先停掉本地服务。我个人的习惯是统一改映射端口,避免影响本机已有环境。
第三,磁盘空间要留够。光 Dify 本身的镜像加数据就有几个 GB,再加上 Ollama 拉的模型(一个 7B 模型量化后大概 4-5GB),建议至少预留 50GB 空间。
3.2 Dify 的 Docker Compose 部署与常见报错
部署流程本身不复杂,拉取官方仓库、复制环境变量文件、启动 compose 就行。但实际跑的时候,我遇到过几个典型报错,这里逐个说。
报错一:credentials validation 失败。这个通常出现在首次启动时,原因是数据库还没初始化完成,Dify 的 API 服务就去连了。解决办法是等 PostgreSQL 容器完全健康后再启动 API 服务,或者直接重启一次 API 容器。Docker Compose 里可以配置depends_on加健康检查来规避。
报错二:SSL 相关错误。如果你在容器里看到 SSL 握手失败的日志,多半是容器内的时间不对,或者证书链有问题。前者重启容器通常能解决,后者需要检查你的网络环境是否有中间代理拦截。我遇到过一次是宿主机时间漂移导致的,校准系统时间后就好了。
报错三:插件离线安装失败。Dify 的插件市场默认从线上拉取,内网环境下会失败。解决办法是提前在有网环境把插件包下载下来,通过本地上传的方式安装。这个后面会单独讲。
启动命令大致是这样:
# 克隆 Dify 仓库 git clone https://github.com/langgenius/dify.git cd dify/docker # 复制环境变量模板 cp .env.example .env # 启动所有服务 docker compose up -d # 查看服务状态 docker compose ps启动后访问本机的 80 端口就能看到 Dify 的初始化界面,第一次进去要设置管理员账号。
3.3 Ollama 的安装与模型存储路径迁移
Ollama 的安装相对独立,可以装在宿主机上,也可以装在 Docker 里。我推荐装在宿主机上,原因是它能直接调用宿主机的 GPU,性能损耗最小;如果装在 Docker 里,还要额外配置 GPU 透传,麻烦且容易出问题。
安装完成后,第一个要处理的问题就是模型存储路径。默认情况下,Ollama 会把模型存在系统盘的用户目录下,一个模型几个 GB,很快就把系统盘撑爆。所以第一件事就是改存储路径。
在 Linux 上,通过设置环境变量OLLAMA_MODELS指向一个大容量磁盘的目录即可。在 Windows 上,则是设置系统环境变量后重启 Ollama 服务。改完之后,之前下载的模型需要手动迁移过去,或者重新拉取。
# Linux 下临时设置(当前会话有效) export OLLAMA_MODELS=/data/ollama/models # 永久生效,写入 shell 配置 echo 'export OLLAMA_MODELS=/data/ollama/models' >> ~/.bashrc source ~/.bashrc3.4 模型拉取慢的应对思路
模型拉取慢是绕不开的问题,尤其是动辄几个 GB 的大模型。我的应对策略有三条:
一是选对模型尺寸。不是越大越好。对于意图分类、简单问答这类任务,3B 到 7B 的模型完全够用,拉取快、推理也快。只有确实需要强推理的场景,才考虑更大的模型。
二是错峰拉取。网络高峰期拉取大模型,速度可能只有几百 KB/s。我一般安排在夜间或者网络空闲时段拉取,速度能提升好几倍。
三是提前准备好离线包。如果目标机器完全没网,可以在有网环境把模型文件导出,拷贝过去再导入。Ollama 的模型文件结构是标准的,直接复制模型目录也能生效。
拉取命令很简单:
# 拉取一个 7B 级别的模型 ollama pull qwen2.5:7b # 查看已安装的模型 ollama list # 测试模型是否可用 ollama run qwen2.5:7b "你好,请做个自我介绍"4. 把 Dify 和 Ollama 接起来:模型接入的完整链路
4.1 在 Dify 里配置 Ollama 供应商
Dify 接入 Ollama 的路径是:进入"设置 → 模型供应商 → 添加 Ollama"。关键要填的是Base URL,这里有个容易踩的坑。
如果你的 Dify 是跑在 Docker 里的,而 Ollama 跑在宿主机上,那么填http://localhost:11434是连不通的——因为在容器里,localhost 指的是容器自己。正确的做法是填宿主机的内网 IP,或者用 Docker 的特殊域名host.docker.internal(Docker Desktop 环境支持)。
# Docker 环境下的正确填法 http://host.docker.internal:11434 # 或者用宿主机内网 IP http://192.168.1.100:11434填完之后点测试,如果显示连接成功,就说明链路通了。这时候 Dify 会自动拉取 Ollama 里已安装的模型列表,你勾选需要的模型即可。
4.2 云端 DeepSeek 的接入与 Key 管理
DeepSeek 的接入更简单,在模型供应商里选 OpenAI 兼容类型,填上 Base URL 和 API Key 就行。但这里我要强调一个安全实践:API Key 绝对不要硬编码在工作流里,也不要在多个应用间共用同一个 Key。
我的做法是:
- 在 Dify 的供应商配置里统一管理 Key,工作流只引用供应商,不直接碰 Key;
- 给不同的应用分配不同的 Key,方便按应用统计用量和排查问题;
- 定期轮换 Key,尤其是团队协作场景。
如果遇到no api key for provider route这类报错,基本就是供应商配置和实际调用不匹配——比如工作流里选了某个供应商,但那个供应商的 Key 没配或者配错了。排查时先确认供应商列表里对应项的状态是正常的。
4.3 模型路由的触发条件设计
这是整套架构的核心逻辑。我在 Dify 的工作流里设计了一个"复杂度判断"节点,用一个本地小模型对用户问题做快速分类,输出"简单"或"复杂"。
判断的依据可以包括:
- 问题长度(超过一定字符数倾向于复杂);
- 是否包含多步推理的关键词(比如"分析""对比""为什么");
- 是否命中知识库的高置信度匹配(命中则本地处理)。
根据判断结果,工作流走不同的分支:简单问题走 Ollama,复杂问题走 DeepSeek。这个逻辑用 Dify 的条件分支节点实现,不需要写代码。
提示:复杂度判断本身也要消耗算力,所以判断用的模型要足够小、足够快,否则就本末倒置了。我一般用 1.5B 到 3B 级别的模型做这件事。
4.4 变量聚合器在兜底逻辑里的妙用
Dify 的变量聚合器(Variable Aggregator)是个容易被忽视但极其好用的节点。它的作用是把多个分支的输出汇聚成一个统一的变量。
在兜底场景里,它的价值就体现出来了:本地分支和云端分支各自生成回答,但下游节点(比如格式化、返回)只认一个变量。用变量聚合器把两个分支的输出合并,下游就不用关心回答到底来自哪个模型。
具体配置时,把本地模型节点的输出和云端模型节点的输出都接到聚合器上,聚合器会按执行顺序取第一个有值的输出。这样即使本地分支因为异常没产出,云端分支的结果也能正常往下走。
5. 知识库流水线的搭建与上下文超长的处理
5.1 文档分段策略对检索质量的影响
知识库的效果,七分靠分段,三分靠模型。分段策略没做好,再强的模型也检索不出正确内容。
我的经验是:
- 分段长度:不要一刀切。技术文档适合 500-800 字符一段,因为技术概念往往成段出现;而 FAQ 类内容适合按问答对切分,一段就是一个完整问题。
- 重叠设置:相邻分段之间保留 10%-20% 的重叠,避免关键信息正好被切断在边界上。
- 分隔符选择:优先按语义分隔(段落、标题),其次才按固定长度硬切。
Dify 的知识库配置里这些都支持,关键是要根据你的文档类型去调,而不是用默认值一把梭。
5.2 检索增强生成链路的节点编排
一个完整的 RAG 链路在 Dify 里大概是这样编排的:
- 知识库检索节点:输入用户问题,输出最相关的若干分段;
- 重排序节点(可选):对检索结果做二次排序,提升相关性;
- 提示词组装节点:把检索到的分段和用户问题拼成一个完整的提示词;
- 模型调用节点:把组装好的提示词发给模型;
- 后处理节点:格式化输出,附上引用来源。
这里有个细节:检索返回的分段数量要控制。返回太多,提示词会超长;返回太少,可能漏掉关键信息。我一般设 top-k 为 3 到 5,再配合重排序,效果比较平衡。
5.3 上下文超长报错的根因与拆解
maximum context length is xxx tokens这个报错,做 RAG 的人几乎都遇到过。它的根因很简单:你拼给模型的提示词,加上模型要生成的回答,总长度超过了模型的上下文窗口。
但拆开看,超长的来源可能有好几个:
- 检索回来的分段太多或太长;
- 历史对话没有做截断,越聊越长;
- 系统提示词本身写得太啰嗦。
对应的解决思路:
| 超长来源 | 解决手段 |
|---|---|
| 检索分段过多 | 降低 top-k,或对分段做摘要压缩 |
| 历史对话过长 | 只保留最近 N 轮,或做对话摘要 |
| 系统提示词冗长 | 精简提示词,去掉冗余描述 |
| 模型窗口太小 | 换用上下文窗口更大的模型 |
我个人的习惯是,在提示词组装节点之前加一个"长度预估"节点,如果预估超过阈值,就先对检索结果做压缩,再拼提示词。这样能从源头避免超长报错。
5.4 知识库迁移与备份的注意事项
知识库搭好之后,迁移和备份是要提前考虑的。Dify 的知识库数据存在它依赖的 PostgreSQL 和向量数据库里,直接备份这两个数据库的卷就能完整迁移。
但要注意两点:一是向量维度和嵌入模型绑定,如果你迁移后换了嵌入模型,原来的向量就失效了,必须重新向量化;二是文档原始文件也要备份,因为重新向量化时需要用到原始文档。
我的做法是定期把知识库的数据库卷和原始文档目录一起打包备份,迁移时整体恢复,避免出现向量和文档对不上的情况。
6. 那些让我熬夜的坑:完整排查链路复盘
6.1 Ollama 连不通的三层排查法
Dify 报"无法连接 Ollama"时,不要急着改配置,按下面三层依次排查,能快速定位问题:
第一层:Ollama 服务本身是否在跑。在宿主机上执行ollama list,如果能列出模型,说明服务正常;如果报连接错误,说明服务没起来,先解决服务问题。
第二层:网络是否可达。从 Dify 所在的容器里 ping 宿主机的 IP,看能不能通。如果不通,是网络配置问题;如果通但端口连不上,是防火墙或端口监听问题。
第三层:地址填得对不对。前面说过,容器里的 localhost 不是宿主机。确认 Base URL 填的是宿主机 IP 或host.docker.internal。
这三层排查下来,90% 的连接问题都能定位。
6.2 插件离线安装的完整流程
内网环境装 Dify 插件是个高频需求。完整流程是:
- 在有网环境,从 Dify 插件市场找到目标插件,下载对应的插件包(通常是
.difypkg格式); - 把插件包拷贝到内网机器;
- 在 Dify 的插件管理页面,选择"从本地安装",上传插件包;
- 等待安装完成,刷新页面确认插件已启用。
要注意的是,有些插件有依赖关系,需要先装依赖插件。另外插件版本要和 Dify 主版本兼容,版本不匹配会安装失败。
6.3 工作流上下文超长的实战处理
前面讲了超长的原理,这里说一个我实际处理的案例。
有个用户反馈,问了一个长文档相关的问题后,系统就报上下文超长。排查发现,是因为这个应用开启了多轮对话记忆,而用户之前已经聊了十几轮,历史对话累积起来非常长,再加上这次检索回来的分段,直接爆了窗口。
我的处理方式是:在对话记忆节点上设置最大保留轮数(我设的是 5 轮),同时对更早的历史做摘要压缩。改完之后,同样的对话场景再没出现过超长报错。
这个案例的教训是:多轮对话的记忆一定要设上限,否则迟早会撞上上下文窗口。
6.4 模型思考过程干扰输出的处理
有些模型(尤其是带推理能力的)会在输出里带上大段的思考过程,比如"让我想想……首先……然后……"。这些内容如果直接返回给用户,体验很差。
处理方式有两种:一是在提示词里明确要求模型只输出最终答案,不要输出思考过程;二是在后处理节点里用规则或小模型把思考过程剥离掉。
我一般两种结合用:提示词先约束,后处理再兜底。对于某些思考过程特别顽固的模型,可以在 Dify 的模型参数里调整,或者换用不带思考输出的模型版本。
7. 稳定运行半年的几点个人体会
7.1 成本与体验的平衡点在哪
跑了半年,我最大的体会是:本地优先不等于本地万能。一开始我试图把所有请求都塞给本地模型,结果发现复杂任务的质量确实不如云端,用户体验下降明显。后来调整策略,把云端调用占比控制在 15% 左右,既保住了成本,又保住了质量。
这个平衡点因业务而异。如果你的场景以简单问答为主,本地占比可以更高;如果以复杂分析为主,云端占比就得提上去。关键是要有数据支撑,我专门做了个统计,记录每天本地和云端的调用量、响应时间、用户满意度,用数据来调参数。
7.2 硬件配置的真实建议
关于硬件,我的真实建议是:
- 内存:32GB 起步,16GB 会很紧张,尤其是同时跑 Dify 和 Ollama 时;
- 显存:如果要用 7B 以上模型,8GB 显存是底线,12GB 以上更从容;
- 存储:SSD 是必须的,模型加载速度对体验影响很大,机械硬盘会拖后腿。
如果暂时没有独显,用 CPU 跑小模型也能凑合,但速度会慢很多,只适合对延迟不敏感的场景。
7.3 后续可以继续优化的方向
这套架构还有不少可以打磨的地方。比如:
- 引入缓存层:对高频重复问题做缓存,进一步降低推理压力;
- 模型微调:用业务数据对本地模型做轻量微调,提升垂直领域表现;
- 多机部署:把 Ollama 单独部署到一台带强显卡的机器上,Dify 跑在另一台,通过网络调用;
- 监控告警:给本地服务加上健康监控,异常时自动通知。
最后分享一个小技巧:把常用的提示词模板固化下来,不要每次都在工作流里现写。我建了一个提示词库,按场景分类,用的时候直接引用,既省时间又保证一致性。这套东西搭起来前期确实要花点功夫,但一旦跑顺,后面维护起来会轻松很多。