LangChain4J-(5)-记忆缓存与持久化
一、大模型聊天系统中的记忆缓存
在大模型对话系统中,记忆缓存(Memory Caching) 是专门针对 “多轮对话场景” 设计的核心优化技术 —— 它通过选择性存储、更新和复用 “历史对话信息”(如用户需求、上下文细节、交互结论),解决大模型原生的 “上下文窗口有限”“多轮交互易遗忘” 问题,最终让对话系统具备连贯、一致、个性化的交互能力,避免 “重复询问用户信息”“前后回答矛盾” 等体验问题。
简单来说,对话系统的记忆缓存就像 “AI 的对话笔记本”:它会实时记录对话过程中的关键信息,当后续交互需要时,直接从 “笔记本” 中调取,无需让大模型重新 “回忆” 或重复计算,既提升响应效率,又保障对话逻辑的连续性。
核心价值解决对话系统的 3 个核心痛点大模型对话系统(如客服 AI、智能助手)若没有记忆缓存,会面临典型问题:
上下文遗忘:若对话轮次超过大模型的上下文窗口(如 GPT-4 标准版 8k Token),模型会 “忘记” 早期对话信息(比如用户前 3 轮说过 “自己是学生”,第 5 轮时模型又询问 “您的身份是?”);
重复计算浪费:多轮对话中若反复提及相同信息(如用户反复确认 “订单号 12345”),无缓存时模型需每次重新处理该信息,消耗额外算力;
个性化缺失:无法长期记住用户的固定偏好(如 “用户喜欢极简风格的回答”“对海鲜过敏”),导致每轮对话都像 “重新认识用户”。
而记忆缓存的核心价值,就是针对性解决以上问题:用 “局部信息存储” 补全大模型的 “长期记忆能力”,让对话更连贯、高效、贴合用户需求。
在实际开发中,无需从零搭建记忆缓存 —— 主流对话框架已封装好成熟的记忆缓存模块,开发者可直接调用,核心代表如下:
框架 / 工具 | 核心记忆缓存组件 | 特点与适用场景 |
---|---|---|
LangChain/LangChain4j |
| 灵活可配置,支持自定义缓存逻辑,适合开发者搭建个性化对话系统(如企业客服、垂直领域助手) |
LangSmith |
| 自带缓存监控与调试功能,方便跟踪 “缓存是否生效”,适合需要 debug 的场景 |
大模型原生 API | OpenAI 的 | 内置轻量记忆缓存逻辑,无需开发者额外配置,适合快速搭建简单对话系统(如个人助手) |
二、Memory VS History
大模型对话系统里 “记忆” 和 “历史” 两个概念。
2.1、核心概念区分
历史(History):是「对话事实的完整记录」,严格保留用户和 AI 交互的每一条消息,相当于对话的 “原始档案”,和 UI 展示的实际内容一致,是客观发生过的对话全貌。
记忆(Memory):是「为让大模型 “理解对话上下文” 而加工后的信息」,不会严格保留所有原始消息 。它会通过算法对历史做筛选、修改(比如删冗余内容、总结多条消息、补充额外信息),最终给大模型提供 “好像记得对话” 的素材,目的是让模型用更高效的方式理解对话逻辑,本质是「服务大模型推理的工具」。
2.2、记忆的 “加工手段”(理解记忆的关键)
记忆不是简单存历史,而是会主动改造,常见做法:
删减:删掉重复、无意义的消息(比如用户重复问相同问题的冗余表述 )。
总结:把多条零散对话浓缩成摘要(比如 5 轮关于 “旅游规划” 的对话,总结成 “用户想 7 月去云南,偏好自然风景,预算 5000” )。
补充信息:结合外部知识(RAG 场景)或规则,给对话注入额外内容(比如用户提 “订酒店”,记忆里补充 “本地酒店旺季价格规则” )。
2.3、LangChain4j 的现状
LangChain4j 只提供 “记忆” 能力 ,不会自动存「完整对话历史」。如果你的场景需要保留每一条原始消息(比如合规要求、复盘需求),得自己额外手动实现存储逻辑。
简单说就是:
「历史」是 “对话录像”,客观完整;
「记忆」是 “给大模型看的对话解说”,经过剪辑加工;
LangChain4j 目前只做 “解说”,想要 “录像” 得自己存 。
三、Eviction polocy
LangChain4j 里的 “对话消息清除策略”,核心是解决大模型对话时的 “上下文窗口限制、成本、延迟” 问题。
3.1、“清除策略必要” 的 3 大原因
对话不能无限制存下去,必须删,因为:
适配大模型上下文窗口:大模型能处理的 Token(对话基本单位)有限(比如 GPT-4 早期是 8k Token),对话长了会超限制,必须删旧消息(通常删最老的,也能自定义复杂逻辑)。
控制调用成本:大模型按 Token 收费 / 消耗算力,删冗余消息能少花钱、少占资源。
控制响应延迟:传给大模型的 Token 越多,模型处理越慢,删消息能让回复更快。
3.2、LangChain4j 提供的 2 种 “清除策略实现”
为了让你不用自己写删消息逻辑,LangChain4j 直接给了两个工具,按需求选:
1. 简单版:MessageWindowChatMemory
逻辑:把对话当 “滑动窗口”,只保留最近 N 条完整消息,超了就删最老的。
缺点:没考虑 “每条消息的 Token 数量”(比如一条长消息可能占几百 Token,短消息占几个),所以精准度弱,适合快速做 Demo、验证想法(原型设计阶段)。
2. 复杂版:TokenWindowChatMemory
逻辑:更精准!把对话当 “Token 滑动窗口”,只保留最近 N 个 Token,超了就删最老的消息(但消息是整体,一条消息里的 Token 不够窗口会直接删整条)。
依赖:需要额外的
TokenCountEstimator
工具,帮你算每条消息的 Token 数量,适合正式项目、对成本 / 窗口控制严格的场景(比如商用客服、复杂对话)。
简单说就是:
大模型对话不能存太多消息,必须删(窗口、成本、延迟逼的);
LangChain4j 给了两种删法:简单版看 “消息条数”,复杂版看 “Token 数量”;
做小项目用
MessageWindowChatMemory
省事,正式场景用TokenWindowChatMemory
更精准。
四、撸代码
4.1、记忆缓存
step1
在之前工程的基础上换一个模型,这次我们改换qwen-long来做实验,在LLMConfig中该换模型
package com.xxx.demo.config;import com.bbchat.demo.service.ChatAssistant;
import com.bbchat.demo.service.ChatMemoryAssistant;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.memory.chat.TokenWindowChatMemory;
import dev.langchain4j.model.TokenCountEstimator;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiTokenCountEstimator;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class LLMConfig {@Beanpublic ChatModel chatModel(){return OpenAiChatModel.builder().apiKey(System.getenv("aliqwen-apikey")).modelName("qwen-long").baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1").build();}@Bean(name = "chat")public ChatAssistant chatAssistant(ChatModel chatModel){return AiServices.create(ChatAssistant.class, chatModel);}@Bean(name = "chatMessageWindowChatMemory")public ChatMemoryAssistant chatMessageWindowChatMemory(ChatModel chatModel){return AiServices.builder(ChatMemoryAssistant.class).chatModel(chatModel)// 注意每个memoryId对应创建一个ChatMemory.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(100)).build();}@Bean(name = "chatTokenWindowChatMemory")public ChatMemoryAssistant chatTokenWindowChatMemory(ChatModel chatModel){// TokenCountEstimator默认的token分词器,需要结合Tokenizer计算ChatMessage的token数量TokenCountEstimator openAiTokenizer = new OpenAiTokenCountEstimator("gpt-4");return AiServices.builder(ChatMemoryAssistant.class).chatModel(chatModel).chatMemoryProvider(memoryId -> TokenWindowChatMemory.withMaxTokens(1000,openAiTokenizer)).build();}
}
step2
新建两个接口--javascripttypescriptshellbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
package com.bbchat.demo.service;import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;/*
一个带缓存的对话接口*/
public interface ChatMemoryAssistant {/*** 聊天带记忆缓存功能** @param userId 用户 ID* @param prompt 消息* @return {@link String }*/String chatWithChatMemory(@MemoryId Long userId, @UserMessage String prompt);
}
step3
新建一个controller做测试使用
package com.xxx.demo.controller;import cn.hutool.core.date.DateUtil;
import com.bbchat.demo.service.ChatAssistant;
import com.bbchat.demo.service.ChatMemoryAssistant;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
public class ChatMemoryController {@Resource(name = "chat")private ChatAssistant chatAssistant;@Resource(name = "chatMessageWindowChatMemory")private ChatMemoryAssistant chatMessageWindowChatMemory;@Resource(name = "chatTokenWindowChatMemory")private ChatMemoryAssistant chatTokenWindowChatMemory;/*没有记忆缓存的功能接口http://localhost:9008/chatmemory/test1*/@GetMapping(value = "/chatmemory/test1")public String chat(){String answer01 = chatAssistant.chat("你好,我的名字叫张三");System.out.println("answer01返回结果:"+answer01);String answer02 = chatAssistant.chat("我的名字是什么");System.out.println("answer02返回结果:"+answer02);return "success : "+ DateUtil.now()+"<br> \n\n answer01: "+answer01+"<br> \n\n answer02: "+answer02;}/*MessageWindowChatMemory实现聊天功能*/@GetMapping(value = "/chatmemory/test2")public String chatMessageWindowChatMemory(){chatMessageWindowChatMemory.chatWithChatMemory(1L, "你好!我的名字是李四.");String answer01 = chatMessageWindowChatMemory.chatWithChatMemory(1L, "我的名字是什么");System.out.println("answer01返回结果:"+answer01);chatMessageWindowChatMemory.chatWithChatMemory(3L, "你好!我的名字是王五");String answer02 = chatMessageWindowChatMemory.chatWithChatMemory(3L, "我的名字是什么");System.out.println("answer02返回结果:"+answer02);return "chatMessageWindowChatMemory success : "+ DateUtil.now()+"<br> \n\n answer01: "+answer01+"<br> \n\n answer02: "+answer02;}/*TokenWindowChatMemory实现聊天功能*/@GetMapping(value = "/chatmemory/test3")public String chatTokenWindowChatMemory(){chatTokenWindowChatMemory.chatWithChatMemory(1L, "你好!我的名字是mysql");String answer01 = chatTokenWindowChatMemory.chatWithChatMemory(1L, "我的名字是什么");System.out.println("answer01返回结果:"+answer01);chatTokenWindowChatMemory.chatWithChatMemory(3L, "你好!我的名字是oracle");String answer02 = chatTokenWindowChatMemory.chatWithChatMemory(3L, "我的名字是什么");System.out.println("answer02返回结果:"+answer02);return "chatTokenWindowChatMemory success : "+ DateUtil.now()+"<br> \n\n answer01: "+answer01+"<br> \n\n answer02: "+answer02;}
}
step4
分别查看结果:
访问接口test1
访问接口test2
访问接口test3
(模型不止记住了我的名字,还自我发挥了一段)
4.2、持久化
将对话结果保存进Redis进行持久化记忆留存,我们继续使用上述工程
step1
在module的pom文件中添加以下依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>
step2
在application.properties中添加redis配置
# ==========config redis===============
spring.data.redis.host=localhost
spring.data.redis.port=6379
spring.data.redis.database=0
spring.data.redis.connect-timeout=3s
spring.data.redis.timeout=2s
step3
新建高阶接口 ChatPersistenceAssistant
package com.xxx.demo.service;import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;/*** packageName com.bbchat.demo.service** @author Zting* @version JDK 17* @InterfaceName ChatPersistenceAssistant* @date 2025/9/15* @description TODO*/
public interface ChatPersistenceAssistant {/*** 聊天** @param userId 用户 ID* @param message 消息* @return {@link String }*/String chat(@MemoryId Long userId, @UserMessage String message);
}
step4
编写redis配置
package com.xxx.demo.config;import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
@Slf4j
public class RedisConfig {/*** RedisTemplate配置* redis序列化的工具配置类,下面这个请一定开启配置* 127.0.0.1:6379> keys ** 1) "ord:102" 序列化过* 2) "\xac\xed\x00\x05t\x00\aord:102" 野生,没有序列化过* this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法* this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法* this.redisTemplate.opsForSet(); //提供了操作set的所有方法* this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法* this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法* @param redisConnectionFactor* @return*/@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactor){RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();redisTemplate.setConnectionFactory(redisConnectionFactor);//设置key序列化方式stringredisTemplate.setKeySerializer(new StringRedisSerializer());//设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.setHashKeySerializer(new StringRedisSerializer());redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());redisTemplate.afterPropertiesSet();return redisTemplate;}
}
step5
自定义RedisChatMemoryStore类实现ChatMemoryStore
package com.xxx.demo.config;import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.List;@Component
public class RedisChatMemoryStore implements ChatMemoryStore {public static final String CHAT_MEMORY_PREFIX = "CHAT_MEMORY:";@Resourceprivate RedisTemplate<String,String> redisTemplate;@Overridepublic List<ChatMessage> getMessages(Object memoryId){String retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);return ChatMessageDeserializer.messagesFromJson(retValue);}@Overridepublic void updateMessages(Object memoryId, List<ChatMessage> messages){redisTemplate.opsForValue().set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages));}@Overridepublic void deleteMessages(Object memoryId){redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId);}
}
step6
拓展LLMConfig类
package com.xxxx.demo.config;import com.bbchat.demo.service.ChatAssistant;
import com.bbchat.demo.service.ChatMemoryAssistant;
import com.bbchat.demo.service.ChatPersistenceAssistant;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.memory.chat.TokenWindowChatMemory;
import dev.langchain4j.model.TokenCountEstimator;
import dev.langchain4j.model.chat.ChatModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.model.openai.OpenAiTokenCountEstimator;
import dev.langchain4j.service.AiServices;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
public class LLMConfig {@Resourceprivate RedisChatMemoryStore redisChatMemoryStore;@Beanpublic ChatModel chatModel(){return OpenAiChatModel.builder().apiKey(System.getenv("aliqwen-apikey")).modelName("qwen-long").baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1").build();}@Beanpublic ChatPersistenceAssistant chatMemoryAssistant(ChatModel chatModel){ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder().id(memoryId).maxMessages(1000).chatMemoryStore(redisChatMemoryStore).build();return AiServices.builder(ChatPersistenceAssistant.class).chatModel(chatModel).chatMemoryProvider(chatMemoryProvider).build();}@Bean(name = "chat")public ChatAssistant chatAssistant(ChatModel chatModel){return AiServices.create(ChatAssistant.class, chatModel);}@Bean(name = "chatMessageWindowChatMemory")public ChatMemoryAssistant chatMessageWindowChatMemory(ChatModel chatModel){return AiServices.builder(ChatMemoryAssistant.class).chatModel(chatModel)// 注意每个memoryId对应创建一个ChatMemory.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(100)).build();}@Bean(name = "chatTokenWindowChatMemory")public ChatMemoryAssistant chatTokenWindowChatMemory(ChatModel chatModel){// TokenCountEstimator默认的token分词器,需要结合Tokenizer计算ChatMessage的token数量TokenCountEstimator openAiTokenizer = new OpenAiTokenCountEstimator("gpt-4");return AiServices.builder(ChatMemoryAssistant.class).chatModel(chatModel).chatMemoryProvider(memoryId -> TokenWindowChatMemory.withMaxTokens(1000,openAiTokenizer)).build();}
}
step7
编写一个用来测试的controller
package com.xxx.demo.controller;import cn.hutool.core.date.DateUtil;
import com.bbchat.demo.service.ChatPersistenceAssistant;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@Slf4j
public class ChatPersistenceController {@Resourceprivate ChatPersistenceAssistant chatPersistenceAssistant;// http://localhost:9010/chatpersistence/redis@GetMapping(value = "/chatpersistence/redis")public String testChatPersistence(){chatPersistenceAssistant.chat(1L, "你好!我的名字是redis");chatPersistenceAssistant.chat(2L, "你好!我的名字是nacos");String chat = chatPersistenceAssistant.chat(1L, "我的名字是什么");System.out.println(chat);chat = chatPersistenceAssistant.chat(2L, "我的名字是什么");System.out.println(chat);return "testChatPersistence success : "+ DateUtil.now();}
}
step8
查看redis中存储的结果