本文是《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),二是任何形式的"云端兜底"都必须彻底关闭,没有例外通道。
(图 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 模型要多少显存"这个问题,标准答案是"看情况”——但可以估。 显存占用由四块构成,很多人只算了第一块,然后在生产环境被后三块打崩。
(图 2:显存预算的四块构成——只算权重是最常见的错误,KV Cache 才是压死线上服务的那一块)
先说权重这一块,它有个好记的换算法则:
| 量化等级 | 每参数字节 | 7B 约占用 | 14B 约占用 | 质量感受 |
|---|---|---|---|---|
| FP16 | 2.0 | 14 GB | 28 GB | 基准(几乎用不上,太贵) |
| Q8 | ~1.0 | 7.5 GB | 15 GB | 与 FP16 差异通常很小 |
| Q5_K_M | ~0.7 | 5.3 GB | 10 GB | 接近 Q8,性价比高 |
| Q4_K_M | ~0.6 | 4.5 GB | 9 GB | 默认甜点档,多数场景够用 |
| Q3_K_M | ~0.45 | 3.5 GB | 7 GB | 开始能感到退化 |
| Q2 | ~0.35 | 2.7 GB | 5.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~3B | 1~2 GB | 关键词抽取、简单分类、短文本改写、意图识别 | 任何需要多步推理的任务 |
| 7B~8B | 4.5~5 GB | RAG 回答生成、摘要、结构化抽取、客服问答(本地方案的主力) | 复杂数学、多跳推理、长文档全局理解 |
| 14B | 9~10 GB | 上述全部 + 更强的指令遵循与中文表达 | 需要深度专业知识的高难度问答 |
| 32B | 19~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 服务"在过去一两年里被反复发现并利用(属于典型的"默认无鉴权 + 用户图省事"组合)。一旦发生,后果不只是资源被白用,还可能是内网拓扑与业务数据被顺藤摸瓜。
所以我把内网部署的正确结构写死:
(图 3:内网私有化的正确部署结构——鉴权、限流、白名单都在代理层,模型端口不直接暴露)
四条硬要求:
- Ollama 只监听回环地址,或仅绑定到"代理所在的那台机器/专用网段",不监听全网段。
- 所有访问必须经过带鉴权的反向代理:用一个固定 Header 或内网令牌校验,并且配上速率限制(这一层同时解决了 5.2 节里"单个客户端压垮服务"的问题)。
- 严禁把模型端口映射到公网,也不要图省事把
11434直接开在办公网段上。 - 应用侧禁止绕过代理直连:这一条靠"网络策略 + 代码规范"双重约束——只有模型端口不可直连,代理层才真正成为唯一入口。
为什么做到这四条就算"合规"了? 因为它把"无鉴权的裸服务"变成了"有身份校验的内部服务",并且所有调用都经过一个可审计的点。审计能力是企业合规里经常被忽略但必查的一项——你需要能回答"谁在什么时候调用了模型、调了多少次"。
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 / Service | chatClient.prompt().user(q).call().content() | 完全相同 | ❌ 不改 |
| 结构化输出 | .call().entity(Ticket.class) | 完全相同 | ❌ 不改 |
| 流式 | .stream().content() | 完全相同 | ❌ 不改 |
| 提示词模板 | .user(u -> u.text(...).param(...)) | 完全相同 | ❌ 不改 |
| 配置 | spring.ai.openai.* + base-url | spring.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 本地优先 + 云端兜底:一个要用合规闸门锁住的模式
"本地模型做默认,遇到难题升级到云端"听起来很美,但它是本篇里最需要警惕的设计,因为它会悄悄把数据送出内网。所以这个模式的第一道关卡不是技术,是合规:
(图 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 节说的那个拐点,它是限流阈值与实例数的依据 |
(图 5:一次本地推理的完整时序——"首次慢"是加载,"后续快"是驻留,"并发崩"是 KV Cache,三个现象三个原因)
这张图把本地方案里最容易混淆的三个现象解释清楚了。 我在 Q1~Q6 里会逐个对应到具体处置,但先记住它们的因果:加载慢 → 用驻留解决;驻留占显存 → 用 keep_alive 与模型数控制;并发崩 → 用 num_parallel 与并发上限控制。 三个旋钮互相牵制,这才是本地方案真正的复杂度所在。
5️⃣ 进阶与优化
5.1 性能:三个旋钮的联动关系
keep_alive(驻留时长)、num_parallel(并行数)、num_ctx(上下文窗口)这三个参数不是独立的,它们共同争抢同一块显存。 我给一个可操作的调优顺序,核心原则是"固定两个、调一个、每次只动一个":
- 先定
num_ctx:由业务需求决定(问答 8K、文档分析 16K~32K)。先定它是因为它最不可妥协——设小了功能就不对。 - 再定
keep_alive:高频服务设长(避免反复加载),多模型混跑设短(避免互相挤占)。判断依据是"加载一次的代价 × 加载频率" 与 “显存被占的机会成本” 哪个大。 - 最后调
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 混合架构:按敏感度分级路由
这是我在企业内落地时最推荐的架构,因为它同时兼顾了"合规"和"效果"。
核心思路是:不是"整个系统用本地"或"整个系统用云端",而是按数据敏感度分流。 把请求分成三级,每级走不同的通道:
(图 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 ⚠️ 生产建议
八条,前四条是"上线前必须有":
- 鉴权与访问控制(3.5 节):代理层做鉴权、限流、白名单;模型端口不暴露;应用禁止绕过代理直连。
- 资源隔离:模型服务与业务应用分机部署。理由在 5.4 节:OOM 会拖死整机。
- 审计日志:记录调用方、时间、模型、token 量、是否脱敏。合规检查必查项,且必须能长期留存。
- 模型文件校验:引入时校验哈希并登记台账(5.4 节)。这既是质量保障,也是安全要求——模型文件是二进制资产,来源不明的模型文件等同于未知可执行文件。
- OOM 兜底:显存不足时要有明确的失败行为(快速失败 + 明确提示),不能让请求在队列里无限等待——排队比失败更糟,因为它把"部分不可用"变成"整体超时"(与第 2 篇 5.2 节同一条纪律)。
- 日志脱敏:本地方案最容易掉以轻心——“数据在内网"不等于"日志可以存明文”。内网日志一样会被导出、被备份、被运维看到。
- 并发保护:在应用侧做并发上限(就是 4.6 节量出来的拐点),不要指望模型服务自己扛住。
- 容量水位监控:显存占用、队列长度、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 节);② 离线环境必须提前跑通"介质导入"流程,包括介质接入本身的安全审批——这件事一旦拖到上线前才发现,会成为不可解的阻塞;③ 更新必须灰度(按会话分流 + 质量指标观察),因为本地换模型要重新加载、要重跑测试集,全量切换风险远高于云端。
(图 7:OOM 的成因树——排查顺序就是这张图的深度优先顺序,从最常见的"上下文设太大"开始)
7️⃣ 总结
7.1 回到开头那句追问
“数据没出境,但出没出我们的机房?” —— 这篇文章的全部内容,其实都是在回答这一个问题。
回答它需要三样东西: 一个清晰的边界判断(三档合规,别把第 1 档的需求当成第 3 档来做,也别把第 2 档的需求当成第 1 档来糊弄);一本算得清楚的账(显存四块构成 + 能力折扣 + 运维成本,别被"本地免费"骗了);以及一套能落地的架构(同一份代码 + 反向代理鉴权 + 资源隔离 + 分级路由)。
7.2 本篇的五个核心判断
- "合规"分三档,只有第 2、3 档必须私有化。 把档位判错,要么白花一大笔钱,要么留下一个不可交付的系统。
- 显存只算权重必崩,KV Cache 才是压死线上服务的那一块;上下文窗口与并发数是同一个资源的两张账单。
- 业务代码真的不用改(这是第 2 篇选兼容模式的兑现),但提示词必须重写、测试集必须重跑——工程量 20% 在部署,80% 在效果适配。
- Ollama 默认无鉴权,是必须正面处理的安全红线;正确结构是"模型端口不暴露 + 代理层鉴权限流 + 应用禁止绕过"。
- 本地部署 ≠ 自动合规:许可证、训练数据来源、信创名录是三个独立且无法事后补救的问题,必须在启动阶段就查,不是上线前补。
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 档必须"配置 + 网络策略"双重关闭 |
参考资料
- Ollama 官方文档 · API 与参数说明(
/api/chat、/api/ps、num_ctx、keep_alive等):https://github.com/ollama/ollama/blob/main/docs/api.md - Ollama 官方 FAQ · 环境变量与并发(
OLLAMA_HOST/OLLAMA_NUM_PARALLEL/OLLAMA_MAX_LOADED_MODELS/OLLAMA_KEEP_ALIVE):https://github.com/ollama/ollama/blob/main/docs/faq.md - Ollama 官方文档 · 模型库与标签(量化等级命名规则):https://ollama.com/library
- Spring AI Reference Documentation · Ollama Chat(
spring.ai.ollama.*配置与num-ctx):https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html - Spring AI Reference Documentation · Ollama Embedding(本地嵌入模型配置):https://docs.spring.io/spring-ai/reference/api/embeddings/ollama-embeddings.html
- Spring AI Reference Documentation · Pull Model Strategy(
init.pull-model-strategy的取值与语义):https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html - Ollama 安全公告与加固建议(无鉴权暴露风险):https://github.com/ollama/ollama/security
- Nginx 官方文档 · 反向代理、
limit_req限流与allow/deny访问控制:https://nginx.org/en/docs/
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/sinat_41617212/article/details/169055916




