AI Agent的记忆力是怎么实现的——LangChain4j Memory机制深度解析

AI Agent的记忆力是怎么实现的——LangChain4j Memory机制深度解析
PeiAI Agent的记忆力是怎么实现的——LangChain4j Memory机制深度解析
系列文章
本篇是 AI Agent 深度解析系列的第 6 篇。以下是系列完整目录,建议收藏作为学习索引。
🏗️ 基础理论篇
- 从零理解 RAG:检索增强生成完整指南
- Embedding 向量化的魔法:从文本到向量的数学之旅与 Java 实战
- 理解 AI Agent 的大脑:ReAct 模式从入门到实战
- Spring AI 核心架构全解析:从 ChatModel 到 Advisor Chain 的设计哲学
🧩 核心组件篇
- AI Agent 的记忆系统:从 ChatMemory 到持久化记忆的 Java 实战
- AI Agent 的记忆力是怎么实现的——LangChain4j Memory 机制深度解析
- AI Agent 的工具箱:深入理解 Tool Use 与 Spring AI Function Calling 实战
- 让 AI 学会”说人话”——Spring AI 结构化输出实战
- AI Agent 的灵魂对话:Prompt Engineering 系统提示词设计的艺术与工程
🏛️ 架构设计篇
- AI Agent 的规划大脑:从任务分解到自适应执行策略
- AI Agent 的工作流编排:从顺序链到自适应 DAG 的 Java 实战
- AI Agent 团队协作:多 Agent 系统架构设计与 Java 实战
- Agent 间如何对话:A2A 协议深度解析与 Java 实战
🔍 知识检索篇
- AI Agent 的知识检索引擎:从向量搜索到智能检索策略的 Java 实战
- 当 RAG 遇上知识图谱:GraphRAG 原理与 Java 实战
- 当 RAG 遇到 Agent:Agentic RAG 的架构设计与 Java 实战
- MCP 模型上下文协议:AI 的万能接口与 MCP Server 实战
🚀 进阶能力篇
- AI Agent 的推理引擎:从 Chain-of-Thought 到推理模型的深度解析与 Java 实战
- AI Agent 的多模态感知:从图片理解到语音交互的 Java 实战
- AI Agent 的自我反思与经验学习:从错误中进化的 Java 实战
- AI Agent 的上下文工程与 Token 预算管理:从窗口压缩到成本优化的 Java 实战
- AI Agent 的人机协作:从 Human-in-the-Loop 到渐进式自治的 Java 实战
🛡️ 生产保障篇
- AI Agent 的安全防线:Prompt 注入防御与生产级安全防护实战
- AI Agent 的可观测性:从链路追踪到成本监控的 Java 实战
- AI Agent 的流式响应与实时交互:从 SSE 到 WebSocket 的 Java 实战
- AI Agent 的容错与韧性:从错误处理到生产级可靠性保障的 Java 实战
- AI Agent 评估与优化:从基准测试到生产环境的质量守护实战
- AI Agent 的成本优化:从模型路由到缓存策略的 Java 实战
🧭 全景总结
一个让人抓狂的场景
你有没有跟某个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 | import dev.langchain4j.memory.ChatMemory; |
优点:实现简单,Token 消耗可控。
缺点:早期的重要信息可能被”滑”出去了。如果用户在第十轮说了”我的数据库是 PostgreSQL”,但在第三十轮才问数据库相关的问题,这个信息可能已经丢了。
2. TokenWindowChatMemory —— Token 级窗口
跟滑动窗口类似,但不是按”消息条数”来限制,而是按 Token 数量 来限制。
为什么要这样?因为 LLM 收费和限制都是按 Token 来算的。你想精确控制成本,就得按 Token 来管理。
1 | import dev.langchain4j.memory.chat.TokenWindowChatMemory; |
优点:精确控制 Token 消耗,不会超出模型上下文窗口。
缺点:需要指定 Tokenizer,不同模型的 Tokenizer 不一样,换模型可能需要调整。
3. 持久化记忆 —— 存到数据库
前面两种都是内存里的,应用重启就没了。如果你想让 Agent 跨会话记住用户信息(比如用户偏好、历史订单),就需要把记忆持久化。
LangChain4j 的 ChatMemory 接口设计得很巧妙——它底层依赖一个 ChatMemoryStore 接口,默认实现是内存版的,但你可以替换为任何存储:
1 | import dev.langchain4j.store.memory.chat.ChatMemoryStore; |
然后把它注入到 ChatMemory 里:
1 | ChatMemory chatMemory = MessageWindowChatMemory.builder() |
生产环境的话,你可以很轻松地写一个 RedisChatMemoryStore 或者 JdbcChatMemoryStore,把对话历史存到 Redis 或 MySQL 里。
动手实战:用 LangChain4j + Spring Boot 构建有记忆的对话助手
光说不练假把式。我们来写一个完整的 Spring Boot 项目,实现一个有记忆的对话助手。
第一步:添加依赖
1 | <dependencies> |
第二步:配置 API Key
1 | # application.yml |
第三步:编写对话服务
1 | import dev.langchain4j.memory.chat.MessageWindowChatMemory; |
这段代码有几个关键点:
- **
@MemoryId**:这是记忆的分区键。不同用户、不同会话用不同的sessionId,各自的对话历史互相隔离。 - **
chatMemoryProvider**:这是一个 Lambda,每次有新会话进来,自动创建一个独立的ChatMemory。 - **
@SystemMessage**:定义 Agent 的”人设”——它是谁,该怎么做。这部分每次都会出现在 Prompt 的最前面。
第四步:写 Controller
1 | import org.springframework.web.bind.annotation.*; |
第五步:测试
1 | # 第一轮:自我介绍 |
如果一切正常,第二轮的回答里会提到你叫小明、是 Java 开发、在学 Spring AI。这就是记忆在起作用——Agent 的 Prompt 里拼上了之前的历史消息。
进阶:记忆的”黑科技”——摘要压缩
滑动窗口虽然好用,但有个硬伤:早期重要信息会丢失。
为了解决这个问题,社区里有一种常见的做法:摘要压缩(Conversation Summary)。思路是:
- 当对话历史快超出窗口时,把前面的内容让 LLM 做一次摘要
- 用摘要替换掉原始的历史消息
- 这样既保留了关键信息,又控制了 Token 消耗
LangChain4j 没有内置这个功能,但我们可以自己实现一个:
1 | import dev.langchain4j.data.message.ChatMessage; |
这个实现的核心思想是:当消息数量超过阈值时,自动把前半部分压缩成摘要,只保留摘要 + 最近的消息。用户感知不到这个过程,但 Agent 的”记忆力”显著增强了。
Memory 的设计哲学:三层架构
总结一下,一个完整的 Agent Memory 系统通常分三层:
| 层级 | 作用 | 对应 LangChain4j 组件 |
|---|---|---|
| 短期记忆 | 当前会话的上下文 | ChatMemory(窗口策略) |
| 长期记忆 | 跨会话的持久化信息 | ChatMemoryStore + 外部存储 |
| 工作记忆 | 当前任务的中间状态 | Agent 的 Tool 调用结果 |
短期记忆保证对话连贯,长期记忆实现个性化,工作记忆支撑复杂任务。三层协作,才能让 Agent 从”工具”进化为”助手”。
下图展示了 Agent Memory 的三层架构——短期记忆、长期记忆和工作记忆各司其职:
如图所示,三层记忆各有分工:短期记忆(蓝色)用滑动窗口策略保持当前会话的上下文连贯,会话结束即丢失;长期记忆(绿色)通过 ChatMemoryStore 或向量数据库持久化存储用户偏好和历史信息,实现跨会话的个性化;工作记忆(橙色)保存当前任务的 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,再聪明也只是一个一次性的问答机器。有了记忆,它才能真正成为你的助手。
这个系列还会继续深入,欢迎关注后续更新。
本文代码基于 LangChain4j 1.0.0-beta1 + Spring Boot 3.x。完整示例项目后续会开源到 GitHub,敬请关注。











