ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring AI+Ollama+MongoDB:私密化大模型应用全栈实战

Spring AI+Ollama+MongoDB:私密化大模型应用全栈实战 先说结论如果你所在的企业正在评估大模型落地最头疼的往往不是模型能力本身而是数据敢不敢交出去、会话记忆怎么长期维护、以及云端API账单会不会越滚越大。我最近在内部环境完整搭了一套基于Spring AI 本地大模型 MongoDB的方案把这三件事一次性解决数据全程不出内网对话和长期记忆落在自己的数据库里日常运行成本几乎为零。这篇文章就是这套方案从选型、部署、开发到排错的全过程包含可直接照抄的Java代码和我在真实环境里踩过的坑。这套方案适合谁适合已经用Spring Boot做后端、想引入大模型能力但又被合规和成本卡住的团队适合想自己掌控数据、不想被某个云厂商API绑定的个人开发者也适合正在做“私密化与记忆能力”相关技术预研的人。你不需要懂复杂的机器学习原理甚至不需要买新电脑只要有一块能跑起来Ollama的显卡按下面的步骤走半天就能把对话链路跑通。1. 整体思路与架构选型1.1 为什么是Spring AI Ollama MongoDB这个组合不是拍脑袋定的我是按企业落地的实际约束倒推出来的。先看约束第一敏感数据不能出内网至少核心业务数据不能进公网模型这是很多金融、政务、制造类客户的硬要求第二团队全是Java背景不想为了接AI单独养一支Python服务第三会话要有记忆而且要能重启不丢第四预算有限云端模型按token计费在长上下文场景下非常肉疼。按这四个约束看Spring AI负责统一模型接入和工具调用它是Spring生态官方出的AI框架Java开发者上手门槛极低后面想换云厂商模型也只是改配置Ollama负责在本地拉起开源模型它对硬件要求比vLLM低得多一条命令就能跑Windows也能玩是对本地部署大模型最友好的运行时之一MongoDB负责存消息、存长期记忆、存文档向量它的文档模型和JSON天然匹配LLM的输入输出结构而且从5.0开始原生支持向量索引一个数据库把业务数据和向量检索全包了。用个生活化类比Spring AI像统一遥控器Ollama是家里自己发电的小型发电机MongoDB是带隔层的记事本。遥控器把指令发给发电机发电机的产出再被记事本记下来下次要用直接翻记录。1.2 核心数据流设计我实际落地的数据流是这样的画成文字版方便理解用户请求 → Spring Boot网关层 → 敏感信息脱敏 → Spring AI ChatClient → Ollama本地模型 → 生成回复 → 写回MongoDB → 返回前端同时在用户发出每一轮对话之后系统后台还会做两件事对话内容 → 嵌入模型nomic-embed-text→ MongoDB向量检索 → 相似记忆/知识片段召回 → 拼入下一轮prompt第一件事是保证数据闭环。所有对话、摘要、用户画像都落在自己数据库里模型进程只在本机暴露端口公网完全访问不到。第二件事是记忆能力的核心。单纯的“把历史消息拼进prompt”只是短期记忆对话一长就会爆上下文。我的做法是每次会话结束后让模型把关键信息抽成结构化的长期记忆摘要存进MongoDB下一轮对话开始时先用向量检索把和当前问题相关的记忆捞出来作为system上下文的一部分再喂给模型。这套设计的好处是上下文可控、成本可控、敏感范围可控。后面第4章我会把每个环节的代码都贴出来。1.3 与现有方案的对比不少团队在选项上犹豫过我直接给一张对比表省得大家再踩弯路的坑。维度云端API方案如OpenAI、百炼PythonLangChain本地方案本方案数据私密性数据出内网受服务商政策限制数据不出内网数据不出内网开发成本低开箱即用中高需要Python服务Java服务配合低纯Java一套搞定记忆能力无原生记忆需自建有内存记忆但重启丢失MongoDB持久化记忆重启不丢与Spring生态集成依赖第三方SDK需要跨语言HTTP缝合官方Spring AI原生集成运行成本按token计费长对话成本高仅硬件电费仅硬件电费上手门槛低偏高要懂Python生态中低熟悉Spring Boot即可我个人更看中的是最后一点Spring AI的方案在Java工程里就是一个starter依赖后续不管换Ollama里的模型还是接私有化部署的其他推理框架代码层面几乎不用动。2. 本地大模型部署与选型2.1 Ollama Windows 11 部署实操热词里频繁出现“ollamawindows11玩转本地大模型”我就在Windows环境上完整走了一遍这里把要点整理成可直接复制的步骤。第一步安装Ollama。 下载官方安装包双击安装完成后命令行执行ollama --version能看到版本号就说明装好了。第二步拉取模型。 我日常用的是千问系列的中文模型一条命令搞定ollama pull qwen2.5:7b-instruct如果机器显存比较大可以拉14b甚至更大的版本。拉完之后执行ollama list查看本地模型列表。第三步启动服务并验证。ollama serve新开一个终端curl http://localhost:11434/api/tags能返回JSON模型列表就说明服务正常。Spring AI默认就是连这个11434端口。第四步确认显卡可用。Windows下打开任务管理器看CUDA利用率或者命令行跑nvidia-smi确认驱动和显存都正常。这里有个关键经验Ollama的默认并发数很低高并发下会排队调环境变量可以改善。在Windows里设置用户环境变量OLLAMA_NUM_PARALLEL并行请求数我设的4OLLAMA_KEEP_ALIVE模型保持加载的时间我设的30m避免频繁换入换出设置完重启Ollama服务生效。显存不足时不要把并发数调太高否则会OOM甚至整个服务崩掉。2.2 模型选型建议很多新人问“本地部署大模型到底选哪个模型”我的建议是按需求和显存来别盲目追新。下面是我实测过、能用于生产的几个选择模型显存建议特点适用场景qwen2.5:7b-instruct8~12GB中文理解好速度快中小团队的默认选择qwen2.5:14b16~24GB中文和逻辑能力更强复杂业务、知识问答qwen3:8b10~14GB新一代推理更强预算和效果平衡点llama3.1:8b8~12GB英文优秀中文一般英文业务为主phi-4:14b16~24GB代码、数学推理强开发辅助、数据分析如果你手头是4张显卡的服务器热词里有人提到“4显卡”我的建议是先用单卡8B/14B跑通全流程并发瓶颈瓶颈出现后再考虑多卡推理。Ollama本身对多卡分布式支持有限多卡机器可以优先考虑vLLM或SGLang做推理后端Spring AI那边通过OpenAI兼容接口对接效果一样。量化版本也要注意。默认拉取的多是Q4_K_M量化速度和质量最平衡。追求更高精度可以用ollama pull qwen2.5:14b-q8_0但显存占用会明显上升7b的q8模型大概要多1~2GB显存。2.3 部署注意事项与内容安全本地方案不代表完全不需要内容治理只是把治理这件事掌握在自己手里。我实际做的有三层第一层关键词过滤在Spring Boot网关层用可配置的词库做输入输出双向过滤命中规则直接返回预设话术第二层模型自审在System Prompt里明确要求模型不做违规回答不自编事实不确定的地方直接说不知道第三层人工复核对高风险会话落库并标记管理员可以事后查看上下文。热词里那个“本地部署大模型怎么解除限制词”的问题我理解的本质是开源模型自带的敏感词拦截很机械容易误伤正常业务词。解决思路不是粗暴删词而是把敏感词替换成可配置的规则引擎——把需要过滤的词放数据库维护而不是写死在新模型里。这样模型升级、规则变更互不干扰。还有个细节Ollama默认监听所有网卡内网部署一定要改成只监听内网地址千万别把11434端口暴露到公网。3. MongoDB配置与数据建模3.1 MongoDB安装与常见失败排解热词里“mongodb安装失败”出现频率很高Windows上我确实遇到过几次帮你把坑提前趟了。Windows最容易出的三个问题缺运行库MongoDB 6.x之后的安装包依赖VC Redistributable装不上就先装这个服务启动失败安装时勾选了“Install MongoDB as a Service”但data目录或log目录权限不对服务起来秒挂。解决方法是手动指定目录并用管理员权限启动mongod --dbpath D:\mongodb\data --logpath D:\mongodb\log\mongod.log --service端口被占用27017被其他程序占了改配置或者是停掉占用进程。Linux服务器上的部署相对省心我一般用官方仓库方式curl -fsSL https://www.mongodb.org/static/pgp/server-7.0.asc | sudo gpg --dearmor -o /usr/share/keyrings/mongodb-server-7.0.gpg echo deb [ signed-by/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list sudo apt-get update sudo apt-get install -y mongodb-org装完后不要裸奔必须做两件事绑定内网IP、开启认证。参考配置net: bindIp: 127.0.0.1,内网IP port: 27017 security: authorization: enabled启动后创建专用账号use admin db.createUser({ user: appUser, pwd: 强密码, roles: [{ role: readWrite, db: ai_memory }] })Spring连接串里带上authSourceadmin这一步不做好后面接入时各种认证报错。3.2 集合结构与嵌套List查询MongoDB的核心概念是数据库、集合、文档对应到我们这套系统分别是ai_memory库conversations、messages、memories三个集合每条对话记录就是一条文档。我建议的表结构设计如下conversations: { _id: ObjectId, userId: u_10086, title: 本周工作计划, createTime: ISODate, updateTime: ISODate } messages: { _id: ObjectId, convId: ObjectId, role: user | assistant, content: 原始对话文本, metadata: { tags: [高优先级, 新客户], score: 0.9 }, createTime: ISODate } memories: { _id: ObjectId, userId: u_10086, type: preference | fact | summary, key: user_notebook_brand, content: 用户偏好联想笔记本, vector: [0.12, -0.05, ...], updatedAt: ISODate }热词里有人问“mongodb 怎么查list嵌套list”实际场景就是metadata里套了tags数组。想统计每个会话里“高优先级”标签出现的次数用聚合就够了db.messages.aggregate([ { $match: { metadata.tags: 高优先级 } }, { $unwind: $metadata.tags }, { $group: { _id: $convId, count: { $sum: 1 } } } ])$unwind是把嵌套数组拆成多行拆完后再分组统计这是处理嵌套数组最常用的套路。3.3 向量存储与聚合统计长期记忆要做相似检索所以memories集合里必须有向量字段。MongoDB从5.0开始支持向量检索但要使用$vectorSearch算子的话如果用的是自建社区版我建议要么升级到7.0并用Atlas功能要么在Java端自己算余弦相似度——小规模数据量下Java端算完全没有性能问题。我是先用自建版本Java端余弦相似度跑通的数据量过万才切换到了MongoDB的向量索引方案。如果后续要上生产直接启用MongoDB的HNSW向量索引db.memories.createIndex( { vector: vector }, { name: vector_idx, vectorOptions: { type: hnsw, similarity: cosine, dimensions: 768 } } )聚合统计也比较常用比如统计每天对话数db.messages.aggregate([ { $group: { _id: { $dateToString: { format: %Y-%m-%d, date: $createTime } }, count: { $sum: 1 } } }, { $sort: { _id: -1 } } ])这些聚合查询在生产环境最实用建议提前把索引建好。messages集合我建了userIdcreateTime复合索引memories集合建了userIdtype组合索引实际查询速度提升非常明显。4. Spring AI项目实战4.1 工程搭建与依赖我用的是Spring Boot 3.3 Spring AI 1.x系列。为什么要强调版本因为Spring AI版本演进很快网上大量示例还是0.8甚至0.9的写法直接拿来对照1.x代码会编译失败。Maven依赖核心部分如下dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama-spring-boot-starter/artifactId version1.0.0/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-mongodb/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency如果用的是Spring AI 2.0.x注意模块坐标可能从spring-ai-ollama-spring-boot-starter调整成了新的ArtifactId我建议直接查官方文档的版本矩阵别再凭记忆写坐标。配置文件的关键项spring: ai: ollama: base-url: http://127.0.0.1:11434 chat: options: model: qwen2.5:14b temperature: 0.7 num-ctx: 8192 data: mongodb: uri: mongodb://appUser:密码127.0.0.1:27017/ai_memory?authSourceadmin4.2 对接本地大模型ChatClient与流式输出Spring AI 1.x里的核心入口是ChatClient代码非常简洁Configuration public class AiConfig { Bean public ChatClient chatClient(ChatClient.Builder builder) { return builder .defaultSystem(你是企业内部AI助手请用中文简洁回答不确定时明确说明。) .build(); } } RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; public ChatController(ChatClient chatClient) { this.chatClient chatClient; } PostMapping public ResponseEntityString chat(RequestBody ChatRequest request) { String answer chatClient.prompt() .user(request.content()) .call() .content(); return ResponseEntity.ok(answer); } }流式输出更符合用户体验改成Flux就行PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString stream(RequestBody ChatRequest request) { return chatClient.prompt() .user(request.content()) .stream() .content(); }实测下来7b模型在单卡上生成速度大约每秒20~40个token体验已经很接近云端API了。4.3 私密化落地脱敏与内网管控数据不出内网只是第一步系统内部还要做敏感信息管控。我的实现是在Controller入口做统一脱敏把手机号、身份证、银行卡等正则匹配出来替换成占位符再进入模型模型返回后如果业务需要再还原。这样即使日志泄漏敏感字段也不会明文落库。正则脱敏工具类的核心方法public class DesensitizedUtils { private static final Pattern PHONE Pattern.compile((?\\d{3})\\d{4}(?\\d{4})); private static final Pattern ID_CARD Pattern.compile((?\\d{6})\\d{8}(?\\d{4})); public static String mask(String input) { if (input null) return null; input PHONE.matcher(input).replaceAll(****); input ID_CARD.matcher(input).replaceAll(********); return input; } }Ollama服务端还要做一层管控绑定127.0.0.1或者只允许内网IP访问生产环境我建议放防火墙白名单Spring Boot服务也只开必要的端口运维时能少很多焦虑。4.4 记忆与知识库实现这是整个方案里最核心的部分。我按记忆的时效分两层短期记忆用“最近N轮消息注入”每次请求时从MongoDB把当前会话最近10条消息按时间排序取出拼成对话历史上下文和当前问题一起发给模型。长期记忆用“摘要抽取向量召回”每轮对话结束后后端异步调用模型生成一条JSON格式的摘要先检查这条摘要和已有记忆的相似度如果相似度很高就更新旧记录否则插入新记录。这样不会无限堆积冗余记忆。摘要抽取的Prompt模板我调整了好几次最终稳定版是这样请从上面的对话中提取关于用户的重要信息包括偏好、事实、身份、任务状态。 只输出JSON格式如下 {memories: [{type: preference|fact|summary, key: 简短键名, content: 一句话描述}]} 如果本轮没有值得记忆的信息输出{memories: []}Java端执行摘要抽取并写库Service public class MemoryService { private final ChatClient chatClient; private final MongoTemplate mongoTemplate; public Memory extractAndSave(String userId, String conversationText) { String json chatClient.prompt() .user(对话内容\n conversationText \n\n请提取长期记忆。) .call() .content(); Memory memory parseMemoryFromJson(json); memory.setVector(embeddingService.embed(memory.getContent())); memory.setUserId(userId); mongoTemplate.save(memory, memories); return memory; } }这里的embeddingService用的是Ollama的嵌入模型Spring AI同样有现成接口配置一下即可EmbeddingModel embeddingModel OllamaEmbeddingModel.builder() .baseUrl(http://127.0.0.1:11434) .modelName(nomic-embed-text) .build();数据召回时把用户当前问题的向量和memories里的向量做余弦相似度取TopK5拼入prompt。这样模型能“想起”很早就聊过的偏好而不是靠上下文硬堆。知识库本质上和长期记忆是同一套机制把文档切块、嵌入、入库查询时召回。区别只在memories集合里的type字段文档片段记成“knowledge”即可。这套逻辑在Spring AI里现在也已经有了RAG相关的独立模块但我更推荐先自己走通整个流程后面再交给框架接管。4.5 Agent与工具调用企业场景下光聊天是不够的经常需要模型去查订单、查库存。Spring AI支持工具调用实现方式非常JavaService public class OrderToolService { Tool(description 根据用户ID查询最近订单信息) public String getRecentOrder(String userId, int limit) { // 走内部订单服务 return orderClient.queryRecentOrders(userId, limit); } }然后在构建ChatClient时把工具注册进去ChatClient chatClient ChatClient.builder(chatModel) .defaultTools(new OrderToolService()) .build();模型会发现用户问题涉及订单信息时自动调用这个Java方法拿到结果后组织回答。这就是Spring AI Agent的基本形态。从这里再演化就是可以组合多个工具按计划执行的复杂Agent后续可以单开一篇展开。5. 常见问题排查与性能优化5.1 高频问题速查表我整理了这半年自己踩过和最常被问到的几个问题一个表说清楚问题现象根本原因解决方案Ollama拉取模型老是中断网络不稳定换镜像源或断点重试ollama pull本身支持断点续传Windows下MongoDB服务启动即停data/log目录无权限使用管理员权限运行mongod --dbpath ... --logpath ... --serviceSpring AI连接Ollama报ConnectExceptionOllama没启动或端口不对先curl http://127.0.0.1:11434/api/tags验证模型重复回复同一句话温度参数太低或上下文冲突调高temperature到0.7检查历史消息拼接是否正确中文乱码MongoDB连接串编码问题连接串加?authSourceadminSpring Boot侧统一UTF-8首次对话很慢后续很快模型冷启动加载设置OLLAMA_KEEP_ALIVE30m输出被截断默认max_tokens太小在ChatOptions里调大num-predict聚合查询耗时高缺少索引按查询条件建复合索引服务重启后记忆丢失用了框架默认的内存记忆按本文方案自行持久化到MongoDB热词里还出现过“mongodb fassert()”报错。这个一般是因为MongoDB版本和驱动版本不匹配导致部分命令被拒绝执行。解决思路很简单把MongoDB Java Driver升级到和MongoDB Server大版本匹配的版本比如Server 7.0对应driver 5.x。5.2 性能优化与并发控制本地大模型最怕的是并发冲垮GPU。我实测过Ollama在单卡24G环境下跑7b模型默认并行度是1多个请求会排队。调到4之后响应变快但GPU显存占用明显上升显存不足时会自动换出模型导致性能剧烈抖动。我的经验值是7b模型、24G显存、并行度2~4比较稳妥14b模型建议并行度1~2。Spring Boot侧还要做一道限流用信号量控制同时进入模型层的请求数Component public class RateLimiter { private final Semaphore semaphore new Semaphore(8); public T T execute(SupplierT supplier) throws InterruptedException { semaphore.acquire(); try { return supplier.get(); } finally { semaphore.release(); } } }8个信号量对应Ollama并行度4实际进入模型的请求不会超过4剩下的是排队等待这样比直接打满GPU更稳定。MongoDB这块主要是连接池和索引。Spring Data MongoDB默认连接池足够用但要注意每条消息都做一次写入的话批量插入比单条插入快得多我建议每轮对话后攒着批量写一次。5.3 云厂商API作为降级方案既然是“企业级免费大模型应用”本地方案就是主链路但云厂商API也不是完全不用。我的做法是配置两套ChatClient本地模型不可用比如模型换出时自动降级到云端的OpenAI兼容接口。用Spring AI 2.0连接百炼服务的热词我看到了思路一样把百炼的DashScope地址配置成OpenAI兼容的base-url做一个Primary默认本地、Secondary云端的路由。这样做的好处是关键业务请求永远有响应而日常流量基本都打在本地上账单控制得住。关键配置片段spring: ai: openai: base-url: https://dashscope.aliyuncs.com/compatible-mode/v1 api-key: ${DASHSCOPE_API_KEY} chat: options: model: qwen3注意走云端接口的数据必须已经过脱敏层反正我们系统前端入口本来就统一脱敏了所以这点风险可接受。6. 从Dify工作流迁移到Spring AI的路径6.1 可视化编排与Java代码的对应关系热词里“dify工作流转成spring ai java代码github”搜索量不低说明不少人起手用Dify做验证但到了生产阶段还是想回到代码里。Dify的工作流节点和Spring AI的组件其实是一一对应的Dify节点Spring AI/Java等价物知识库检索节点EmbeddingModel MongoDB向量检索大模型节点ChatClient.prompt().call()条件分支节点简单if/else或者Switch问题分类器ChatModel调用或规则路由HTTP请求节点RestTemplate/WebClient调用Agent节点Tool工具注册Dify胜在可视化、迭代快适合业务人员先验证Prompt和流程Spring AI胜在可控、可测试、可嵌入现有工程。我见过最合理的组合是Dify做DemoSpring AI做生产两边各发挥各的强项。6.2 一个迁移实例知识问答Agent举个例子Dify里一个典型的知识问答工作流是用户问题→知识库检索→拼Prompt→LLM回答→输出。迁移到Spring AI分四步走第一步把Dify的Prompt搬到ChatClient的System Message里第二步把知识库检索写成DocumentRetriever组件内部用MongoDB向量查询第三步在Prompt模板里加一个{{knowledge}}占位符检索结果动态填充第四步用单元测试固化场景确保切到Java后和Dify里的输出风格一致。核心代码结构public String answer(String question) { ListDocument docs retriever.search(question, 5); String knowledge docs.stream().map(Document::getText).collect(Collectors.joining(\n)); return chatClient.prompt() .system(你是企业知识助手请基于提供的资料回答资料中没有的信息直接说明。) .user(资料\n knowledge \n问题 question) .call() .content(); }这套迁移路径我走了两遍第一遍踩了不少版本坑第二遍就顺畅了。建议所有在Dify里验证过的工作流都尽早做这样的代码化迁移不然业务越跑越大重构成本也会越来越高。最后分享一点个人体会吧。这套方案我前后折腾了大半年最大的感悟是不要一上来就追最新的模型和框架先把Spring AI Ollama MongoDB这条链路彻底跑通让数据在里面转起来之后再换模型、加Agent能力都只是配置层面的事情。MongoDB的坑反而比模型多集合设计、索引规划、嵌套查询这些基本功花的时间都是值得的。如果你也在做类似的企业级私密化大模型应用建议从今天开始先在自己电脑上把Ollama跑起来再把一条对话存进MongoDB你就已经超过一半只停留在概念阶段的人了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进