解决 Spring Boot 3 + Spring AI 实现 AI 助手的常见问题
·
解决 Spring Boot 3 + Spring AI 实现 AI 助手的常见问题
在使用 Spring Boot 3 整合 Spring AI 实现 AI 助手的过程中,可能会遇到一些常见问题,如用户聊天记录隔离、服务器重启后上下文丢失以及 Redis 序列化等问题。本文将详细介绍这些问题的原因和解决方案。
一、用户之间的聊天记录没有实现隔离
问题描述
不同用户的聊天记录相互干扰,一个用户可以看到其他用户的聊天历史,这严重违反了用户隐私保护原则。
问题原因
- Redis Key 设计不当:使用了全局唯一的聊天 ID 作为 Redis Key,没有包含用户标识
- 缺少用户认证:API 接口没有验证用户身份,允许任意用户访问任意聊天记录
- 数据存储结构不合理:没有为每个用户创建独立的聊天历史列表
解决方案
1. 实现用户认证
确保每个 API 请求都经过用户认证,验证用户身份:
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> 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();
// 后续代码...
}
2. 设计用户隔离的 Redis Key
在 Redis 中使用包含用户 ID 的 Key 结构:
// Redis key前缀
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:";
/**
* 保存会话记录
*/
public void save(String type, String chatId, Long userId) {
// 保存会话信息
String sessionKey = CHAT_SESSION_PREFIX + chatId;
Map<String, Object> 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);
// 设置过期时间为30天
redisTemplate.expire(sessionKey, 30, TimeUnit.DAYS);
// 将chatId添加到用户的聊天历史列表中
String historyKey = CHAT_HISTORY_PREFIX + userId + ":" + type;
redisTemplate.opsForSet().add(historyKey, chatId);
// 设置过期时间为30天
redisTemplate.expire(historyKey, 30, TimeUnit.DAYS);
}
3. 实现用户专属的聊天历史获取
确保只返回当前用户的聊天历史:
/**
* 获取用户的会话ID列表
*/
public List<String> getChatIds(Long userId, String type) {
String historyKey = CHAT_HISTORY_PREFIX + userId + ":" + type;
Set<Object> chatIds = redisTemplate.opsForSet().members(historyKey);
if (chatIds == null || chatIds.isEmpty()) {
return Collections.emptyList();
}
return chatIds.stream()
.map(Object::toString)
.collect(Collectors.toList());
}
4. 实现用户专属的聊天历史删除
确保只能删除当前用户的聊天历史:
/**
* 删除会话
*/
public void deleteChat(Long userId, String type, String chatId) {
// 从用户的聊天历史列表中删除
String historyKey = CHAT_HISTORY_PREFIX + userId + ":" + type;
redisTemplate.opsForSet().remove(historyKey, chatId);
// 删除会话信息
String sessionKey = CHAT_SESSION_PREFIX + chatId;
redisTemplate.delete(sessionKey);
// 删除聊天消息
String messagesKey = CHAT_MESSAGES_PREFIX + chatId;
redisTemplate.delete(messagesKey);
}
二、后端服务器重启后 AI 助手上下文丢失
问题描述
后端服务器重启后,AI 助手失去了之前的聊天上下文,无法记住之前的对话内容,表现为"失忆"状态。
问题原因
- 仅使用内存存储:聊天记忆只存储在应用内存中,重启后内存被清空
- 缺少持久化机制:没有将聊天上下文持久化到外部存储介质
- Spring AI 默认配置:Spring AI 默认使用 InMemoryChatMemory,不支持持久化
解决方案
1. 实现 Redis 聊天记忆
创建 RedisChatMemory 类,实现 Spring AI 的 ChatMemory 接口:
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 java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
@Component
public class RedisChatMemory implements ChatMemory {
private final RedisTemplate<String, Object> redisTemplate;
private static final String MEMORY_KEY_PREFIX = "chat:memory:";
private static final long EXPIRATION_DAYS = 30;
public RedisChatMemory(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void add(String key, List<Message> messages) {
// 为特定会话添加多条消息
for (Message message : messages) {
addMessage(key, message);
}
}
@Override
public List<Message> get(String key, int maxCount) {
// 实现 get 方法,根据 key 获取消息
List<Message> 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<Message> getMessages(String chatId) {
String key = MEMORY_KEY_PREFIX + chatId;
List<Object> objects = redisTemplate.opsForList().range(key, 0, -1);
List<Message> 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);
}
}
2. 配置 ChatClient 使用 RedisChatMemory
在 CommonConfiguration 中配置 ChatClient 使用我们实现的 RedisChatMemory:
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();
}
}
3. 在控制器中使用 RedisChatMemory
在 ChatController 中使用 RedisChatMemory 来管理聊天上下文:
@RequestMapping(value = "/chat", produces = "text/html;charset=UTF-8")
public Flux<String> 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.save("chat", chatId, userId);
// 获取历史对话消息作为上下文
List<Message> messages = new ArrayList<>();
// 添加系统消息
SystemMessage systemMessage = new SystemMessage(
"你是一个智能图片社区的AI助手,名为虹小智。请用友好、专业的语气回答用户问题," +
"提供关于图片社区的相关信息和帮助。"
);
messages.add(systemMessage);
// 获取并解析历史消息
List<String> 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.saveMessage(chatId, prompt, "user");
// 调用AI模型获取响应
return chatClient.stream(messages)
.doOnNext(response -> {
// 保存AI响应到历史记录
chatHistoryRepository.saveMessage(chatId, response, "ai");
});
}
三、Redis 序列化的问题
问题描述
在使用 Redis 存储聊天记录和记忆时,出现序列化和反序列化错误,如 ClassCastException,导致系统无法正常工作。
问题原因
- 默认序列化器不匹配:Spring Boot 默认使用 JdkSerializationRedisSerializer,不支持复杂对象的序列化
- 类型转换错误:存储时使用一种序列化器,读取时使用另一种序列化器
- 缺少类型信息:序列化时没有保存对象的类型信息,反序列化时无法正确识别对象类型
解决方案
1. 配置 Redis 使用 Jackson2JsonRedisSerializer
修改 RedisConfiguration 类,使用 Jackson2JsonRedisSerializer 作为值的序列化器:
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<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板对象...");
RedisTemplate<String, Object> 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<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// value 采用 JSON 的序列化方式
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2. 确保所有存储的对象可序列化
为所有需要存储到 Redis 的对象实现 Serializable 接口:
package com.spc.smartpiccommunitybackend.model.entity.ai;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.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");
}
}
3. 处理序列化异常
在代码中添加异常处理,确保序列化失败时系统仍能正常工作:
@Override
public void saveMessage(String chatId, String message, String sender) {
String messagesKey = CHAT_MESSAGES_PREFIX + chatId;
// 创建消息对象
Map<String, Object> messageInfo = new HashMap<>();
messageInfo.put("content", message);
messageInfo.put("sender", sender);
messageInfo.put("timestamp", System.currentTimeMillis());
// 使用JSON格式保存消息
ObjectMapper objectMapper = new ObjectMapper();
try {
String jsonMessage = objectMapper.writeValueAsString(messageInfo);
redisTemplate.opsForList().rightPush(messagesKey, jsonMessage);
} catch (JsonProcessingException e) {
e.printStackTrace();
// 如果JSON序列化失败,使用原始消息
redisTemplate.opsForList().rightPush(messagesKey, message);
}
// 设置过期时间为30天
redisTemplate.expire(messagesKey, 30, TimeUnit.DAYS);
// 更新会话的最后更新时间
String sessionKey = CHAT_SESSION_PREFIX + chatId;
redisTemplate.opsForHash().put(sessionKey, "lastUpdateTime", System.currentTimeMillis());
// 确保会话信息也有过期时间
redisTemplate.expire(sessionKey, 30, TimeUnit.DAYS);
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)