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 {
@Bean
public 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
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactor)
{
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactor);
//设置key序列化方式string
redisTemplate.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:";
@Resource
private RedisTemplate<String,String> redisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId)
{
String retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);
return ChatMessageDeserializer.messagesFromJson(retValue);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages)
{
redisTemplate.opsForValue().set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages));
}
@Override
public 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 {
@Resource
private RedisChatMemoryStore redisChatMemoryStore;
@Bean
public ChatModel chatModel()
{
return OpenAiChatModel.builder()
.apiKey(System.getenv("aliqwen-apikey"))
.modelName("qwen-long")
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.build();
}
@Bean
public 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 {
@Resource
private 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中存储的结果
更多推荐
所有评论(0)