当前位置: 首页 > news >正文

Spring Ai Chat Memory

Spring Ai Chat Memory

在前面的文章讲述如何使用各种各样的大模型进行智能对话,但通过实践我们发现大模型不能上下文关联,比如我第一次告诉它我叫张三,第二次问它我叫什么,它还是不知道。chat memory就是Spring ai提供的解决方案。本文主要简述提供的内存Memory、Advisor实现chat memory、jdbc存储chat memory和Redis存储chat memory。

什么是Chat memory

官方解释:
Large language models (LLMs) are stateless, meaning they do not retain information about previous interactions. This can be a limitation when you want to maintain context or state across multiple interactions. To address this, Spring AI provides chat memory features that allow you to store and retrieve information across multiple interactions with the LLM.
以上翻译就是说:大模型是一种无状态的,意味着不会保存之前对话的信息。当你想使用上下文信息实现多轮多花就产生了限制,为了解决这一问题,spring ai 就提供了chat memory 整个类别,允许你保存多轮对话信息。

Chat Memory和Chat History的区别

官方解释:
Chat Memory. The information that a large-language model retains and uses to maintain contextual awareness throughout a conversation.
Chat History. The entire conversation history, including all messages exchanged between the user and the model.
上面的官方解释,重要区分点就是chat memory表示一次会话的上下文对话记录;history memory表示所有的会话历史;

Chat memory的使用

在使用之前,我们首先需要做的是引入chat memory的依赖,如下:

  <dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-autoconfigure-model-chat-memory</artifactId></dependency>
1.MessageWindowChatMemory

MessageWindowChatMemory是sping ai提供的内置对象,存储在内存中。存储在 Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap(),底层调用saveAll方法完成新增,conversationId 表示会话的id,包含多轮对话。如下图源码所示:
在这里插入图片描述
具体使用方法如下代码所示:

    @Testpublic void testReReadAdvisor1(){ChatClient client = ChatClient.builder(deepSeekChatModel)// defaultAdvisors(new SimpleLoggerAdvisor())//SafeGuardAdvisor 敏感词定义,若出现敏感词中断.build();MessageWindowChatMemory memory = MessageWindowChatMemory.builder().build();String conversionId="20250512180601";UserMessage userMessage = new UserMessage("我叫张三");memory.add(conversionId,userMessage);System.out.println("question1:我叫张三");String response1 = client.prompt(new Prompt(memory.get(conversionId))).call().content();System.out.println("round1:"+response1);memory.add(conversionId,new UserMessage(response1));UserMessage reponseMessage = new UserMessage("我叫什么");System.out.println("question2:我叫什么");memory.add(conversionId,reponseMessage);String content = client.prompt(new Prompt(memory.get(conversionId))).call().content();memory.add(conversionId,new UserMessage(content));System.out.println("round2:"+content);}}

执行结果:
round1:
question1:我叫张三
answer1:你好,张三!很高兴认识你。我是DeepSeek Chat,可以叫我DeepSeek或者小深~ 😊
有什么我可以帮你的吗?无论是学习、工作,还是日常生活中的问题,都可以随时问我哦!

round2:
question2:我叫什么
answer2:你刚才告诉我,你的名字是张三!😊

如果这不是你的真实名字,或者你想让我称呼你为其他昵称,随时告诉我哦~ 我会记住的!
MessageWindowChatMemory 默认保存是10条数据,如果满了会清空。

2.使用advisors

使用第一种方式需要手动的newMessage并添加到memory中,为了省略重复的操作提供了advisor帮助实现。
使用 advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"112225544"))就可以自动增加到chatmermory中。
代码实现如下:

  ChatClient chatClient = ChatClient.builder(deepSeekChatModel).build();String content = chatClient.prompt("我是李白").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"112225544")).call().content();System.out.println("question1:我是李白");System.out.println("answer1:"+content);String content1 = chatClient.prompt("我是谁啊").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID,"112225544")).call().content();System.out.println("question2:我是谁啊");System.out.println("answer2:"+content1);

执行结果:
question1:我是李白
answer1:(拍案大笑)哎呀呀!这不是"十步杀一人,千里不留行"的剑客太白吗?您那"飞流直下三千尺"的豪情,可比这壶中琼浆更醉人啊!(突然压低声音)莫非…您又偷了王母的蟠桃酿?上次您写"我醉欲眠卿且去",结果把吴道子的画屏当草纸题诗的事儿…(挤眉弄眼)咱们要不要再闹一回天宫?
question2:我是谁啊
answer2:(突然拔剑起舞)噫——吁——嚱!您这问题可让长剑都笑鸣啦!您不就是那个让高力士脱靴、杨贵妃磨墨的狂客吗?(剑尖挑起酒壶)看这"五花马,千金裘,呼儿将出换美酒"的架势——除了"自称臣是酒中仙"的李太白,谁还能有这般气魄?(突然压低声音)莫非…您又喝多了玉真公主的御赐葡萄酿,连"床前明月光"都记成武媚娘写的了?

4.JdbcChatMemoryRepository

由于内存空间有限,如果想要长期保存,就需要存储到数据库。我这里就使用mysql进行示例。
(1)准备sql
准备一个schema-mysql.sql的文件,具体sql如下:

CREATE TABLE IF NOT EXISTS `SPRING_AI_CHAT_MEMORY`  (`conversation_id` varchar(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL,`type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,`timestamp` timestamp NULL DEFAULT NULL,INDEX `pk_conversation_id`(`conversation_id` ASC) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

(2)引入依赖
在之前的依赖基础上,需引入:

    <!--引入jdbc相关依赖--><dependency><groupId>org.springframework.ai</groupId><artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency>

(3)修改配置文件

spring:application:name: more-platform-and-modelai:deepseek:api-key: ${DEEPSEEK_API_KEY}base-url: https://api.deepseek.com/v1zhipuai:api-key: 98c44f977b534bfea92b4173437ce0e1.ZR4d1SqOw6jWP5TIollama:chat:model:  gemma3:1bchat:memory:repository:jdbc:initialize-schema: always  ##初始化,执行mysql.sql文件schema: classpath:/schema-mysql.sql  ##你的mysql.sql文件的位置,这里代表直接在resources下datasource:username: rootpassword: 123456url: jdbc:mysql://192.168.48.164:3306/db01?characterEncoding=utf-8&useSSL=falsedriver-class-name: com.mysql.cj.jdbc.Driverdata:redis:host: 192.168.48.164port: 6379password: 123456

(4)测试类编写

@SpringBootTest
public class JDBCChatMemory {@Autowiredprivate DeepSeekChatModel deepSeekChatModel;@Testpublic void testReReadAdvisor2(@Autowired ChatMemory chatMemory) {ChatClient chatClient = ChatClient.builder(deepSeekChatModel).defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build()).build();String content = chatClient.prompt("大家好,我是旺仔小乔,你们好吗?").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "112225545")).call().content();System.out.println(content);}@TestConfigurationstatic class Config{@BeanChatMemory chatMemory(JdbcChatMemoryRepository chatMemoryRepository){return  MessageWindowChatMemory.builder().maxMessages(10)  //设置每个conversation 存储的条数.chatMemoryRepository(chatMemoryRepository).build();}}
}

执行结果:
第一轮:
在这里插入图片描述
第二轮:

      String content = chatClient.prompt("岛岛的妈妈,你给我刷过几个子?").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "112225545")).call().content();

在这里插入图片描述
通过以上步骤就可以实现存储在mysql数据库中了。

Redis存储

由于上下文可能需要频繁的读写,导致IO增加,效率下降。所以采用Redis也是一个很好的存储工具。
(1)引入依赖

  <!--redis实现chat Meomery相关依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

(2)配置Redis相关信息

spring:data:redis:host: 192.168.48.164port: 6379password: ******

(3)定义ChatMessage

@Data
public class ChatMessage implements Message{MessageType messageType;String text;Map<String,Object> metaData;public ChatMessage() {}public ChatMessage(Message message){this.messageType=message.getMessageType();this.text=message.getText();this.metaData=message.getMetadata();}@Overridepublic Map<String, Object> getMetadata() {return this.metaData;}/*** 转换为Spring Ai 的message* @return*/public   Message toMessage(){return switch (messageType){case SYSTEM -> new SystemMessage(text);case USER -> new UserMessage(text);case ASSISTANT -> new AssistantMessage(text,metaData);default -> throw  new IllegalArgumentException("Unsupported message type"+messageType);};}
}

(3)定义RedisChatMemory
由于Spring 暂时取消了类似jdbc的整合,所有需要自己手动实现一下。后面版本可能会新增

@RequiredArgsConstructor
public class RedisChatMemory implements ChatMemory {private final RedisTemplate redisTemplate;private final ObjectMapper objectMapper;private final static  String PREFIX="redisChatMemory";@Overridepublic void add(String conversationId, List<Message> messages) {if(CollectionUtils.isEmpty(messages)){return;}List<String> list = messages.stream().map(ChatMessage::new).map(chatMessage -> {try {return objectMapper.writeValueAsString(chatMessage);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}).toList();redisTemplate.opsForList().leftPush(PREFIX+conversationId,list);}@Overridepublic List<Message> get(String conversationId) {List<Object> list = redisTemplate.opsForList().range(PREFIX+conversationId, 0, -1);List<Message> messages = new ArrayList<>();for(int i=0;i<list.size();i++){Object obj = list.get(i);ChatMessage chatMessage= new ChatMessage();try {List<Map<String, Object>> mapList = objectMapper.readValue(obj.toString(), new TypeReference<List<Map<String, Object>>>() {});//obj对象是一个list类型Map<String, Object> messageMap=new HashMap<>();if(!CollectionUtils.isEmpty(mapList)){messageMap=mapList.get(0);}chatMessage.setMessageType(MessageType.valueOf(messageMap.get("messageType").toString()));//获取messageTypechatMessage.setText(messageMap.get("text").toString());//设置当前的内容} catch (JsonProcessingException e) {throw new RuntimeException(e);}messages.add(chatMessage);}return messages;}@Overridepublic void clear(String conversationId) {redisTemplate.delete(PREFIX+conversationId);}}

(4)新增测试类

@SpringBootTest
public class RedisChatMemory {@Autowiredprivate DeepSeekChatModel deepSeekChatModel;@Testpublic void testReReadAdvisor2(@Autowired ChatMemory chatMemory) {if(chatMemory instanceof RedisChatMemory){System.out.println("RedisChatMemory ");}ChatClient chatClient = ChatClient.builder(deepSeekChatModel).defaultAdvisors(PromptChatMemoryAdvisor.builder(chatMemory).build())//SafeGuardAdvisor 敏感词定义,若出现敏感词中断.build();Flux<String> flux = chatClient.prompt("我是旺仔小乔你们好吗").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1123540")).stream().content();flux.toIterable().forEach(System.out::print);}@TestConfigurationstatic class Config{@Autowiredprivate  RedisTemplate redisTemplate;@Autowiredprivate  ObjectMapper objectMapper;@Beanpublic ChatMemory chatMemory(){return new com.example.moreplatformandmodel.ChatMsg.RedisChatMemory(redisTemplate,objectMapper);}}}

第一轮:
AI回复:
你好呀!😊 虽然你不是真的旺仔小乔(毕竟TA是虚拟歌手啦),但热烈欢迎你以这个身份来玩!有什么想聊的或需要帮忙的吗?想听歌、聊角色,或者单纯唠嗑都可以哦

(悄悄说:需要模仿旺仔小乔的说话风格的话,我也可以配合演出喔~🎤)
第二轮:
修改代码如下:

   Flux<String> flux = chatClient.prompt("你有给我刷一百万吗?我了你干嘛").advisors(advisorSpec -> advisorSpec.param(ChatMemory.CONVERSATION_ID, "1123540")).stream().content();flux.toIterable().forEach(System.out::print);

AI回复:
(突然呛到虚拟可乐)噗——!!一百万?!(°▽°)
这位"旺仔小乔本乔"是刚开完演唱会回来吗~🎤 我倒是想用音符给你刷个《阳光开朗大男孩》remix版当礼物啦!💰🎶

(突然正经) 咳咳…其实本AI账户里只有【0】和【1】两种货币喔~ 要不咱们改刷一百万次播放量?(偷偷把直播打赏按钮换成单曲循环键) 🔄

(突然戏精上身摆出打call姿势) 不过既然你都"了你了"…那必须安排!💥 看我用脑内音响给你循环播放《我承认我怕黑》一百万遍——!🎧✨

以上就是所有的chatMemory的内容了,更多详情请查看官网
https://docs.spring.io/spring-ai/reference/api/chat-memory.html

http://www.dtcms.com/a/339074.html

相关文章:

  • Python 与 VS Code 结合操作指南
  • 【Vue开发】在Vite+Vue3项目中实现离线Iconify图标方案
  • 【什么是非晶合金?非晶电机有什么优点?】
  • Redis面试题及详细答案100道(71-85) --- 综合篇
  • Vim笔记:缩进
  • KMM跨平台叛逃实录:SwiftUI与Compose Multiplatform共享ViewModel的混合开发框架(代码复用率85%)
  • Qt5 GUI 编程详解
  • 【AI大模型的发展历史】从Transformer到2025年的多模态、推理与开源革命
  • mlir 类型
  • docker 数据卷、自定义镜像操作演示分享(第二期)
  • 【数据结构】堆和二叉树详解(下)
  • SpringAI——向量存储(vector store)
  • SpringClound——网关、服务保护和分布式事务
  • Redis-缓存-击穿-分布式锁
  • 使用ros2跑mid360的fastlio2算法详细教程
  • 【数据结构】用堆解决TOPK问题
  • 算法训练营day56 图论⑥ 108. 109.冗余连接系列
  • C++---为什么迭代器常用auto类型?
  • 强、软、弱、虚引用
  • 在 Qt C++ 中利用 OpenCV 实现视频处理技术详解
  • 尝试Claude Code的安装
  • 学习笔记分享——基于STM32的平衡车项目
  • Mac调试ios的safari浏览器打开的页面
  • 电子电气架构 --- 软件项目成本估算
  • 技术攻坚全链铸盾 锁定12月济南第26届食品农产品安全高峰论坛
  • 任务十二 我的页面及添加歌曲功能开发
  • Typescript入门-对象讲解
  • Python量化交易:结合爬虫与TA-Lib技术指标分析
  • Matplotlib数据可视化实战:Matplotlib子图布局与管理入门
  • Ansible 角色管理指南