在使用 Spring Boot 3 整合 Spring AI 实现 AI 助手的过程中,可能会遇到一些常见问题,如用户聊天记录隔离、服务器重启后上下文丢失以及 Redis 序列化等问题。本文将详细介绍这些问题的原因和解决方案。

一、用户之间的聊天记录没有实现隔离

问题描述

不同用户的聊天记录相互干扰,一个用户可以看到其他用户的聊天历史,这严重违反了用户隐私保护原则。

问题原因

  1. Redis Key 设计不当:使用了全局唯一的聊天 ID 作为 Redis Key,没有包含用户标识
  2. 缺少用户认证:API 接口没有验证用户身份,允许任意用户访问任意聊天记录
  3. 数据存储结构不合理:没有为每个用户创建独立的聊天历史列表

解决方案

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 助手失去了之前的聊天上下文,无法记住之前的对话内容,表现为"失忆"状态。

问题原因

  1. 仅使用内存存储:聊天记忆只存储在应用内存中,重启后内存被清空
  2. 缺少持久化机制:没有将聊天上下文持久化到外部存储介质
  3. 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,导致系统无法正常工作。

问题原因

  1. 默认序列化器不匹配:Spring Boot 默认使用 JdkSerializationRedisSerializer,不支持复杂对象的序列化
  2. 类型转换错误:存储时使用一种序列化器,读取时使用另一种序列化器
  3. 缺少类型信息:序列化时没有保存对象的类型信息,反序列化时无法正确识别对象类型

解决方案

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);
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐