章节五:基于MySQL的持久化存储会话(spring-ai系列)

背景

spring-ai官方提供了多种方式来存储对话:

  • InMemoryChatMemory:内存存储
  • CassandraChatMemory:在 Cassandra 中带有过期时间的持久化存储
  • Neo4jChatMemory:在 Neo4j 中没有过期时间限制的持久化存储
  • JdbcChatMemory:在 JDBC 中没有过期时间限制的持久化存储

基于jdbc的持久化目前支持:

  • PostgreSQL
  • MySQL / MariaDB
  • SQL Server
  • HSQLDB

但是官方文档(https://docs.spring.io/spring-ai/reference/api/chat-memory.html)对于jdbc持久化方式的介绍草草带过😩,直接放到内存中,项目重启数据就会丢失,并且不断的往内存中存数据,后面可能会导致oom。
于是打算自己通过实现ChatMemory来实现基于MySQL的持久化机制。😏

配置

依赖配置

jdk用的是21版本

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
    <relativePath/>
</parent>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud.ai</groupId>
    <artifactId>spring-ai-alibaba-starter</artifactId>
    <version>1.0.0-M5.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.32</version>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.12</version>
</dependency>
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-all</artifactId>
    <version>5.8.16</version>
</dependency>

❗️注意:mybatis-plus版本可能会和springboot冲突,出现:Invalid value type for attribute 'factoryBeanObjectType',切换mybatis-plus版本即可。

yml配置

api-key从阿里云百炼平台获取:
https://bailian.console.aliyun.com/?tab=model#/api-key

spring:
  # ai相关配置
  ai:
    dashscope:
      api-key: your-api-key
    chat:
      client:
        enabled: false #禁止ChatClient.Builder的自动装配
  # mysql连接配置
  datasource:
    url: jdbc:mysql://localhost:3306/super_ai_agent?characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
# mybatis-plus配置
mybatis-plus:
  configuration:
    # 下划线转驼峰
    map-underscore-to-camel-case: true
  # 全局配置
  global-config:
    db-config:
      # 数据库id配置
      id-type: auto
      logic-delete-field: is_del # 全局逻辑删除字段名
      logic-delete-value: 1   # 逻辑已删除值。可选,默认值为 1
      logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
  mapper-locations: classpath:/mapper/**.xml

库表设计

✨Tip:spring-ai默认生成的会话Id为defalut,不是UUID。

CREATE TABLE `ai_chat_memory` (
  `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
  `chat_id` varchar(32) NOT NULL COMMENT '会话id',
  `type` varchar(10) NOT NULL DEFAULT 'user' COMMENT '消息类型',
  `content` text NOT NULL COMMENT '消息内容',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `is_del` tinyint(1) NOT NULL DEFAULT '0' COMMENT '删除标记,0-未删除;1-已删除'
 )

索引

👉我这里选择将单条消息作为数据库的一行数据,而不是单次会话,因此chat_id不是唯一的。

CREATE index idx_chat_id ON ai_chat_memory (chat_id);

InMySqlChatMemory

AiChatMemory为实体类,AiChatMemoryService为服务层,直接用mybatis-plus插件生成就行。

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.*;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;

/**
* 基于MySQL的ChatMemory实现
*/
@Component
public class InMySqlChatMemory implements ChatMemory {
   @Resource
   private AiChatMemoryService aiChatMemoryService;

   @Override
   public void add(String conversationId, List<Message> messages) {
       List<AiChatMemory> aiChatMemorieList = new ArrayList<>();
       messages.forEach(message -> {
           AiChatMemory aiChatMemory = new AiChatMemory();
           aiChatMemory.setChatId(conversationId);
           aiChatMemory.setType(message.getMessageType().getValue());
           aiChatMemory.setContent(message.getText());
           aiChatMemorieList.add(aiChatMemory);
       });
       aiChatMemoryService.saveBatch(aiChatMemorieList);
   }

   @Override
   public List<Message> get(String conversationId, int lastN) {
       if (lastN >0){
           List<AiChatMemory> aiChatMemoryList = aiChatMemoryService.list(new QueryWrapper<AiChatMemory>()
                   .eq("chat_id", conversationId)
                   .orderByDesc("create_time")
                   .last("limit " + lastN));
           if (CollectionUtils.isEmpty(aiChatMemoryList)){
                return List.of();
           }
           return aiChatMemoryList.stream()
                   .map(aiChatMemory -> {
                       String type = aiChatMemory.getType();
                       String content = aiChatMemory.getContent();
                       Message message;
                       return switch (type) {
                           case "system" -> message = new SystemMessage(content);
                           case "user" -> message = new UserMessage(content);
                           case "assistant" -> message = new AssistantMessage(content);
                           default -> throw new IllegalArgumentException("Unknown message type: " + type);
                       };
                   })
                   .toList();
       }
       return List.of();
   }

   @Override
   public void clear(String conversationId) {
       aiChatMemoryService.remove(new QueryWrapper<AiChatMemory>()
               .eq(conversationId!=null,"chat_id",conversationId));
   }
}

使用

ChatClient配置

👉LOVE_PROMPT为系统预设prompt

import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@Configuration
public class ChatClientConfig {
    @Value("${spring.ai.dashscope.api-key}")
    public String apiKey;
    @Resource
    ChatMemory InMySqlChatMemory;

    @Bean
    public ChatClient qwenPlusInMemoryChatClient(){
        if (apiKey == null)
            throw new RuntimeException("apiKey is null");
        
        return ChatClient.builder(new DashScopeChatModel(new DashScopeApi(apiKey),
                        DashScopeChatOptions.builder().withModel("qwen-plus").build()
                ))
                .defaultSystem(LOVE_PROMPT)
                .defaultAdvisors(
                        //自定义持久化记忆advisor
                        new MessageChatMemoryAdvisor(InMySqlChatMemory)
                )
                .build();
    }
}

测试

import jakarta.annotation.Nonnull;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/love")
public class LoveDemoController {
    @Resource
    private ChatClient qwenPlusInMemoryChatClient;
     
     @GetMapping("chat")
     public  String simpleChat(String message) {
        return qwenPlusInMemoryChatClient.prompt()
                .user(message)
                .call().content();
     }
}

运行结果

在这里插入图片描述
数据库:
在这里插入图片描述

可以看到,我们没有传chatId默认就是default👌

### 关于 SpringAI 与 DeepSeek 的数据库集成 SpringAI 提供了一个灵活的框架来实现人工智能功能,尤其是在 Java 应用程序中的集成。DeepSeek 是一种先进的大型语言模型,在自然语言理解和生成方面表现出色。当两者结合时,可以通过数据库技术进一步增强其能力。 #### 数据库集成的关键点 1. **配置管理** 在 `application.properties` 或 `application.yml` 中完成 API 配置后,可以利用这些设置连接外部服务或本地存储的数据源。例如,如果需要将用户的查询历史记录保存到关系型数据库中,则可以在应用程序中定义 JPA 实体类并与之交互[^3]。 2. **数据持久化** 对于基于 RAG(检索增强生成)的知识库构建场景,通常会涉及以下几个部分: - 文档索引:使用 Elasticsearch 等搜索引擎对文档进行向量化处理。 - 查询解析:通过 DeepSeek 解析用户输入并返回结构化的语义表示。 - 结果缓存:为了提高性能,可采用 Redis 缓存机制存储频繁访问的结果集[^1]。 3. **测试接口设计** 创建 RESTful 接口用于验证整个流程是否正常工作。假设我们有一个简单的 GET 请求 `/search?q={query}` 来触发后台逻辑链路运行,具体实现如下所示: ```java @RestController @RequestMapping("/api/search") public class SearchController { @Autowired private AiService aiService; @GetMapping public ResponseEntity<String> search(@RequestParam String q) { try { String result = aiService.query(q); return new ResponseEntity<>(result, HttpStatus.OK); } catch (Exception e) { return new ResponseEntity<>("Error processing request", HttpStatus.INTERNAL_SERVER_ERROR); } } } ``` 上述代码片段展示了如何封装调用 AI 模型的服务层方法,并对外暴露 HTTP 协议下的资源路径。 --- ### 示例应用场景分析 考虑一个实际案例——企业内部知识管理系统。该平台允许员工提交问题并通过机器学习算法自动匹配最佳答案。以下是主要步骤概述: 1. 收集公司资料作为训练素材; 2. 将原始文件转换成适合 NLP 处理的形式; 3. 调用 DeepSeek 执行推理操作获取候选回复列表; 4. 把最终选定的内容写入 MySQL 表格以便后续审计追踪用途。 这种架构不仅简化了传统方式下繁琐的手动编辑过程,而且显著提升了用户体验满意度水平[^2]。 ---
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

码冬梅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值