七夜zippoe头像
Spring AI + Ollama 本地模型:数据不出内网的合规方案(落地全流程)封面图

Spring AI + Ollama 本地模型:数据不出内网的合规方案(落地全流程)

本文是《Java 后端 + AI 落地(Spring AI 主线)》系列第 3 篇。前两篇的方案有一个共同前提——数据要出网。但对政企、金融、医疗和涉密单位来说,"把内部制度文档发给第三方 API"这条线本身就是红线,再好的效果也不能用。这篇讲清三件事:"合规"其实分三档,只有一档必须私有化;本地模型的能力折扣与显存账要怎么算(不算清这笔账,后面的架构全部白做);以及怎么用同一份 ChatClient 代码把模型从云端换成本地——这正是第 2 篇选兼容模式的价值兑现时刻。文中还给出一条被严重低估的安全红线:Ollama 默认没有鉴权,一次图省事的监听配置就等于把模型免费送人。

📌 版本声明:本文基于 2026-09-27 的版本线撰写:Spring AI 2.0.1 + Spring Boot 4.1.x + Java 21,本地推理引擎用 Ollama(版本与 API 细节迭代较快,请以官方文档与 ollama --version 实测为准)。文中所有显存占用、吞吐量均为量级参考,真实数值受量化等级、上下文长度、并发数、驱动与硬件差异影响,不要当作硬指标使用。模型许可协议差异很大(尤其部分开源权重带商用限制),上线前请由法务或合规同事确认许可证,不要由工程师自行判断。

文章目录


在这里插入图片描述


1️⃣ "数据不出内网"不是一个需求,而是三个不同级别的需求

1.1 先讲一个我见过的真实决策现场

某单位的项目评审会上,业务方提出要"给内部规章做智能问答"。技术方案第一版用的是云端 API,评审到一半被一位领导打断,问了一句:“这些制度文件里有些是内部文件,发出去了吗?”

技术负责人回答:“走的是境内节点,数据没有出境。”

领导追问:“没出境,但出没出我们的机房?”

现场安静了几秒。技术负责人说:“出了。”

这个追问就是这篇文章存在的理由。 因为在工程语境里,"合规"经常被笼统地当成一个词用,但在实际评审中,它是三个级别完全不同的要求,对应三套完全不同的方案,成本差一个数量级。

1.2 三档合规边界:把它们分清,选型就不会错

第一档:数据不出境。 要求是"数据不能传到境外服务器"。这一档云端方案可以满足——只要使用境内合规的云服务节点、并确认服务商的数据处理条款。大多数互联网业务和民营企业的"合规要求"其实停在这一档,用第 2 篇的方案就够了。

第二档:数据不出内网。 要求是"数据不能离开单位的内网"(可以到内网里的任何一台机器,但不能出防火墙)。这一档云端方案直接作废,必须私有化部署——这就是本篇的主题。

第三档:数据不出机房 / 物理隔离。 要求更极端:连内网的其他区域都不能走,只能在同一机房内闭环,甚至要求离线(无外网)。这一档在方案上与第二档同源,但多出两个硬约束:一是模型与依赖必须能离线获取(不能 docker pull、不能 ollama pull),二是任何形式的"云端兜底"都必须彻底关闭,没有例外通道。

不能,必须境内

可以出境但需评估

不能,必须内网

可以到内网

需要离线

不需要

均通过

任一存疑

业务提出'AI 要合规'

数据能否离开
境内?

数据能否离开
单位内网?

第 1 档:境内云端方案
本文不适用(见第 2 篇)

是否需要
完全离线?

第 2 档:内网私有化
单机 / 多机 Ollama + 反向代理鉴权

第 3 档:离线私有化
模型离线导入 + 关闭一切出网通道

再查三件事:
模型许可证 / 训练数据来源 / 是否需进信创名录

可以实施

暂停实施,交法务/合规确认

(图 1:三档合规边界与方案映射——注意最后那个"查许可证"的闸门,本地部署不等于自动合规)

这张图里我要特别提醒最后那个闸门。 我在后面的 6.3 节会展开,但这里先把结论放上来:"本地部署 = 合规"是一个危险的等号。 本地部署只解决了"数据流向"这一个问题;模型本身的许可证(能否商用)、训练数据的来源(是否有版权与合规争议)、以及是否在信创名录内,是另外三个独立问题,而且它们是事后无法用技术手段补救的。

1.3 本地模型的真实代价:不要把它说成"免费的替代方案"

我不喜欢"本地模型省钱"这种说法,因为它只算了一半的账。 显式成本(GPU 或高配服务器的采购)是可见的,隐形成本(能力折扣、运维负担、迭代速度)往往更大。把两边的账摊开:

维度云端 API(第 2 篇)本地 Ollama(本篇)
数据流向出内网(第 1 档合规可接受)不出内网(第 2/3 档唯一选项)
前期投入几乎为零GPU 服务器或高配工作站;或先拿一台开发机试水
单位成本按 token 计费,随用量线性增长固定成本为主,用满才便宜,用不满就是浪费
能力上限顶级模型,持续迭代受显存约束:单卡能跑的多数量级在 7B~32B 之间
延迟受网络与排队影响,波动大局域网内稳定;但冷启动要加载模型(见 Q1)
并发能力受配额限制,可付费提升受显存限制,是硬上限(并发 × KV cache)
运维负担无(服务商负责)模型文件、显存、并发、版本全归你
迭代速度换模型只改一个字符串换模型要下载几十 GB、重新验证质量、重跑回归
最适合的场景追求效果与速度、数据可出网数据绝对不能出内网;或高频调用量大到云端账单难看

这张表里最容易被低估的是"迭代速度"那一行。 云端方案换模型是分钟级的事,本地方案换模型是小时到天级的事——不只是下载时间,还包括"重新跑一遍你的 30 条测试集看质量有没有退化、重新压一遍并发看吞吐有没有下降"。这个差异会深刻影响你的技术决策节奏:在云端方案里,我可以鼓励你"先用小模型试,不行再换大的";在本地方案里,换一次模型的成本高到必须一次定准,所以下面是那些"提前算清楚"的内容。

1.4 什么情况下该上本地模型(判断清单)

满足任意两条以上,本地方案就值得认真评估:

  • 数据明确不能出内网(第 2、3 档合规)——这是唯一不可替代的理由。
  • 调用量足够大:云端账单已经大到"半年的费用能买一台 GPU 机器"。注意这里要说"半年到一年回本",而不是"立刻更便宜"。
  • 对网络稳定性敏感:业务不能接受"外网抖动导致 AI 功能不可用"(比如生产线上的辅助系统)。
  • 需要离线运行:现场环境没有外网(工控、野外、特殊场景)。
  • 对模型行为有"可复现"要求:需要长期锁定同一个模型版本,不接受服务商静默升级。

反过来说,下面这些理由不成立: “本地模型免费”(不免费,只是成本结构不同)、“本地模型更安全”(只解决了数据流向,见 6.3)、“本地模型效果差不多”(同参数量级下确实接近,但你能跑起来的参数量往往远小于云端顶级模型)。

在这里插入图片描述


2️⃣ 先算显存账:这一步算错,后面全白做

2.1 显存不是"模型有多大就占多大"

“7B 模型要多少显存"这个问题,标准答案是"看情况”——但可以估。 显存占用由四块构成,很多人只算了第一块,然后在生产环境被后三块打崩。

总显存占用

① 模型权重
= 参数量 × 每参数字节数

② KV Cache
= 层数 × 头数 × 维度 × 上下文长度 × 并发数 × 精度

③ 上下文激活与中间张量
随 batch 与序列长度波动

④ 框架与运行时开销
CUDA context / 图形占用 / 碎片

7B @ Q4_K_M ≈ 4.5 GB
7B @ Q8 ≈ 7.5 GB
14B @ Q4_K_M ≈ 9 GB

随上下文线性增长
是最容易低估的一块

峰值出现在长输入
与并发较高时

1~2 GB 起
Windows 桌面上还要扣掉显示占用

预留 = 权重 + KV(按最大上下文×并发) + 2GB
再加 20% 余量

(图 2:显存预算的四块构成——只算权重是最常见的错误,KV Cache 才是压死线上服务的那一块)

先说权重这一块,它有个好记的换算法则:

量化等级每参数字节7B 约占用14B 约占用质量感受
FP162.014 GB28 GB基准(几乎用不上,太贵)
Q8~1.07.5 GB15 GB与 FP16 差异通常很小
Q5_K_M~0.75.3 GB10 GB接近 Q8,性价比高
Q4_K_M~0.64.5 GB9 GB默认甜点档,多数场景够用
Q3_K_M~0.453.5 GB7 GB开始能感到退化
Q2~0.352.7 GB5.4 GB不建议:语法与事实性明显劣化

关于量化,我有一条经验性的判断:Q4_K_M 是"默认档",Q5 是"能承受就上",Q3 以下不要碰。 原因不是"低了不好",而是质量退化不是线性的:从 Q8 到 Q5 的差异在很多任务上测不出来,从 Q4 到 Q3 开始出现"格式遵守变差、长句逻辑断裂",到 Q3 以下会出现一种很难排查的现象——模型能流畅说话,但会忘记你给它的格式要求。在结构化输出场景里,这类退化表现为"校验失败率突然从 2% 涨到 15%",而如果你没有埋这个指标,只会觉得"最近模型好像有点不听话"。

再说 KV Cache,它是全篇最该记住的一块。 简单说:模型每读一个 token,都要为每一层保存一份"键值对"用于后续注意力计算,所以它的占用随上下文长度线性增长,并且随并发数成倍增长。这意味着"上下文窗口调大"这个动作,代价不是线性的,而是"每个并发都要多占一份"。

举一个会让人当场愣住的例子: 一台 16GB 显存的机器,跑 7B @ Q4(约 4.5GB 权重,加框架开销算 6GB),剩 10GB 给 KV Cache。如果每个会话的上下文需要 8K,单会话 KV 大约占 1~2GB,那么理论并发只有 5~8 个;如果为了"体验更好"把上下文改成 32K,单会话 KV 就涨到 4 倍,并发直接掉到 1~2 个。“我把上下文调大了,结果并发崩了”——这就是根因。

2.2 模型档位:多大规模能干哪些活

参数量不是越大越好,它由"任务难度"决定。 我把实际经验整理成一张对照表,这张表的价值在于帮你避免"用 32B 干 3B 的活"和"指望 7B 干 70B 的活"这两种浪费:

规模典型显存(Q4)能干的活明显干不了的活
1B~3B1~2 GB关键词抽取、简单分类、短文本改写、意图识别任何需要多步推理的任务
7B~8B4.5~5 GBRAG 回答生成、摘要、结构化抽取、客服问答(本地方案的主力)复杂数学、多跳推理、长文档全局理解
14B9~10 GB上述全部 + 更强的指令遵循与中文表达需要深度专业知识的高难度问答
32B19~20 GB接近"可用"的通用能力,复杂任务的及格线与云端顶级模型仍差一个档次
70B+40 GB+质量明显更好,但需要多卡或大显存单卡成本效益比在多数业务上不成立

我的建议是:从 7B~8B 这一档起步,把 14B 作为"如果质量不够再升"的目标。 原因很实际:7B 档在单张消费级显卡上跑得动,试水成本最低;而绝大多数企业内场景(问答、摘要、分类、抽取)在 7B 档配合好的提示词就能达到"可用"水平。 我见过团队一上手就要上 32B,结果发现瓶颈根本不是模型不够聪明,而是他们的检索没做好、提示词没调好——在 RAG 场景里,检索精度决定上限,模型规模只决定你能不能逼近这个上限(这条在第 4 篇会展开)。

2.3 上下文窗口:比参数量更容易踩的坑

这里有一个反直觉的事实:Ollama 拉下来的模型,默认上下文窗口往往远小于模型本身支持的最大值。 这跟两个因素有关:一是为了省显存,运行时会选一个保守的默认值;二是上下文越长,KV Cache 越大,"默认给你开满"会让很多机器直接 OOM。

所以你必须显式设置它,并且理解代价:

  • 设小了:长文档、长对话历史会被截断,表现是"模型好像忘了前面说的"——这类问题最容易被误判成"模型记性差",实际是上下文被裁掉了。
  • 设大了:KV Cache 按比例膨胀,并发能力线性下降;同时长上下文的推理速度也会明显变慢(注意力计算量随长度增长)。

我的经验取值是:问答场景 8K 起步,文档分析场景 16K~32K,并把这个值与你的并发上限一起算进显存预算里。 关键是不要把上下文当成"免费的旋钮"去拧——它是这个系统里最贵的资源之一。

2.4 没有独显怎么办:CPU 推理的真实量级

必须说清楚:CPU 推理是可行的,但要做到"能接受",需要管理预期。

  • 速度:消费级 CPU 上跑 7B @ Q4,大致是每秒几个 token 的量级——意味着生成 200 字的一段回答要十几秒。 这对"后台批处理"完全够用(摘要、分类),对"实时对话"就很难受。
  • 瓶颈不是算力,是内存带宽。CPU 推理时,每一层都要把权重从内存搬进缓存,所以内存频率和通道数比核心数更重要——双通道比单通道快得多,这是很多人忽略的一点。
  • 内存占用按权重估算:7B @ Q4 大约要 5~6GB 可用内存(还要给系统和你的 Java 应用留空间)。如果机器同时跑数据库、应用服务,内存很容易不够。
  • 现实的替代方案:CPU 上跑 1B~3B 的小模型做"分类 / 抽取 / 预筛",把生成任务交给 7B 以上的 GPU 机器或内网的其他节点。分工比硬扛更划算——这也是 5.3 节混合架构的雏形。

在这里插入图片描述


3️⃣ 环境准备:把 Ollama 跑起来,并验证它真的可用

3.1 安装与两个必须改的默认行为

安装本身很简单(各平台官方安装包或一行脚本),但有两件事我建议装完立刻处理:

第一,模型存储路径。 默认会把模型放在系统盘的用户目录下,而一个 7B @ Q4 模型就是 4~5GB,跑几个模型很快把系统盘吃掉(这个坑和系列一第 12 篇的镜像层问题是同一个性质:都是在"看起来无关的地方"悄悄吃磁盘)。装完先把存储目录改到大盘。

第二,服务监听地址。 默认只监听 127.0.0.1(这是安全的默认值,不要随手改掉)。只有在需要被其他机器访问时才改,而改了之后必须立刻补上鉴权与访问控制——见 3.5。

另外,Windows 上要注意"服务化"这件事:如果你把 Ollama 注册成系统服务(这是生产环境应该做的,否则登录用户一注销模型服务就停了),要留意服务运行账号与模型目录权限。我见过最典型的问题:服务以系统账号运行、模型目录建在某个用户目录下,结果是"命令行里能用、服务里拉不到模型";排查时因为报错信息很含糊(只说找不到模型),容易误判成模型损坏。

3.2 拉模型:读懂模型标签

# 拉一个明确指定量化等级的模型
ollama pull qwen2.5:7b-instruct-q4_K_M

# 查看本地已有哪些模型(注意判断依据是这一条,不是你以为拉过)
ollama list

# 查看当前有谁正在占用显存(排查 OOM 的第一现场)
ollama ps

模型标签(tag)通常由三段信息组成:型号 + 规模 + 量化(可能还有指令微调标识)。理解这三段,你就能自己判断"这个模型该占多少显存、适不适合我的任务"。 有个很实用的细节:同一型号的"默认 tag"与"指定量化的 tag",实际拉到的可能是不同的量化版本——所以"我拉的和同事拉的不一样、效果也不一样"这件事,往往不是玄学,而是 tag 不同。

3.3 三个必查命令:验证"能对话"和"是谁在占资源"

# ① 有什么(模型清单)
curl -s http://localhost:11434/api/tags | head -c 600

# ② 能不能对话(最小请求,注意 stream 字段)
curl -s http://localhost:11434/api/chat -d '{
  "model": "qwen2.5:7b-instruct-q4_K_M",
  "messages": [{"role": "user", "content": "用一句话说明什么是向量检索。"}],
  "stream": false,
  "options": { "num_ctx": 8192, "temperature": 0.2 }
}' | head -c 800

# ③ 当前谁在占显存(关键:keep_alive 期间模型会常驻)
curl -s http://localhost:11434/api/ps

第 ③ 条命令是我最推荐新手先跑一次的,因为它能直观地回答一个让很多人困惑的问题:“我明明没有在调用,为什么显存还占着?” 答案是 keep_alive 机制——模型不会在回答完立刻卸载,而是驻留一段时间,以便下一次请求快速响应(这是"首次请求特别慢、后续很快"的原因,见 Q1)。理解这个机制,你就理解了一个重要的取舍:驻留越久响应越快,但越多模型驻留就越容易把显存用满。

3.4 四个关键配置项(每个都直接影响线上表现)

配置项作用我的建议取值与理由
OLLAMA_HOST监听地址默认 127.0.0.1 不要动;需要内网访问时才改为内网 IP,并必须补鉴权
OLLAMA_KEEP_ALIVE模型驻留时长高频服务设长(如 24h)避免反复加载;多模型混跑的场景要设短,否则显存被占满
OLLAMA_NUM_PARALLEL并行请求数这是显存与吞吐的核心旋钮:调大提升吞吐,但 KV Cache 会成倍增长,可能直接 OOM
OLLAMA_MAX_LOADED_MODELS同时驻留的模型数单模型服务设 1(省显存);需要"对话模型 + 嵌入模型"共存时至少设 2(第 4 篇会用到)

关于 OLLAMA_NUM_PARALLEL 我要多说一句,因为它是最容易"调上去就崩"的一项。 正确做法是先用 1,量出单请求延迟;再逐步调到 2、4,观察延迟与显存。你会看到一个典型的曲线:吞吐先随并发上升,到某个点后延迟急剧恶化(甚至 OOM 掉服务)——那个拐点就是你机器在这个模型与上下文配置下的真实并发上限,任何"配大一点更保险"的想法都会在拐点之后变成事故。

3.5 ⚠️ 安全红线:Ollama 默认没有鉴权

这一节请务必读完,因为它是本篇里"代价最高"的一段。

Ollama 的设计是面向本机开发者的工具:它的 HTTP API 没有任何身份认证机制。它默认只监听 127.0.0.1,这个默认值替你挡住了风险。但一旦为了"让同事也能用"把监听改成 0.0.0.0 或内网 IP,这台机器上的模型服务就对整个网络开放了——任何能连到这个端口的人都能:用完你的 GPU(压垮你的服务)、读取模型、甚至通过某些方式让它加载任意模型。

更严重的一种情况是端口被映射到公网。 这类"暴露在公网的 Ollama 服务"在过去一两年里被反复发现并利用(属于典型的"默认无鉴权 + 用户图省事"组合)。一旦发生,后果不只是资源被白用,还可能是内网拓扑与业务数据被顺藤摸瓜。

所以我把内网部署的正确结构写死:

携带内网令牌

仅内网回环或专用网段

拒绝

拒绝

路径拦截:禁止直连

业务应用
Java / Spring Boot

反向代理
Nginx / 网关
鉴权 + 限流 + 白名单

Ollama
监听 127.0.0.1 或专用网段

GPU / 显存

未鉴权的外部请求

非白名单 IP

不允许应用绕过代理
直接访问模型端口

(图 3:内网私有化的正确部署结构——鉴权、限流、白名单都在代理层,模型端口不直接暴露)

四条硬要求:

  1. Ollama 只监听回环地址,或仅绑定到"代理所在的那台机器/专用网段",不监听全网段。
  2. 所有访问必须经过带鉴权的反向代理:用一个固定 Header 或内网令牌校验,并且配上速率限制(这一层同时解决了 5.2 节里"单个客户端压垮服务"的问题)。
  3. 严禁把模型端口映射到公网,也不要图省事把 11434 直接开在办公网段上。
  4. 应用侧禁止绕过代理直连:这一条靠"网络策略 + 代码规范"双重约束——只有模型端口不可直连,代理层才真正成为唯一入口。

为什么做到这四条就算"合规"了? 因为它把"无鉴权的裸服务"变成了"有身份校验的内部服务",并且所有调用都经过一个可审计的点。审计能力是企业合规里经常被忽略但必查的一项——你需要能回答"谁在什么时候调用了模型、调了多少次"。


4️⃣ Spring AI 接入 Ollama:同一份业务代码,换掉底层模型

4.1 依赖:一个 starter 的事

<!-- 沿用第 2 篇的 BOM(2.0.1),只换/加一个 starter -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>

<!-- 如果需要同时保留云端兜底能力,则两个 starter 并存(Bean 隔离见 4.4) -->
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

关键点仍然是第 2 篇那条铁律:只引 starter,不引中间模块,版本统一交给 BOM。Ollama 这条线上还有一个额外好处:它不需要任何密钥,所以配置里"最危险的那一项"直接消失了——但请注意,它换来的是"网络位置本身成了安全边界"(3.5 节讲的就是这件事)。

4.2 配置

spring:
  ai:
    ollama:
      # 指向内网模型服务。注意:生产环境这里应该是"反向代理的地址",
      # 而不是 Ollama 的裸端口(见 3.5 节)
      base-url: http://ai-gateway.internal:8080
      chat:
        model: qwen2.5:7b-instruct-q4_K_M
        temperature: 0.2
        # 上下文窗口必须显式设置,默认值通常远小于模型上限(见 2.3 节)
        num-ctx: 8192
      # 生产环境禁止启动时自动拉取模型:
      # 它会去公网下载几十 GB,在离线环境直接失败,
      # 在联网环境则可能悄悄占满磁盘——而这一切发生在你不知情的时候
      init:
        pull-model-strategy: never

pull-model-strategy 这个配置我要单独说,因为它是一个典型的"开发期很爽、生产期很危险"的默认行为。 它的作用是当本地没有这个模型时自动去拉。在开发机上,这很贴心;在生产环境,它意味着"你改错一个模型名,服务就会去公网下载一个几十 GB 的文件"——在离线环境是启动失败,在联网环境是磁盘被静默吃掉。这件事的正确做法是:模型由部署流程显式准备,应用只负责使用;启动时如果模型不存在,就让它启动失败。

4.3 最有价值的一节:业务代码真的不用改

这是第 2 篇那个决策的价值兑现时刻。 因为第 2 篇我们选了 OpenAI 兼容的抽象方式,业务层的写法完全一样,变的只有配置:

位置第 2 篇(云端)本篇(本地)是否要改代码
Controller / ServicechatClient.prompt().user(q).call().content()完全相同❌ 不改
结构化输出.call().entity(Ticket.class)完全相同❌ 不改
流式.stream().content()完全相同❌ 不改
提示词模板.user(u -> u.text(...).param(...))完全相同❌ 不改
配置spring.ai.openai.* + base-urlspring.ai.ollama.* + base-url✅ 改配置
模型名qwen-plus 一类本地 tag(含量化等级)✅ 改配置
特定参数协议内参数provider 特有的参数(如 num-ctx)⚠️ 少量改动

这张表就是"解耦"的实际收益:一次选型决策,换来的是"迁移到内网只改配置"。 如果当初选的是原生 SDK 路线(第 2 篇的路线 A),这次迁移就要改代码、改依赖、改 Bean 装配,还要重跑一遍所有回归测试。

但我要诚实说清"零改动"的边界,一共三条:

第一,代码结构不动 ≠ 行为不动。 同一个提示词,7B 本地模型和云端顶级模型的输出质量不是一个档次的。所以"切到本地"这件事,工作量的大头不在工程,在提示词适配与测试集回归(5.2 节展开)。

第二,provider 特有的参数会漏出来。 上下文窗口(num-ctx)、量化、并行数这些概念在云端是"服务商替你决定",在本地是"你必须决定"。这些参数没有出现在 Controller 里,但会以"线上表现"的形式出现在你面前。

第三,错误的形态不一样。 云端方案里你看到的是 HTTP 状态码(401 / 429 / 超时);本地方案里你会看到OOM、模型加载超时、队列等待、推理速度断崖——这些都不是"请求失败",而是"服务劣化",更难被发现。这也是为什么第 5 章的调优和监测在本地方案里比云端方案更重要。

4.4 本地优先 + 云端兜底:一个要用合规闸门锁住的模式

"本地模型做默认,遇到难题升级到云端"听起来很美,但它是本篇里最需要警惕的设计,因为它会悄悄把数据送出内网。所以这个模式的第一道关卡不是技术,是合规:

第 2 / 3 档
数据不可出内网

第 1 档
数据可出内网

简单任务
分类 / 抽取 / 摘要

复杂任务
多步推理 / 长文档

失败 / 超时 / 质量不达标

第 1 档:允许

第 2/3 档:禁止

用户请求

合规闸门:
本系统属于哪一档?

只用本地模型
云端兜底必须物理关闭
(配置 + 网络策略双重锁定)

任务路由

本地 7B

云端模型

允许兜底?

明确失败 + 人工介入
不允许静默降级

返回结果

(图 4:本地优先 + 云端兜底的路由——注意最上面那道"合规闸门",它决定了下面所有分支是否存在)

为什么说"第 2/3 档必须物理关闭兜底"? 因为只要兜底通道在代码里存在,它迟早会被触发:某个工作日的高峰、某次本地服务重启、某个开发同学临时的调试。而"一次云端调用"就足以让整条合规链路失效——审计看的是"有没有可能出网",不是"出网了几次"。 所以正确做法是双重锁定:配置上不注入云端模型的 Bean,网络策略上让内网机器在物理上无法访问外网模型域名。后者才是真正可靠的那一层——代码里的开关会被人改,防火墙不会。

如果确实需要第 1 档的兜底,实现上要注意 Bean 隔离(与第 2 篇 Q3 同样的手法):

@Configuration
public class ModelRoutingConfig {

    // 本地模型作为默认
    @Bean
    @Primary
    ChatClient localChatClient(OllamaChatModel model) {
        return ChatClient.create(model);
    }

    // 云端模型只在"合规允许 + 显式点名"时使用
    @Bean("cloudEscalationClient")
    ChatClient cloudChatClient(OpenAiChatModel model) {
        return ChatClient.create(model);
    }
}

注意 @Primary 加在本地模型上——这个顺序是有意的:默认走本地,云端必须被显式点名。如果反过来(云端是默认、本地是旁路),任何一次新增接口的自动注入都会悄悄走云端,而那正是你最不希望发生的事。

4.5 本地嵌入模型:为下一篇文章铺路

RAG(第 4 篇)需要一个嵌入模型把文本变成向量,而它也只能是本地的。 这里先讲三个必须提前知道的点:

第一,嵌入模型也吃显存。 好在它很小(通常在几百 MB 到 1GB 量级,远小于对话模型),但它必须与对话模型同时驻留——所以 3.4 节的 OLLAMA_MAX_LOADED_MODELS 至少要设成 2,否则每次检索都要卸载/加载一遍,性能会难看到无法接受。

第二,向量维度决定存储量。 存储估算很简单:向量条数 × 维度 × 4 字节(float32)+ 索引开销。 举个例子:10 万个分块、768 维,原始向量约 300MB——这个量级用普通数据库就完全能扛住,不必一上来就上专用向量库(第 4 篇会给选型表)。

第三,嵌入模型换了,必须全量重嵌入。 不同嵌入模型产出的向量在同一个向量空间里不可比,混用会导致检索结果完全错乱。所以"换嵌入模型"不是一个可以灰度的事,它是一次性的全量重建——这一条经常被低估:文档量大的时候,重嵌入要重跑几个小时甚至几天。 因此嵌入模型的选型要比对话模型更慎重,因为它绑定的是"你的全部历史数据"。

4.6 三个必须实测的数

本地方案里,我要求你在上线前量出这三个数,它们是后面所有容量规划的依据:

指标怎么量为什么要量
首次请求延迟(含模型加载)重启服务后立刻打一次请求它会比稳态慢一个数量级,不知道这个数,你会把它误判成"性能很差"
稳态 tokens/s连续打 5 次短请求,算平均输出速度决定"一个回答要等多久",也决定能不能做流式体验
并发拐点并发从 1 逐步加到 2/4/8,记录延迟与显存找到 3.4 节说的那个拐点,它是限流阈值与实例数的依据
GPU Ollama 反向代理 应用 GPU Ollama 反向代理 应用 服务刚启动,模型不在显存里 这一段的耗时与显存此时才被占用 模型驻留(keep_alive 期间不回退) 并发上升 → KV Cache 成倍增长 → 接近显存上限 第一次请求 /api/chat 从磁盘加载模型(数秒到数十秒) 加载完成,开始推理 首 token(延迟明显偏大) 第二次请求 /api/chat 首 token(快一个数量级) 并发 8 个请求 KV Cache 申请失败 OOM / 请求排队 部分请求超时或失败

(图 5:一次本地推理的完整时序——"首次慢"是加载,"后续快"是驻留,"并发崩"是 KV Cache,三个现象三个原因)

这张图把本地方案里最容易混淆的三个现象解释清楚了。 我在 Q1~Q6 里会逐个对应到具体处置,但先记住它们的因果:加载慢 → 用驻留解决;驻留占显存 → 用 keep_alive 与模型数控制;并发崩 → 用 num_parallel 与并发上限控制。 三个旋钮互相牵制,这才是本地方案真正的复杂度所在。


5️⃣ 进阶与优化

5.1 性能:三个旋钮的联动关系

keep_alive(驻留时长)、num_parallel(并行数)、num_ctx(上下文窗口)这三个参数不是独立的,它们共同争抢同一块显存。 我给一个可操作的调优顺序,核心原则是"固定两个、调一个、每次只动一个":

  1. 先定 num_ctx:由业务需求决定(问答 8K、文档分析 16K~32K)。先定它是因为它最不可妥协——设小了功能就不对。
  2. 再定 keep_alive:高频服务设长(避免反复加载),多模型混跑设短(避免互相挤占)。判断依据是"加载一次的代价 × 加载频率" 与 “显存被占的机会成本” 哪个大。
  3. 最后调 num_parallel:从 1 开始往上加,盯着延迟与显存的拐点(4.6 节的方法)。

如果显存不够,处置顺序绝对不能反:先降量化等级 → 再降上下文 → 最后降并发。 理由是这三者对用户体验的伤害程度递增:降量化损失的是"细微质量"(多数业务测不出来);降上下文损失的是"能力边界"(有些长文档直接处理不了);而降并发损失的是"可用性"——用户直接开始排队和超时。 我见过最常见的错误正是反着来的:一遇到 OOM 就把并发调到 1,结果吞吐塌掉、用户全在等,而质量明明还有下降的余地。

5.2 质量:提示词必须为本地模型"降维"

这一节是我认为最容易省下大量返工时间的内容。

给云端顶级模型写的提示词,直接搬到本地 7B 上,通常会掉一个档次。 原因不是"模型笨",而是本地模型在"长指令遵循"和"复杂格式约束"上更脆弱——指令条数越多、格式要求越复杂,它丢三落四的概率越高。具体表现:只完成了你要求的 5 件事里的 3 件;或者格式大体对但字段名拼错;或者在长上下文里把早期指令忘掉。

四条实操规则:

  • 把多步指令拆成单步。 与其写"先总结、再分类、然后抽取三个字段",不如拆成三次调用——本地模型的单步准确率远高于多步串联。 代价是延迟与调用次数增加,但在本地部署里"算力是自己的",这笔交换通常划算。
  • 格式要求要简单且给出示例。 对本地模型,一个 few-shot 示例比三段文字描述有效得多。 这也正是 Spring AI 的提示词模板能派上用场的地方(.user(u -> u.text(...).param(...)))。
  • 降低上下文的"信息密度"。 长上下文里本地模型更容易丢信息,所以 RAG 场景要控制注入片段的数量(宁少而准,不要多而杂)——这一点和第 4 篇的核心判断是同一个道理。
  • 改完提示词必须重跑测试集。 本地模型对提示词更敏感,"改了一句话、某个场景从对变成错"的概率显著高于云端;没有测试集,你就是在盲改。

这里有一条从云端迁到本地的通用经验:把"提示词工程"当成一次独立的工作项来排期,而不是"顺手改改"。 我见过的实际比例是:从云端迁到本地,工程量大约 20% 在配置与部署,80% 在提示词适配与效果验证。 提前知道这个比例,排期就不会失控。

5.3 混合架构:按敏感度分级路由

这是我在企业内落地时最推荐的架构,因为它同时兼顾了"合规"和"效果"。

核心思路是:不是"整个系统用本地"或"整个系统用云端",而是按数据敏感度分流。 把请求分成三级,每级走不同的通道:

A 级:公开
产品手册 / 公开制度

B 级:内部
内部流程 / 非敏感业务数据

C 级:敏感
个人信息 / 涉密 / 合同金额

脱敏规则升级

用户输入

敏感度分级

云端模型
(仅第 1 档合规时存在)

本地模型

规则脱敏 + 本地小模型处理

统一输出层
统一话术 / 统一审计

审计日志
记录:级别 / 模型 / 是否脱敏

返回用户

规则库
由本地模型辅助生成候选
人工确认后入库

(图 6:敏感度分级路由——注意脱敏环节用的是"规则 + 本地模型",绝不能用云端模型做脱敏)

这张图里有一个反直觉但至关重要的点:脱敏本身也必须在本地方案里完成。 我见过一个错误的实现:为了"更智能地识别敏感信息",把原文先发给云端模型让它标注哪些是敏感字段——这个做法荒谬但真实存在,因为它把数据送出去了两次(一次脱敏、一次正式请求),反而比不脱敏更糟。

正确的做法是"规则优先 + 本地模型辅助": 用正则与字典处理结构化、可枚举的敏感字段(身份证号、手机号、银行卡号、工号);用本地小模型处理"难以用规则描述"的部分(比如判读一句话里是否隐含了客户身份)。规则库的升级可以由本地模型生成候选、人工确认后入库——这样既没有数据出网,又保持了规则的迭代能力。

再补一条边界:分级本身要"默认更严"。 分级判错的代价是不对称的——把敏感数据误判为公开(数据泄露)远严重于把公开数据误判为敏感(效果略差)。所以分级的默认档应该是"内部",未知一律保守处理。

在这里插入图片描述

5.4 运维与迭代:把模型当成"二进制依赖"

这一条改变了团队的工作方式,值得单独讲。

在云端方案里,模型是"服务";在本地方案里,模型是"你必须自己管控的二进制资产"。 这意味着四件事:

第一,模型要进版本台账。 记录:模型型号、tag、量化等级、文件哈希、来源、引入日期、配套的提示词版本、当时的测试集通过率。为什么哈希也要记? 因为**"同一个 tag 却拉到了不同内容"这件事在本地环境是可能发生的**(上游更新了同名文件),有了哈希你才能判断"效果变化"是不是模型变了导致的。

第二,离线更新要有流程。 第 3 档合规环境里没有外网,模型更新只能靠介质导入或内网仓库。请提前把这个流程跑通一次——我见过项目上线前才发现"模型拉不下来、内网也没有镜像",最后靠人肉拷 U 盘,而那个 U 盘的接入本身又需要走安全审批。

第三,换模型要灰度。 做法与系列一第 11 篇的会话级灰度完全一致:双模型并存、按会话分流、先给内部用户试、观察质量指标(结构化校验失败率、用户点踩率、平均对话轮数)。本地换模型比云端更麻烦(要重新加载、要重跑测试集),所以"一次性全量切换"的风险更高,灰度是必需品。

第四,模型服务与业务应用必须分开部署。 这一条是硬要求,理由很直接:模型 OOM 会把整台机器拖死,如果业务应用同机部署,你的接口会跟着一起挂。 分开部署之后,OOM 的表现退化成了"AI 功能不可用",其他业务仍然正常——这就是"故障隔离"的价值,成本只是多一台机器。


6️⃣ 适用边界与风险提示

6.1 ⚠️ 适用场景

  • 数据明确不能出内网(第 2、3 档合规)——本方案唯一不可替代的价值所在。
  • 任务类型以"理解与转换"为主:问答、摘要、改写、分类、信息抽取。这类任务不要求模型"很聪明",只要求它"稳定按格式输出",本地 7B 配合好提示词可以达到可用。
  • 配合 RAG 使用(第 4 篇):本地模型 + 内网知识库是政企场景最常见的组合——因为 RAG 把"知识"从模型参数里搬到了可控的检索库里,正好补上本地模型知识面不足的短板。
  • 调用量大且稳定:算力是自己的,用得越满越划算。

6.2 ⚠️ 不适用场景

你的需求为什么本方案不行
复杂推理与多跳问答你能跑起来的参数量(7B~14B)在这类任务上明显吃力;要上 32B+ 就是另一个成本量级
超长文档的全局理解上下文窗口是"用显存买的",几十万字的总结在单卡上不现实
多模态(图片/音频/视频理解)能力与显存需求都不是本方案的覆盖范围
追求"最好效果"而不考虑数据出网云端顶级模型当前仍领先一个档次;如果能出网,就该用云端
首次投入就要求"必须比云端便宜"本地方案是"固定成本换线性成本",用量不足时一定更贵
没有运维人力的团队模型、显存、并发、版本全归你管,它不是一个"装上就不管"的组件

在这里插入图片描述

6.3 ⚠️ "本地部署 = 合规"的三个误区

这一节请务必与法务或合规同事一起过一遍,因为这三个问题都不是工程师能单独判断的。

误区一:本地部署了,许可证就没问题。 事实上,开源权重并不等于可自由商用:有些模型的许可协议对商用、对月活规模、对署名方式有明确限制,有些明确禁止用于特定用途。"我能下载"和"我能商用"是两件完全不同的事。

误区二:模型的训练数据来源与本项目无关。 在部分行业(尤其涉及版权、内容生产的场景),"模型是用什么数据训练的"本身就可能成为风险点。这类问题没有任何技术手段可以事后补救,只能事前审查。

误区三:本地部署了就满足信创要求。 在政企与国企场景里,“是否在信创名录内”"软硬件是否国产化适配"往往是独立的评审项,与"数据不出内网"是两件事。本地部署只是满足了其中一项。

这三条的共同点是:它们都无法用技术手段补救,且成本极高。 所以我的建议是把它们放在项目启动阶段的第一份检查清单里,而不是在技术方案做完之后再补——否则你可能做出一个技术上很漂亮、但根本不能交付的系统。

6.4 ⚠️ 生产建议

八条,前四条是"上线前必须有":

  1. 鉴权与访问控制(3.5 节):代理层做鉴权、限流、白名单;模型端口不暴露;应用禁止绕过代理直连。
  2. 资源隔离:模型服务与业务应用分机部署。理由在 5.4 节:OOM 会拖死整机。
  3. 审计日志:记录调用方、时间、模型、token 量、是否脱敏。合规检查必查项,且必须能长期留存。
  4. 模型文件校验:引入时校验哈希并登记台账(5.4 节)。这既是质量保障,也是安全要求——模型文件是二进制资产,来源不明的模型文件等同于未知可执行文件。
  5. OOM 兜底:显存不足时要有明确的失败行为(快速失败 + 明确提示),不能让请求在队列里无限等待——排队比失败更糟,因为它把"部分不可用"变成"整体超时"(与第 2 篇 5.2 节同一条纪律)。
  6. 日志脱敏:本地方案最容易掉以轻心——“数据在内网"不等于"日志可以存明文”。内网日志一样会被导出、被备份、被运维看到。
  7. 并发保护:在应用侧做并发上限(就是 4.6 节量出来的拐点),不要指望模型服务自己扛住。
  8. 容量水位监控:显存占用、队列长度、tokens/s。这三个指标是"服务即将劣化"的先行指标——等到用户报慢的时候,你已经在事后处置了。

6.5 六个高频问题

Q1:为什么第一次请求特别慢,后面就快了?

因为第一次要"把模型从磁盘加载进显存"。 这个加载时间与模型大小、磁盘速度(NVMe 与机械盘差异巨大)有关,量级可能从几秒到数十秒。这不是故障,是本地方案的固有特性(图 5 已经画了这段)。

处置方式有三种,按推荐度排: ① 用 keep_alive 让模型常驻(代价是显存长期被占);② 服务启动后预热一次(应用启动脚本里打一个空请求,这是生产环境的常规做法);③ 接受它(低频场景可以不管)。注意:如果你把它误判成"性能太差",就会去优化一个不存在的问题。

Q2:明明有 16G 显存,为什么还是 OOM?

这是本地方案最典型的问题,成因有四个,按概率排序:

  • 上下文窗口设太大(KV Cache 超预期)——最常见的根因。
  • 并发数 × 单会话 KV 超过了余量——num_parallel 调大了但没重算显存。
  • 多个模型同时驻留(对话模型 + 嵌入模型 + 一个你以为已经卸载的旧模型)。
  • 没扣掉系统与驱动的占用:Windows 桌面上图形界面本身就要吃掉一部分显存,“标称 16G"不等于"可用 16G”。

Q3:本地模型回答质量明显不如云端,怎么补?

先别急着换更大的模型,按这个顺序排查: ① 检索是否精准(RAG 场景下 80% 的质量问题在检索层,见第 4 篇);② 提示词是否为本地模型做了降维(5.2 节);③ 上下文是否被默认值截断了(你以为是模型忘了,实际是窗口太小);④ 量化等级是否过低;⑤ 上下文里是否塞了太多无关片段;⑥ 最后才是"参数量是否不够"。把这六步走完再换模型,能省下大量无效的硬件投入。

Q4:没有独立显卡,能不能只用 CPU?

能,但要把任务类型对齐预期。 详见 2.4 节:CPU 推理适合"批处理型"任务(摘要、分类、抽取),不适合"实时对话"。推荐的折中是"CPU 跑小模型做预筛与分类、GPU 机器跑生成"——分工比硬扛划算。 另外提醒:CPU 推理的瓶颈是内存带宽,双通道内存比单通道快得多,这是很多人忽略的一点。

Q5:一台机器能同时服务几个人?

答案不是"几个人",而是"并发几个请求"——因为一个人可能不并发,而一个页面加载可能触发三次调用。正确做法是拿 4.6 节量出来的并发拐点作为容量上限,再留 30% 余量。 举例:如果拐点是 6 个并发,那你的上限就设 4,然后按"单次回答耗时 ÷ 平均思考时间"去估能承载多少用户。千万不要凭人数估算——这是本地方案容量规划里最常见的错误。

Q6:模型怎么更新?离线环境怎么办?

三件事: ① 模型是二进制依赖,要进版本台账并记哈希(5.4 节);② 离线环境必须提前跑通"介质导入"流程,包括介质接入本身的安全审批——这件事一旦拖到上线前才发现,会成为不可解的阻塞;③ 更新必须灰度(按会话分流 + 质量指标观察),因为本地换模型要重新加载、要重跑测试集,全量切换风险远高于云端。

是

否

是

否

是

否

否

是

显存不足 / OOM

上下文窗口
是否设得过大?

按业务需求重定 num_ctx
(问答 8K / 文档 16~32K)

并发数 × 单会话 KV
是否超过余量?

下调 num_parallel
直到拐点以内

是否有多个模型
同时驻留?

控制 MAX_LOADED_MODELS
缩短 keep_alive

是否扣掉了系统 / 驱动
占用的显存?

按'标称 - 系统占用'重算可用显存

降量化等级
(Q4_K_M 起步,Q5 可行,Q3 以下不要)

复测:连续请求 + 并发爬坡
确认延迟与显存都在安全区

(图 7:OOM 的成因树——排查顺序就是这张图的深度优先顺序,从最常见的"上下文设太大"开始)


7️⃣ 总结

7.1 回到开头那句追问

“数据没出境,但出没出我们的机房?” —— 这篇文章的全部内容,其实都是在回答这一个问题。

回答它需要三样东西: 一个清晰的边界判断(三档合规,别把第 1 档的需求当成第 3 档来做,也别把第 2 档的需求当成第 1 档来糊弄);一本算得清楚的账(显存四块构成 + 能力折扣 + 运维成本,别被"本地免费"骗了);以及一套能落地的架构(同一份代码 + 反向代理鉴权 + 资源隔离 + 分级路由)。

7.2 本篇的五个核心判断

  1. "合规"分三档,只有第 2、3 档必须私有化。 把档位判错,要么白花一大笔钱,要么留下一个不可交付的系统。
  2. 显存只算权重必崩,KV Cache 才是压死线上服务的那一块;上下文窗口与并发数是同一个资源的两张账单。
  3. 业务代码真的不用改(这是第 2 篇选兼容模式的兑现),但提示词必须重写、测试集必须重跑——工程量 20% 在部署,80% 在效果适配。
  4. Ollama 默认无鉴权,是必须正面处理的安全红线;正确结构是"模型端口不暴露 + 代理层鉴权限流 + 应用禁止绕过"。
  5. 本地部署 ≠ 自动合规:许可证、训练数据来源、信创名录是三个独立且无法事后补救的问题,必须在启动阶段就查,不是上线前补。

7.3 一页纸速查

我要做什么怎么做
判断该不该本地部署先定合规档位(图 1),再算"用量是否撑得起固定成本"
估算显存权重(参数量 × 每参数字节)+ KV Cache(随上下文与并发线性/成倍增长)+ 2GB 框架开销 + 20% 余量
选量化等级Q4_K_M 默认;Q5 可行;Q3 以下不要(格式遵守会先坏)
选模型规模7B~8B 起步(RAG/摘要/抽取够用),14B 为升级目标
验证服务可用ollama list / api/chat / ollama ps 三条命令
避免"改错模型名就下载几十 GB"pull-model-strategy: never(生产必须)
内网安全部署反向代理 + 鉴权 + 限流 + 白名单;模型端口不暴露;应用禁止直连
调性能固定两个调一个:先定 num_ctx → 再定 keep_alive → 最后调 num_parallel
显存不够的处置顺序降量化 → 降上下文 → 降并发(顺序反了会伤可用性)
从云端迁移代码不改,提示词重写 + 测试集重跑(这是工作量大头)
兜底到云端只有第 1 档合规允许;第 2/3 档必须"配置 + 网络策略"双重关闭

参考资料

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/sinat_41617212/article/details/169055916

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--