资讯详情

资讯详情

Spring AI (第二章)大模型对话上下文记忆

Spring AI (第二章)大模型对话上下文记忆文章目录Spring AI (第二章)大模型对话上下文记忆一、章节目的1.1 需求分析1.2 方案一内存存储In-memory1.3 方案二文件存储Kryo 序列化1.4 方案三MongoDB或类似文档数据库1.5 选型建议二、代码2.1 上下文聊天记忆的大模型对象2.2 基于本地文件存储的方式实现代码(可选)2.3 测试代码一、章节目的1.1 需求分析基于第一章实现的内容当前对话仅按单轮请求-响应执行模型不会利用历史消息导致上下文丢失、回答前后不一致、重复确认较多。目标为每个会话维护“对话历史”在下一轮请求时一并提交从而实现连贯、多轮的上下文记忆。1.2 方案一内存存储In-memory官方提供了基于计算机内存存储上下文记忆的依赖、会把用户和大模型的上下文存储在内存当中、可以解决上下文记忆的问题优点实现最简单、延迟最低、无外部依赖开发/调试成本低缺点进程退出或重启即丢失、无法跨实例共享数据量大时占用内适用场景PoC、单机小流量、对持久化不敏感的场景1.3 方案二文件存储Kryo 序列化通过Kryo序列化对象来实现数据的文件写入、文件读取、根据用户聊天会话ID去写入和读取文件、可以解决上下文记忆的问题优点重启不丢失部署简单不依赖外部服务。缺点小文件众多碎片化、目录遍历和清理成本高、并发写入需加锁跨机无法共享备份/检索能力有限。适用场景单机生产/测试环境需要一定持久化但不想引入数据库。1.4 方案三MongoDB或类似文档数据库将会话与消息存为文档按会话ID/时间索引查询与追加。优点持久化可靠、天然支持多实例共享与水平扩展、易于做查询、统计与归档备份/副本集可靠。缺点需运维数据库推荐使用 Docker引入网络延迟与成本。适用场景生产环境、多副本部署、需要可观测性与扩展性。1.5 选型建议三种方案选择一种即可快速落地/教学演示选内存存储配上限与TTL最快见效。轻量持久化单机选文件存储注意分桶与锁。正式生产/多实例/高并发选 MongoDB或 Redis Streams/Postgres JSONB推荐开启 TTL、归档与监控。二、代码2.1 上下文聊天记忆的大模型对象importlombok.extern.slf4j.Slf4j;importorg.exi.exiaiagent.chatmemory.FileBasedChatMemory;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.InMemoryChatMemory;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.stereotype.Component;importstaticorg.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_CONVERSATION_ID_KEY;importstaticorg.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.CHAT_MEMORY_RETRIEVE_SIZE_KEY;/** * 初始化可以存储上下文聊天记忆的大模型对象 */ComponentSlf4jpublicclassChatMemoryApp{privatefinalChatClientchatClient;/** * 构造函数 初始化ChatClient * param dashscopeChatModel */publicChatMemoryApp(ChatModeldashscopeChatModel){/** * 1 内存存储 * 2 计算机资源文件存储 * 3 MongoDB数据库存储 */introute1;ChatMemorychatMemory;if(route1){chatMemoryUseMemoryChatMemory();}elseif(route2){chatMemoryUseFileChatMemory();}else{chatMemoryUseMemoryChatMemory();// 默认为基于内存存储}chatClientChatClient.builder(dashscopeChatModel).defaultAdvisors(newMessageChatMemoryAdvisor(chatMemory)// 选择消息格式分层存储的拦截器).build();}/** * 基于本地文件存储的方式 实现读取、写入聊天上下文 * return */publicChatMemoryUseFileChatMemory(){// 项目根目录、定义对话存储文件路径StringfileDirSystem.getProperty(user.dir)/tmp/chat-memory;// 创建自定义文件存储的对象ChatMemorychatMemorynewFileBasedChatMemory(fileDir);returnchatMemory;}/** * 基于计算机内存存储、实现读取、写入聊天上下文 * return */publicChatMemoryUseMemoryChatMemory(){/** * 默认使用MessageWindowChatMemory * 存储位置: JVM 内存 (InMemoryChatMemoryRepository) * 数据持久化: 不持久化应用重启后丢失 * 消息窗口: maxMessages 20默认可配置 */InMemoryChatMemorychatMemorynewInMemoryChatMemory();returnchatMemory;}/** * 基于MongoDB数据库存储、实现读取、写入聊天上下文 */publicvoidUseMongoDBChatMemory(){}/** * 根据用户消息和用户的聊天ID进行回复 * param message * param chatId * return */publicStringdoChat(Stringmessage,StringchatId){ChatResponsechatResponsechatClient.prompt().user(message)// 用户消息.advisors(advisorSpec-advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY,chatId)// 指定聊天记忆存储ID.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY,10)// 获取最近10条聊天记忆).call().chatResponse();StringcontentchatResponse.getResult().getOutput().getText();// 获取chatResponse的结果数据log.info(【对话请求】用户消息{},message);log.info(【模型响应】内容{},content);returncontent;}}2.2 基于本地文件存储的方式实现代码(可选)Kryo序列化依赖!-- Kryo高性能序列化框架依赖、作用提供快速、高效的Java对象序列化和反序列化功能 --!-- 在自定义对话记忆存储对象序列化中使用到了 --dependencygroupIdcom.esotericsoftware/groupIdartifactIdkryo/artifactIdversion5.6.2/version/dependency实现代码importcom.esotericsoftware.kryo.Kryo;importcom.esotericsoftware.kryo.io.Input;importcom.esotericsoftware.kryo.io.Output;importorg.objenesis.strategy.StdInstantiatorStrategy;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.messages.Message;importjava.io.File;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.io.IOException;importjava.util.ArrayList;importjava.util.List;/** * 参考内存数据存储InMemoryChatMemory自定义存储实现ChatMemory */publicclassFileBasedChatMemoryimplementsChatMemory{privateStringBASE_DIR;// 会话文件存储路经/** * 创建一个Kryo序列化对象 * * 问题Message对象有多种子类型(UserMessage/SystemMessage/AssistantMessage) * 字段结构不同JSON序列化难以统一处理 * * 解决使用Kryo二进制序列化自动处理不同Message子类自动处理复杂对象结构的序列化 * 保持完整类型信息提供高性能序列化 * * 总结可以通过Kryo解决不同子类字段不一致的存储和读取序列化问题 * * 通俗解释 * - 就像有3种不同的盒子UserMessage、SystemMessage、AssistantMessage * - 每个盒子里装的东西都不一样字段不同 * - JSON就像用文字描述盒子内容很难统一描述 * - Kryo就像直接拍照保存盒子不管里面装什么都能完整保存 * - 读取时也能完整还原出原来的盒子 */privatestaticfinalKryokryonewKryo();static{// 不要求注册类允许序列化任意类kryo.setRegistrationRequired(false);// 设置实例化策略kryo.setInstantiatorStrategy(newStdInstantiatorStrategy());}// 构造对象时、初始化存储目录、没有则创建publicFileBasedChatMemory(Stringdir){this.BASE_DIRdir;FilebaseDirnewFile(dir);if(!baseDir.exists()){baseDir.mkdirs();}}/** * 根据会话ID 添加会话列表 * param conversationId * param messages */Overridepublicvoidadd(StringconversationId,ListMessagemessages){// 根据会话ID、获取之前的消息列表ListMessageconversationMessagesgetOrCreateConversation(conversationId);// 添加新的消息列表conversationMessages.addAll(messages);// 根据会话ID、写入最新的消息列表saveConversation(conversationId,conversationMessages);}/** * 根据会话ID获取会话列表数据 * param conversationId * param lastN * return */OverridepublicListMessageget(StringconversationId,intlastN){ListMessagemessageListgetOrCreateConversation(conversationId);// 根据lastN获取最近存储的消息列表returnmessageList.stream().skip(Math.max(0,messageList.size()-lastN)).toList();}/** * 根据会话ID、删除会话 * param conversationId */Overridepublicvoidclear(StringconversationId){FilefilegetConversationFile(conversationId);// 如果文件对象存储则删除if(file.exists()){file.delete();}}/** * 根据会话ID创建或获取会话列表 * param conversationId * return */privateListMessagegetOrCreateConversation(StringconversationId){// 获取本地存储的kryo序列化文件对象FilefilegetConversationFile(conversationId);ListMessagemessagesnewArrayList();if(file.exists()){try(InputinputnewInput(newFileInputStream(file))){// 通过kryo读取文件输入流并转为ArrayList.class格式messageskryo.readObject(input,ArrayList.class);}catch(IOExceptione){e.printStackTrace();}}returnmessages;}/** * 把会话消息列表通过kryo序列化存储至文件当中 * param conversationId * param messages */privatevoidsaveConversation(StringconversationId,ListMessagemessages){FilefilegetConversationFile(conversationId);try(OutputoutputnewOutput(newFileOutputStream(file))){kryo.writeObject(output,messages);}catch(IOExceptione){e.printStackTrace();}}/** * 根据会话ID获取本地存储的文件对象 * param conversationId * return */privateFilegetConversationFile(StringconversationId){returnnewFile(BASE_DIR,conversationId.kryo);}}2.3 测试代码测试代码importjakarta.annotation.Resource;importorg.junit.jupiter.api.Test;importorg.springframework.boot.test.context.SpringBootTest;importstaticorg.junit.jupiter.api.Assertions.*;SpringBootTestclassChatMemoryAppTest{/** * Resource 注解的作用是先根据字段名后根据类型 来自动匹配注入对应的 Bean */ResourceprivateChatMemoryAppchatMemoryApp;TestvoiddoChat(){StringchatIda5daa1bb-e5e2-427e-9a74-68650459511f;Stringmessage我有两个朋友张三和李四张三喜欢性感火辣的、李四喜欢斯文儒雅的;StringanswerchatMemoryApp.doChat(message,chatId);message我有两个朋友你知道他们分别是谁不、各自喜欢的女生类型;answerchatMemoryApp.doChat(message,chatId);}}测试结果
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →