LangChain4j ChatMemory 实战
没有记忆的 AI 每次对话都当第一次见面——用户问“上一条你提到了 Java 21这个特性具体是什么”没有记忆的 AI 会回答“我没提过”用户体验直接崩掉。我曾在项目里碰到过用户投诉“AI 太笨了说了一遍的事还要说第二遍”根本原因就是没有接入记忆。这一节我们把 LangChain4j 的 ChatMemory 讲透。一、为什么需要 ChatMemory大模型本身是无状态的——每次 API 调用都是独立的模型不记得上一次说了什么。要实现多轮对话你必须在每次请求时把历史对话一并发给模型[ {role: system, content: 你是一个助手}, {role: user, content: Java 21 有什么新特性}, {role: assistant, content: Java 21 主要新特性有Virtual Thread、Record...}, {role: user, content: Virtual Thread 怎么用} ← 当前问题 ]ChatMemory 的作用就是帮你自动管理这个历史消息列表——自动追加、自动截断、自动传给模型。你只需要告诉它“保留多少条消息”。二、MessageWindowChatMemory按消息条数截断最常用的记忆策略保留最近 N 条消息超过就删最旧的。import dev.langchain4j.memory.chat.MessageWindowChatMemory; // 保留最近 10 条消息约 5 轮对话 ChatMemory memory MessageWindowChatMemory.withMaxMessages(10);在AiServices.builder()中直接使用单用户场景Bean public ChatAssistant chatAssistant(ChatModel model) { return AiServices.builder(ChatAssistant.class) .chatModel(model) .chatMemory(MessageWindowChatMemory.withMaxMessages(20)) .build(); } interface ChatAssistant { String chat(String message); }这种写法所有用户共享同一个 ChatMemory适合单用户场景如内部工具、管理后台。生产环境的多用户系统需要按会话隔离下面马上讲。三、多用户会话隔离MemoryId如果你想让每个用户或每个会话拥有独立的对话历史就需要MemoryId。第一步在接口方法参数上标记MemoryIdinterface MultiSessionAssistant { // sessionId 不同 → 对话历史完全独立 String chat(MemoryId String sessionId, UserMessage String message); }第二步使用chatMemoryProvider而不是chatMemoryBean public MultiSessionAssistant multiSessionAssistant(ChatModel model) { return AiServices.builder(MultiSessionAssistant.class) .chatModel(model) .chatMemoryProvider(memoryId - MessageWindowChatMemory.withMaxMessages(20)) .build(); }chatMemoryProvider接收一个memoryId就是你的MemoryId参数返回该会话对应的ChatMemory实例。LangChain4j 内部维护一个MapObject, ChatMemory不同sessionId自动创建并隔离。四、TokenWindowChatMemory按 Token 数量截断当消息长度差异很大时有时几十个字有时几千字按消息条数截断不够精确。此时可以使用Token 窗口记忆按 Token 数量限制上下文。import dev.langchain4j.model.openai.OpenAiTokenizer; import dev.langchain4j.memory.chat.TokenWindowChatMemory; // 保留最近不超过 4096 个 Token 的消息 ChatMemory memory TokenWindowChatMemory.builder() .maxTokens(4096, new OpenAiTokenizer(gpt-4o)) .build();注意OpenAiTokenizer使用 GPT 的 tiktoken 分词对其他模型如通义千问是近似值误差约 10-20%。如果需要精准控制建议直接用MessageWindowChatMemory选型建议场景推荐消息长度比较均匀MessageWindowChatMemory简单够用消息长度差异大有时几十字有时几千字TokenWindowChatMemory更精准需要严格控制上下文大小TokenWindowChatMemory快速原型开发MessageWindowChatMemory五、完整示例多用户多轮对话服务把所有知识点串起来一个可以直接跑的实现。5.1 定义 AI 服务接口package com.jichi.langchain4j.service; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; public interface ChatAssistant { SystemMessage(你是一个 Java 技术助手记住用户在对话中提到的技术栈和问题背景) String chat(MemoryId String sessionId, UserMessage String message); }5.2 实现服务层手动构建 Assistantpackage com.jichi.langchain4j.service; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.service.AiServices; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; Service Slf4j public class ChatService { private final ChatAssistant assistant; public ChatService(ChatModel model) { this.assistant AiServices.builder(ChatAssistant.class) .chatModel(model) .chatMemoryProvider(memoryId - MessageWindowChatMemory.withMaxMessages(20)) .build(); } public String chat(String sessionId, String message) { log.info(会话 [{}] 消息{}, sessionId, message); String response assistant.chat(sessionId, message); log.info(会话 [{}] 回复{}, sessionId, response); return response; } }ChatController——接收 sessionId同时提供开新会话接口package com.jichi.langchain4j.controller; import org.springframework.web.bind.annotation.*; import java.util.Map; import java.util.UUID; RestController RequestMapping(/memory/chat) public class ChatMemoryController { private final ChatService chatService; public ChatMemoryController(ChatService chatService) { this.chatService chatService; } /** * 多轮对话接口 * X-Session-Id Header 用来标识会话相同值共享对话历史 */ PostMapping public MapString, String chat( RequestBody ChatRequest req, RequestHeader(value X-Session-Id, required false) String sessionId) { if (sessionId null || sessionId.isBlank()) { sessionId UUID.randomUUID().toString(); } String reply chatService.chat(sessionId, req.message()); return Map.of( sessionId, sessionId, reply, reply ); } /** * 开始新会话生成新 sessionId等同于清空历史 */ PostMapping(/new-session) public MapString, String newSession() { String newSessionId UUID.randomUUID().toString(); return Map.of(sessionId, newSessionId); } record ChatRequest(String message) {} }六、System Message 在记忆里的处理有同学问过System Message 会不会随着对话轮数越来越多被挤出窗口不会。LangChain4j 自动处理第1轮发给模型[System, User1, AI1] 第2轮发给模型[System, User1, AI1, User2, AI2] 第3轮发给模型[System, User1, AI1, User2, AI2, User3, AI3]System 始终只有 1 条不会累积当消息超过窗口大小时最旧的 User/AI 消息对会被删除System Message 始终保留这个行为是 LangChain4j 内置的你不需要额外处理。七、持久化的核心接口ChatMemoryStore7.1ChatMemoryStoreLangChain4j 的ChatMemory内部通过ChatMemoryStore存取消息。默认实现是InMemoryChatMemoryStore内存。要实现自定义存储只需要实现这个接口public interface ChatMemoryStore { ListChatMessage getMessages(Object memoryId); void updateMessages(Object memoryId, ListChatMessage messages); void deleteMessages(Object memoryId); }7.2实现 JpaChatMemoryStore这是核心将ChatMemoryStore的接口方法映射到数据库操作。package com.jichi.langchain4j.memory; import com.jichi.langchain4j.entity.ChatMessageEntity; import com.jichi.langchain4j.repository.ChatMessageRepository; import dev.langchain4j.data.message.*; import dev.langchain4j.store.memory.chat.ChatMemoryStore; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.Objects; Component public class JpaChatMemoryStore implements ChatMemoryStore { private final ChatMessageRepository repository; public JpaChatMemoryStore(ChatMessageRepository repository) { this.repository repository; } Override public ListChatMessage getMessages(Object memoryId) { return repository.findBySessionIdOrderByCreatedAtAsc(memoryId.toString()) .stream() .map(this::toMessage) .filter(Objects::nonNull) .toList(); } Override Transactional public void updateMessages(Object memoryId, ListChatMessage messages) { // 全量替换先删后插生产环境可优化为增量更新 repository.deleteBySessionId(memoryId.toString()); ListChatMessageEntity entities messages.stream() .map(msg - toEntity(memoryId.toString(), msg)) .toList(); repository.saveAll(entities); } Override Transactional public void deleteMessages(Object memoryId) { repository.deleteBySessionId(memoryId.toString()); } private ChatMessageEntity toEntity(String sessionId, ChatMessage message) { ChatMessageEntity entity new ChatMessageEntity(); entity.setSessionId(sessionId); if (message instanceof SystemMessage m) { entity.setRole(SYSTEM); entity.setContent(m.text()); } else if (message instanceof UserMessage m) { entity.setRole(USER); entity.setContent(m.singleText()); } else if (message instanceof AiMessage m) { entity.setRole(AI); entity.setContent(m.text() ! null ? m.text() : ); } else if (message instanceof ToolExecutionResultMessage m) { entity.setRole(TOOL); entity.setContent(m.text()); entity.setToolName(m.toolName()); } return entity; } private ChatMessage toMessage(ChatMessageEntity entity) { return switch (entity.getRole()) { case SYSTEM - new SystemMessage(entity.getContent()); case USER - new UserMessage(entity.getContent()); case AI - new AiMessage(entity.getContent()); case TOOL - new ToolExecutionResultMessage( entity.getToolName(), entity.getToolName(), entity.getContent()); default - null; }; } }7.3注册持久化 ChatMemory 到 AiService定义和之前一样的接口注意MemoryId仍然是分离会话的关键package com.jichi.langchain4j.service; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.service.UserMessage; public interface PersistentChatAssistant { SystemMessage(你是一个 Java 技术助手记住用户在对话中提到的技术栈和问题背景) String chat(MemoryId String sessionId, UserMessage String message); }配置类中绑定JpaChatMemoryStorepackage com.jichi.langchain4j.config; import com.jichi.langchain4j.memory.JpaChatMemoryStore; import com.jichi.langchain4j.service.PersistentChatAssistant; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.service.AiServices; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class PersistentMemoryConfig { Bean public PersistentChatAssistant chatAssistant(ChatModel model, JpaChatMemoryStore memoryStore) { return AiServices.builder(PersistentChatAssistant.class) .chatModel(model) .chatMemoryProvider(memoryId - MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(20) .chatMemoryStore(memoryStore) // 关键绑定持久化 Store .build()) .build(); } }现在对话历史会存入数据库。即使重启服务用同一个sessionId继续对话历史依然存在。