Spring Boot 3.2 + Ollama本地大模型实战:5分钟搞定私有化AI对话API
Spring Boot 3.2 + Ollama:在本地构建你的私有化AI对话服务
最近和几个做企业服务的朋友聊天,他们都在为一个问题头疼:想给自家产品加上智能对话能力,但一提到要把用户数据送到第三方AI平台,法务和客户就直摇头。数据隐私、API调用成本、还有那说不清道不明的网络延迟,都成了拦路虎。
其实,这个问题早就有解了。把大模型“请”到本地服务器上,用你最熟悉的Java和Spring Boot技术栈,自己搭一套AI服务,数据不出内网,成本可控,响应速度还快。听起来很复杂?我刚开始也这么觉得,但上手后发现,借助Spring AI和Ollama,整个过程比想象中简单太多。
这篇文章,我就带你走一遍完整的流程。从零开始,用Spring Boot 3.2和Ollama,在本地搭建一个功能完备的私有AI对话API。我们不止实现基础的问答,还会搞定流式输出、对话记忆这些提升体验的关键功能。目标是让你看完就能动手,快速为你的项目注入AI能力。
1. 环境准备与核心工具选型
在敲下第一行代码之前,我们需要把“舞台”搭好。这个阶段的核心是选择并配置好那些能让我们事半功倍的工具。
为什么是Spring Boot 3.2 + Ollama? 这不是随意组合。Spring Boot 3.2是当前Java生态中成熟且广泛使用的版本,它对现代Java特性(如虚拟线程)的支持更好。而Ollama,则是目前在本地运行和管理开源大模型最顺手的工具。它把模型下载、加载、服务化这些繁琐步骤打包成简单的命令,让你能像使用Docker一样管理模型。Spring AI作为中间的“粘合剂”,提供了一套标准化的API,让你用同样的Java代码去调用不同的模型后端,无论是本地的Ollama还是云端的OpenAI。
第一步:安装基础运行时 确保你的机器上已经安装了JDK 17或更高版本。我推荐使用Amazon Corretto 17或Eclipse Temurin 17,它们都是免费的OpenJDK发行版,企业级支持也做得不错。用下面的命令检查一下:
java -version
输出应该类似这样:
openjdk version "17.0.10" 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-17.0.10.7.1 (build 17.0.10+7-LTS)
OpenJDK 64-Bit Server VM Corretto-17.0.10.7.1 (build 17.0.10+7-LTS, mixed mode, sharing)
第二步:安装并启动Ollama Ollama的安装极其简单。访问其官网,根据你的操作系统下载安装包。以macOS或Linux为例,安装后只需在终端执行一条命令就能启动服务:
# 启动Ollama服务,它会默认监听11434端口
ollama serve
服务启动后,另开一个终端窗口,拉取一个适合本地运行的轻量级模型。对于中文场景,qwen2.5:7b或deepseek-r1:7b都是不错的选择,它们在7B参数量级上提供了不错的推理能力和中文支持。
# 拉取Qwen2.5 7B模型(约4.5GB)
ollama pull qwen2.5:7b
# 拉取完成后,可以立即运行一个简单对话测试
ollama run qwen2.5:7b "你好,请介绍一下你自己。"
如果模型能正常回复,说明Ollama环境已经就绪。至此,我们的“模型服务器”已经准备妥当。
工具选型对比
为了让你更清楚我们为什么选择这套组合,我简单对比一下其他常见方案:
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| Spring AI + Ollama | 数据完全私有、零API成本、延迟极低、Java原生集成 | 需要本地GPU/CPU资源、模型能力受硬件限制 | 企业内部工具、对数据隐私要求高的C端产品、离线环境 |
| Spring AI + OpenAI API | 模型能力最强、无需管理基础设施、开箱即用 | 数据需出境、持续产生API费用、网络依赖性强 | 快速原型验证、对模型效果要求极高的场景、无隐私顾虑的应用 |
| Python FastAPI + 本地模型 | Python生态AI库丰富、灵活度高 | 需要维护Python技术栈、与企业现有Java架构整合成本高 | AI研究、算法团队主导的项目、纯Python技术栈团队 |
可以看到,对于大多数Java技术栈的企业来说,Spring AI + Ollama在数据安全、成本控制和架构统一性上取得了最佳平衡。
2. 项目初始化与Spring AI集成
环境就绪,现在让我们创建Spring Boot项目,并把Spring AI引入进来。我会带你避开几个常见的“坑”。
使用Spring Initializr快速创建项目 最省心的方式是访问 start.spring.io。在页面上进行如下选择:
- Project: Maven
- Language: Java
- Spring Boot: 3.2.x (建议选择最新的3.2.x版本,如3.2.11)
- Group & Artifact: 按你的项目命名习惯填写
- Packaging: Jar
- Java: 17
在Dependencies一栏,先添加 Spring Web。至于Spring AI的依赖,我们稍后在pom.xml里手动添加会更清晰,因为需要引入其特定的BOM(物料清单)来管理版本。
点击“GENERATE”下载项目压缩包,解压后用你喜欢的IDE(IntelliJ IDEA或VS Code)打开。
配置Maven依赖与仓库 打开项目根目录的pom.xml文件,这是整个项目的依赖蓝图。我们需要做三件事:1) 添加Spring AI的BOM;2) 引入Ollama starter依赖;3) 配置Spring的快照仓库(因为Spring AI某些版本可能还在里程碑阶段)。
找到<project>标签下的<properties>部分,我们可以先定义Spring AI的版本,方便管理:
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0-M6</spring-ai.version> <!-- 以当时最新稳定版为准 -->
</properties>
接下来,在<dependencies>标签之前,添加<dependencyManagement>部分,导入Spring AI的BOM。这是关键一步,它能确保所有Spring AI相关组件的版本一致,避免冲突。
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后,在<dependencies>部分,添加我们实际需要的依赖:
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI for Ollama 核心依赖 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
最后,由于Spring AI的一些版本可能存放在Spring的里程碑仓库里,我们需要在<repositories>部分(如果没有就新建)添加以下配置:
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
注意:Spring AI版本迭代较快,
1.0.0-M6是撰写本文时的一个稳定里程碑版。建议你动手时,去Spring AI的GitHub Releases页面查看最新版本,替换掉上面的版本号。通用版本(GA)发布后,将无需配置里程碑仓库。
编写核心配置文件 依赖搞定后,我们来配置应用如何连接Ollama。在src/main/resources目录下,创建或编辑application.yml文件(用YAML格式更清晰)。这里我们做最基础的配置:
server:
port: 8080
spring:
application:
name: local-ai-service
ai:
ollama:
# Ollama服务地址,默认就在本地11434端口
base-url: http://localhost:11434
chat:
options:
# 必须指定你通过`ollama pull`下载的模型名称
model: qwen2.5:7b
# 温度参数,控制输出的随机性。0.0最确定,1.0更多样。
temperature: 0.7
这个配置告诉Spring AI:去找本地11434端口上的Ollama服务,并使用qwen2.5:7b这个模型进行对话。temperature设为0.7是一个比较折中的值,让回答既有一定创造性,又不至于太天马行空。
至此,项目骨架和基础连接配置已经完成。你可以尝试运行主类(通常是*Application.java),如果控制台没有报错,并且能看到Spring AI相关的自动配置日志,那么恭喜你,集成工作成功了。
3. 实现基础与流式对话API
现在进入最有趣的部分——让我们的服务“开口说话”。我们将实现两种最常用的接口:同步响应和流式响应。你会发现,有了Spring AI的抽象,这比调用一个普通的Web Service还要简单。
同步对话接口:快速获取完整回答 同步接口适用于那些不需要即时反馈、回答较短(比如翻译、摘要)的场景。它的特点是客户端发送请求后,会一直等待,直到服务器端模型生成完整的回答,一次性返回。
创建一个新的Controller类,例如AiChatController:
package com.yourcompany.localai.controller;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/ai")
public class AiChatController {
private final ChatModel chatModel;
// 通过构造器注入ChatModel,Spring AI会自动配置Ollama的实现
public AiChatController(ChatModel chatModel) {
this.chatModel = chatModel;
}
/**
* 同步对话接口
* GET /api/ai/chat?message=你的问题
*/
@GetMapping("/chat")
public Map<String, String> chat(@RequestParam String message) {
// 1. 构建消息列表。可以加入SystemMessage来设定AI的角色。
List<Message> messages = List.of(
new SystemMessage("你是一个乐于助人且专业的AI助手,请用中文回答用户的问题。"),
new UserMessage(message)
);
// 2. 将消息列表封装成Prompt对象
Prompt prompt = new Prompt(messages);
// 3. 调用模型,获取完整的响应
String response = chatModel.call(prompt).getResult().getOutput().getContent();
// 4. 以JSON格式返回
return Map.of("response", response);
}
}
这个接口非常直观。你可以在浏览器或Postman里访问 http://localhost:8080/api/ai/chat?message=Spring Boot是什么?,很快就会收到一个完整的JSON响应。
流式对话接口:实现“打字机”效果 对于真正的聊天场景,用户希望看到文字逐个蹦出来的效果,而不是盯着空白页等待。这就需要流式接口。它基于Server-Sent Events (SSE) 技术,服务器会持续发送多个数据块,直到回答完成。
在同一个Controller里,我们添加流式接口:
import org.springframework.http.MediaType;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.model.ChatResponse;
/**
* 流式对话接口
* GET /api/ai/chat/stream?message=你的问题
* 注意:produces属性指定了SSE的媒体类型
*/
@GetMapping(value = "/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStream(@RequestParam String message) {
List<Message> messages = List.of(
new SystemMessage("你是一个乐于助人且专业的AI助手,请用中文回答用户的问题。"),
new UserMessage(message)
);
Prompt prompt = new Prompt(messages);
// 关键区别:使用stream()方法,返回一个Flux流
return chatModel.stream(prompt)
.map(chatResponse -> {
// 从每个响应块中提取文本内容
if (chatResponse.getResult() != null && chatResponse.getResult().getOutput() != null) {
return chatResponse.getResult().getOutput().getContent();
}
return "";
});
}
这个接口的响应是持续的。你可以用curl命令来测试:
curl -N http://localhost:8080/api/ai/chat/stream?message=讲一个关于编程的笑话
你会看到文字一段一段地输出到终端。在前端,你可以使用EventSource API来连接这个端点,轻松实现聊天界面的打字机效果。
两种方式的对比与选择
为了帮你更好地决策,我整理了一个对比表格:
| 特性 | 同步接口 (/chat) |
流式接口 (/chat/stream) |
|---|---|---|
| 响应方式 | 一次性返回完整结果 | 分多次返回文本块 |
| 用户体验 | 需等待,有延迟感 | 实时感强,体验更佳 |
| 网络开销 | 单次响应,数据包大 | 多次小数据包,总大小可能略增 |
| 后端压力 | 请求处理时间长,连接占用久 | 连接占用久,但可提前释放部分资源 |
| 适用场景 | 工具类调用(总结、翻译)、后台任务 | 实时对话、长文本生成、需要即时反馈的交互 |
| 前端实现 | 普通的HTTP请求 | 使用 EventSource 或 fetch 读取流 |
我的经验是,对于面向用户的聊天产品,流式接口是必选项。它能极大提升感知速度和交互体验。同步接口则可以用于内部工具或不需要即时UI反馈的批量处理。
4. 进阶功能:为AI注入记忆与上下文
基础的问答机器人每次都是“金鱼记忆”,回答完就忘。要实现多轮有逻辑的对话,我们必须给AI加上“记忆”功能。这里,我们设计一个将会话历史存储到数据库的方案,这是生产环境中最可靠的做法。
第一步:关闭Spring AI默认的内存记忆 Spring AI为了方便,提供了一个基于内存的会话记忆实现。但我们要用数据库,为了避免冲突,先在application.yml中禁用它:
spring:
ai:
chat:
memory:
enabled: false # 关闭默认的内存记忆
第二步:设计数据库表结构 我们需要两张核心表。一张记录每次对话的会话(chat_session),另一张记录会话内的每一条消息(chat_message)。
-- 会话表:每个独立的聊天窗口对应一条记录
CREATE TABLE `chat_session` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`session_id` VARCHAR(100) NOT NULL UNIQUE COMMENT '前端生成的唯一会话ID',
`title` VARCHAR(255) COMMENT '会话标题,通常用首条问题生成',
`user_id` VARCHAR(64) COMMENT '关联的用户ID,用于多用户隔离',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`status` TINYINT DEFAULT 1 COMMENT '1: 活跃, 0: 已删除'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 消息表:记录会话中的所有问答对
CREATE TABLE `chat_message` (
`id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`session_id` VARCHAR(100) NOT NULL COMMENT '关联的会话ID',
`role` ENUM('user', 'assistant', 'system') NOT NULL COMMENT '消息角色',
`content` TEXT NOT NULL COMMENT '消息内容',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_session_id` (`session_id`),
FOREIGN KEY (`session_id`) REFERENCES `chat_session`(`session_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
第三步:实现数据持久层与服务层 使用Spring Data JPA来简化数据库操作。首先在pom.xml中添加相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
然后创建实体类ChatMessage和ChatSession(此处省略JPA注解细节),以及对应的Repository接口。接着,我们创建一个服务类ChatMemoryService,它的核心方法是根据sessionId获取历史对话,并组装成Spring AI能识别的Message列表:
@Service
public class ChatMemoryService {
@Autowired
private ChatMessageRepository messageRepository;
/**
* 根据会话ID加载历史消息,并转换为Spring AI的Message列表
*/
public List<Message> loadConversationHistory(String sessionId) {
List<ChatMessage> dbMessages = messageRepository.findBySessionIdOrderByCreatedAtAsc(sessionId);
return dbMessages.stream().map(dbMsg -> {
switch (dbMsg.getRole()) {
case "user":
return new UserMessage(dbMsg.getContent());
case "assistant":
return new AssistantMessage(dbMsg.getContent());
case "system":
return new SystemMessage(dbMsg.getContent());
default:
throw new IllegalArgumentException("Unknown role: " + dbMsg.getRole());
}
}).collect(Collectors.toList());
}
/**
* 保存一轮对话(用户问题 + AI回答)
*/
@Transactional
public void saveMessagePair(String sessionId, String userId, String userQuestion, String aiResponse) {
// 保存用户消息
ChatMessage userMsg = new ChatMessage();
userMsg.setSessionId(sessionId);
userMsg.setRole("user");
userMsg.setContent(userQuestion);
messageRepository.save(userMsg);
// 保存AI助手消息
ChatMessage aiMsg = new ChatMessage();
aiMsg.setSessionId(sessionId);
aiMsg.setRole("assistant");
aiMsg.setContent(aiResponse);
messageRepository.save(aiMsg);
// 如果是新会话,创建会话记录
ChatSession session = sessionRepository.findBySessionId(sessionId);
if (session == null) {
session = new ChatSession();
session.setSessionId(sessionId);
session.setUserId(userId);
// 用用户的第一条问题前20字作为会话标题
session.setTitle(userQuestion.length() > 20 ? userQuestion.substring(0, 20) + "..." : userQuestion);
sessionRepository.save(session);
}
}
}
第四步:创建带记忆的流式对话接口 最后,我们改造之前的流式接口,让它支持上下文记忆。这里有个技术细节:我们需要在流式输出完成后,再将完整的AI回复保存到数据库。
@GetMapping(value = "/chat/stream/with-memory", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatStreamWithMemory(@RequestParam String sessionId,
@RequestParam String message,
@RequestParam(required = false) String userId) {
// 1. 加载该会话的历史消息
List<Message> historyMessages = chatMemoryService.loadConversationHistory(sessionId);
// 2. 构建本次对话的消息列表:历史 + 新问题 + 系统指令
List<Message> currentMessages = new ArrayList<>(historyMessages);
currentMessages.add(new UserMessage(message));
currentMessages.add(new SystemMessage("请用中文友好且专业地回答问题。"));
Prompt prompt = new Prompt(currentMessages);
// 3. 用于收集流式输出中AI的完整回复
StringBuilder aiCompleteResponse = new StringBuilder();
// 4. 发起流式调用,并在流结束时保存历史
return chatModel.stream(prompt)
.map(chatResponse -> {
String chunk = chatResponse.getResult().getOutput().getContent();
if (chunk != null) {
aiCompleteResponse.append(chunk); // 收集片段
return chunk; // 实时发送给前端
}
return "";
})
.doOnComplete(() -> {
// 5. 流式输出全部完成后,异步保存本轮对话到数据库
if (aiCompleteResponse.length() > 0) {
// 使用异步任务,避免阻塞主响应线程
CompletableFuture.runAsync(() -> {
chatMemoryService.saveMessagePair(sessionId,
userId != null ? userId : "anonymous",
message,
aiCompleteResponse.toString());
});
}
});
}
这个接口的调用方式变成了:/api/ai/chat/stream/with-memory?sessionId=abc123&message=新问题&userId=user001。前端需要在开始一个新对话时生成一个唯一的sessionId(比如UUID),并在后续同一对话中持续传递它。这样,AI就能基于之前的所有对话历史来回答新问题,实现了真正的上下文连贯性。
我曾在项目中采用这种方案,将用户与AI的完整对话记录留存下来,不仅提升了用户体验,还为后续分析用户意图、优化提示词提供了宝贵的数据基础。
更多推荐


所有评论(0)