SpringBoot3整合SpringAI:打造带记忆的AI助手

2026-06-08阅读 0热度 0
ai

SpringBoot3 集成 SpringAI 打造具备上下文记忆的AI助手

在开发智能图片社区项目时,用户需要一个能持续记忆对话上下文的AI助手。经过多方案对比,最终采用 Spring Boot 3 + Spring AI + Redis 组合。下面从环境搭建到核心代码,再到测试验证,一步步拆解完整实现流程。

1. 项目全景概览

本指南详细讲解如何基于 Spring Boot 3 整合 Spring AI,构建一个具备记忆能力的AI助手。方案使用 Redis 作为持久化存储,支持按用户隔离会话,并保留30天的对话历史记录。

SpringBoot3 整合 SpringAI 实现ai助手(记忆)

技术选型栈

  • 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_HOMEMAVEN_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 = 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;
    }
}

这块有几个关键坑需要留意:

  • 选用 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 getSessionInfo(String chatId);
}

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 chatIds = redisTemplate.opsForSet().members(historyKey);
        if (chatIds == null || chatIds.isEmpty()) return Collections.emptyList();
        return chatIds.stream().map(Object::toString).collect(Collectors.toList());
    }

    @Override
    public void sa veMessage(String chatId, String message, String sender) {
        String messagesKey = CHAT_MESSAGES_PREFIX + chatId;
        Map messageInfo = new HashMap<>();
        messageInfo.put("content", message);
        messageInfo.put("sender", sender);
        messageInfo.put("timestamp", System.currentTimeMillis());
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            String jsonMessage = objectMapper.writeValueAsString(messageInfo);
            redisTemplate.opsForList().rightPush(messagesKey, jsonMessage);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            redisTemplate.opsForList().rightPush(messagesKey, message);
        }
        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);
    }

    @Override
    public List getMessages(String chatId) {
        String messagesKey = CHAT_MESSAGES_PREFIX + chatId;
        List messages = redisTemplate.opsForList().range(messagesKey, 0, -1);
        if (messages == null || messages.isEmpty()) return Collections.emptyList();
        return messages.stream().map(Object::toString).collect(Collectors.toList());
    }

    @Override
    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);
    }

    @Override
    public Map getSessionInfo(String chatId) {
        String sessionKey = CHAT_SESSION_PREFIX + chatId;
        return redisTemplate.opsForHash().entries(sessionKey);
    }
}

实现中的设计亮点:

  • 通过不同 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 启动服务

  1. 确认 Redis 和 MySQL 服务处于运行状态
  2. 启动 Spring Boot 应用
  3. 访问 http://localhost:9527/api/ai/chat?prompt=你好&chatId=test123 验证 AI 响应

6.2 验证聊天记忆

  1. 发送第一轮:http://localhost:9527/api/ai/chat?prompt=你好,我叫张三&chatId=test123
  2. 发送第二轮:http://localhost:9527/api/ai/chat?prompt=你知道我叫什么名字吗?&chatId=test123
  3. 检查 AI 能否准确复述你的名字

6.3 用户隔离测试

  1. 用不同用户账号登录
  2. 验证各用户的聊天历史是否相互独立

6.4 历史持久化测试

  1. 连续发送多条消息
  2. 重启应用
  3. 确认历史记录依然存在

6.5 历史管理测试

  1. 获取用户聊天列表:GET http://localhost:9527/api/ai/history/chat
  2. 删除指定会话:DELETE http://localhost:9527/api/ai/history/chat/test123
  3. 验证删除结果
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策