AI Agent的记忆力是怎么实现的——LangChain4j Memory机制深度解析
一个让人抓狂的场景
你有没有跟某个AI助手聊过一段很长的对话?
聊到第十轮的时候,你问它:”我刚才说的那个需求,你觉得用什么方案好?”
它回你一句:”请问您指的是什么需求呢?”
——就好像前面的对话完全没发生过一样。
这就是大语言模型的”先天缺陷”:它没有记忆。每次调用它,它都是从零开始的。你之前说了什么,它压根不知道。更准确地说,LLM 本身是一个无状态的函数——你给它输入,它给你输出,然后就忘了。
那问题来了:我们用 LangChain4j 构建 AI Agent 的时候,它是怎么让 Agent “记住”上下文的?记忆系统的底层原理是什么?有哪些策略可以选择?Java 开发者该怎么用?
这篇文章,一次性给你讲透。
先搞清楚一件事:LLM 真的没有记忆吗?
严格来说,LLM 不是”没有记忆”,而是没有跨请求的状态。
单次推理的时候,你给它一个 Prompt,它会基于这个 Prompt 里的每一个 token 来生成回复。这意味着——如果你把之前所有的对话历史都塞进 Prompt 里,LLM 就能”看到”之前聊了什么。
所以,所谓的”记忆”,本质上是一个工程问题:
在每次调用 LLM 之前,把相关的历史对话拼到 Prompt 里去。
就这么简单。但”简单”不意味着”好做”。因为这里有几个关键问题需要解决:
- 历史对话放多少? 全放进去?还是只放最近几轮?
- 对话太长了怎么办? LLM 的上下文窗口是有上限的。
- 哪些信息重要,哪些可以丢掉? 用户三小时前说了他叫张三,这个要保留;他问了一句”今天星期几”,这个可能就不重要了。
这三个问题,分别对应了 Memory 系统的三种策略。
LangChain4j 的三种记忆策略
LangChain4j 提供了开箱即用的记忆实现,核心接口是 ChatMemory。它内置了三种策略:
1. MessageWindowChatMemory —— 滑动窗口
这是最常用的策略。思路非常直觉:只保留最近 N 条消息。
想象你在一个群里聊天,群消息刷了几千条。你不可能把所有消息都读一遍才能参与讨论,你只关心最近几十条说了什么。MessageWindowChatMemory 就是这个逻辑。
1 2 3 4 5 6
| import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.memory.chat.MessageWindowChatMemory;
ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .build();
|
优点:实现简单,Token 消耗可控。
缺点:早期的重要信息可能被”滑”出去了。如果用户在第十轮说了”我的数据库是 PostgreSQL”,但在第三十轮才问数据库相关的问题,这个信息可能已经丢了。
2. TokenWindowChatMemory —— Token 级窗口
跟滑动窗口类似,但不是按”消息条数”来限制,而是按 Token 数量 来限制。
为什么要这样?因为 LLM 收费和限制都是按 Token 来算的。你想精确控制成本,就得按 Token 来管理。
1 2 3 4 5 6
| import dev.langchain4j.memory.chat.TokenWindowChatMemory; import dev.langchain4j.model.openai.OpenAiTokenizer;
ChatMemory chatMemory = TokenWindowChatMemory.builder() .maxTokens(4000, new OpenAiTokenizer("gpt-4o")) .build();
|
优点:精确控制 Token 消耗,不会超出模型上下文窗口。
缺点:需要指定 Tokenizer,不同模型的 Tokenizer 不一样,换模型可能需要调整。
3. 持久化记忆 —— 存到数据库
前面两种都是内存里的,应用重启就没了。如果你想让 Agent 跨会话记住用户信息(比如用户偏好、历史订单),就需要把记忆持久化。
LangChain4j 的 ChatMemory 接口设计得很巧妙——它底层依赖一个 ChatMemoryStore 接口,默认实现是内存版的,但你可以替换为任何存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import dev.langchain4j.store.memory.chat.ChatMemoryStore; import dev.langchain4j.data.message.ChatMessage; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
public class InMemoryChatMemoryStore implements ChatMemoryStore {
private final Map</**memoryId**/Object, List<ChatMessage>> store = new ConcurrentHashMap<>();
@Override public List<ChatMessage> getMessages(Object memoryId) { return store.getOrDefault(memoryId, List.of()); }
@Override public void updateMessages(Object memoryId, List<ChatMessage> messages) { store.put(memoryId, messages); }
@Override public void deleteMessages(Object memoryId) { store.remove(memoryId); } }
|
然后把它注入到 ChatMemory 里:
1 2 3 4
| ChatMemory chatMemory = MessageWindowChatMemory.builder() .maxMessages(20) .store(new InMemoryChatMemoryStore()) .build();
|
生产环境的话,你可以很轻松地写一个 RedisChatMemoryStore 或者 JdbcChatMemoryStore,把对话历史存到 Redis 或 MySQL 里。
动手实战:用 LangChain4j + Spring Boot 构建有记忆的对话助手
光说不练假把式。我们来写一个完整的 Spring Boot 项目,实现一个有记忆的对话助手。
第一步:添加依赖
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId> <version>1.0.0-beta1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
|
第二步:配置 API Key
1 2 3 4 5 6 7
| langchain4j: open-ai: chat-model: api-key: ${OPENAI_API_KEY} model-name: gpt-4o temperature: 0.7
|
第三步:编写对话服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.MemoryId; import dev.langchain4j.service.UserMessage; import dev.langchain4j.service.SystemMessage; import dev.langchain4j.model.openai.OpenAiChatModel; import org.springframework.stereotype.Service;
@Service public class ChatService {
interface Assistant { @SystemMessage("你是一个友好的技术助手,用中文回答问题。") String chat(@MemoryId String sessionId, @UserMessage String message); }
private final Assistant assistant;
public ChatService(OpenAiChatModel chatModel) { this.assistant = AiServices.builder(Assistant.class) .chatLanguageModel(chatModel) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() .maxMessages(20) .id(memoryId) .build() ) .build(); }
public String chat(String sessionId, String message) { return assistant.chat(sessionId, message); } }
|
这段代码有几个关键点:
- **
@MemoryId**:这是记忆的分区键。不同用户、不同会话用不同的 sessionId,各自的对话历史互相隔离。
- **
chatMemoryProvider**:这是一个 Lambda,每次有新会话进来,自动创建一个独立的 ChatMemory。
- **
@SystemMessage**:定义 Agent 的”人设”——它是谁,该怎么做。这部分每次都会出现在 Prompt 的最前面。
第四步:写 Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import org.springframework.web.bind.annotation.*; import java.util.Map;
@RestController @RequestMapping("/api/chat") public class ChatController {
private final ChatService chatService;
public ChatController(ChatService chatService) { this.chatService = chatService; }
@PostMapping public Map<String, String> chat(@RequestBody ChatRequest request) { String reply = chatService.chat(request.sessionId(), request.message()); return Map.of("reply", reply); }
record ChatRequest(String sessionId, String message) {} }
|
第五步:测试
1 2 3 4 5 6 7 8 9
| curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "user-001", "message": "我叫小明,是个Java开发,最近在学Spring AI"}'
curl -X POST http://localhost:8080/api/chat \ -H "Content-Type: application/json" \ -d '{"sessionId": "user-001", "message": "你还记得我是做什么的吗?"}'
|
如果一切正常,第二轮的回答里会提到你叫小明、是 Java 开发、在学 Spring AI。这就是记忆在起作用——Agent 的 Prompt 里拼上了之前的历史消息。
进阶:记忆的”黑科技”——摘要压缩
滑动窗口虽然好用,但有个硬伤:早期重要信息会丢失。
为了解决这个问题,社区里有一种常见的做法:摘要压缩(Conversation Summary)。思路是:
- 当对话历史快超出窗口时,把前面的内容让 LLM 做一次摘要
- 用摘要替换掉原始的历史消息
- 这样既保留了关键信息,又控制了 Token 消耗
LangChain4j 没有内置这个功能,但我们可以自己实现一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import dev.langchain4j.data.message.ChatMessage; import dev.langchain4j.data.message.SystemMessage; import dev.langchain4j.memory.ChatMemory; import dev.langchain4j.model.chat.ChatLanguageModel; import java.util.ArrayList; import java.util.List;
public class SummarizingChatMemory implements ChatMemory {
private final ChatMemory delegate; private final ChatLanguageModel model; private final int summaryThreshold; private String summary = "";
public SummarizingChatMemory(ChatMemory delegate, ChatLanguageModel model, int summaryThreshold) { this.delegate = delegate; this.model = model; this.summaryThreshold = summaryThreshold; }
@Override public Object id() { return delegate.id(); }
@Override public void add(ChatMessage message) { delegate.add(message); if (delegate.messages().size() >= summaryThreshold) { compressHistory(); } }
private void compressHistory() { List<ChatMessage> messages = delegate.messages(); List<ChatMessage> toSummarize = messages.subList(0, messages.size() / 2); String summaryPrompt = "请将以下对话历史总结为一段简洁的摘要," + "保留所有关键信息(用户身份、需求、决策结论):\n\n" + toSummarize.toString(); String newSummary = model.generate(summaryPrompt); this.summary = this.summary.isEmpty() ? newSummary : this.summary + "\n" + newSummary; delegate.clear(); delegate.add(SystemMessage.from("对话历史摘要:" + summary)); List<ChatMessage> recent = messages.subList(messages.size() / 2, messages.size()); recent.forEach(delegate::add); }
@Override public List<ChatMessage> messages() { return delegate.messages(); }
@Override public void clear() { delegate.clear(); summary = ""; } }
|
这个实现的核心思想是:当消息数量超过阈值时,自动把前半部分压缩成摘要,只保留摘要 + 最近的消息。用户感知不到这个过程,但 Agent 的”记忆力”显著增强了。
Memory 的设计哲学:三层架构
总结一下,一个完整的 Agent Memory 系统通常分三层:
| 层级 |
作用 |
对应 LangChain4j 组件 |
| 短期记忆 |
当前会话的上下文 |
ChatMemory(窗口策略) |
| 长期记忆 |
跨会话的持久化信息 |
ChatMemoryStore + 外部存储 |
| 工作记忆 |
当前任务的中间状态 |
Agent 的 Tool 调用结果 |
短期记忆保证对话连贯,长期记忆实现个性化,工作记忆支撑复杂任务。三层协作,才能让 Agent 从”工具”进化为”助手”。
实战中的几个坑
1. MemoryId 的设计
不要用随机 UUID 做 MemoryId。最好是 userId + sessionId 的组合。userId 保证跨会话的用户识别,sessionId 保证同一用户不同会话的隔离。
2. System Message 不要放 Memory
SystemMessage 定义的是 Agent 的角色和规则,应该每次固定注入,不要参与记忆的滑动窗口。LangChain4j 的实现已经帮你处理了这一点——@SystemMessage 注解的内容不会被计入消息窗口。
3. Token 预算要留余量
如果你用 TokenWindowChatMemory,别把 maxTokens 设成模型的上下文窗口上限。要留出空间给模型的回复和你的 System Prompt。一般建议:**历史消息占 60-70%,System Prompt 占 10%,留给回复 20-30%**。
4. 敏感信息不要存 Memory
用户的密码、身份证号、支付信息,永远不要存进对话记忆。如果用户不小心说了,要在存入前做脱敏处理。
总结
回顾一下这篇文章的核心内容:
- LLM 本身无状态,”记忆”是一个工程问题——把历史对话拼进 Prompt。
- LangChain4j 提供三种策略:消息窗口、Token 窗口、持久化存储。
@MemoryId 是记忆分区的关键,不同会话互不干扰。
- 摘要压缩是进阶技巧,能在有限 Token 内保留更多关键信息。
- 三层架构(短期/长期/工作记忆)是构建复杂 Agent 的基础。
记忆系统看起来简单,但它是 Agent 能不能”像人一样交流”的关键。一个没有记忆的 Agent,再聪明也只是一个一次性的问答机器。有了记忆,它才能真正成为你的助手。
下一篇文章,我们来聊 Agent 的另一个核心能力——Tool Use(工具调用),看看 LangChain4j 是怎么让 Agent 从”只会聊天”进化到”能干活”的。
本文代码基于 LangChain4j 1.0.0-beta1 + Spring Boot 3.x。完整示例项目后续会开源到 GitHub,敬请关注。