Spring AI + Ollama:内网私有化部署的合规与避坑指南

0 阅读

为什么“数据不出内网”是个伪命题

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

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

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

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

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

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

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

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

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

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

本地模型的真实代价

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

云端方案换模型是分钟级的事,本地方案换模型是小时到天级的事——不只是下载时间,还包括重新跑一遍测试集看质量有没有退化、重新压一遍并发看吞吐有没有下降。这个差异会深刻影响你的技术决策节奏:在本地方案里,换一次模型的成本高到必须一次定准。

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

  • 数据明确不能出内网(第 2、3 档合规)——这是唯一不可替代的理由。
  • 调用量足够大:云端账单已经大到“半年的费用能买一台 GPU 机器”。
  • 对网络稳定性敏感:业务不能接受“外网抖动导致 AI 功能不可用”。
  • 需要离线运行:现场环境没有外网。
  • 对模型行为有“可复现”要求:需要长期锁定同一个模型版本。

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

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

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

总显存占用 = 模型权重 + KV Cache + 上下文激活与中间张量 + 框架与运行时开销。

权重与量化

关于量化,有一条经验性的判断:Q4_K_M 是“默认档”,Q5 是“能承受就上”,Q3 以下不要碰。原因不是“低了不好”,而是质量退化不是线性的:从 Q8 到 Q5 的差异在很多任务上测不出来,从 Q4 到 Q3 开始出现“格式遵守变差、长句逻辑断裂”,到 Q3 以下会出现一种很难排查的现象——模型能流畅说话,但会忘记你给它的格式要求。

KV Cache:最容易被低估的一块

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

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

模型档位选择

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

没有独显怎么办

必须说清楚:CPU 推理是可行的,但要做到“能接受”,需要管理预期。消费级 CPU 上跑 7B @ Q4,大致是每秒几个 token 的量级——意味着生成 200 字的一段回答要十几秒。这对“后台批处理”完全够用,对“实时对话”就很难受。瓶颈不是算力,是内存带宽。CPU 推理时,每一层都要把权重从内存搬进缓存,所以内存频率和通道数比核心数更重要。

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

安装本身很简单,但有两件事我建议装完立刻处理:

第一,模型存储路径。默认会把模型放在系统盘的用户目录下,而一个 7B @ Q4 模型就是 4~5GB,跑几个模型很快把系统盘吃掉。装完先把存储目录改到大盘。

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

拉模型:读懂模型标签

模型标签(tag)通常由三段信息组成:型号 + 规模 + 量化。理解这三段,你就能自己判断“这个模型该占多少显存、适不适合我的任务”。有个很实用的细节:同一型号的“默认 tag”与“指定量化的 tag”,实际拉到的可能是不同的量化版本。

三个必查命令

  1. curl -s http://localhost:11434/api/tags:查看有什么模型。
  2. curl -s http://localhost:11434/api/chat ...:验证能不能对话。
  3. curl -s http://localhost:11434/api/ps:查看当前谁在占显存。

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

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

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

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

更严重的一种情况是端口被映射到公网。这类“暴露在公网的 Ollama 服务”在过去一两年里被反复发现并利用。一旦发生,后果不只是资源被白用,还可能是内网拓扑与业务数据被顺藤摸瓜。

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

  1. Ollama 只监听回环地址,或仅绑定到“代理所在的那台机器/专用网段”,不监听全网段。
  2. 所有访问必须经过带鉴权的反向代理:用一个固定 Header 或内网令牌校验,并且配上速率限制。
  3. 严禁把模型端口映射到公网,也不要图省事把 11434 直接开在办公网段上。
  4. 应用侧禁止绕过代理直连:这一条靠“网络策略 + 代码规范”双重约束。

为什么做到这四条就算“合规”了?因为它把“无鉴权的裸服务”变成了“有身份校验的内部服务”,并且所有调用都经过一个可审计的点。审计能力是企业合规里经常被忽略但必查的一项。

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

依赖与配置

沿用 Spring AI 的 BOM,只引 starter,不引中间模块。Ollama 这条线上还有一个额外好处:它不需要任何密钥,所以配置里“最危险的那一项”直接消失了——但请注意,它换来的是“网络位置本身成了安全边界”。

spring:
  ai:
    ollama:
      base-url: http://ai-gateway.internal:8080
      chat:
        model: qwen2.5:7b-instruct-q4_K_M
        temperature: 0.2
        num-ctx: 8192
      init:
        pull-model-strategy: never

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

业务代码真的不用改

这是之前选择 OpenAI 兼容抽象方式的价值兑现时刻。因为第 2 篇我们选了兼容模式,业务层的写法完全一样,变的只有配置。Controller、Service、结构化输出、流式调用、提示词模板,代码一行都不用改。

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

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

第二,provider 特有的参数会漏出来。上下文窗口、量化、并行数这些概念在云端是“服务商替你决定”,在本地是“你必须决定”。

第三,错误的形态不一样。云端方案里你看到的是 HTTP 状态码;本地方案里你会看到 OOM、模型加载超时、队列等待、推理速度断崖——这些都不是“请求失败”,而是“服务劣化”,更难被发现。

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

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

对于第 2/3 档(数据不可出内网),云端兜底必须物理关闭。只要兜底通道在代码里存在,它迟早会被触发。而“一次云端调用”就足以让整条合规链路失效。所以正确做法是双重锁定:配置上不注入云端模型的 Bean,网络策略上让内网机器在物理上无法访问外网模型域名。

如果确实需要第 1 档的兜底,实现上要注意 Bean 隔离,将本地模型设为 @Primary,云端模型只在“合规允许 + 显式点名”时使用。

本地嵌入模型

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

第一,嵌入模型也吃显存。好在它很小,但它必须与对话模型同时驻留——所以 OLLAMA_MAX_LOADED_MODELS 至少要设成 2。

第二,向量维度决定存储量。存储估算很简单:向量条数 × 维度 × 4 字节 + 索引开销。

第三,嵌入模型换了,必须全量重嵌入。不同嵌入模型产出的向量在同一个向量空间里不可比,混用会导致检索结果完全错乱。所以“换嵌入模型”不是一个可以灰度的事,它是一次性的全量重建。

三个必须实测的数

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

  1. 首次请求延迟(含模型加载):它会比稳态慢一个数量级,不知道这个数,你会把它误判成“性能很差”。
  2. 稳态 tokens/s:决定“一个回答要等多久”,也决定能不能做流式体验。
  3. 并发拐点:并发从 1 逐步加到 2/4/8,记录延迟与显存。找到那个拐点,它是限流阈值与实例数的依据。

进阶与优化

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

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

  1. 先定 num_ctx:由业务需求决定。
  2. 再定 keep_alive:高频服务设长,多模型混跑设短。
  3. 最后调 num_parallel:从 1 开始往上加,盯着延迟与显存的拐点。

如果显存不够,处置顺序绝对不能反:先降量化等级 → 再降上下文 → 最后降并发。理由是这三者对用户体验的伤害程度递增:降量化损失的是“细微质量”;降上下文损失的是“能力边界”;而降并发损失的是“可用性”。

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

给云端顶级模型写的提示词,直接搬到本地 7B 上,通常会掉一个档次。原因不是“模型笨”,而是本地模型在“长指令遵循”和“复杂格式约束”上更脆弱。

四条实操规则:

  • 把多步指令拆成单步。本地模型的单步准确率远高于多步串联。
  • 格式要求要简单且给出示例。对本地模型,一个 few-shot 示例比三段文字描述有效得多。
  • 降低上下文的“信息密度”。长上下文里本地模型更容易丢信息,所以 RAG 场景要控制注入片段的数量。
  • 改完提示词必须重跑测试集。本地模型对提示词更敏感。

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

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

这是我在企业内落地时最推荐的架构,因为它同时兼顾了“合规”和“效果”。核心思路是:不是“整个系统用本地”或“整个系统用云端”,而是按数据敏感度分流。

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

正确的做法是“规则优先 + 本地模型辅助”:用正则与字典处理结构化、可枚举的敏感字段;用本地小模型处理“难以用规则描述”的部分。规则库的升级可以由本地模型生成候选、人工确认后入库。

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

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

第一,模型要进版本台账。记录模型型号、tag、量化等级、文件哈希、来源、引入日期、配套的提示词版本、当时的测试集通过率。

第二,离线更新要有流程。第 3 档合规环境里没有外网,模型更新只能靠介质导入或内网仓库。请提前把这个流程跑通一次。

第三,换模型要灰度。做法与会话级灰度完全一致:双模型并存、按会话分流、先给内部用户试、观察质量指标。

第四,模型服务与业务应用必须分开部署。这一条是硬要求,理由很直接:模型 OOM 会把整台机器拖死,如果业务应用同机部署,你的接口会跟着一起挂。

适用边界与风险提示

适用与不适用场景

本方案唯一不可替代的价值在于数据明确不能出内网。它适合任务类型以“理解与转换”为主(问答、摘要、改写、分类、信息抽取),以及配合 RAG 使用。本地模型 + 内网知识库是政企场景最常见的组合。

但不适合复杂推理与多跳问答、超长文档的全局理解、多模态理解,以及追求“最好效果”而不考虑数据出网的场景。如果没有运维人力的团队,也不建议使用,因为模型、显存、并发、版本全归你管,它不是一个“装上就不管”的组件。

“本地部署 = 合规”的三个误区

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

误区一:本地部署了,许可证就没问题。事实上,开源权重并不等于可自由商用。

误区二:模型的训练数据来源与本项目无关。在部分行业,“模型是用什么数据训练的”本身就可能成为风险点。

误区三:本地部署了就满足信创要求。在政企与国企场景里,“是否在信创名录内”往往是独立的评审项。

这三条的共同点是:它们都无法用技术手段补救,且成本极高。所以我的建议是把它们放在项目启动阶段的第一份检查清单里。

生产建议

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

  1. 鉴权与访问控制:代理层做鉴权、限流、白名单;模型端口不暴露;应用禁止绕过代理直连。
  2. 资源隔离:模型服务与业务应用分机部署。
  3. 审计日志:记录调用方、时间、模型、token 量、是否脱敏。
  4. 模型文件校验:引入时校验哈希并登记台账。
  5. OOM 兜底:显存不足时要有明确的失败行为,不能让请求在队列里无限等待。
  6. 日志脱敏:本地方案最容易掉以轻心——“数据在内网”不等于“日志可以存明文”。
  7. 并发保护:在应用侧做并发上限,不要指望模型服务自己扛住。
  8. 容量水位监控:显存占用、队列长度、tokens/s。

六个高频问题

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

因为第一次要“把模型从磁盘加载进显存”。处置方式有三种:用 keep_alive 让模型常驻;服务启动后预热一次;或者接受它。

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

成因有四个,按概率排序:上下文窗口设太大;并发数 × 单会话 KV 超过了余量;多个模型同时驻留;没扣掉系统与驱动的占用。

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

先别急着换更大的模型,按这个顺序排查:检索是否精准;提示词是否为本地模型做了降维;上下文是否被默认值截断了;量化等级是否过低;上下文里是否塞了太多无关片段;最后才是“参数量是否不够”。

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

能,但要把任务类型对齐预期。CPU 推理适合“批处理型”任务,不适合“实时对话”。推荐的折中是“CPU 跑小模型做预筛与分类、GPU 机器跑生成”。

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

答案不是“几个人”,而是“并发几个请求”。正确做法是拿量出来的并发拐点作为容量上限,再留 30% 余量。

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

三件事:模型是二进制依赖,要进版本台账并记哈希;离线环境必须提前跑通“介质导入”流程;更新必须灰度。

总结

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

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

本篇的五个核心判断:

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