SpringBoot3整合SpringAI:打造带记忆的AI助手
SpringBoot3 集成 SpringAI 打造具备上下文记忆的AI助手
在开发智能图片社区项目时,用户需要一个能持续记忆对话上下文的AI助手。经过多方案对比,最终采用 Spring Boot 3 + Spring AI + Redis 组合。下面从环境搭建到核心代码,再到测试验证,一步步拆解完整实现流程。
1. 项目全景概览
本指南详细讲解如何基于 Spring Boot 3 整合 Spring AI,构建一个具备记忆能力的AI助手。方案使用 Redis 作为持久化存储,支持按用户隔离会话,并保留30天的对话历史记录。
技术选型栈
- Spring Boot 3.3.0
- Java 17
- Spring AI
- Redis 6.0+
- MyBatis Plus
- MySQL 8.0
- Sa-Token(用户鉴权)
2. 前期环境准备
2.1 安装必要软件
- JDK 17+:Oracle JDK 或 OpenJDK 均可
- Maven 3.9+:从 Maven 官网下载
- Redis 6.0+:Redis 官网 或通过 Docker 快速部署
- MySQL 8.0+:MySQL 官网 或 Docker 容器
- IDE:IntelliJ IDEA 或 Eclipse
2.2 环境变量配置
确认 JAVA_HOME 与 MAVEN_HOME 已正确设置。
3. 项目初始化流程
3.1 创建 Spring Boot 项目
通过 Spring Initializr 生成项目骨架:
- 访问 Spring Initializr 网站
- 选择 Spring Boot 3.3.0
- 选择 Java 17
- 添加依赖:Spring Web, Spring Data Redis, MyBatis Plus, MySQL Driver, Spring Boot DevTools
3.2 Maven 依赖配置
在 pom.xml 中引入下述依赖:
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-data-redis
com.baomidou
mybatis-plus-boot-starter
3.5.5
com.mysql
mysql-connector-j
runtime
org.springframework.ai
spring-ai-openai
1.0.0
cn.dev33
sa-token-spring-boot-starter
1.38.1
com.fasterxml.jackson.core
jackson-databind
org.projectlombok
lombok
true
org.springframework.boot
spring-boot-starter-test
test
4. 核心配置详解
4.1 应用配置文件 (application.yml)
新建 src/main/resources/application.yml,填入以下应用参数:
server:
port: 9527
servlet:
context-path: /api
spring:
application:
name: smart-pic-community-backend
# Redis 配置
data:
redis:
database: 2
host: localhost
port: 6379
timeout: 5000
# 数据库配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/smart_pic_community
username: root
password: your_password
# Spring AI 配置
ai:
openai:
base-url: https://api.deepseek.com/ # 使用 DeepSeek API
api-key: your_api_key
chat:
options:
model: deepseek-chat
4.2 Redis 配置类 (RedisConfiguration.java)
编写 Redis 配置,确保对象序列化正确:
package com.spc.smartpiccommunitybackend.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象...");
RedisTemplate redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value
Jackson2JsonRedisSerializer
这块有几个关键坑需要留意:
- 选用
Jackson2JsonRedisSerializer而非StringRedisSerializer,避免ClassCastException - 启用默认类型功能,保障反序列化时对象类型正确识别
5. 核心功能实现
5.1 模型类创建
5.1.1 MessageVO.java
定义消息视图对象,供前端渲染使用:
package com.spc.smartpiccommunitybackend.model.vo.ai;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ai.chat.messages.Message;
@NoArgsConstructor
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO(Message message) {
this.role = switch (message.getMessageType()) {
case USER -> "user";
case ASSISTANT -> "assistant";
case SYSTEM -> "system";
default -> "";
};
this.content = message.getText();
}
}
作用说明:
- 将 Spring AI 的
Message对象转换为前端可识别的格式 - 依据消息类型动态设置角色字段
- 提取消息文本用于展示
5.1.2 SerializableMessage.java
创建可序列化的消息模型,用于 Redis 持久化:
package com.spc.smartpiccommunitybackend.model.entity.ai;
import lombok.Data;
import lombok.NoArgsConstructor;
import ja va.io.Serializable;
@Data
@NoArgsConstructor
public class SerializableMessage implements Serializable {
private static final long serialVersionUID = 1L;
private String role;
private String content;
private String messageType;
private Long timestamp;
public SerializableMessage(String role, String content, String messageType) {
this.role = role;
this.content = content;
this.messageType = messageType;
this.timestamp = System.currentTimeMillis();
}
public SerializableMessage(String role, String content) {
this(role, content, "user");
}
}
功能要点:
- 实现
Serializable接口,保障 Redis 序列化能力 - 包含角色、内容、消息类型及时间戳四个字段
- 提供多个构造器,便于灵活使用
5.2 仓库接口定义 (ChatHistoryRepository.java)
声明聊天历史仓库接口:
package com.spc.smartpiccommunitybackend.repository;
import ja va.util.List;
import ja va.util.Map;
public interface ChatHistoryRepository {
/** 保存会话记录 */
void sa ve(String type, String chatId, Long userId);
/** 获取用户的会话ID列表 */
List getChatIds(Long userId, String type);
/** 保存聊天消息 */
void sa veMessage(String chatId, String message, String sender);
/** 获取聊天消息历史 */
List getMessages(String chatId);
/** 删除会话 */
void deleteChat(Long userId, String type, String chatId);
/** 获取会话信息 */
Map
5.3 Redis 聊天历史仓库实现 (RedisChatHistoryRepository.java)
基于 Redis 实现上述接口:
package com.spc.smartpiccommunitybackend.repository;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import ja va.util.*;
import ja va.util.concurrent.TimeUnit;
import ja va.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class RedisChatHistoryRepository implements ChatHistoryRepository {
private final RedisTemplate redisTemplate;
private static final String CHAT_HISTORY_PREFIX = "chat:history:";
private static final String CHAT_SESSION_PREFIX = "chat:session:";
private static final String CHAT_MESSAGES_PREFIX = "chat:messages:";
@Override
public void sa ve(String type, String chatId, Long userId) {
String sessionKey = CHAT_SESSION_PREFIX + chatId;
Map sessionInfo = new HashMap<>();
sessionInfo.put("userId", String.valueOf(userId));
sessionInfo.put("type", type);
sessionInfo.put("createTime", System.currentTimeMillis());
sessionInfo.put("lastUpdateTime", System.currentTimeMillis());
redisTemplate.opsForHash().putAll(sessionKey, sessionInfo);
redisTemplate.expire(sessionKey, 30, TimeUnit.DAYS);
String historyKey = CHAT_HISTORY_PREFIX + userId + ":" + type;
redisTemplate.opsForSet().add(historyKey, chatId);
redisTemplate.expire(historyKey, 30, TimeUnit.DAYS);
}
@Override
public List getChatIds(Long userId, String type) {
String historyKey = CHAT_HISTORY_PREFIX + userId + ":" + type;
Set
实现中的设计亮点:
- 通过不同 Redis key 前缀隔离数据类型
- 所有键均设置过期时间,防止内存泄漏
- JSON 序列化失败时降级处理,增强系统鲁棒性
5.4 聊天记忆组件 (RedisChatMemory.java)
实现 Spring AI 的 ChatMemory 接口,基于 Redis 存储记忆:
package com.spc.smartpiccommunitybackend.config;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import ja va.util.ArrayList;
import ja va.util.List;
import ja va.util.concurrent.TimeUnit;
@Component
public class RedisChatMemory implements ChatMemory {
private final RedisTemplate redisTemplate;
private static final String MEMORY_KEY_PREFIX = "chat:memory:";
private static final long EXPIRATION_DAYS = 30;
public RedisChatMemory(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void add(String key, List messages) {
for (Message message : messages) {
addMessage(key, message);
}
}
@Override
public List get(String key, int maxCount) {
List messages = getMessages(key);
if (maxCount > 0 && messages.size() > maxCount) {
return messages.subList(messages.size() - maxCount, messages.size());
}
return messages;
}
@Override
public void clear() {
// 清理所有会话记忆,谨慎使用
}
public void addMessage(String chatId, Message message) {
String key = MEMORY_KEY_PREFIX + chatId;
redisTemplate.opsForList().rightPush(key, message);
redisTemplate.expire(key, EXPIRATION_DAYS, TimeUnit.DAYS);
}
public List getMessages(String chatId) {
String key = MEMORY_KEY_PREFIX + chatId;
List objects = redisTemplate.opsForList().range(key, 0, -1);
List messages = new ArrayList<>();
if (objects != null) {
for (Object obj : objects) {
if (obj instanceof Message) {
messages.add((Message) obj);
}
}
}
return messages;
}
public void clear(String chatId) {
String key = MEMORY_KEY_PREFIX + chatId;
redisTemplate.delete(key);
}
}
重点说明:
- 实现
ChatMemory接口,让 Spring AI 的消息记忆机制无缝对接 - 每个消息设置过期时间,控制内存占用
- 批量添加与获取方法提升操作效率
5.5 配置 ChatClient (CommonConfiguration.java)
装配 Spring AI 的 ChatClient:
package com.spc.smartpiccommunitybackend.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient(OpenAiChatModel openAiChatModel, ChatMemory chatMemory) {
return ChatClient.builder(openAiChatModel)
.defaultAdvisors(
new SimpleLoggerAdvisor(),
new MessageChatMemoryAdvisor(chatMemory)
)
.build();
}
}
核心在于注入 MessageChatMemoryAdvisor 激活聊天记忆功能,SimpleLoggerAdvisor 用于记录交互日志辅助调试。
5.6 聊天控制器 (ChatController.java)
处理 AI 对话请求的接口:
package com.spc.smartpiccommunitybackend.controller;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spc.smartpiccommunitybackend.repository.RedisChatHistoryRepository;
import com.spc.smartpiccommunitybackend.service.UserService;
import com.spc.smartpiccommunitybackend.utils.ErrorCode;
import com.spc.smartpiccommunitybackend.utils.ThrowUtils;
import com.spc.smartpiccommunitybackend.pojo.User;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.beans.factory.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
import ja vax.servlet.http.HttpServletRequest;
import ja va.io.IOException;
import ja va.util.ArrayList;
import ja va.util.List;
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
private final RedisChatHistoryRepository chatHistoryRepository;
@Resource
private UserService userService;
public ChatController(ChatClient chatClient, RedisChatHistoryRepository chatHistoryRepository) {
this.chatClient = chatClient;
this.chatHistoryRepository = chatHistoryRepository;
}
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux chat(@RequestParam(defaultValue = "讲个笑话") String prompt,
String chatId,
HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
Long userId = loginUser.getId();
chatHistoryRepository.sa ve("chat", chatId, userId);
List messages = new ArrayList<>();
SystemMessage systemMessage = new SystemMessage(
"你是一个智能图片社区的AI助手,名为虹小智。请用友好、专业的语气回答用户问题," +
"提供关于图片社区的相关信息和帮助。");
messages.add(systemMessage);
List historyMessages = chatHistoryRepository.getMessages(chatId);
ObjectMapper objectMapper = new ObjectMapper();
for (String messageStr : historyMessages) {
try {
JsonNode node = objectMapper.readTree(messageStr);
String sender = node.get("sender").asText();
String content = node.get("content").asText();
if ("user".equals(sender)) {
messages.add(new UserMessage(content));
} else if ("ai".equals(sender)) {
messages.add(new AssistantMessage(content));
}
} catch (IOException e) {
e.printStackTrace();
}
}
messages.add(new UserMessage(prompt));
chatHistoryRepository.sa veMessage(chatId, prompt, "user");
return chatClient.stream(messages)
.doOnNext(response -> {
chatHistoryRepository.sa veMessage(chatId, response, "ai");
});
}
}
设计要点:
- 校验用户登录态,确保会话归属隔离
- 将用户消息与 AI 回复均持久化到历史记录
- 采用
Flux流式响应,提升交互体验 - 历史消息解析异常时容错处理,保证系统稳定
5.7 聊天历史管理控制器 (ChatHistoryController.java)
提供历史记录的查询与删除接口:
package com.spc.smartpiccommunitybackend.controller;
import com.spc.smartpiccommunitybackend.repository.RedisChatHistoryRepository;
import com.spc.smartpiccommunitybackend.service.UserService;
import com.spc.smartpiccommunitybackend.utils.ErrorCode;
import com.spc.smartpiccommunitybackend.utils.ThrowUtils;
import com.spc.smartpiccommunitybackend.pojo.User;
import org.springframework.web.bind.annotation.*;
import ja vax.servlet.http.HttpServletRequest;
import ja va.util.List;
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {
private final RedisChatHistoryRepository chatHistoryRepository;
private final UserService userService;
public ChatHistoryController(RedisChatHistoryRepository chatHistoryRepository, UserService userService) {
this.chatHistoryRepository = chatHistoryRepository;
this.userService = userService;
}
@GetMapping("/{type}")
public List getChatHistory(@PathVariable String type, HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
Long userId = loginUser.getId();
return chatHistoryRepository.getChatIds(userId, type);
}
@DeleteMapping("/{type}/{chatId}")
public boolean deleteChatHistory(@PathVariable String type, @PathVariable String chatId, HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
Long userId = loginUser.getId();
chatHistoryRepository.deleteChat(userId, type, chatId);
return true;
}
}
同样进行登录校验,保证用户仅能操作自身历史。接口设计简洁,前端调用成本低。
6. 测试与验证
6.1 启动服务
- 确认 Redis 和 MySQL 服务处于运行状态
- 启动 Spring Boot 应用
- 访问
http://localhost:9527/api/ai/chat?prompt=你好&chatId=test123验证 AI 响应
6.2 验证聊天记忆
- 发送第一轮:
http://localhost:9527/api/ai/chat?prompt=你好,我叫张三&chatId=test123 - 发送第二轮:
http://localhost:9527/api/ai/chat?prompt=你知道我叫什么名字吗?&chatId=test123 - 检查 AI 能否准确复述你的名字
6.3 用户隔离测试
- 用不同用户账号登录
- 验证各用户的聊天历史是否相互独立
6.4 历史持久化测试
- 连续发送多条消息
- 重启应用
- 确认历史记录依然存在
6.5 历史管理测试
- 获取用户聊天列表:
GET http://localhost:9527/api/ai/history/chat - 删除指定会话:
DELETE http://localhost:9527/api/ai/history/chat/test123 - 验证删除结果
