LangChain4j- Chat Memory
目录
- 什么是 Chat Memory
- Memory vs History
- Eviction Policy(驱逐策略)
- Persistence(持久化)
- SystemMessage 的特殊处理
- 实际代码实现
- API 接口说明
1. 什么是 Chat Memory
手动维护和管理 ChatMessage 很麻烦。因此,LangChain4j 提供了 ChatMemory 抽象,以及多个现成的实现。
ChatMemory 可以作为独立的低级组件使用,也可以作为高级组件(例如 AI Services)的一部分使用。
ChatMemory 作为容器,用于存储 ChatMessage(由 List 提供支持),具有以下附加功能:
- Eviction policy: 自动管理消息数量,避免超出上下文窗口限制
- Persistence: 支持消息持久化存储
SystemMessage的特殊处理: 系统消息自动保留- 工具消息的特殊处理: 自动清理工具执行结果
2. Memory vs History
请记住,“内存”(Memory)和"历史"(History)是相似但不同的概念:
History(历史记录)
- 完整保存用户和 AI 之间的所有消息
- 是用户在 UI 中看到的内容
- 代表了实际所说的内容
Memory(内存)
- 保留一些"信息",这些信息会呈现给 LLM,使其看起来像是"记住"了对话
- 与历史有很大不同
- 根据所使用的内存算法,它可以以多种方式修改历史:
- 删除(evict)一些消息
- 总结多个消息
- 从消息中删除无关紧要的细节
- 向消息中注入额外的信息(例如,用于 RAG)或指令(例如,用于结构化输出)
重要提示:
目前,LangChain4j 只提供"内存",不提供"历史"。如果您需要保存整个历史,请手动保存。
3. Eviction Policy(驱逐策略)
Eviction policy(驱逐策略)是必需的,出于以下几个原因:
1. 适应 LLM 的上下文窗口
LLM 一次处理的令牌数量有限。在某个时候,对话可能会超过这个限制。在这种情况下,应该删除一些消息。通常情况下,最古老的消息会被删除,但如果需要,可以实现更复杂的算法。
2. 控制成本
每张令牌都有成本,这意味着每次调用 LLM 都会越来越昂贵。删除不必要的消息可以降低成本。
3. 控制延迟
发送给 LLM 的令牌越多,处理它们所需的时间就越长。
LangChain4j 提供的两种实现
更简单的版本:MessageWindowChatMemory
- 作为一个滑动窗口运行
- 保留最近的 N 条消息
- 删除不再适合的旧消息
- 主要用于快速原型制作
更复杂的选项:TokenWindowChatMemory
- 也作为滑动窗口运行
- 但重点是保持最近的 N 个令牌
- 根据需要删除旧消息
- 消息不可分割:如果一条消息无法容纳,它将被完全删除
- 需要一个
TokenCountEstimator来计算每个ChatMessage中的令牌数
4. Persistence(持久化)
默认行为
默认情况下,ChatMemory 实现会将 ChatMessage 存储在内存中。
自定义持久化
如果需要持久性,可以实现自定义的 ChatMemoryStore,在任何持久存储中存储 ChatMessage:
class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// 从持久存储中获取所有消息
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
// 更新持久存储中的所有消息
}
@Override
public void deleteMessages(Object memoryId) {
// 删除持久存储中的所有消息
}
}
ChatMemory chatMemory = MessageWindowChatMemory.builder()
.id("12345")
.maxMessages(10)
.chatMemoryStore(new PersistentChatMemoryStore())
.build();
重要机制
-
updateMessages() 调用时机:
- 每次向 ChatMemory 添加新的 ChatMessage 时都会调用
- 通常在每次与 LLM 交互时发生两次:
- 一次是在添加新的 UserMessage 时
- 另一次是添加新的 AiMessage 时
-
getMessages() 调用时机:
- 每次使用 ChatMemory 请求所有消息时都会调用
- 通常在每次与 LLM 交互时发生一次
-
删除联动:
请注意,从 ChatMemory 中删除的消息也将从 ChatMemoryStore 中删除
- 当一条消息被驱逐时,
updateMessages()方法会被调用,其中包含一个不包括被驱逐消息的消息列表
- 当一条消息被驱逐时,
5. SystemMessage 的特殊处理
SystemMessage 是特殊消息类型,因此与其他消息类型不同:
- 一旦添加,
SystemMessage将始终保留 - 一次只能有一个
SystemMessage - 如果添加了具有相同内容的新
SystemMessage,则忽略它 - 如果添加了一个新内容不同的
SystemMessage,它会替换之前的SystemMessage - 默认情况下,新
SystemMessage会添加到消息列表的末尾 - 您可以通过设置创建
ChatMemory时的alwaysKeepSystemMessageFirst属性来改变这一行为
代码样例
@PostMapping("/system-prompt")
@Operation(summary = "设置系统提示词", description = "演示 SystemMessage 的特殊处理:始终保留、自动替换")
public String setSystemPrompt(
@Parameter(description = "会话ID/用户ID", required = true, example = "user123")
@RequestParam String memoryId,
@Parameter(description = "系统提示词", required = true, example = "你是一个专业的Java开发助手,请用简洁的语言回答问题")
@RequestParam String systemPrompt
) {
chatMemoryService.setSystemPrompt(memoryId, systemPrompt);
return "系统提示词已设置";
}
public void setSystemPrompt(String memoryId, String systemPrompt) {
MessageWindowChatMemory memory = getOrCreateMemory(memoryId);
memory.add(SystemMessage.from(systemPrompt));
}
private MessageWindowChatMemory getOrCreateMemory(String memoryId) {
// 使用computeIfAbsent方法,如果memoryId不存在,则创建一个新的MessageWindowChatMemory对象
// 使用建造者模式构建新的内存对象,设置ID和最大消息数为默认值
return memories.computeIfAbsent(memoryId, id ->
MessageWindowChatMemory.builder()
.id(id) // 设置内存对象的ID
.maxMessages(DEFAULT_MAX_MESSAGES) // 设置最大消息数为默认值
.build() // 构建并返回内存对象
);
}
6. 实际代码实现
6.1 ChatMemoryService(服务层)
ChatMemoryService.java 实现了基于 MessageWindowChatMemory 的对话服务:
@Service
public class ChatMemoryService {
@Resource
private ChatModel chatModel;
private final Map<String, MessageWindowChatMemory> memories = new ConcurrentHashMap<>();
private static final int DEFAULT_MAX_MESSAGES = 10;
public String chat(String memoryId, String userMessage) {
MessageWindowChatMemory memory = getOrCreateMemory(memoryId);
UserMessage message = UserMessage.from(userMessage);
memory.add(message);
ChatResponse response = chatModel.chat(memory.messages());
String aiResponse = response.aiMessage().text();
AiMessage aiMessage = AiMessage.from(aiResponse);
memory.add(aiMessage);
return aiResponse;
}
public int getMessageCount(String memoryId) {
MessageWindowChatMemory memory = memories.get(memoryId);
if (memory == null) {
return 0;
}
return memory.messages().size();
}
public void clearMemory(String memoryId) {
MessageWindowChatMemory memory = memories.get(memoryId);
if (memory != null) {
memory.clear();
}
}
public void deleteMemory(String memoryId) {
memories.remove(memoryId);
}
private MessageWindowChatMemory getOrCreateMemory(String memoryId) {
return memories.computeIfAbsent(memoryId, id ->
MessageWindowChatMemory.builder()
.id(id)
.maxMessages(DEFAULT_MAX_MESSAGES)
.build()
);
}
}
核心功能说明
1. 多用户/多会话支持
- 使用
ConcurrentHashMap存储每个会话的 ChatMemory - 每个用户/会话都有独立的内存空间
memoryId作为唯一标识符
2. 自动创建内存
private MessageWindowChatMemory getOrCreateMemory(String memoryId) {
return memories.computeIfAbsent(memoryId, id ->
MessageWindowChatMemory.builder()
.id(id)
.maxMessages(DEFAULT_MAX_MESSAGES)
.build()
);
}
- 首次对话时自动创建 ChatMemory
- 使用滑动窗口,自动限制最多 10 条消息
3. 对话流程
public String chat(String memoryId, String userMessage) {
// 1. 获取或创建内存
MessageWindowChatMemory memory = getOrCreateMemory(memoryId);
// 2. 添加用户消息
UserMessage message = UserMessage.from(userMessage);
memory.add(message);
// 3. 发送完整历史给 AI
ChatResponse response = chatModel.chat(memory.messages());
// 4. 添加 AI 回复
String aiResponse = response.aiMessage().text();
AiMessage aiMessage = AiMessage.from(aiResponse);
memory.add(aiMessage);
return aiResponse;
}
4. Eviction Policy 自动生效
- MessageWindowChatMemory 自动管理消息数量
- 当消息超过 10 条时,自动删除最旧的消息
- 确保对话不会超出上下文窗口限制
7. API 接口说明
7.1 ChatMemoryController(控制器层)
ChatMemoryController.java 提供了 RESTful API 接口:
@RestController
@RequestMapping("/api/chat-memory")
@Tag(name = "Chat Memory", description = "基于 MessageWindowChatMemory 的多轮对话功能")
public class ChatMemoryController {
@Resource
private ChatMemoryService chatMemoryService;
@PostMapping("/chat")
@Operation(summary = "发送消息", description = "使用 ChatMemory 进行多轮对话,自动管理对话历史")
public String chat(
@Parameter(description = "会话ID/用户ID", required = true, example = "user123")
@RequestParam String memoryId,
@Parameter(description = "用户消息", required = true, example = "Hello, my name is Klaus")
@RequestParam String message
) {
return chatMemoryService.chat(memoryId, message);
}
@GetMapping("/count/{memoryId}")
@Operation(summary = "获取消息数量", description = "查看指定会话的当前消息数量")
public int getMessageCount(
@Parameter(description = "会话ID/用户ID", required = true)
@PathVariable String memoryId
) {
return chatMemoryService.getMessageCount(memoryId);
}
@PostMapping("/clear/{memoryId}")
@Operation(summary = "清空会话历史", description = "清空指定会话的对话历史")
public String clearMemory(
@Parameter(description = "会话ID/用户ID", required = true)
@PathVariable String memoryId
) {
chatMemoryService.clearMemory(memoryId);
return "会话历史已清空";
}
@DeleteMapping("/{memoryId}")
@Operation(summary = "删除会话", description = "完全删除指定会话")
public String deleteMemory(
@Parameter(description = "会话ID/用户ID", required = true)
@PathVariable String memoryId
) {
chatMemoryService.deleteMemory(memoryId);
return "会话已删除";
}
}
7.2 接口使用示例
1. 发送消息(维护上下文)
POST /api/chat-memory/chat?memoryId=user123&message=Hello, my name is Klaus
响应:
Hi Klaus! How can I help you today?
2. 继续对话(测试记忆)
POST /api/chat-memory/chat?memoryId=user123&message=What is my name?
响应:
Your name is Klaus.
3. 查看消息数量
GET /api/chat-memory/count/user123
响应:
4
(包含:SystemMessage + User1 + AiMessage1 + User2 + AiMessage2)
4. 清空历史
POST /api/chat-memory/clear/user123
响应:
会话历史已清空
5. 删除会话
DELETE /api/chat-memory/user123
响应:
会话已删除
7.3 多用户隔离示例
# 用户1 (Klaus) 的对话
POST /api/chat-memory/chat?memoryId=1&message=Hello, my name is Klaus
# 返回: Hi Klaus! How can I help you today?
POST /api/chat-memory/chat?memoryId=1&message=What is my name?
# 返回: Your name is Klaus.
# 用户2 (Francine) 的对话(独立内存)
POST /api/chat-memory/chat?memoryId=2&message=Hello, my name is Francine
# 返回: Hello Francine! How can I help you today?
POST /api/chat-memory/chat?memoryId=2&message=What is my name?
# 返回: Your name is Francine.
每个用户的对话历史是完全独立的,互不影响。
总结
本教程涵盖了 LangChain4j Chat Memory 的核心概念:
- ChatMemory 抽象:简化了对话历史的手动管理
- Memory vs History:理解两者的区别和适用场景
- Eviction Policy:自动控制消息数量,避免超出上下文窗口
- Persistence:支持消息持久化存储
- SystemMessage 特殊处理:自动保留系统提示
- 实际实现:基于 MessageWindowChatMemory 的完整代码
- REST API:多用户隔离的对话接口
通过这个 MVP 实现,你可以:
- ✅ 快速集成 Chat Memory 功能
- ✅ 支持多轮对话(记住之前的上下文)
- ✅ 自动管理对话历史(滑动窗口)
- ✅ 隔离多用户/多会话的数据
下一步可以探索:
- 使用
TokenWindowChatMemory进行更精细的令牌控制 - 实现自定义
ChatMemoryStore进行持久化存储 - 结合 AI Services 实现更高级的功能











