Spring AI 系列1: ChatMemory聊天记忆总结

article/2025/8/17 2:37:05

一、ChatMemory 的核心作用与功能解析

Spring AI 中的的ChatMemory(聊天记忆)提供了维护 AI 聊天应用程序的对话上下文和历史的机制。聊天记忆使 AI 应用程序能够:维护对话历史、提供上下文感知的响应、实现不同的记忆策略、管理对话状态。

1. 核心功能:对话上下文管理

  • 历史对话存储:记录用户与AI模型之间的完整交互消息(包括用户提问、模型回应及工具调用记录),确保多轮对话的连贯性。

  • 上下文关联:通过分析历史对话内容,帮助模型理解当前问题的背景和意图(例如:「刚才提到的合同条款是指哪一条?」)[[历史对话]]。

2. 多轮对话支持

  • 会话ID绑定:通过 chatId (如用户ID或UUID)区分不同对话会话,实现对话数据的隔离与管理[[历史对话]]。

  • 动态上下文加载:每次请求自动附加相关历史消息到模型输入中,无需手动拼接上下文。

3. 性能与资源优化

  • 滑动窗口机制:通过 MessageWindowChatMemory 等策略限制存储的消息数量(如仅保留最近5条),避免无限增长导致内存压力。

  • Token控制:减少冗余历史信息的传输,降低模型处理的Token消耗及API成本。

4. 数据隔离与安全

  • 多用户支持:在分布式系统中,结合 chatMemoryProvider 为每个用户创建独立的对话存储空间,防止数据混淆。

  • 敏感信息过滤:可扩展实现消息内容的脱敏处理(如自动屏蔽电话号码或隐私关键词)。

5. 持久化与扩展

  • 存储适配:支持内存、Redis、关系型数据库等多种存储方式,满足不同场景的持久化需求。

  • 长期记忆增强:结合向量数据库实现语义检索,支持长期对话记忆(例如数月前的历史咨询)

1.1、典型应用场景

场景作用
智能客服保持用户咨询的连续性,自动关联历史工单或解决方案
教育助手跟踪学习进度,基于过往提问推荐相关知识模块
医疗咨询记录患者病史,确保诊断建议的连贯

1.2、配置建议

# Spring AI 示例配置 
spring:ai:chat:memory:type: redis  # 可选内存/redis/jdbcwindow-size: 5  # 滑动窗口保留消息数retrieval-size: 3  # 每次检索的历史消息条数

该组件已在多个实际场景中验证,例如某电商客服系统通过集成 ChatMemory + Redis 实现了日均百万级对话的上下文管理]。开发者可根据需求选择适配存储方案,并通过扩展接口实现自定义逻辑(如消息加密或归档)。


二、ChatMemory 存储方式分类

2.1 内存存储(In-Memory)

  • 实现方式
    使用 Java 堆内存存储对话记录,会话数据保存在 ConcurrentHashMap 等结构中,以 chatId 作为键。

  • 特点

    • 读写速度极快(微秒级响应)

    • 服务器重启后数据丢失,仅适用于开发和临时会话场景

  • 配置示例
    spring.ai.chat.memory.type=memory

2. 2 文件系统存储(File-Based)

  • 实现方式
    将会话历史序列化为 JSON 或 XML 文件,存储到本地磁盘14。

  • 特点

    • 数据持久化,适合离线场景1

    • 读写性能受磁盘 I/O 限制(毫秒级响应)4

    • 需注意文件锁机制避免并发冲突6

2.3 关系型数据库(RDBMS)

  • 典型方案
    MySQL、PostgreSQL 等,通过 JdbcTemplate 或 ORM 框架存储。

  • 表结构示例

    CREATE TABLE chat_memory ( session_id VARCHAR(64) PRIMARY KEY, messages JSON, last_accessed TIMESTAMP )
  • 特点

    • 支持事务和复杂查询

    • 数据安全性高,但延迟较高(10-100ms)

2.4  分布式存储(Redis/MongoDB)

  • Redis 实现
    使用 RedisTemplate 存储序列化会话数据,支持过期时间管理。

    redisTemplate.opsForValue().set(chatId, messages, 24, TimeUnit.HOURS);
  • MongoDB 实现
    利用文档数据库特性存储多轮对话的非结构化数据。

  • 特点

    • 高并发支持(10万+ QPS)

    • 天然支持分布式会话同步


2.5  向量数据库增强存储

  • 技术方案
    结合 Qdrant/Pinecone 等向量数据库,将对话内容编码为向量存储。

  • 优势

    • 支持语义检索历史对话

    • 可实现长期记忆和上下文关联分析

选型建议

存储方式适用场景性能指标数据持久性
内存存储开发测试/短时交互≤1ms
Redis高并发生产环境1-5ms可选
关系型数据库审计/复杂分析需求10-100ms
向量数据库智能客服/上下文关联场景5-20ms

扩展优化

  • 混合存储:热数据存 Redis,冷数据存 MySQL

  • 压缩策略:对历史消息使用 GZIP 压缩减少存储开销

  • TTL 机制:通过 @Scheduled 任务自动清理过期会话

以上方案已在多个实际项目中验证,例如某金融客服系统通过「Redis + 向量数据库」架构实现了 10万级并发对话管理。开发者可根据场景需求选择或组合存储方式。


三、ChatMemory 实现

3.1 基本配置

@Configuration
public class ChatMemoryConfig {@Beanpublic ChatMemory chatMemory() {return new InMemoryChatMemory();}
}

3.2 使用聊天记忆

@Service
public class ChatService {private final ChatClient chatClient;private final ChatMemory chatMemory;public ChatService(ChatClient chatClient, ChatMemory chatMemory) {this.chatClient = chatClient;this.chatMemory = chatMemory;}public String chat(String message, String sessionId) {// 将消息添加到记忆中chatMemory.addMessage(sessionId, message);// 获取对话历史List<Message> history = chatMemory.getMessages(sessionId);// 使用上下文生成响应String response = chatClient.generate(history);// 将响应存储在记忆中chatMemory.addMessage(sessionId, response);return response;}
}

3.3 记忆策略

(1) 固定窗口记忆

@Bean
public ChatMemory fixedWindowMemory() {return new FixedWindowChatMemory(10); // 保留最后10条消息
}

(2)基于令牌的记忆

@Bean
public ChatMemory tokenBasedMemory() {return new TokenBasedChatMemory(1000); // 在令牌限制内保留消息
}

(3)摘要记忆

@Bean
public ChatMemory summaryMemory() {return new SummaryChatMemory(summarizer);
}

3.4 配置属性

spring:ai:chat:memory:max-messages: 100max-tokens: 1000ttl: 3600type: in-memory

3.5 最佳实践

在实现聊天记忆时,请考虑以下最佳实践:

  • 记忆大小:根据您的用例选择适当的记忆大小

  • 持久化:在生产环境中使用持久化存储

  • 清理:为旧对话实现适当的清理机制

  • 安全性:确保适当的数据保护和隐私措施

  • 可扩展性:考虑为高规模应用程序使用分布式记忆

3.6 高级功能

自定义记忆实现
@Component
public class CustomChatMemory implements ChatMemory {@Overridepublic void addMessage(String sessionId, String message) {// 自定义实现}@Overridepublic List<Message> getMessages(String sessionId) {// 自定义实现return messages;}
}
记忆监控

监控记忆使用情况和性能:

management.endpoints.web.exposure.include=chat-memory
management.endpoint.chat-memory.enabled=true

3.7 故障排除

常见问题和解决方案:

  1. 内存泄漏

    1. 实现适当的清理

    2. 设置适当的 TTL

    3. 监控内存使用情况

  2. 性能问题

    1. 使用适当的记忆类型

    2. 实现缓存

    3. 优化存储

  3. 可扩展性

    1. 使用分布式记忆

    2. 实现适当的分区

    • 考虑缓存策略


四、关于代码段 advisors(a-> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)) 的代码作用解析

    /*** 会话记忆 已通过* @param message* @param chatId* @return** 此次新增chatId字段,目的是为一次会话添加唯一标识,基于该标识存储会话记忆,目前使用的内存记忆功能,可以尝试使用向量数据库,永久有效。** 问题1:共12个苹果,分给12个人,怎么分* 问题2:分给3个人呢*/@PostMapping(value = "/memoryChat",produces = "text/html;charset=utf-8")public Flux<String> memoryChat(@RequestParam("message") String message, String chatId) {return chatClient.prompt().user(message).advisors(a-> a.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)).stream().content();}

这是一个典型的 Spring AI 框架中 Advisor 参数绑定代码,主要用于管理多轮对话的上下文记忆。

其核心功能如下:

1. 参数定义

  • CHAT_MEMORY_CONVERSATION_ID_KEY
    预定义常量,标识对话的唯一会话ID键(如 spring.ai.chat.memory.conversation-id )

  • chatId
    动态生成的会话唯一标识符(通常为UUID或用户ID),用于追踪特定对话的完整上下文

2. 功能实现

  • 对话内存绑定
    将 chatId 与会话管理器(如 ChatMemory)关联,确保每次请求能正确检索历史对话记录

  • 数据隔离
    通过唯一ID隔离不同用户的对话数据,避免内存混用问题

3. 运行机制

// 伪代码流程 
1. 用户发起新对话 → 生成 chatId (UUID/用户ID)
2. 调用 advisors() 配置链 → 绑定 chatId 到当前请求上下文 
3. Advisor 链处理请求时 → 通过 chatId 加载历史消息 
4. 模型响应后 → 根据 chatId 保存最新对话记录到内存/数据库 

关联技术组件

组件作用
ChatMemory存储对话历史记录的接口,支持内存/Redis等存储方式
VectorStoreChatMemoryAdvisor将对话记录存储到向量数据库的增强组件,依赖 chatId 实现长期记忆
MessageChatMemoryAdvisor基础上下文管理组件,自动附加历史消息到模型请求中

典型应用场景

  1. 多轮对话服务
    用户连续提问时,通过 chatId 保持上下文连贯性

  2. 用户个性化服务
    结合用户ID作为 chatId,实现基于用户画像的定制应答

  3. 分布式会话
    将会话ID与Redis等分布式存储结合,支持集群环境下的状态同步


配置建议

# 需配合的配置项示例(application.yaml )
spring:ai:chat:memory:type: redis # 可选内存/数据库等 retrieval-size: 5 # 历史消息检索条数

该模式已在客服系统、智能助手等场景广泛应用2,需注意及时清理过期 chatId 防止内存泄漏


五、Spring AI实现ChatMemory接口,实现MySQL持久化

Spring AI官方提供的InMemoryChatMemory是存储在内存中的,电脑关机后和AI的聊天记录就无了,将聊天记录存储在MySQL数据库中就能持久保存聊天记录了。

在使用InMemoryChatMemory基础上改造成将聊天记录存储到MySQL可参照下面代码:

5.1、数据库表的设计

CREATE TABLE chat_messages (id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID,自增',conversation_id VARCHAR(255) NOT NULL COMMENT '会话ID,用于区分不同的会话',message_type ENUM('USER', 'ASSISTANT') NOT NULL COMMENT '消息类型:USER表示用户,ASSISTANT表示AI助手',content TEXT NOT NULL COMMENT '消息内容',created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '消息创建时间',KEY idx_conversation_id (conversation_id),KEY idx_conversation_time (conversation_id, created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='聊天消息记录表';

5.2 创建数据表实体类

import lombok.Data;
import java.time.LocalDateTime;@Data
public class ChatMessageEntity {private Long id;private String conversationId;private MessageType messageType;private String content;private LocalDateTime createdAt;public enum MessageType {USER, ASSISTANT}
}

5.3 创建Mapper接口

import com.zry.ai.entity.ChatMemoryEntity.ChatMessageEntity;
import org.apache.ibatis.annotations.*;
import java.util.List;@Mapper
public interface MessageMapper {@Insert({"<script>","INSERT INTO chat_messages (conversation_id, message_type, content, created_at)","VALUES ","<foreach collection='messages' item='msg' separator=','>","(#{msg.conversationId}, #{msg.messageType}, #{msg.content}, #{msg.createdAt})","</foreach>","</script>"})void insertMessages(@Param("messages") List<ChatMessageEntity> messages);@Select("SELECT * FROM chat_messages " +"WHERE conversation_id = #{conversationId} " +"ORDER BY created_at DESC " +"LIMIT #{lastN}")List<ChatMessageEntity> findLastNMessages(@Param("conversationId") String conversationId, @Param("lastN") int lastN);@Delete("DELETE FROM chat_messages WHERE conversation_id = #{conversationId}")void deleteByConversationId(String conversationId);
}

5.4 实现SpringAI的ChatMemery接口

ChatMemery接口:


实现ChatMemery接口:

 
import com.zry.ai.entity.ChatMemoryEntity.ChatMessageEntity;
import com.zry.ai.mapper.MessageMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.stereotype.Component;import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;@Component
@RequiredArgsConstructor
public class MysqlChatMemory implements ChatMemory {private final MessageMapper messageMapper;@Overridepublic void add(String conversationId, List<Message> messages) {List<ChatMessageEntity> entities = messages.stream().map(msg -> {ChatMessageEntity entity = new ChatMessageEntity();entity.setConversationId(conversationId);entity.setContent(msg.getText());if (msg instanceof UserMessage) {entity.setMessageType(ChatMessageEntity.MessageType.USER);} else if (msg instanceof AssistantMessage) {entity.setMessageType(ChatMessageEntity.MessageType.ASSISTANT);}entity.setCreatedAt(LocalDateTime.now());return entity;}).collect(Collectors.toList());messageMapper.insertMessages(entities);}@Overridepublic List<Message> get(String conversationId, int lastN) {List<ChatMessageEntity> entities = messageMapper.findLastNMessages(conversationId, lastN);Collections.reverse(entities);return entities.stream().map(entity -> {switch (entity.getMessageType()) {case USER:return new UserMessage(entity.getContent());case ASSISTANT:return new AssistantMessage(entity.getContent());default:throw new IllegalArgumentException("未知的消息类型");}}).collect(Collectors.toList());}@Overridepublic void clear(String conversationId) {messageMapper.deleteByConversationId(conversationId);}
}

5.5 在配置ChatClient时将MysqlChatMemery加入环绕增强

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class ChatConfig {@Beanpublic ChatMemory chatMemory(MessageMapper messageMapper) {return new MysqlChatMemory(messageMapper);}@Beanpublic ChatClient chatClient(OllamaChatModel model, ChatMemory chatMemory) {return ChatClient.builder(model).defaultSystem(SystemConstants.CHAT_AI_SYSTEM_PROMPT).defaultAdvisors(new SimpleLoggerAdvisor(),new MessageChatMemoryAdvisor(chatMemory)).build();}
}

参考链接:

Spring AI实现ChatMemory接口,实现MySQL持久化_springai inmemorychatmemory-CSDN博客


http://www.hkcw.cn/article/TysKBpaIUq.shtml

相关文章

西装男子连吃两天免费小面 老板吃不消:别来了

在上海的一家面馆里,老板为了帮助遇到困难的人,推出了免费的重庆小面。只要顾客进店说一句“来碗重庆小面”,就可以免费享用,无需付款。然而,一名穿着西装、打着领带的男子却连续两天来吃,并且每次都要吃二斤。这让老板感到非常困扰。这名男子的行为引起了老板的不满。老…

银行员工完不成消费贷任务一周扣三百 倒贴利息拉业务

银行员工完不成消费贷任务一周扣三百,倒贴利息拉业务。:早听各家银行消费贷业务“卷”,现在都“卷”到员工互助了么?:早听各家银行消费贷业务“卷”,现在都“卷”到员工互助了么?:早听各家银行消费贷业务“卷”,现在都“卷”到员工互助了么?责任编辑:zx0002

昌都车辆坠河1人失联 搜救行动持续进行

昌都市公安局交通管理支队于5月29日通报,5月23日下午5时左右,昌都市公安局接到群众报警,称在昌日公路6公里加800米弯道处有一车辆坠河。接报后,警方立即会同消防、120急救等部门赶往现场展开救援。调查发现,当天下午5时左右,乘客邹某、孙某和唐某三人租赁贡某驾驶的藏A1*…

为何胖东来爆火,许多连锁超市却接连倒闭?

胖东来为什么能在商超红海中稳坐“顶流”,而许多连锁超市却接连倒闭?秘密就藏在一张A4纸上——它叫商业模式画布!用这张图来拆解胖东来的经营密码。其实胖东来的成功是回答好了做生意的5个关键问题,第1问:谁在抢着买单?胖东来的顾客画像清晰得惊人:家庭主妇冲着免费加工海…

彩礼“限价”,河北一村明确彩礼最高6万

最近河北又出了个大新闻!有个村子明确规定,彩礼最高不能超过6万,消息一出,村民们纷纷叫好,直呼“太及时了”!这到底是咋回事呢,今天咱就唠唠。原来,这个村子叫东孙村。东孙村结合实际,修订了《村规民约》,从5月27日起,正式施行新的红白喜事操办标准。除了限定彩礼,…

烟台科技学院校长论文抄袭 免职!

近日,“烟台科技学院校长硕士论文涉嫌严重抄袭”一事引发社会广泛关注。5月29日,烟台科技学院对此事发布声明,经核查,情况属实。学校董事会研究决定,免去马红坤烟台科技学院校长职务。责任编辑:0764

关晓彤演白化病女孩 命运交错的坚韧成长

在四川乐山沙湾区,电视剧《生逢其时》的最后一个镜头拍摄完成,这部由爱奇艺与浩瀚娱乐联手制作、滕华涛监制、林妍执导的作品正式杀青。故事发生在北方厂矿小镇青梧镇,关晓彤和王子奇领衔主演,从筹备之初就备受关注。剧中的核心情节源于一个阴差阳错的起点:白化病女孩齐时…

门店回应冰淇淋刺客卖268一个 真果壳制成解释高价

近日,一位女士在苏州一家餐厅用餐时遇到了所谓的“冰淇淋刺客”。她发布视频表示,一份装在花生壳里的冰淇淋售价高达28元,虽然有服务员帮忙切开,仪式感十足,但她认为这个价格并不合理。记者随后前往位于苏州市姑苏区美罗百货二楼的这家冰淇淋店进行实地探访。店内冰淇淋的…

一厨工在幼儿园洗碗池里洗墩布被辞退 法院:支持

一名厨工在幼儿园洗碗池里洗墩布被辞退 法院:支持!儿童安全重于一切洗碗池里洗拖把,洗菜池里随便洗手!如果孩子幼儿园的饭菜是在这样“混用”的水池旁准备的,哪个当父母的不心头一紧?北京某幼儿园的一名厨工雷女士就多次这样操作,幼儿园有专门的墩布池,而且要求专池专用…

优云智算-GPU实例使用指南

优云智算&#xff1a;GPU实例使用指南 推荐一个个人觉得比AutoDL更好用的GPU平台&#xff1a;优云智算&#xff0c;优云智算提供了一个高效、便捷的GPU算力平台&#xff0c;特别适合需要进行深度学习训练、科学计算等高性能计算任务的用户。相较于AutoDL和蓝耘等平台&#xff…

Jupyter MCP服务器部署实战:AI模型与Python环境无缝集成教程

Jupyter MCP 服务器是基于模型上下文协议&#xff08;Model Context Protocol, MCP&#xff09;的 Jupyter 环境扩展组件&#xff0c;它能够实现大型语言模型与实时编码会话的无缝集成。该服务器通过标准化的协议接口&#xff0c;使 AI 模型能够安全地访问和操作 Jupyter 的核心…

混合搜索再探:引入线性检索器!

作者&#xff1a;来自 Elastic Panagiotis Bailis Elasticsearch 拥有大量新功能&#xff0c;帮助你为你的使用场景构建最佳搜索解决方案。深入阅读我们的示例笔记本了解更多内容&#xff0c;开始免费云试用&#xff0c;或立即在本地机器上体验 Elastic。 在我们之前的博客文章…

Spring 面经

1.说说什么是IOC&#xff1f; IOC作为Spring的核心技术模块&#xff0c;其主要是讲对象的实例过程交由容器进行管理&#xff0c;而无需我们开发者去处理对应的实例。通过反射创建对象&#xff0c;由容器管理对象生命周期和依赖关系。当然在里面不同Bean之间存在着DI&#xff0…

【开源推荐】HuLa:跨平台聊天应用的新星

在数字化通讯的浪潮中&#xff0c;一款名为HuLa的开源聊天应用正悄然崛起&#xff0c;以其现代化的技术栈和优雅的用户体验&#xff0c;为我们带来了全新的桌面通讯解决方案。 &#x1f680; 什么是HuLa&#xff1f; HuLa是一款基于Tauri Vue 3 TypeScript构建的跨平台聊天应…

2025端午档新片票房破5000万 《碟中谍8》领跑预售榜

截至5月30日13时08分,2025年端午档新片预售总票房(含点映)突破了5000万。其中,《碟中谍8:最终清算》在预售票房榜上表现突出,领跑其他新片。责任编辑:zhangxiaohua

3000亿之后的“Labubu困境”! 泡泡玛特LABUBU“一娃难求”

泡泡玛特的股价还在继续涨,它的同行们却正在经历“冰火两重天”。截至2025年5月29日收盘,泡泡玛特报225港元/股,上涨4.31%,总市值再创新高,已经成功迈入“3000亿港元俱乐部”。同日,名创优品虽然也有2.49%的涨幅,市值仅为432.81亿港元,与泡泡玛特相去甚远。泡泡玛特、名…

重庆直辖以来最大国资改革交卷 成效显著止损瘦身

历时约一年半,重庆直辖以来最大规模的国资改革取得了显著成果。5月27日,重庆市国资委召开新闻发布会,介绍了重庆国资国企改革的情况。重庆市国资委党委书记、主任曾菁华表示,这是重庆直辖以来最大最艰苦的国企改革攻坚战。数据显示,2024年重庆市属重点国企利润总额突破377…

VS Code 插件 Git History Diff

插件名 进命令行&#xff0c;进Git自己那个分支 查看分支 提交到Git的后想再把另一个也提交到那个分支&#xff0c;用这个命令

挖洞日记 | Js中的奇妙旅行

目录&#xff1a; 前言&#xff1a; 正片开始&#xff1a; js 中的奇妙发现&#xff1a; 验证 jwt 是否有效&#xff1a; 继续深入&#xff1a; 拿下&#xff1a; 前言&#xff1a; 本文涉及的相关漏洞均已修复、本文中技术和方法仅用于教育目的&#xff1b;文中讨论的所有…

前缀和实现题目:二维区域和检索 - 矩阵不可变

文章目录 题目标题和出处难度题目描述要求示例数据范围 前言解法思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二维区域和检索 - 矩阵不可变 出处&#xff1a;304. 二维区域和检索 - 矩阵不可变 难度 5 级 题目描述 要求 给定一个二维矩阵 matrix \text…