Spring Boot 3.2 + Spring AI + Ollama 本地大模型部署实战教程
上个月给一个内部管理系统加 AI 问答功能本来打算直接调云端大模型 API结果客户随口一句“数据能不能不出内网”把我问住了。仔细一想工单、合同、客户备注这些都是敏感信息走云端确实不合适。于是我把目光转向本地部署方案最后用 Spring Boot 3.2 Spring AI Ollama 把智能对话跑通了。这篇教程就是那两周实战踩坑的完整总结适合想在内网或本机快速接入大模型又不希望在代码里硬拼 HTTP 请求的 Java 开发者。1. 为什么把大模型请回本地从接口调用的痛点说起1.1 云端大模型遇到的三道坎先说结论如果你的项目只是个人练手调用云端 API 确实是最快的路。但一旦落到企业系统尤其是数据敏感的业务系统云端 API 往往会遇到三个比较现实的问题。第一是数据边界。客户信息、财务数据、内部流程文本只要发出请求就必须经过第三方服务。哪怕对方承诺不留存安全评审这一关也很难过。我接触的项目里很多甲方直接要求“数据不能出当前环境”这时候本地大模型就成了唯一选择。第二是成本模型的不可控。云端 API 按 token 计费看起来单价不高但对话场景的特点是请求频繁、上下文越来越长。当系统里每个用户都在闲聊式提问时账单会涨得比想象中快得多。本地部署则是一次性硬件投入长期运行成本主要就是电费。第三是延迟和可用性的不确定性。云端 API 的响应时间波动很大高峰期可能要等好几秒才返回第一个 token。内网部署的大模型虽然绝对速度不一定快但响应时间更稳定不会因为某个云区域故障而集体超时。1.2 为什么是 Ollama 而不是 LM Studio / llama.cpp 系本地跑大模型的工具其实很多除了 Ollama还有 LM Studio、llama.cpp、vLLM 等。我最终选择 Ollama主要是看中它对 Java 生态的友好程度。Ollama 本质上是一个大模型运行时管理器它把模型的下载、量化、推理封装成了一条条简单命令比如ollama run qwen2.5:7b。它自带一个 HTTP API监听在 11434 端口任何语言都能通过 REST 调用。Spring AI 官方就提供了 Ollama 的集成模块依赖一引入配置一写就能像操作RestTemplate一样操作大模型。LM Studio 更适合桌面端那种“下载模型、点开聊天界面”的场景它对开发者开放接口的能力弱一些。llama.cpp 能力很强但需要自己编译、管理模型文件还得处理各种权重格式工程成本偏高。vLLM 则更偏向高并发推理服务对硬件要求也更高普通开发机跑起来有点浪费。如果你只是想在一个 Spring Boot 项目里快速接上本地大模型Ollama 是当前最省事的路径。1.3 这套方案适合谁、解决什么问题这套方案的定位很明确让 Java 后端服务拥有一个可调用的本地大模型对话接口。它适合下面几类场景企业内部知识库问答机器人数据不出内网。个人开发者在本地电脑上做 AI 功能原型验证。低成本搭建一个私有化 ChatGPT 风格服务供小团队内部使用。教学演示需要在一台普通电脑上完整展示“后端接入大模型”的链路。它不适合的场景也很清楚如果单次对话量非常大、并发要求极高或者需要训练微调那么 Ollama 的性能和功能就不太够用了应该考虑更专业的推理框架。2. 环境准备Ollama 安装与模型拉取的完整操作2.1 各平台安装与版本选择Ollama 的安装非常简单它提供了 Windows、macOS、Linux 三种系统的安装包。Windows 版是 exe 安装包双击后会自动安装在用户目录下并注册开机启动服务。macOS 同样有 dmg 包装上就能用。Linux 则是一条命令curl -fsSL https://ollama.com/install.sh | sh如果服务器在内网没有外网访问权限也可以从官方 GitHub Releases 页面下载离线安装包拷贝进去手动安装。手动安装的本质是下载解压二进制文件放到/usr/local/bin下再通过 systemd 启动服务。这一步没什么黑魔法。安装完成后通过ollama --version验证安装是否成功。然后还需要确认服务是否在运行。在 Windows 和 macOS 上安装包已经自动启动了后台服务Linux 上如果是手动下载的包需要先执行ollama serve 等到服务启动后再用curl http://127.0.0.1:11434/api/tags测试 API 是否通了。返回一串 JSON 说明服务正常。2.2 模型选型qwen2.5、llama3.2、gemma2 怎么选模型选型是很多人纠结的地方。搜“ollama本地部署大模型哪个模型最佳”你能看到一堆推荐但真正要考虑的只有两件事硬盘空间和内存容量。我个人的经验是在普通开发机16GB 内存左右上首推qwen2.5:7b。Qwen 系列对中文的理解明显好于同规模的 Llama 模型而且 7B 参数配合 4bit 量化大约需要 5-6GB 内存普通电脑跑得动。Llama 3.2 系列如果要做英文为主的场景也可以选但中文输出有时候会出现语序别扭的问题。Gemma 2 我试过对中文的流畅度也一般。如果你的内存有 32GB 以上可以考虑qwen2.5:14b对话质量会上一个台阶。但超过 14B 的模型在纯 CPU 机器上基本没有实用性每个 token 要等好几秒体验很差。如果只有 8GB 内存只能选更小的模型比如qwen2.5:3b或llama3.2:3b。这类小模型做摘要、提取关键词够用但做有深度的对话还是差点意思。2.3 国内下载慢的解决办法与模型文件校验Ollama 默认从官方仓库拉模型国内网络环境下经常出现“卡在 0% 或下载了几天都完不成”的情况。这里分享几个我试过有效的方法。第一个办法是修改 OLLAMA_MODELS 环境变量把模型存放目录指向磁盘剩余空间较大的位置。模型文件动辄 4-8GB如果系统盘小拉取很容易失败。Windows 用户可以在“系统属性 - 环境变量”里新增OLLAMA_MODELSD:\ollama_models设置后重启 Ollama 服务。第二个办法是使用国内镜像源。官方支持通过OLLAMA_HOST等环境变量调整服务地址但对于模型下载更实用的做法是看看你的网络环境有没有可用的镜像加速服务。不同地区情况不同网上有整理好的镜像地址把环境变量配置到 Ollama 的启动脚本里即可。我自己实测用镜像后下载速度从“几 KB/s”提到了“几十 MB/s”。第三个办法是下载模型变体的 GGUF 文件再导入。如果你有特殊渠道拿到模型的 GGUF 文件比如从 ModelScope 下载可以写一个 ModelfileFROM /home/user/qwen2.5-7b-instruct-q4_k_m.gguf然后在同一目录下执行ollama create qwen2.5-local。这样就不走官方仓库下载而是直接使用本地文件速度取决于磁盘内拷贝。2.4 用命令行快速验证模型可用性模型下载完成后先不要着急去写 Java 代码先用命令行验证一下ollama run qwen2.5:7b输入一句“你好”如果模型能够正常回答说明推理链路是通的。按 CtrlD 退出对话。然后测试一下 API 接口curl http://127.0.0.1:11434/api/generate -d { model: qwen2.5:7b, prompt: 用一句话介绍 Spring Boot, stream: false }返回的 JSON 里有一个response字段内容就是模型生成的文本。这一步能排除掉“Ollama 装了但 API 没起”这种基础问题。等 API 通了再进 Spring Boot 的阶段。3. Spring AI 接入前的关键认知ChatModel 与 ChatClient 的关系3.1 Spring AI 项目现状与版本选择Spring AI 是 Spring 生态里专门做 AI 应用集成的项目。它目前还处于快速迭代阶段版本号变化比较频繁但基本思路已经很清晰把各种大模型提供商OpenAI、Ollama、智谱、通义等抽象成统一的接口让业务代码不依赖具体某个大模型。我用的时候最新稳定版是1.0.0-M6配 Spring Boot 3.2.x 是兼容的。如果以后版本更新注意把spring.ai.version换成对应版本就行。版本匹配很重要不然会出现NoSuchMethodError这类启动异常。一个值得注意的趋势是Spring AI 已经开始区分ChatModel和ChatClient。ChatModel是底层抽象负责与大模型通信ChatClient是一个更高层的工具类提供了流式、记忆、工具调用等便捷方法。初学时不要被这两个类搞混你只需要记住业务代码主要和 ChatClient 打交道。3.2 为什么需要 Spring AI 的 Ollama 模块而不是手写 HTTP 调用有人可能会说Ollama 不是已经提供 REST API 了吗我直接用RestTemplate或WebClient调不就行了吗为什么还要引入 Spring AI这么想也没错手写 HTTP 调用完全能跑通。但如果你要做的是对话接口手写会遇到几个麻烦流式输出需要处理 SSE 协议、上下文历史需要自己管理 token 拼接、prompt 模板要自己写解析逻辑、不同模型之间的参数调整要自己适配。这些重复工作Spring AI 已经在框架层解决了一部分。Spring AI 还提供了一套PromptTemplate机制可以在 Java 里写类似“你现在是一个客服助手用户问题是 {{question}}”这样的模板然后通过PromptTemplate渲染变量。这样业务代码很干净模型、prompt、服务逻辑各司其职。所以我的建议是如果你的场景只需要一个简单接口手写没问题如果打算做成相对完整的对话服务用 Spring AI 会省心很多。3.3 依赖引入与自动装配原理在pom.xml中加入以下依赖以 Maven 为例parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-ollama/artifactId version1.0.0-M6/version /dependency /dependencies dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version1.0.0-M6/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement如果有些 Spring AI 的依赖还没有发布到 Maven Central你需要在仓库配置中加入 Spring 的 Milestone 仓库repositories repository idspring-milestones/id nameSpring Milestones/name urlhttps://repo.spring.io/milestone/url /repository /repositories引入依赖后Spring Boot 的自动配置会扫描到spring-ai-ollama读取配置文件中spring.ai.ollama前缀的属性自动创建OllamaChatModel和ChatClient.Builder等 Bean。你不需要手动写多少配置类只需要在application.yml里声明spring: ai: ollama: base-url: http://127.0.0.1:11434 chat: model: qwen2.5:7b options: temperature: 0.7到这里Spring Boot 已经把 Ollama 连接好了。接下来就是写代码。4. 核心代码实现从最简对话到可设定人设的接口4.1 设计一个支持历史上下文的聊天控制器业务需求通常是用户发一句话后端返回 AI 的回答。但聊天不能每次都没记忆。我推荐用一个ConversationService来管理会话历史每个会话用sessionId区分。先定义一个请求体public record ChatRequest(String sessionId, String message) {}再定义一个响应体流式接口需要多个响应非流式则返回完整回答。为了简单这里先做非流式RestController RequestMapping(/api/chat) public class ChatController { private final ChatClient chatClient; private final ConversationService conversationService; public ChatController(ChatClient.Builder builder, ConversationService conversationService) { this.chatClient builder.build(); this.conversationService conversationService; } PostMapping public MapString, String chat(RequestBody ChatRequest request) { String sessionId request.sessionId(); String userMessage request.message(); ListMessage history conversationService.getHistory(sessionId); String answer chatClient.prompt() .messages(history) .user(userMessage) .call() .content(); conversationService.append(sessionId, userMessage, answer); return Map.of(answer, answer); } }这段代码最关键的点是.messages(history)。如果你不给它历史消息模型就是“金鱼记忆”状态每次对话都不记得刚才说了什么。这一行把我踩的第一个坑填平了后面会细讲。4.2 用 ChatClient 做系统人设与 Prompt 模板很多时候我们不想让 AI 随便回答而是要它扮演某个角色。Spring AI 提供了system方法可以直接传入系统提示词ChatClient chatClient builder .defaultSystem(你是一个严谨的 Java 技术顾问回答问题时优先考虑代码安全性和性能语气简洁直接。) .build();如果系统提示词需要动态插入参数使用PromptTemplateString template 你是{{role}}。 用户的问题是{{question}} 请用不超过200字回答。 ; PromptTemplate promptTemplate new PromptTemplate(template); MapString, Object params Map.of(role, Java 技术顾问, question, userMessage); Prompt prompt promptTemplate.create(params); String answer chatClient.prompt(prompt).call().content();这种方式适合做一些规则化的问答比如“用一句话解释”“翻译成英文”“提取关键词”等。把 prompt 模板抽成常量或者配置项后面调整 AI 行为时就不用改 Java 代码了。4.3 接入流式响应SSE 推送让对话不卡顿非流式接口有个体验问题如果模型生成速度慢前端要等好几秒才收到完整结果。更好的方案是用 SSEServer-Sent Events把 token 一个个推送出去前端边收边显示体感上快很多。Spring AI 原生支持流式调用把.call().content()换成.stream().content()PostMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString streamChat(RequestBody ChatRequest request) { return chatClient.prompt() .messages(history) .user(request.message()) .stream() .content(); }返回类型从String变成FluxStringSpring Boot 会自动用 SSE 协议输出。前端用EventSource或fetch的ReadableStream就能逐段渲染。需要注意的一点是流式接口没办法在返回前自动保存历史消息。因为回答是分段产出的历史记录必须在全部流结束后再统一追加。更优雅的做法是服务端持久化Flux在前端断开时仍然继续保存。不过小项目里也可以选择在流完成后调用doOnComplete去保存上下文这样实现起来简单得多。4.4 上下文历史的存储与裁剪策略我的ConversationService用了很简单的内存 Map 存储Component public class ConversationService { private final MapString, ListMessage conversations new ConcurrentHashMap(); public ListMessage getHistory(String sessionId) { return conversations.getOrDefault(sessionId, new ArrayList()); } public void append(String sessionId, String userMsg, String assistantMsg) { ListMessage history conversations.computeIfAbsent(sessionId, k - new ArrayList()); history.add(new UserMessage(userMsg)); history.add(new AssistantMessage(assistantMsg)); // 裁剪只保留最近10轮 if (history.size() 20) { history.subList(0, history.size() - 20).clear(); } } }内存存储只适合单机、演示项目。如果生产环境有多实例部署建议把历史消息放到 Redis 里。消息的裁剪很重要因为大模型的上下文窗口是有限制的。比如 qwen2.5:7b 支持 32K 上下文但太长会导致响应速度变慢。一般我只保留最近 10 轮对话既能维持上下文连贯又不会拖慢速度。4.5 从 Controller 方法到完整可运行的最小闭环把所有文件准备完之后启动 Spring Boot 项目用 curl 试一下curl -X POST http://localhost:8080/api/chat \ -H Content-Type: application/json \ -d {sessionId: s1, message: Spring Boot 3.2 有什么新特性}返回的 JSON 里就是模型的回答。如果这一步通了说明整个链路已经打通。接下来可以接前端页面、做 API 鉴权、或者增加更多 prompt 场景。5. 实测调优参数配置、并发控制与资源管理5.1 temperature、top_p 和 num_predict 怎么配很多人直接跳过参数配置用默认值跑。模型本身能用但回答质量会飘。temperature控制在 0 到 1 之间值越高回答越发散值越低越保守。技术问答类场景我习惯设0.7如果是写诗、头脑风暴可以设0.9以上。num_predict控制生成最大 token 数。默认可能很长业务接口往往不需要长篇大论我一般限制在 500 以内防止响应时间过长。在application.yml中示例spring: ai: ollama: chat: model: qwen2.5:7b options: temperature: 0.7 top-p: 0.9 num-predict: 512Ollama 本身还有OLLAMA_NUM_PARALLEL环境变量用来设置并发请求处理数量。默认值是 1 或 4 取决于显存大小。如果多用户同时访问这个参数建议调到 4 以上。但注意并发太高会吃满 GPU 或 CPU反而所有请求都变慢。5.2 在纯 CPU 机器上提高响应速度的经验如果没有 NVIDIA 显卡只能靠 CPU 推理那就要做好“慢”的心理准备。7B 模型在 CPU 上大概每秒只能生成 5~10 个 token一个回答可能要等 20 秒以上。这时候有几个技巧可以救急换更小的模型比如 3B 模型速度能快一倍。使用量化程度更高的模型例如 Q4_K_M 比 Q8_0 快不少。开启 Ollama 的OLLAMA_FLASH_ATTENTION1环境变量部分模型能加速。尽量用流式输出让用户看到第一个 token 出现的时间尽可能地早心理上就没那么卡。我在这台 MacBook ProM1 Pro上测试qwen2.5:7b走 Metal 加速速度大概 30 token/s体验已经接近云端了。如果你用的是公司老旧台式机建议还是买个带 CUDA 的显卡或者用 Mac 系列。5.3 模型量化等级 Q4、Q8 和 F16 如何取舍Ollama 在拉取模型时通常会默认选择量化版本。量化就是减少每个权重的位数来交换内存占用和推理速度。Q4_K_M 是我最常用的体积小、速度好、质量损失在大多数场景下感知不到。Q8_0 质量更好但体积接近翻倍。F16 是完整精度除非内存非常宽裕否则不推荐用于对话场景。有一句经验总结**能跑 Q4 就跑 Q4不要迷信精度。**大模型的能力瓶颈更多来自模型大小和训练数据而不是最后几位小数。6. 避坑实录我在整合过程中踩过的五个坑6.1 版本不匹配导致 NoSuchMethodError我第一次引入 Spring AI 时没有使用 BOM直接写了spring-ai-ollama的版本号和 Spring Boot 版本对不上启动时报了NoSuchMethodError: void org.springframework.util.MultiValueMap.add(...)。后来才发现Spring AI 的版本与新版本 Spring Boot 耦合很紧密。解决方法是使用官方 BOM并保持spring-ai-bom和spring-ai-ollama版本一致。如果升级 Spring Boot也要同步升级 Spring AI 版本。这个坑在官方文档里写了但很多人会忽略。6.2 模型名带标签导致 404 或 400当我从命令行拉取模型时模型全名可能是qwen2.5:7b-instruct-q4_K_M。在 Spring AI 配置里如果直接把带特殊字符的 tag 写进去有时候 API 返回not found。原因是 Ollama API 要求模型名是唯一的带标签时需要写全但一些特殊字符可能在配置解析时被转义。建议做法是在 Ollama 里先把模型重命名成简短的名字ollama cp qwen2.5:7b-instruct-q4_K_M my-chat-model然后在 Spring AI 配置里使用my-chat-model。这样既稳定又不会把超长 tag 暴露在代码里。6.3 中文输出乱码或截断Spring Boot 返回 JSON 时中文乱码通常不是模型的问题而是响应头缺charsetUTF-8。在 Controller 里可以显式指定PostMapping(produces MediaType.APPLICATION_JSON_VALUE ;charsetUTF-8)另外有些模型在上下文较长之后会出现输出截断。这不是 bug而是 token 数达到num_predict上限。把配置文件里的num-predict调大或者裁剪历史消息问题就能缓解。6.4 Ollama 服务没有自动启动Linux 服务器上手动安装 Ollama 后经常出现重启机器后服务不启动的情况。安装脚本一般会注册 systemd 服务如果你使用手动解压的方式则需要自己创建 service 文件[Unit] DescriptionOllama Service Afternetwork-online.target [Service] ExecStart/usr/local/bin/ollama serve Userollama Groupollama Restartalways RestartSec3 [Install] WantedBydefault.target然后执行systemctl daemon-reload systemctl enable ollama。此后再也不用担心重启后 11434 端口没反应。6.5 Spring Security / 网关引起的路径拦截如果你的项目里已经有 Spring Security那么/api/chat会被拦截前端调用直接返回 401。我一开始没注意前端反馈“接口报错”排查半天才发现是 Security 配置把所有/api/**都拦截了。如果这个接口允许匿名访问需要在 Security 配置里放行http.authorizeHttpRequests(auth - auth .requestMatchers(/api/chat/**).permitAll() .anyRequest().authenticated() );如果是微服务架构还要注意网关转发时别把/api/chat/stream的 SSE 响应缓冲掉。部分网关默认缓冲响应会导致流式效果失效需要在网关层关闭缓冲。这套方案我目前已经跑了快一个月稳定性还是不错的。如果只是做内部工具或学习演示用 Spring Boot Spring AI Ollama 是非常合适的组合——它把模型管理、上下文处理、流式输出这些脏活都封装好了让 Java 开发者能专注于业务逻辑。最后分享一个小技巧生产环境可以把 Ollama 单独部署在一台 GPU 服务器上Spring Boot 应用跑在另一台机器上通过base-url指向 Ollama 服务器的 IP 地址两者互不影响。这样后续扩展成多服务共享同一个本地大模型也只是改一行配置的事。