LangChain4j 实战:ChatMemory 聊天记忆完全指南
LangChain4j 实战:ChatMemory 聊天记忆完全指南
💡 导语:手动管理
ChatMessage太麻烦了?LangChain4j 提供了开箱即用的ChatMemory抽象,让你轻松实现对话记忆功能。本文将从基础概念到持久化实战,带你完整掌握聊天记忆的核心用法。
📖 什么是 ChatMemory?
手动维护和管理 ChatMessage 是很麻烦的。因此,LangChain4j 提供了 ChatMemory 抽象以及多种开箱即用的实现。
ChatMemory 可以作为独立的低级组件使用,也可以作为高级组件(如 AI 服务)的一部分。
作为 ChatMessage 的容器(由 List 支持),ChatMemory 提供了以下核心能力:
- 🔄 淘汰策略 — 自动淘汰旧消息,控制上下文长度
- 💾 持久化 — 将对话数据存储到磁盘或数据库
- ⚙️ 对 SystemMessage 的特殊处理 — 系统消息始终保留
- 🔧 对工具消息的特殊处理 — 自动清理孤立的工具调用结果
🔍 记忆 vs 历史:两个容易混淆的概念
⚠️ 注意:"记忆"和"历史"是相似但不同的概念。
| 概念 | 说明 | 特点 |
|---|---|---|
| 📜 历史(History) | 保持用户和 AI 之间的所有消息完整无缺 | 用户在 UI 中看到的内容,代表实际对话 |
| 🧠 记忆(Memory) | 保存部分信息呈现给 LLM,使其表现得像"记住"了对话 | 可以淘汰消息、总结、注入额外信息等 |
LangChain4j 目前只提供"记忆",而不是"历史"。 如果你需要保存完整的历史记录,请手动进行。
🗑️ 淘汰策略:为什么需要它?
淘汰策略是必要的,原因有三个:
- 📏** 适应上下文窗口** — LLM 一次能处理的 Token 数量有上限,对话过长时必须淘汰旧消息
- 💰** 控制成本** — 每个 Token 都有成本,淘汰不必要的消息可以降低费用
- ⚡** 控制延迟** — 发送给 LLM 的 Token 越多,处理时间越长
两种开箱即用的实现
1️⃣ MessageWindowChatMemory(消息窗口)
较简单的一种,作为滑动窗口运行:
- 保留最近的 N 条消息
- 淘汰不再适合的旧消息
- ⚠️ 每条消息的 Token 数量可能不同,适合快速原型开发
2️⃣ TokenWindowChatMemory(Token 窗口)
更复杂的选项,同样作为滑动窗口运行:
- 保留最近的 N 个 Token(而非消息条数)
- 消息是不可分割的 — 如果一条消息放不下,整条淘汰
- 需要一个
Tokenizer来计算每个ChatMessage中的 Token 数
💾 持久化:让记忆不随程序重启而消失
默认情况下,ChatMemory 实现在内存中存储 ChatMessage。程序一重启,记忆就没了 😅
如果需要持久化,可以实现自定义的 ChatMemoryStore,将消息存储到你选择的任何持久化存储中。
核心接口
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// TODO: 通过 memoryId 从持久化存储中获取所有消息
// 可使用 ChatMessageDeserializer.messagesFromJson(String) 反序列化
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// TODO: 通过 memoryId 更新持久化存储中的所有消息
// 可使用 ChatMessageSerializer.messagesToJson(List<ChatMessage>) 序列化
}
@Override
public void deleteMessages(Object memoryId) {
// TODO: 通过 memoryId 删除持久化存储中的所有消息
}
}
使用方式
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id("12345")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
方法调用时机
| 方法 | 调用时机 | 说明 |
|---|---|---|
updateMessages() | 每次添加新消息时 | 每次 LLM 交互调用 2 次(添加 UserMessage 和 AiMessage 各一次) |
getMessages() | 用户请求所有消息时 | 每次 LLM 交互调用 1 次 |
deleteMessages() | 调用 ChatMemory.clear() 时 | 如果不用可留空 |
📌 注意:从
ChatMemory中淘汰的消息也会从ChatMemoryStore中淘汰。淘汰时会调用updateMessages(),传入不含被淘汰消息的列表。
⚙️ SystemMessage 的特殊处理
SystemMessage 是一种特殊类型的消息,处理规则与其他消息不同:
- ✅ 一旦添加,始终保留 — 不会被淘汰
- 🔒 一次只能保存一条 SystemMessage
- 🚫 相同内容的新 SystemMessage 会被忽略
- 🔄 不同内容的新 SystemMessage 会替换之前的
🔧 工具消息的特殊处理
如果包含 ToolExecutionRequest 的 AiMessage 被淘汰,随后的孤立 ToolExecutionResultMessage 也会自动被淘汰。
这是为了避免与某些 LLM 提供商(如 OpenAI)出现问题 — 它们禁止在请求中发送孤立的工具执行结果。
🚀 实战示例
环境准备
模块结构
新建 memorymok 模块:

pom.xml 依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.whc</groupId>
<artifactId>langChain4j-whc</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>langchain4j-whc-memory</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- langchain4j 核心 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
</dependency>
<!-- 阿里百炼平台 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- 工具库 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
application.yml 配置
server:
port: 9005
servlet:
encoding:
enabled: true
force: true
charset: UTF-8
spring:
application:
name: langchain_whc_memory
ai:
dashScope:
apiKey: ${AI_DASHSCOPE_API_KEY}
modelName: ${AI_DASHSCOPE_MODEL_NAME}
baseUrl: ${AI_DASHSCOPE_BASE_URL}
配置类
@Slf4j
@Configuration
public class LlmConfig {
@Value("${ai.dashScope.apiKey}")
private String dashScopeApiKey;
@Value("${ai.dashScope.modelName}")
private String dashScopeModelName;
@Value("${ai.dashScope.baseUrl}")
private String dashScopeBaseUrl;
@Bean
public ChatModel chatModel() {
return OpenAiChatModel.builder()
.apiKey(dashScopeApiKey)
.modelName(dashScopeModelName)
.baseUrl(dashScopeBaseUrl)
.build();
}
@Bean
public StreamingChatModel streamingChatModel() {
return OpenAiStreamingChatModel.builder()
.apiKey(dashScopeApiKey)
.modelName(dashScopeModelName)
.baseUrl(dashScopeBaseUrl)
.build();
}
/**
* 基于消息窗口的 ChatMemory — 保留最近 N 条消息
*/
@Bean
public ChatMemory messageWindowChatMemory() {
return MessageWindowChatMemory.withMaxMessages(10);
}
/**
* 基于 Token 窗口的 ChatMemory — 保留最近 N 个 Token
*/
@Bean
public ChatMemory tokenWindowChatMemory() {
TokenCountEstimator tokenCountEstimator = new OpenAiTokenCountEstimator(dashScopeModelName);
return TokenWindowChatMemory.withMaxTokens(3000, tokenCountEstimator);
}
}
📌 根据前面介绍的两种淘汰策略,这里分别定义了两个 Bean,方便对比测试。
Service 层
AiService 注解的 WiringMode
@AiService 注解的 WiringMode 有两种模式:
- ALL — 自动将应用中所有组件注入 AI Service(同类型多个会抛异常)
- EXPLICIT — 只注入显式指定的组件,通过属性名指定 Bean 名称

💡 如果需要自己对 AiService 中的参数赋值,需要设置
mode = EXPLICIT。
NoMemoryAssist(无记忆模式)
@AiService(wiringMode = EXPLICIT, chatModel = "chatModel")
public interface NoMemoryAssist {
/**
* 聊天(无记忆)
*/
String chat(String userMessage);
}
MemoryAssist(带记忆模式)
@AiService(wiringMode = EXPLICIT,
chatModel = "chatModel",
streamingChatModel = "streamingChatModel",
chatMemory = "messageWindowChatMemory")
public interface MemoryAssist {
/** 普通聊天 */
String chat(String userMessage);
/** 带 MemoryId 的聊天 — 区分不同用户 */
String chat(@MemoryId int memoryId, @UserMessage String userMessage);
/** 流式聊天 */
Flux<String> streamChat(String userMessage);
/** 带 MemoryId 的流式聊天 */
Flux<String> streamChat(@MemoryId int memoryId, @UserMessage String userMessage);
}
Controller 层
NoChatMemoryController(无记忆测试)
@RestController
@RequiredArgsConstructor
public class NoChatMemoryController {
private final NoMemoryAssist noMemoryAssist;
@GetMapping("/noMemory/chat")
public String chat(@RequestParam String userMessage) {
return noMemoryAssist.chat(userMessage);
}
}
ChatMemoryController(带记忆)
@Slf4j
@RestController
@RequiredArgsConstructor
public class ChatMemoryController {
private final MemoryAssist memoryAssist;
@GetMapping("/chat")
public String chat(@RequestParam String userMessage) {
return memoryAssist.chat(userMessage);
}
@GetMapping("/chat-with-memory")
public String chatWithMemory(@RequestParam int memoryId, @RequestParam String userMessage) {
return memoryAssist.chat(memoryId, userMessage);
}
@GetMapping(value = "/stream-chat", produces = MediaType.TEXT_PLAIN_VALUE)
public Flux<String> streamChat(@RequestParam String userMessage) {
return memoryAssist.streamChat(userMessage);
}
@GetMapping(value = "/stream-chat-with-memory", produces = MediaType.TEXT_PLAIN_VALUE)
public Flux<String> streamChatWithMemory(@RequestParam int memoryId, @RequestParam String userMessage) {
return memoryAssist.streamChat(memoryId, userMessage);
}
}
🧪 测试验证
踩坑记录:Token 估算器报错
启动时报错:
Caused by: java.lang.IllegalArgumentException: Model 'qwen-max' is unknown to jtokkit
原因分析:OpenAiTokenCountEstimator 底层使用 jtokkit 库,该库只支持 OpenAI 官方模型(GPT-4、GPT-3.5 等),不支持阿里通义千问模型。
查看源码可以发现,getEncodingForModel() 方法对 ModelType 枚举进行判断:

里面没有包含阿里模型 😅
解决方案:使用 gpt-3.5-turbo 作为 Token 估算的替代模型:
@Bean
public ChatMemory tokenWindowChatMemory() {
// 使用 gpt-3.5-turbo 作为 token 计算模型
// 因为 jtokkit 库不支持 dashscope 的 qwen-max 模型
TokenCountEstimator tokenCountEstimator = new OpenAiTokenCountEstimator("gpt-3.5-turbo");
return TokenWindowChatMemory.withMaxTokens(3000, tokenCountEstimator);
}
测试一:无记忆聊天

第一次问"我是派大星",AI 进入了角色扮演的状态。

第二次问"我是谁",AI 完全不记得刚才的对话,给出了一个通用的回答。
✅ 结论:不带记忆的聊天无法联系上下文,每次都是全新的对话。
测试二:带记忆聊天(chat 接口)

第一次说"我是派大星"。

第二次问"我是谁",AI 记得刚才的对话,回答"你是派大星呀!"
✅ 结论:
chat接口已经自带记忆功能,可以联系上下文。
踩坑记录:多用户记忆的 NPE 问题
使用 @MemoryId 参数时报错:
java.lang.NullPointerException
at java.base/java.util.Objects.requireNonNull(Objects.java:208)
at dev.langchain4j.service.DefaultAiServicesInvoker.invoke(DefaultAiServicesInvoker.java)
原因分析:使用 @MemoryId 注解时,系统需要一个 ChatMemoryProvider 来为每个 memoryId 创建独立的聊天记忆实例。但当前配置只提供了一个具体的 ChatMemory 实例。
解决方案:增加一个 ChatMemoryProvider Bean:
/**
* 创建一个 ChatMemoryProvider 实例,用于根据 memoryId 提供独立的聊天记忆
*/
@Bean
public ChatMemoryProvider messageChatMemoryProvider() {
return memoryId -> MessageWindowChatMemory.withMaxMessages(10);
}
修改 Service 接口,使用 chatMemoryProvider 替代 chatMemory:
@AiService(wiringMode = EXPLICIT,
chatModel = "chatModel",
streamingChatModel = "streamingChatModel",
chatMemoryProvider = "messageChatMemoryProvider")
public interface MemoryAssist {
// ...
}
测试三:分用户记忆(memoryId)
用户 1:派大星

用户 2:酷奇

用户 2 追问:我是谁?

AI 记得用户 2 是"酷奇"!
用户 1 追问:我是谁?

AI 记得用户 1 是"派大星"!两个用户的记忆完全独立。
✅ 结论:分用户记忆成功!不同
memoryId的对话历史完全隔离。
💾 记忆持久化实战
持久化示例
参考官网案例,我们来做一个基于文件的持久化示例。
定义持久化类
⚠️ 注意:这里 Map 的 key 和 value 都是 String 类型,所以
memoryId也需要改成 String 类型。
@Slf4j
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {
// 使用 DBMaker 进行文件级持久化
private static final DB DB = DBMaker
.fileDB("multi-user-chat-memory.db")
.transactionEnable()
.make();
// 哈希映射表:memoryId -> JSON 格式的聊天消息
private static final Map<String, String> MAP = DB
.hashMap("messages", STRING, STRING)
.createOrOpen();
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = MAP.get((String) memoryId);
log.info("getMessage->json:{}", json);
return messagesFromJson(json);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = messagesToJson(messages);
MAP.put((String) memoryId, json);
log.info("updateMessages->json:{}", json);
DB.commit();
}
@Override
public void deleteMessages(Object memoryId) {
MAP.remove((String) memoryId);
log.info("deleteMessages->memoryId:{}", memoryId);
DB.commit();
}
}
改造配置类
这一次使用 TokenWindowChatMemory 进行测试:
/**
* 基于 Token 窗口的持久化 ChatMemory
*/
public ChatMemory tokenWindowChatMemory(String memoryId) {
TokenCountEstimator tokenCountEstimator = new OpenAiTokenCountEstimator("gpt-3.5-turbo");
return TokenWindowChatMemory
.builder()
.id(memoryId)
.maxTokens(3000, tokenCountEstimator)
.chatMemoryStore(PersistentChatMemoryStore.INSTANCE)
.build();
}
@Bean
public ChatMemoryProvider tokenChatMemoryProvider() {
return memoryId -> tokenWindowChatMemory((String) memoryId);
}
Assist 接口
@AiService(wiringMode = EXPLICIT,
chatModel = "chatModel",
streamingChatModel = "streamingChatModel",
chatMemoryProvider = "tokenChatMemoryProvider")
public interface MemoryStoreAssist {
/** 带 MemoryId 的流式聊天 */
Flux<String> streamChat(@MemoryId String memoryId, @UserMessage String userMessage);
}
Controller
@Slf4j
@RestController
@RequiredArgsConstructor
public class MemoryStoreController {
private final MemoryStoreAssist memoryStoreAssist;
@GetMapping(value = "/stream/memory/store", produces = MediaType.TEXT_PLAIN_VALUE)
public Flux<String> streamChatWithMemory(
@RequestParam String memoryId,
@RequestParam String userMessage) {
return memoryStoreAssist.streamChat(memoryId, userMessage);
}
}
测试:持久化验证
用户 1:派大星

用户 2:佩奇
新发送的请求中包含了前面的消息:

用户 3:听风轩

听风轩再次访问

后台日志:因为已经进行了持久化,所以会把用户前面的 message 也带上。


查看持久化数据
获取用户 1 的数据:

请求也会附带上以前的 message 信息:

获取用户 3 的数据:
可以看到 Map 中对所有聊天数据按用户进行了独立存储。


✅ 结论:持久化测试成功!即使程序重启,用户的对话记忆也不会丢失。
📝 总结
| 功能 | 实现方式 | 适用场景 |
|---|---|---|
| 简单记忆 | MessageWindowChatMemory | 快速原型、消息条数固定 |
| Token 控制 | TokenWindowChatMemory | 成本敏感、Token 预算有限 |
| 多用户隔离 | ChatMemoryProvider + @MemoryId | 多用户系统 |
| 持久化 | 自定义 ChatMemoryStore | 需要跨会话保留记忆 |
💡 最佳实践:
- 开发阶段用
MessageWindowChatMemory快速验证- 生产环境推荐
TokenWindowChatMemory控制成本- 多用户场景务必使用
ChatMemoryProvider- 重要对话建议做持久化,避免重启丢失
🦐 本文完 — 希望这篇实战指南能帮你快速上手 LangChain4j 的聊天记忆功能!
LangChain4j 提示词完全指南:SystemMessage、UserMessage 与 PromptTemplate
LangChain4j 实战:ChatMemory 聊天记忆完全指南
手把手教你用 LangChain4j 集成图片理解与图片生成
LangChain4j 流式模式实战指南
LangChain4j 与 Spring Boot集成指南
LangChain4j简述与示例
更多推荐


所有评论(0)