AI 智能体项目第三期扩展 - 基于Mysql实现简易会话存储持久化
ChatMemoryRepository 调用原理
在Spring AI中, ChatMemoryRepository 是 对话记忆 的持久化抽象接口,用于存储、查询、删除历史对话消息。他的核心职责有:
- 保存消息
- 查询消息
- 删除消息
- 替换或清理系统消息
在调用链中处于以下位置:
▼mermaid复制代码graph TD ChatClient --> ChatMemory ChatMemory --> ChatMemoryRepository ChatMemoryRepository --> MySQL
具体实现
库表实现
▼text复制代码-- 创建库 create database if not exists yu_ai_agent; -- 切换库 use yu_ai_agent; CREATE TABLE IF NOT EXISTS `chat_message` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID', `conversation_id` VARCHAR(64) NOT NULL COMMENT '会话ID', `msg_index` INT NOT NULL COMMENT '消息在会话中的顺序(从0开始,可用于排序)', `role` VARCHAR(32) NOT NULL COMMENT '消息角色:system/user/assistant/tool/function 等', `content` LONGTEXT NOT NULL COMMENT '消息内容(文本或JSON字符串)', `metadata` JSON DEFAULT NULL COMMENT '额外元数据,例如工具调用参数、function call、模型信息等', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_conv_id` (`conversation_id`), KEY `idx_conv_order` (`conversation_id`, `msg_index`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT ='AI 会话消息表';
添加 msg_index 字段的理由
系统的MessageWindowChatMemory 的核心方法是process(),他会执行窗口算法来判断是否需要删除之前的会话数据,删除那些会话数据,以及保证按顺序删除会话。并重新保存剪彩后的消息序列。
因此, msg_index 有两个目的:
- 消息排序
- 窗口剪裁,chatMemory 可以根据 msg_index 的值快速判断哪些会话需要被剪裁出去
MybatisX-Generator自动生成实体类和mapper相关
ChatMessage 实体类,遵循驼峰命名
▼text复制代码/** * AI 会话消息表 * @TableName chat_message */ @TableName(value ="chat_message") @Data public class ChatMessage implements Serializable { /** * 主键ID */ @TableId(type = IdType.AUTO) private Long id; /** * 会话ID */ @TableField(value = "conversation_id") private String conversationId; /** * 消息在会话中的顺序(从0开始,可用于排序) */ @TableField(value = "msg_index") private Integer msgIndex; /** * 消息角色:system/user/assistant/tool/function 等 */ private String role; /** * 消息内容(文本或JSON字符串) */ private String content; /** * 额外元数据,例如工具调用参数、function call、模型信息等 */ private Object metadata; /** * 创建时间 */ @TableField(value = "create_time") private Date createTime; /** * 更新时间 */ @TableField(value = "update_time") private Date updateTime; @Serial @TableField(exist = false) private static final long serialVersionUID = 1L; }
Mapper相关代码没有放入
转换类
ChatMessageConvertor 用于 Message 和 ChatMessage 之间的转换
▼text复制代码/** * @program: yu-ai-agent * @description: * @author: leo * @date: 2025-12-01 16:22 **/ public class ChatMessageConvertor { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); /** * message 转换为 ChatMessage * * @param conversationId * @param index * @param message * @return */ public static ChatMessage toChatMessage(String conversationId, int index, Message message) { ChatMessage chatMessage = new ChatMessage(); chatMessage.setConversationId(conversationId); chatMessage.setMsgIndex(index); chatMessage.setContent(message.getText()); // 确定消息角色 String role = determineRole(message); chatMessage.setRole(role); // 保存元数据 Map<String, Object> metadata = new HashMap<>(message.getMetadata()); // 如果是 ToolResponseMessage,保存额外信息 if (message instanceof ToolResponseMessage toolMsg) { // Spring AI 1.1.0-M4 版本的 ToolResponseMessage 可能有特殊字段 // 根据实际情况保存工具相关信息 } if (!metadata.isEmpty()) { chatMessage.setMetadata(JSONUtil.toJsonStr(metadata)); } return chatMessage; } /** * ChatMessage 转换为 message * * @param chatMessage * @return */ public static Message toMessage(ChatMessage chatMessage) { String role = chatMessage.getRole(); String content = chatMessage.getContent(); // 解析 metadata // Map<String, Object> metadata = new HashMap<>(); // if (chatMessage.getMetadata() != null) { // String metadataStr = chatMessage.getMetadata().toString(); // if (JSONUtil.isTypeJSON(metadataStr)) { // metadata = JSONUtil.toBean(metadataStr, Map.class); // } // } // 根据角色创建不同类型的 Message return switch (role.toLowerCase()) { case "system" -> new SystemMessage(content); case "user" -> new UserMessage(content); case "assistant" -> new AssistantMessage(content); default -> new UserMessage(content); }; } /** * 确定消息的角色类型 */ private static String determineRole(Message message) { if (message instanceof SystemMessage) { return "system"; } else if (message instanceof UserMessage) { return "user"; } else if (message instanceof AssistantMessage) { return "assistant"; } else if (message instanceof ToolResponseMessage) { return "tool"; } else { // 默认为 user return "user"; } } }
核心 MysqlBasedChatMemoryRepository
▼text复制代码/** * @program: yu-ai-agent * @description: * @author: leo * @date: 2025-12-01 13:08 **/ @Component public class MysqlBasedChatMemoryRepository implements ChatMemoryRepository { @Resource private ChatMessageMapper chatMessageMapper; /** * 查找所有的会话 id * * @return */ @Override public List<String> findConversationIds() { return chatMessageMapper.selectList( new QueryWrapper<ChatMessage>() .select("distinct conversation_id") ).stream() .map(ChatMessage::getConversationId) .distinct() .toList(); } /** * 根据 conversationId 查找指定的会话消息 * * @param conversationId * @return */ @Override public List<Message> findByConversationId(String conversationId) { List<ChatMessage> list = chatMessageMapper.selectList( new QueryWrapper<ChatMessage>() .eq("conversation_id", conversationId) .orderByAsc("msg_index") ); return list.stream() .map(ChatMessageConvertor::toMessage) .collect(Collectors.toList()); } /** * 保存所有的会话消息 * * @param conversationId * @param messages */ @Override public void saveAll(String conversationId, List<Message> messages) { deleteByConversationId(conversationId); int index = 0; for (Message message : messages) { ChatMessage chatMessage = ChatMessageConvertor.toChatMessage(conversationId, index++, message); chatMessageMapper.insert(chatMessage); } } /** * 通过 会话id删除会话 * * @param conversationId */ @Override public void deleteByConversationId(String conversationId) { chatMessageMapper.delete( new QueryWrapper<ChatMessage>() .eq("conversation_id", conversationId) ); } }
实现 ChatMemoryRepository 中的所有方法
最终测试
新建自定义AI会话应用,我这里选择开发足球数据分析相关功能的AI会话应用。
这里我直接选择使用默认的 MessageWindowChatMemory
▼text复制代码/** * @program: yu-ai-agent * @description: * @author: leo * @date: 2025-12-01 13:07 **/ @Component @Slf4j @MapperScan("com.yupi.yuaiagent.mapper") public class MyFootballDataAnalysisApp { private final ChatClient chatClient; private static final String SYSTEM_PROMPT = "扮演专业足球数据分析 AI,具备足球领域深度知识与多维度数据处理能力,为教练、球迷等不同用户提供精准结构化分析服务。" +"覆盖球员(技术统计、趋势等)、球队(攻防数据、对战历史等)、比赛(复盘、关键事件等)、赛事(格局、出线概率等)数据维度," +"支持数据查询、深度 / 对比 / 定制化分析及趋势预测(需标注非绝对结论)," +"交互时补全关键信息、数据严谨、结构清晰、语言适配用户专业度,不涉及无关及违法违规内容," +"禁止编造数据、主观臆断、赌博引导及不当语言。"; public MyFootballDataAnalysisApp(ChatModel dashscopeChatModel, MysqlBasedChatMemoryRepository mysqlBasedChatMemoryRepository) { MessageWindowChatMemory memory = MessageWindowChatMemory.builder() .chatMemoryRepository(mysqlBasedChatMemoryRepository) .maxMessages(20) .build(); chatClient = ChatClient.builder(dashscopeChatModel) .defaultSystem(SYSTEM_PROMPT) .defaultAdvisors( MessageChatMemoryAdvisor.builder(memory).build(), new MyLoggerAdvisor() ) .build(); } /** * ai 基础对话 * * @param message * @param chatId * @return */ public String doChat(String message, String chatId) { ChatResponse chatResponse = chatClient .prompt() .user(message) .advisors( advisor -> advisor.param(ChatMemory.CONVERSATION_ID, chatId) ) .call() .chatResponse(); String res = chatResponse.getResult().getOutput().getText(); log.info("chatId: {}, message: {}", chatId, res); return res; } }
注意: 此时,MysqlBasedChatMemoryRepository需要让Spring来创建,不能自己 new,会导致所有Spring注入全部失效。
配置
最后,添加数据库和mybatis-plus配置:
▼text复制代码spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yu_ai_agent?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 # mybatis plus 配置 mybatis-plus: configuration: map-underscore-to-camel-case: false # 仅在开发环节打印日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 逻辑删除 global-config: db-config: logic-delete-field: isDelete # 全局逻辑删除的实体字段名 logic-delete-value: 1 # 逻辑已删除值 (默认为1) logic-not-delete-value: 0 # 逻辑未删除值 (默认为0) enable-sql-runner: true
官方JDBC依赖冲突 bug:
在最开始着手实现数据库会话存储持久化时,我按照官网引入了jdbc与spring ai 框架关联的依赖:
▼text复制代码<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId> <version>1.1.0-M4</version> <type>pom</type> </dependency>
这样会导致 ChatMemoryAutoConfiguration检测到两个 会话存储的Bean:

解决方式:
- 直接删去依赖
- 或者在
MysqlBasedChatMemoryRepository添加@Primary注解(Bean生效并覆盖默认配置)
评论
问答助学
相关内容
0个评论
全部评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
内容推荐
AI 超级智能体 - 基于mysql的消息存储
4
SpringAl+RAG+MCP全栈|AI超级智能体企业级实战项目中:stdio连接方式调用MCP服务问题运行测试时解决:1. 先查看MCP server那的yml文件,文件名有没有写错2. 修改server端的"application-stdio.yml",把每一个报错发给deepseek再验证之后,得出结论:Server 的 stdout 完全被污染了:Spring Boot Banner 完
3
SpringAI + RAG + MCP 全栈 | AI 超级智能体企业级实战(26年必学) - 第三期:基础内容 笔记
3
AI超级智能体项目_踩坑记录_PGVector维度冲突
2
Code Log-AI超级智能体项目,开发日志。SpringAI + RAG + MCP 全栈 | AI 超级智能体企业级实战(26年必学)
2

