概述
系列定位与引言
本文是 “AI Agent 基础概念与架构总览” 系列的第 4 篇。在 [第 3 篇:LLM:Agent 的“大脑”——能力、局限与调用方式] 中,我们已深入拆解了 LLM 作为自回归概率模型的本质,系统梳理了其四大核心能力与六大核心局限。其中,“缺乏持久状态” 与 “上下文窗口限制” 这两大局限,直接引出了本文的核心主题——Memory(记忆)。
如果你已经理解了 LLM 的“无状态”特性——每次 generate(List<ChatMessage> messages) 调用都是独立且失忆的,它完全不知道上一轮对话说了什么,也不知道用户是谁、偏好什么。但你一定也体验过 ChatGPT 那种流畅的多轮对话能力:它能记住你刚说的“我叫张三”,然后在后续对话中自然地称呼你为“张三先生”。这背后的魔法并非 LLM 本身的记忆能力,而是 Memory 系统在“作弊”——在每轮 LLM 调用之前,Memory 小心翼翼地将“用户刚说的话 + 历史对话 + 工具调用结果 + 检索到的用户偏好”全部塞进一个 List<ChatMessage>,完整地递给 LLM。LLM “看到”的是一个精心拼凑的、似乎包含了一切过往的全景上下文,于是它便“记起来”了。
当我们把 Memory 的三层模型映射到 Java 工程师极为熟悉的 多级缓存架构 时,一切变得更加清晰:
- 工作记忆(Working Memory) = L1 Cache(寄存器/纳秒级,容量极小)。它是 LLM 当前直接“看见”的上下文窗口,数据就是
List<ChatMessage>,受模型maxTokens严格限制。 - 短期记忆(Short-Term Memory) = L2 Cache(Redis/毫秒级,容量适中)。它存储当前会话内累积的临时信息,如用户偏好、本轮实体,带有 TTL 自动过期。
- 长期记忆(Long-Term Memory) = Database(向量库/关系库,毫秒至秒级,容量近乎无限)。它持久化跨会话的用户知识与经验,通过 Embedding 语义检索。
信息在三层之间像数据在不同缓存层级一样流动:每轮对话的热数据写入工作记忆,关键实体被异步提取到短期记忆,会话结束后,经过 Embedding 加工存入长期记忆;下一次新会话开始时,从长期记忆检索相关实体,重新注入工作记忆,让 LLM “想起”跨越时空的偏好。
本文将沿着 “为什么需要 Memory → 三层记忆模型详解 → 读写时机与信息流动 → 记忆裁剪策略 → 混合记忆管理模式 → LangChain4j 代码骨架拆解 → 与前后系列衔接 → 面试高频专题” 的逻辑主线,带你从理论到源码,建立设计合理分层记忆方案的系统性能力。
核心要点速览
- Memory 的核心任务:为无状态的 LLM 提供“状态假象”。每轮调用前,将历史对话、工具结果、用户偏好组装为完整
List<ChatMessage>传入 LLM。 - 三层记忆模型:工作记忆(LLM 上下文窗口,消息列表,纳秒级,Token 限制)、短期记忆(会话级 Redis,毫秒级,TTL 管理)、长期记忆(跨会话向量库,持久化,语义检索)。
- 读写时机:读——每轮 LLM 调用前,从工作/短期/长期记忆读取并组装上下文;写——每轮调用后,追加消息到工作记忆,异步提取实体写入短期/长期记忆。
- 记忆裁剪策略:窗口滑动(保留最近 N 条,
LinkedList.removeFirst())、Token 计数裁剪(Tokenizer.estimateTokenCount(),精确控制)、摘要压缩(LLM 生成摘要,保留语义)。 - 混合记忆管理:窗口+摘要结合(保近期,压早期),实体+向量检索结合(语义召回跨会话实体),时间衰减与重要性固化(模拟遗忘曲线)。
- Java 工程类比:Memory 三层模型 ≡ L1 Cache(工作记忆)+ L2 Cache(短期记忆)+ Database(长期记忆)。将对话状态管理转化为多级缓存设计问题。
文章组织架构
flowchart TD
1["1. 为什么需要 Memory:LLM 的无状态与 Memory 的补偿使命"]
2["2. 三层记忆模型详解:工作记忆、短期记忆、长期记忆的六维对比"]
3["3. Memory 的读写时机与信息流动:一次 LLM 调用前后的完整时序"]
4["4. 记忆裁剪策略:窗口滑动、Token 计数、摘要压缩的源码行为与适用场景"]
5["5. 混合记忆管理:窗口+摘要、实体+向量、时间衰减与重要性固化"]
6["6. LangChain4j 代码骨架拆解:ChatMemory 接口体系与三层记忆的代码映射"]
7["7. 与前后系列的衔接"]
8["8. 面试高频专题"]
1 --> 2 --> 3 --> 4 --> 5 --> 6 --> 7 --> 8
架构图说明:
- 总览说明:全文 8 个模块遵循“问题驱动→模型定义→动态行为→工程策略→代码落地→系列串联→面试巩固”的认知路径。
- 逐模块说明:模块 1 建立必要性——Memory 是对 LLM 无状态与窗口限制的直接补偿;模块 2-3 是核心——三层记忆的静态定义与动态读写构成了 Memory 系统的骨架;模块 4-5 是工程化关键——裁剪与混合管理决定了生产环境下的效率与体验;模块 6 用 LangChain4j 源码验证理论,实现从概念到代码的映射;模块 7 承上启下,锚定系列坐标;模块 8 通过面试专题强化系统设计能力。
- 关键结论:Memory 的设计本质是“多级缓存架构在对话系统中的迁移”。 工作记忆是 L1,追求极致速度但容量受 Token 硬限制;短期记忆是 L2,通过 Redis 在容量与延迟间取得平衡;长期记忆是 Database,依靠向量检索提供无限容量的持久化知识。三层之间的信息流动遵循“热数据向上汇聚、冷数据向下沉淀”的缓存原则。一旦掌握这个思维模型,面对任何记忆需求,你都能快速划分出什么数据放在哪一层、何时读写、何时裁撤、何时过期。
1. 为什么需要 Memory:LLM 的无状态与 Memory 的补偿使命
1.1 LLM 的无状态特性回顾
在第 3 篇中我们强调过,LLM 本质是一个函数:
// 伪代码:LLM 的接口契约
Response<AiMessage> generate(List<ChatMessage> messages);
每一次 generate() 调用都是完全独立的。模型内部的推理完全基于当前传入的 messages 列表。它没有内部变量去“记住”上一轮对话的内容,不知道之前调用过什么工具、得到过什么结果,更遑论用户的姓名、偏好或历史决策。
假设用户与 Agent 进行两轮对话:
- 第 1 轮请求:
generate([UserMessage("我叫张三")])→ LLM 回复:“你好张三!” - 第 2 轮请求(如果无 Memory):
generate([UserMessage("我叫什么?")])→ LLM 会一头雾水,因为它看到的只是一个孤立的新问题。
要让 LLM 正确回答第二问,我们必须把历史消息也传进去:
- 第 2 轮请求(有 Memory):
generate([UserMessage("我叫张三"), AiMessage("你好张三!"), UserMessage("我叫什么?")])→ LLM 看到完整的对话历史,回答:“你叫张三。”
这就是 Memory 系统的核心使命:为每次 generate() 调用拼凑出包含所有必要上下文的消息列表,营造出 LLM 拥有连续记忆的“假象”。
1.2 Memory 的核心任务与 Java 类比
Memory 需要在下一次 LLM 调用之前,将以下信息组装成一个 List<ChatMessage>:
- 系统提示(角色定义、工具列表)
- 最近 N 轮对话历史
- 本轮及历史工具调用的结果
- 从长期记忆中检索到的用户偏好与知识
这个模式对 Java 工程师来说再熟悉不过:HTTP Session。HTTP 协议本身就是无状态的,每次请求都是独立的,但 Servlet Container 通过 HttpSession(基于 Cookie + 服务端存储)为每次请求关联上用户的会话状态。同样,Memory 模块为每次 LLM 调用关联上 Agent 的对话状态。
区别在于,HTTP Session 存的是序列化的 Java 对象,而 Memory 存的是序列化的 List<ChatMessage>,并且有更复杂的裁剪、检索与分层策略。
1.3 两类补偿
Memory 系统直接补偿了 LLM 的两大短板:
- 补偿“缺乏持久状态”:通过工作记忆+短期记忆+长期记忆的三层模型,为 Agent 提供从当前对话到跨会话的连续性。
- 补偿“上下文窗口限制”:通过记忆裁剪策略(窗口滑动、Token 计数、摘要压缩),确保传入 LLM 的消息列表永远不会超出模型的最大 Token 限制(例如 GPT-4o 的 128K,但出于成本与延迟考虑,实际往往控制在 4K~8K),同时尽量保留最关键的信息。
2. 三层记忆模型详解:工作记忆、短期记忆、长期记忆的六维对比
为了在速度、容量、持久性之间取得平衡,Agent 的记忆被划分为三个明确的层级。每一层都可以从六个维度进行精确定义:定义、存储形态、存储位置、大小限制、生命周期、核心职责。
2.1 工作记忆(Working Memory)
| 维度 | 说明 |
|---|---|
| 定义 | LLM 当前“看到”的上下文窗口,是本次推理的直接信息来源。 |
| 存储形态 | List<ChatMessage>,包含 SystemMessage、多对 UserMessage/AiMessage 以及 ToolExecutionResultMessage。 |
| 存储位置 | JVM 堆内存(ChatMemory 默认实现)或 Redis(ChatMemoryStore 的 Redis 实现)。 |
| 大小限制 | 受模型 maxTokens 严格限制,如 GPT-4o 为 128K tokens,Claude 3.5 Sonnet 为 200K tokens。实践中常通过裁剪策略将其控制在更小的优化窗口内(如 4K-8K)。 |
| 生命周期 | 与单次 Agent 循环同步。循环开始前组装,循环结束后清空或移入短期记忆归档。 |
| 核心职责 | 提供 LLM 推理所需的即时上下文,确保回复的连贯性与准确性。 |
对 Java 工程师来说,工作记忆就像一个 方法调用栈上的局部变量。当方法(一次 Agent 循环)执行时,它是可见且快速访问的;方法结束,它就随着栈帧弹出而消失(或部分持久化)。
2.2 短期记忆(Short-Term Memory)
| 维度 | 说明 |
|---|---|
| 定义 | 当前会话内累积的信息,跨多次 LLM 调用共享,但不跨会话。 |
| 存储形态 | 键值对形式,如 sessionId → {userId, preferences, extractedEntities, recentActions}。 |
| 存储位置 | Redis(生产环境)或本地 ConcurrentHashMap(开发/测试)。 |
| 大小限制 | 受 Redis 内存限制,通常设置 TTL(如 30 分钟会话超时)自动清理。 |
| 生命周期 | 与用户会话同步。会话结束(主动登出/超时)后由 TTL 机制清理。 |
| 核心职责 | 在会话范围内缓存用户状态、临时偏好、本轮已提取的关键实体,避免重复计算,加速上下文组装。 |
短期记忆相当于 Web 应用中的 HttpSession 属性。你可以在一个会话的多次请求之间共享 session.setAttribute("shoppingCart", cart),一旦会话过期,数据自动清除。在 Agent 中,你可以在一个会话的多轮 LLM 调用之间共享“用户提到了订单 12345”,而不必每次都从长期存储中查询。
2.3 长期记忆(Long-Term Memory)
| 维度 | 说明 |
|---|---|
| 定义 | 跨会话的持久化知识库,存储用户偏好、历史决策、经验教训等。 |
| 存储形态 | 向量嵌入(Embedding)+ 结构化元数据,例如 {userId, entityType: "preference", entityValue: "靠窗座位", timestamp, importance}。 |
| 存储位置 | 向量数据库(Milvus、Qdrant、Pinecone)或关系数据库(MySQL/PostgreSQL)。 |
| 大小限制 | 几乎无限,向量库可通过分片水平扩展。 |
| 生命周期 | 永久持久化,除非用户主动删除或触发遗忘策略。 |
| 核心职责 | 跨会话保留有价值的用户知识,通过语义检索将相关记忆注入新会话的工作记忆中,实现个性化与学习能力。 |
长期记忆对应于数据库中的 用户画像表 或 历史订单表。它不直接参与每次推理,但在需要时通过查询(检索)提取相关信息,放入当前上下文。
2.4 Memory 三层模型架构图
flowchart TD
subgraph L1["工作记忆 (L1 Cache)"]
WM_Storage["JVM堆/Redis<br/>List<ChatMessage>"]
WM_Speed["延迟:纳秒~微秒级"]
WM_Limit["容量:受 maxTokens 限制(如 8K tokens)"]
WM_Life["生命周期:单次循环"]
end
subgraph L2["短期记忆 (L2 Cache)"]
SM_Storage["Redis<br/>键值对(sessionId → state)"]
SM_Speed["延迟:毫秒级"]
SM_Limit["容量:受内存限制,TTL 30min"]
SM_Life["生命周期:用户会话"]
end
subgraph L3["长期记忆 (Database)"]
LM_Storage["向量数据库/关系数据库<br/>Embedding + 元数据"]
LM_Speed["延迟:毫秒~秒级"]
LM_Limit["容量:近乎无限"]
LM_Life["生命周期:持久化"]
end
L1 -->|异步提取实体| L2
L2 -->|会话结束 Embedding 写入| L3
L3 -->|新会话检索注入| L1
图1说明:
- 图表主旨概括:展示 Memory 三层模型的存储介质、性能特征、容量及生命周期,并使用多级缓存的类比。
- 逐层分解:顶层工作记忆最快速但容量最小,直接受 LLM Token 限制;中层短期记忆通过 Redis 提供会话级别的低延迟共享;底层长期记忆借助向量数据库实现持久化与语义搜索。
- 设计原理映射:这正是 CPU 多级缓存架构在对话系统中的应用。热数据(最近对话)停留在离计算核心最近的 L1(工作记忆),温数据(会话内偏好)驻留在 L2(短期记忆),冷数据(长期知识)持久化在 L3(长期记忆)。数据逐级向上传递或向下沉淀。
- 工程联系与关键结论:设计 Agent 记忆时,首先要决策的是“这条信息应该在哪一层存活”。 当前对话上下文必须在工作记忆,会话临时状态放短期记忆,有跨会话价值的知识必须落入长期记忆。错误的层级放置会导致丢失上下文或增加不必要的成本与延迟。
3. Memory 的读写时机与信息流动:一次 LLM 调用前后的完整时序
Memory 并非静态的存储结构,它在一个 Agent 循环中有着精确的读取时机(组装上下文)和写入时机(更新状态)。这种动态行为可以用一个完整的时序图来描述。
3.1 读取时机(每轮 LLM 调用开始前)
每当用户输入到达,Agent 需要为 LLM 准备一份完整的 List<ChatMessage>。这个组装过程遵循严格的顺序:
- 用户输入
UserMessage到达。 - 从工作记忆读取当前消息列表:包含最近的对话历史、前序工具调用结果。这部分直接通过
ChatMemory.messages()获得。 - 从短期记忆读取会话累积信息:根据
sessionId从 Redis 中获取已缓存的用户偏好、临时实体。例如,用户在前一轮中提到“我的订单 12345”,这个信息可能作为字符串缓存,此时提取并拼接到系统消息中。 - 从长期记忆语义检索相关记忆:将当前用户意图(或最近几轮对话摘要)通过 Embedding 模型向量化,在向量数据库中执行 ANN(近似最近邻)检索,返回 Top-K 相关的长期记忆(如“用户偏好:喜欢靠窗座位”、“历史决策:上次使用微信支付”)。
- 组装完整消息列表:顺序拼接
SystemMessage(包含角色定义、工具列表、以及步骤 3 和 4 检索到的记忆上下文)+ 步骤 2 的历史消息 + 当前UserMessage。 - 传入 LLM:调用
model.generate(assembledMessages)。
3.2 写入时机(每轮 LLM 调用结束后)
LLM 返回响应后,Memory 系统需要将新信息分层写入:
- LLM 返回
AiMessage(可能包含文本回复或ToolCall请求)。 - 追加到工作记忆:调用
ChatMemory.add(aiMessage),将 AI 回复加入消息列表。这是保持对话连续性的最基础动作。 - 工具执行结果追加:若
AiMessage包含工具调用,ToolExecutor执行完毕后生成ToolExecutionResultMessage,同样调用ChatMemory.add(toolResultMessage)。 - 异步提取关键实体到短期记忆:通过一个轻量级的实体提取器(可以是 LLM 调用,也可以是规则解析),从本轮
UserMessage和AiMessage中提取关键信息(如偏好、决策、新实体),并写入 Redis 短期记忆,刷新 TTL。 - 异步写入长期记忆:当会话结束(或达到一定轮次阈值),将短期记忆中累积的有价值实体 Embedding 后,连同元数据批量写入向量数据库。同时可以计算重要性权重。
3.3 Agent 一次完整循环中的 Memory 读写时序图
sequenceDiagram
participant User
participant Agent
participant WorkMem as 工作记忆
participant ShortMem as 短期记忆(Redis)
participant LongMem as 长期记忆(向量库)
participant LLM
User->>Agent: 输入“帮我查订单12345”
Note over Agent,LongMem: == 阶段1:读 - 组装上下文 ==
Agent->>WorkMem: 读取当前消息列表(历史对话)
WorkMem-->>Agent: List<ChatMessage>
Agent->>ShortMem: 获取会话缓存(偏好/临时实体)
ShortMem-->>Agent: {orderId: null, pref: "微信"}
Agent->>LongMem: Embedding(当前意图) + 语义检索
LongMem-->>Agent: [“用户偏好:靠窗座位”,“默认支付:微信”]
Agent->>Agent: 组装 SystemMessage + 历史消息 + 用户输入
Agent->>LLM: generate(assembledMessages)
Note over Agent,LongMem: == 阶段2:写 - 更新记忆 ==
LLM-->>Agent: AiMessage("正在查询订单12345...")
Agent->>WorkMem: add(aiMessage)
alt 有工具调用
Agent->>Agent: 执行“查询订单”工具
Agent->>WorkMem: add(ToolExecutionResultMessage)
end
Agent->>Agent: 提取实体 {orderId: "12345"}
Agent->>ShortMem: 缓存实体与偏好,刷新TTL
opt 会话结束或阈值触发
Agent->>LongMem: Embedding(实体) + Insert(向量+元数据)
end
Agent->>User: 回复查询结果
图2说明:
- 图表主旨概括:展示一次 Agent 循环中,Memory 系统如何分别在 LLM 调用前执行“读”操作以组装上下文,在调用后执行“写”操作以持久化新信息。
- 逐层/逐元素分解:读取阶段从左到右依次查询三层记忆;写入阶段首先将 LLM 响应及工具结果写入工作记忆,然后异步地向下传递至短期记忆和长期记忆。
- 设计原理映射:这完全遵循了 CPU 缓存读写策略——计算前从 L3/L2 加载数据到 L1(组装上下文),计算后将修改的数据写回(Write-back)。短期记忆可看作是带缓存的存储,长期记忆是后备存储。
- 工程联系与关键结论:读写分离是 Memory 设计的核心原则。读操作必须同步完成以保证 LLM 获得完整上下文;写操作(特别是长期记忆写入)应异步化,避免增加用户感知延迟。在 Java 实现中,长期记忆的写入可借助
@Async或CompletableFuture实现。
3.4 三层记忆的信息流动图
flowchart LR
Input["用户输入"] --> WM_In["注入工作记忆"]
WM_In --> WM["工作记忆<br/>(当前对话上下文)"]
WM --> Extract["实体提取"]
Extract --> SM["短期记忆<br/>(会话缓存, Redis)"]
SM --> Embed["Embedding 向量化"]
Embed --> LM["长期记忆<br/>(向量库, Milvus)"]
LM --> Retrieve["语义检索"]
Retrieve --> WM_In
style WM fill:#f9f,stroke:#333
style SM fill:#bbf,stroke:#333
style LM fill:#bfb,stroke:#333
图3说明:
- 图表主旨概括:描绘信息从用户输入开始,逐级沉淀到长期记忆,并在新会话中反向流动回工作记忆的完整路径。
- 逐层/逐元素分解:用户输入首先进入工作记忆形成对话上下文;经过实体提取,有价值的信息被缓存在短期记忆;会话结束后经 Embedding 加工存入长期记忆;新会话到来时,通过语义检索从长期记忆中拉取相关信息,重新注入工作记忆。
- 设计原理映射:这是一个典型的 Data Pipeline,结合了流处理与批处理的思想。短期记忆作为缓冲区,避免了对向量库的频繁写入;长期记忆作为持久化层,保证了跨会话的知识复用。
- 工程联系与关键结论:“信息生命周期管理” 是 Memory 工程的要点。开发者必须定义清楚:哪些信息只在一次对话中有效?哪些信息在一个会话中有效?哪些信息值得永久保留?这直接决定了实体提取的策略、TTL 的设置以及 Embedding 的时机。
4. 记忆裁剪策略:窗口滑动、Token 计数、摘要压缩的源码行为与适用场景
工作记忆是受 Token 限制的。当对话持续进行,List<ChatMessage> 的 Token 数量逼近或超过模型限制时,必须进行裁剪。三种主流策略各有优劣,且在 LangChain4j 中有直接实现。
4.1 窗口滑动(Message Window)
原理:保留最近 N 条消息,超出时直接丢弃最早的消息。
LangChain4j 源码行为:
MessageWindowChatMemory 内部维护一个 LinkedList<ChatMessage>。在 add(ChatMessage message) 方法中,如果 size() > maxMessages,则调用 removeFirst() 丢弃队头。
// MessageWindowChatMemory 关键逻辑简化
public class MessageWindowChatMemory implements ChatMemory {
private final LinkedList<ChatMessage> messages = new LinkedList<>();
private final int maxMessages;
@Override
public void add(ChatMessage message) {
messages.add(message);
if (messages.size() > maxMessages) {
messages.removeFirst(); // 丢弃最早的消息
}
}
// ...
}
- 优点:实现极其简单,O(1) 性能,无需额外 Tokenizer 或 LLM 调用。
- 缺点:N 太小可能丢失关键上下文。例如用户在第 1 轮说“我是VIP”,当
maxMessages=10而对话进行到第 12 轮时,这条身份信息已被丢弃,LLM 将“失忆”。 - 适用场景:短对话(<10 轮)、对早期上下文不敏感的场景,如简单的问答或闲聊。
4.2 Token 计数裁剪(Token Window)
原理:按 Token 数量精确限制,超出时从最早的消息开始逐条删除,直到总 Token 数 ≤ 设定的 maxTokens。
LangChain4j 源码行为:
TokenWindowChatMemory 依赖 Tokenizer 组件(如 OpenAiTokenizer)。在 add() 之后,调用 Tokenizer.estimateTokenCount(messages) 计算总 Token 数,若超出 maxTokens,则执行 trim() 从头部删除。
// TokenWindowChatMemory 关键逻辑简化
public class TokenWindowChatMemory implements ChatMemory {
private final List<ChatMessage> messages = new ArrayList<>();
private final Tokenizer tokenizer;
private final int maxTokens;
@Override
public void add(ChatMessage message) {
messages.add(message);
int tokenCount = tokenizer.estimateTokenCount(messages);
while (tokenCount > maxTokens && !messages.isEmpty()) {
messages.remove(0); // 移除最早的消息
tokenCount = tokenizer.estimateTokenCount(messages);
}
}
// ...
}
- 优点:精确控制 Token 消耗,避免超出模型限制导致的调用失败或高成本。
- 缺点:需要一个可靠的
Tokenizer来估算 Token 数。简单的消息裁剪仍可能丢失关键信息。 - 适用场景:需要严格控制 Token 消耗的生产环境。这是推荐作为默认基线的策略。
4.3 摘要压缩(Summarization)
原理:当消息列表 Token 超限时,将最早的一部分消息通过 LLM 压缩成一段文本摘要,摘要 + 最近 N 条消息构成新的工作记忆。
LangChain4j 行为:
SummarizingChatMemory 内部持有一个 ChatMemory(用于存储最近消息)和一个摘要文本。当添加消息导致超限时,它会取出部分消息,调用 LLM 生成摘要,然后将旧的摘要与新生成的摘要合并(或替换)。
- 优点:保留早期对话的语义精华,不会像窗口裁剪那样彻底丢失。LLM 可以从摘要中了解历史脉络。
- 缺点:每次裁剪需要额外调用一次 LLM,增加延迟和费用,且摘要质量依赖 LLM 能力。
- 适用场景:长对话且需保留早期上下文语义的场景,如客服工单处理、复杂决策任务。
4.4 三种记忆裁剪策略对比图
flowchart TD
subgraph 策略1[窗口滑动]
Win_Input["消息1..消息N"] --> Win_Check{"size > N?"}
Win_Check -->|是| Win_Remove["removeFirst()"]
Win_Remove --> Win_Output["保留最近 N 条"]
Win_Check -->|否| Win_Output
end
subgraph 策略2[Token 计数裁剪]
Tok_Input["消息1..消息N"] --> Tok_Count["estimateTokenCount()"]
Tok_Count --> Tok_Check{"tokenCount > maxTokens?"}
Tok_Check -->|是| Tok_Remove["messages.remove(0)"]
Tok_Remove --> Tok_Count
Tok_Check -->|否| Tok_Output["保留 Token ≤ maxTokens 的消息"]
end
subgraph 策略3[摘要压缩]
Sum_Input["早期消息"] --> Sum_LLM["调用 LLM 生成摘要"]
Sum_LLM --> Sum_Merge["摘要 + 最近消息"]
Sum_Merge --> Sum_Output["新工作记忆"]
end
图4说明:
- 图表主旨概括:对比三种记忆裁剪策略的核心工作流程。
- 逐元素分解:窗口滑动通过计数
maxMessages简单裁剪;Token 计数通过Tokenizer循环检查 Token 量进行精确裁剪;摘要压缩引入 LLM 调用,将历史转化为摘要文本。 - 设计原理映射:这三种策略在 精确度、计算开销与语义保留度 三维上存在权衡。窗口滑动计算开销最低但语义保留度最差;摘要压缩语义保留度最高但计算开销(额外 LLM 调用)最大;Token 计数在精确控制开销与中等语义保留间取得了最佳工程平衡。
- 工程联系与关键结论:生产环境推荐使用 Token 计数裁剪作为基线,并叠加以摘要压缩或实体记忆的混合策略。LangChain4j 的
TokenWindowChatMemory可以直接作为默认实现。
4.5 适用场景决策表
| 策略 | 原理 | 实现复杂度 | Token 精确度 | 语义保留度 | 额外 LLM 调用 | 适用场景 |
|---|---|---|---|---|---|---|
| 窗口滑动 | 保留最近 N 条 | 极低 | 低 | 低 | 无 | 短对话、闲聊 |
| Token 计数裁剪 | 按 Token 数精确截断 | 中(需 Tokenizer) | 极高 | 中 | 无 | 生产环境基线 |
| 摘要压缩 | LLM 压缩早期对话 | 高 | 中(摘要占用 Token) | 高 | 有(每次裁剪) | 长对话、需保留全貌 |
5. 混合记忆管理:窗口+摘要、实体+向量、时间衰减与重要性固化
单一的裁剪策略或记忆层往往无法满足复杂场景。混合模式通过组合多种技术,实现对记忆的精细化管理。
5.1 窗口+摘要结合
这是最直观的混合策略:工作记忆 = 最近 N 条完整消息 + 早期对话的文本摘要。
- 实现:使用 Token 计数裁剪保留最近约 4000 Token 的原始消息。当要丢弃早期消息时,不直接扔掉,而是将它们送入一个“摘要生成器”。摘要生成器维护一个累积摘要,每次有新消息被移出窗口,就更新摘要(例如“用户询问了订单状态,表示对延迟不满,客服承诺补偿”)。最终组装上下文时,将这段摘要作为
SystemMessage的一部分放在最前面。 - 效果:保留了最近对话的完整措辞与细节,同时通过摘要保留了对更早历史的粗略记忆,大幅缓解了信息丢失问题。
5.2 实体记忆+向量检索结合
这个策略直接打通了短期与长期记忆。
- 实现:每轮对话后,异步用一个轻量级 LLM 调用或规则引擎提取关键实体,如“用户偏好:靠窗座位”、“订单号:12345”、“决策:同意退款”。这些实体被 Embedding 后存入向量库(Milvus)。
- 检索:下次新会话开始时,将用户的初始意图(如“我想订机票”)Embedding 后,在向量库中检索 Top-K 相似实体。检索结果(如“历史偏好:靠窗座位,常用支付:微信支付”)与元数据一同作为上下文注入新会话的工作记忆。
- 优势:跨会话记忆可以通过语义相似度被精准召回,而不依赖于死板的时间窗口。即使“靠窗座位”这个偏好是三个月前表达的,只要查询意图相关,就能被检索出来。
5.3 时间衰减与重要性固化
模拟人脑的遗忘曲线,避免记忆库无限膨胀且陈旧记忆占据检索结果头部。
- 时间衰减:为每条长期记忆设置一个权重,例如
weight = 1 / (1 + days_since_creation)。在检索排序时,将原始向量相似度与时间权重相乘,使得新记忆更易被召回。 - 重要性固化:那些被频繁检索并使用的记忆,应提高其重要性权重,甚至达到不随时间衰减的“固化”状态。这可以通过一个简单的计数器实现:每次该记忆被检索并实际注入上下文,
importance += 1。当importance超过阈值,时间衰减因子失效。 - 技术实现:在 Milvus 中,可以将
weight或importance作为标量字段存储,在搜索时结合向量相似度与标量过滤进行混合排序。
这整套机制——窗口+摘要保障近期细节,实体+向量保障长期相关性,时间衰减管理信息生命周期——构成了一个健壮且可扩展的 Agent 记忆系统。
6. LangChain4j 代码骨架拆解:ChatMemory 接口体系与三层记忆的代码映射
LangChain4j 为 Memory 提供了简洁而强大的抽象,直接映射到我们讨论的理论。
6.1 ChatMemory 接口体系类图
classDiagram
class ChatMemory {
<<interface>>
+List~ChatMessage~ messages()
+void add(ChatMessage message)
+void clear()
}
class MessageWindowChatMemory {
-int maxMessages
-LinkedList~ChatMessage~ messages
+static MessageWindowChatMemory withMaxMessages(int)
}
class TokenWindowChatMemory {
-int maxTokens
-Tokenizer tokenizer
-List~ChatMessage~ messages
+static TokenWindowChatMemory withMaxTokens(int, Tokenizer)
}
class SummarizingChatMemory {
-ChatMemory memory
-String summary
-Summarizer summarizer
}
class ChatMemoryStore {
<<interface>>
+List~ChatMessage~ getMessages(Object memoryId)
+void updateMessages(Object memoryId, List~ChatMessage~ messages)
+void deleteMessages(Object memoryId)
}
ChatMemory <|-- MessageWindowChatMemory
ChatMemory <|-- TokenWindowChatMemory
ChatMemory <|-- SummarizingChatMemory
ChatMemory --> ChatMemoryStore : 可选的持久化后端
图5说明:
- 图表主旨概括:展示 LangChain4j 中
ChatMemory的核心接口及其主要实现,以及用于持久化的ChatMemoryStore。 - 逐元素分解:
ChatMemory定义了记忆的基础契约messages()与add()。MessageWindowChatMemory与TokenWindowChatMemory分别是窗口滑动与 Token 计数裁剪的具体实现。ChatMemoryStore负责将消息列表持久化到外部存储(如 Redis)。 - 设计原理映射:这是一种典型的 策略模式 + 桥接模式 的应用。
ChatMemory是抽象策略,不同的裁剪方式是具体策略;ChatMemoryStore作为桥接,将记忆内容与存储介质解耦,开发者可以选择内存存储或 Redis 持久化。 - 工程联系与关键结论:理解这个类图,就能快速在代码中落地任何记忆策略。 你可以组合
TokenWindowChatMemory与 RedisChatMemoryStore,获得一个生产可用的、支持持久化与精确 Token 控制的记忆组件。
6.2 核心接口与实现代码演示
创建 MessageWindowChatMemory
// 保留最近 10 条消息的工作记忆
ChatMemory memory = MessageWindowChatMemory.withMaxMessages(10);
// 使用方式:
memory.add(userMessage);
memory.add(aiMessage);
List<ChatMessage> history = memory.messages();
创建 TokenWindowChatMemory
// 需要 OpenAI Tokenizer(依赖 openai4j)
OpenAiTokenizer tokenizer = new OpenAiTokenizer(GPT_4_O_MINI);
// 限制 Token 数为 4000
ChatMemory memory = TokenWindowChatMemory.withMaxTokens(4000, tokenizer);
memory.add(userMessage);
// add 后内部自动进行 token 估算与裁剪
ChatMemoryStore 的 Redis 实现示例
public class RedisChatMemoryStore implements ChatMemoryStore {
private final RedisClient redisClient;
private final Duration ttl = Duration.ofMinutes(30);
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String json = redisClient.get("memory:" + memoryId);
if (json == null) return new ArrayList<>();
// 反序列化 JSON 字符串为 List<ChatMessage>
return deserialize(json);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
String json = serialize(messages); // 序列化消息列表为 JSON
redisClient.set("memory:" + memoryId, json);
redisClient.expire("memory:" + memoryId, ttl); // 设置会话 30 分钟过期
}
}
向量化长期记忆伪代码
// 写入长期记忆
EmbeddingModel embeddingModel = ...; // 例如 OpenAiEmbeddingModel
MilvusClient milvusClient = ...;
String userPreference = "用户偏好:靠窗座位,常用支付:微信";
Embedding vector = embeddingModel.embed(userPreference).content(); // float[]
// 构造元数据
Map<String, Object> fields = new HashMap<>();
fields.put("userId", "user_001");
fields.put("entityType", "preference");
fields.put("entityValue", userPreference);
fields.put("timestamp", System.currentTimeMillis());
milvusClient.insert("long_term_memory", vector, fields);
// 检索长期记忆
String query = "我想订一张机票";
Embedding queryVector = embeddingModel.embed(query).content();
SearchResult result = milvusClient.search("long_term_memory", queryVector, 5);
// 将检索到的记忆实体注入 SystemMessage
这些骨架代码直接体现了三层记忆的工程映射:TokenWindowChatMemory 负责工作记忆,RedisChatMemoryStore 将工作记忆和短期状态持久化,向量数据库操作则实现了长期记忆。
7. 与前后系列的衔接
本文作为系列第 4 篇,在知识体系中起着承上启下的关键作用。
- 关联前文(第 3 篇:LLM 深度):LLM 的 “缺乏持久状态” 与 “上下文窗口限制” 是 Memory 存在的直接原因。工作记忆解决了“无状态”问题(为每次调用提供完整上下文),短期记忆缓解了“窗口限制”(避免全部历史塞入窗口),长期记忆解决了“跨会话”问题。
- 关联前文(第 2 篇:四要素架构):在第 2 篇的 Agent 信息流循环时序图中,Memory 的读写时机已在每一步标注。本文是对这些时机的深度展开——详细剖析了何时读、读什么、从哪层读,以及何时写、写什么、写到哪层。
- 关联后续(第 5 篇:Tools 深度):Memory 与 Tools 紧密协作。工具执行的结果(
ToolExecutionResultMessage)通过 Memory 写入工作记忆,成为 LLM 后续推理的上下文。同时,长期记忆中的用户偏好可以指导 Tools 的参数填充(如“上次用的是微信支付,这次默认选微信”),详见第 5 篇。 - 关联后续(系列四第 4 篇:Agent 记忆系统实战):本文建立了三层记忆的理论模型和设计原则,系列四第 4 篇将用完整的 Java 代码实现窗口/Token/摘要/向量化记忆,并提供 Redis 持久化与 Milvus 向量检索的生产级配置,实现从理论到代码的全链路落地。
8. 面试高频专题
以下问题覆盖 Agent Memory 的核心原理与设计,遵循 ① 一句话回答 → ② 详细解释 → ③ 多角度追问 → ④ 加分回答 结构。
Q1. Agent 为什么需要 Memory?Memory 是如何为无状态的 LLM 提供“状态假象”的?
① 一句话回答 Agent 需要 Memory 来补偿 LLM 每次调用的无状态特性;Memory 通过在每轮调用前将完整的历史上下文组装进消息列表,使 LLM “看到”连续对话,从而产生记忆的假象。
② 详细解释
LLM 的 generate 接口每次独立执行,不会自动记住上一轮的输入或输出。Memory 系统在后台维护了一个不断增长的 List<ChatMessage>。用户每次输入,Memory 便将之前所有的对话历史、工具调用结果以及检索到的用户知识拼接到请求中。这样,LLM 处理时的上下文就包含了之前的全部相关交互,能够在回复中体现出连续性。
③ 多角度追问
- 追问架构:如果不用 Memory,仅通过前端将历史消息每次发回,有什么缺点?前端无法持久化工具调用结果和长期用户画像,且 Token 消耗不可控。
- 追问性能:每次都传全量历史消息,网络和算力开销如何控制?这正是裁剪策略要解决的问题,Token 计数裁剪可以精确控制。
- 追问安全:Memory 中可能包含敏感信息,如何保证多租户隔离?通过
memoryId(如userId+sessionId)严格隔离,且在 Redis/数据库中实施加密存储。
④ 加分回答 从设计模式看,Memory 相当于 Agent 的 Memento 模式 + 装饰器模式。它负责保存对话状态(备忘录),并在 LLM 调用前装饰请求(装饰)。同时也与 HTTP Session 的类比呼应了 Web 工程中的状态管理思维。
Q2. Agent 的三层记忆模型是什么?工作记忆、短期记忆、长期记忆分别存储什么?它们的读写时机是怎样的?
① 一句话回答 三层记忆分别是工作记忆(LLM 直接看到的上下文窗口)、短期记忆(会话内跨调用共享的状态缓存)和长期记忆(跨会话持久化的知识库);工作时 LLM 调用前读取三层组装上下文,调用后分别写入更新。
② 详细解释
- 工作记忆:存储
List<ChatMessage>,包含系统提示、历史对话、工具结果。每轮调用前组装,调用后追加新消息。 - 短期记忆:存储会话级键值对,如用户偏好、实体。通过 Redis 实现,TTL 控制生命周期。写入在每轮实体提取后异步进行,读取在下一轮组装时发生。
- 长期记忆:存储 Embedding 向量和结构化元数据,持久化在向量数据库中。写入在会话结束时触发,读取在新会话开始时通过语义检索加载。
③ 多角度追问
- 追问区别:短期记忆和传统缓存有什么不同?它管理的是对话上下文中的状态,而不是简单的 DB 查询结果。需要配合 TTL 和对话逻辑过期。
- 追问一致性:如果短期记忆中的偏好与长期记忆冲突怎么办?通常以会话内短期记忆为准(最新表达意愿),同时可触发长期记忆更新。
- 追问故障:Redis 故障导致短期记忆丢失怎么办?会话会退化为仅依赖工作记忆,体验下降但不会崩溃。长期记忆仍是完整的。
④ 加分回答 三层模型映射到 CPU 缓存体系:L1 工作记忆(寄存器速度,极小),L2 短期记忆(内存速度,中等),L3 长期记忆(磁盘/数据库速度,巨大)。这种类比帮助工程师快速理解容量-速度权衡。
Q3. 记忆裁剪策略有哪些?窗口滑动、Token 计数裁剪、摘要压缩分别如何工作?什么场景选哪种?
① 一句话回答 三种主要策略为窗口滑动(保留最近 N 条)、Token 计数裁剪(按 Token 数精确限制)和摘要压缩(将旧消息压缩为摘要);生产环境基线推荐 Token 计数,长对话且需保留历史语义时叠加摘要压缩。
② 详细解释
- 窗口滑动:
maxMessages=10,超过则removeFirst(),简单粗暴。 - Token 计数:通过
Tokenizer.estimateTokenCount()精确控制至maxTokens,超出则从头部删除。 - 摘要压缩:LLM 将早期消息生成摘要,用摘要替换原始消息,保留语义但增加一次 LLM 调用。 选择时需权衡上下文重要性、Token 预算和延迟成本。
③ 多角度追问
- 追问 Tokenizer:Token 估算不准确会有什么后果?可能导致实际 Token 超限调用失败,因此需选用与模型匹配的 Tokenizer。
- 追问摘要质量:摘要可能遗漏关键信息(如订单号),如何补偿?可与实体记忆结合,将关键实体硬性注入系统消息。
- 追问混合使用:如何组合这些策略?可使用 Token 窗口保留最近 4K Token,超出部分送入摘要生成器,形成“窗口+摘要”混合。
④ 加分回答 MemGPT 论文提出的“虚拟内存管理”思想进一步扩展了记忆裁剪,将上下文分为主上下文(类似工作记忆)和外存(类似长期记忆),通过函数调用实现数据换入换出,是更复杂的记忆管理方案。
Q4. 如何设计一个跨会话的用户偏好记忆系统?用户上次说“我喜欢靠窗座位”,下次订机票时如何自动推荐?
① 一句话回答 利用实体记忆+向量检索:将用户偏好提取、Embedding 后存入向量库;下次会话时,用当前意图向量检索相似历史偏好,注入系统消息指导 LLM 推荐。
② 详细解释
- 写入路径:在对话中通过 LLM 或规则提取“偏好:靠窗座位”实体 →
embeddingModel.embed(实体文本)→milvusClient.insert("preferences", vector, metadata)。 - 读取路径:新会话“我想订机票”到来 → embed 该意图 →
milvusClient.search("preferences", queryVector, topK=5)→ 返回“靠窗座位” → 拼接进SystemMessage:“已知用户偏好:靠窗座位” → LLM 自动推荐靠窗选项。
③ 多角度追问
- 追问冲突处理:用户偏好可能变化(上次靠窗,这次却想要靠过道),系统如何响应?应以当前会话的明确声明为准,并将新偏好写入,旧偏好权重衰减。
- 追问隐私:偏好数据属于敏感信息,如何合规?必须支持用户查看、删除偏好数据,底层存储加密。
- 追问冷启动:新用户没有长期记忆怎么办?可依赖短期记忆快速积累,或使用默认画像。
④ 加分回答 可与推荐系统结合,不仅检索用户显式偏好,还可根据行为(如多次选择靠窗)隐式推断,并存储为加权偏好向量,提升推荐准确性。
Q5. 短期记忆和长期记忆的区别是什么?什么数据应该放短期记忆,什么数据应该放长期记忆?
① 一句话回答 短期记忆是会话生命周期内的临时状态缓存(如当前订单ID),长期记忆是跨会话持久化的知识(如用户永久偏好);区别在于生命周期、存储介质和检索方式。
② 详细解释
- 短期记忆:存储于 Redis,有 TTL,会话结束消失。存放临时信息:当前意图、本轮已提取实体、中间推理结果。类比 Web 应用中的
HttpSession。 - 长期记忆:存储于向量库/关系库,持久保存。存放稳定信息:用户画像、历史偏好、经验教训。类比数据库中的用户表。
③ 多角度追问
- 追问转化:什么时机短期记忆应转化为长期记忆?会话结束时,通过实体重要性评估,重要性高的实体(如明确表达的偏好)写入长期记忆。
- 追问数据量:短期记忆撑爆 Redis 怎么办?设置严格的 TTL 和内存上限,配合 LRU 淘汰。
- 追问一致性:如何保持短期与长期偏好一致?以短期(最新)为准,长期作为补充和召回源。
④ 加分回答 这相当于计算机系统中的 内存与硬盘。内存(短期)存取快但易失,硬盘(长期)持久但速度慢。操作系统通过分页机制(检索)将硬盘数据映射到内存,Memory 正是通过检索将长期记忆注入工作记忆(内存)。
Q6. LangChain4j 的 MessageWindowChatMemory 和 TokenWindowChatMemory 有什么区别?源码行为是怎样的?
① 一句话回答
前者按消息条数限制(maxMessages),后者按 Token 总数限制(maxTokens);MessageWindowChatMemory 直接操作 LinkedList 的 removeFirst(),TokenWindowChatMemory 则依赖 Tokenizer 估算并循环删除头部消息。
② 详细解释
见第 4 节源码分析。MessageWindowChatMemory 实现简单,适合对 Token 不敏感的场景;TokenWindowChatMemory 精确控制成本,适合生产。源码中后者会持续估算并删除,直至 Token 数满足要求,可能导致一次性删除多条消息。
③ 多角度追问
- 追问性能:
TokenWindowChatMemory的循环估算在消息量巨大时是否成为瓶颈?通常工作记忆消息数量有限(<50条),每次估算性能开销可忽略。若消息极多,可优化为批量估算。 - 追问 Tokenizer:如果切换模型(GPT 到 Claude),Tokenizer 是否需要更换?是的,必须使用对应模型的 Tokenizer,否则估算不准。
- 追问持久化:这两个实现都是内存记忆,如何持久化?通过构造时注入
ChatMemoryStore,例如 Redis Store,则在操作add时同步更新 Redis。
④ 加分回答
LangChain4j 的 ChatMemory 设计使用了策略模式。你可以轻松替换自己的实现,例如实现一个基于时间戳的动态裁剪策略,而无需修改 Agent 主逻辑。
Q7. Memory 如何与 Tools 和 Planning 协作?Memory 在 Agent 的 ReAct 循环中扮演什么角色?
① 一句话回答 Memory 为 Tools 的执行结果和 Planning 的中间推理提供存储与上下文支撑;在 ReAct 循环中,Memory 负责在观察(Observation)之后、思考(Thought)之前,将工具结果写入工作记忆,使 LLM 能基于完整历史进行下一步推理。
② 详细解释 在 ReAct(Reasoning + Acting)循环中:
- 用户输入→读取 Memory 组装上下文。
- LLM 思考(Thought)并决定行动(Action,工具调用)。
- 工具执行,返回结果(Observation)。
- Memory 将工具调用与结果成对写入工作记忆。
- 下一轮循环开始时,Memory 提供包含工具结果的完整上下文,LLM 据此继续思考。 长期记忆还可以提供历史成功 Planning 的轨迹,作为 Few-Shot 示例辅助 LLM 制定计划。
③ 多角度追问
- 追问状态膨胀:多步工具调用会快速撑大工作记忆,如何控制?这正是 Token 裁剪策略的用武之地,对于旧工具结果可用摘要压缩。
- 追问错误纠正:工具返回错误信息,Memory 如何帮助恢复?LLM 可以在看到历史工具错误时调整参数重试,Memory 保留错误轨迹以供参考。
- 追问并行工具调用:多个工具结果同时返回,写入顺序如何?应保持调用顺序写入,保证逻辑连贯。
④ 加分回答 这体现了 Blackboard 架构 的思想。Memory(尤其是工作记忆)就是黑板,各个组件(LLM、Tools)都在黑板上读写信息,协同解决问题。
Q8. Memory 的“时间衰减”和“重要性固化”是什么?如何实现?
① 一句话回答 时间衰减模拟遗忘曲线,让旧的长期记忆权重随天数降低;重要性固化则让被频繁引用的记忆权重提升甚至免于衰减;可通过在向量库检索排序时结合时间因子和重要性计数实现。
② 详细解释
- 时间衰减:为每条记忆存储时间戳,检索得分 = 向量相似度 ×
1/(1+天数)。这意味着同样相似度下,越新的记忆排位越前。 - 重要性固化:为记忆添加一个
importance字段。每次该记忆被检索并用于回答,importance++。当importance超过阈值(如 5),衰减因子便固定为 1,不再随时间降低。
③ 多角度追问
- 追问实现位置:这些逻辑在哪里执行?通常在 Application 层,从向量库获取候选后、组装上下文前进行重排序。
- 追问动态衰减:如果用户偏好突然改变(不再喜欢靠窗),如何加速旧记忆衰减?可以显式降低其
importance或标记为deprecated。 - 追问数据库支持:Milvus 如何支持这种混合排序?Milvus 支持 scalar filtering,可以在向量检索后结合 metadata 字段在应用层重排序,或使用带标量加权的向量距离计算。
④ 加分回答 这与认知心理学中的 艾宾浩斯遗忘曲线 和 记忆的巩固理论 高度一致。工程上借鉴了缓存替换策略(如 LRU 结合 LFU),LFU 部分即重要性固化。
Q9. (系统设计题)设计一个智能客服 Agent 的记忆系统
需求:
- 记住当前会话的对话上下文(最近 10 轮)。
- 记住用户在本次会话中提到的所有诉求(如“我的订单 12345 还没发货”)。
- 跨会话记住用户的偏好(如“我喜欢微信支付”“我的常用地址是北京”)。
请给出: ① 三层记忆的选型与配置。 ② 一次“查询订单状态”的完整 Memory 读写时序图。 ③ 记忆裁剪策略的选择与理由。 ④ 跨会话偏好记忆的写入与检索流程。 ⑤ 架构图。
答案:
① 三层记忆选型与配置
| 记忆层 | 存储介质与配置 | 参数 |
|---|---|---|
| 工作记忆 | TokenWindowChatMemory + Redis ChatMemoryStore | maxTokens=4096(GPT-4o mini),使用 OpenAiTokenizer;Redis TTL=30min |
| 短期记忆 | Redis,键为 session:{sessionId},值为 JSON 对象 {诉求列表,临时偏好} | TTL=30min,会话续期 |
| 长期记忆 | Milvus 向量库,Collection user_memory 字段 userId, entityType, entityValue, embedding, timestamp, importance | 索引类型 IVF_FLAT,Top-K=5 检索 |
② “查询订单状态”的完整 Memory 读写时序图
sequenceDiagram
participant U as 用户
participant A as Agent
participant WM as 工作记忆
participant SM as 短期记忆(Redis)
participant LM as 长期记忆(Milvus)
participant LLM as LLM
U->>A: “我的订单12345还没发货”
Note over A,LM: 读阶段
A->>WM: messages()
WM-->>A: 前序对话(若有)
A->>SM: get session:{id}
SM-->>A: {诉求: []}
A->>LM: embed("查询订单") → search
LM-->>A: [偏好:微信支付, 常用地址:北京]
A->>LLM: generate(组装消息)
LLM-->>A: ToolCall(查询订单, args: orderId=12345)
Note over A,LM: 写阶段
A->>WM: add(AiMessage含ToolCall)
A->>A: 执行查询订单工具
A->>WM: add(ToolResult “状态:配送中”)
A->>SM: set session:{id} 诉求列表追加“订单12345未发货查询”
alt 会话结束
A->>LM: embed(“偏好:微信支付, 常用地址:北京”) + insert
end
A->>U: “您的订单正在配送中,预计明天到达”
③ 记忆裁剪策略的选择与理由
选择 Token 计数裁剪(TokenWindowChatMemory) 作为工作记忆基线,maxTokens=4096。理由:
- 客服对话可能产生大量工具调用结果,Token 消耗快,必须精确控制避免超限。
- 客户诉求往往集中在最近交互中,窗口滑动(保留最近 10 轮)基本可覆盖,但 Token 计数更精确。
- 当检测到工作记忆将被大幅裁剪时,可辅助摘要压缩:将被移除的消息压缩为“客服会话摘要”存入短期记忆,以备用户回溯时使用。
④ 跨会话偏好记忆的写入与检索流程
写入:在会话中识别到明确的偏好表述(如“微信支付更方便”)→ 实体提取器提取为 preference: 微信支付 → 会话结束时批量 Embedding → 插入 Milvus,元数据 {userId: "u1", entityType: "preference", entityValue: "微信支付", timestamp: now, importance: 1}。
检索:新会话“我要下单”→ Embed 意图 → Milvus 检索 userId=u1 下的 Top-5 记忆 → 返回“偏好:微信支付” → 注入 SystemMessage:“已知用户偏好:支付方式-微信支付” → LLM 在生成支付选项时自动优先微信。
⑤ 架构图
flowchart TB
classDef agentSub fill:#f8fafc,stroke:#94a3b8,stroke-width:1.5px
classDef wmSub fill:#f0f4ff,stroke:#93a3d3,stroke-width:1.5px
classDef smSub fill:#fef9f0,stroke:#c4a77d,stroke-width:1.5px
classDef lmSub fill:#fdf4ff,stroke:#c4b0d0,stroke-width:1.5px
classDef agentNode fill:#f1f5f9,stroke:#334155,stroke-width:1.5px,color:#1e293b
classDef wmNode fill:#dbeafe,stroke:#2563eb,stroke-width:1.5px,color:#1e3a8a
classDef smNode fill:#fef3c7,stroke:#d97706,stroke-width:1.5px,color:#92400e
classDef lmNode fill:#ede9fe,stroke:#8b5cf6,stroke-width:1.5px,color:#4c1d95
classDef userNode fill:#d1fae5,stroke:#10b981,stroke-width:1.5px,color:#064e3b
subgraph Agent["Agent 引擎"]
Orchestrator["协调器"]
MemoryManager["记忆管理器"]
end
subgraph WmSub["工作记忆"]
TokenWindow["TokenWindowChatMemory"]
RedisStore[("Redis ChatMemoryStore")]
end
subgraph SmSub["短期记忆"]
RedisSM[("Redis: session:{id}")]
end
subgraph LmSub["长期记忆"]
Milvus[("Milvus 向量库")]
Embedder["Embedding 模型"]
end
User(["用户"]) --> Orchestrator
Orchestrator --> MemoryManager
MemoryManager --> TokenWindow
MemoryManager --> RedisSM
MemoryManager --> Embedder --> Milvus
TokenWindow --> RedisStore
class Agent agentSub
class WmSub wmSub
class SmSub smSub
class LmSub lmSub
class Orchestrator,MemoryManager agentNode
class TokenWindow,RedisStore wmNode
class RedisSM smNode
class Milvus,Embedder lmNode
class User userNode
组件职责:
- MemoryManager:统一协调三层记忆的读写时机,执行裁剪策略和实体提取。
- TokenWindowChatMemory + RedisStore:提供持久化的窗口工作记忆,支持水平扩展。
- RedisSM:轻量会话缓存,存放临时诉求列表,避免重复解析。
- Milvus + Embedder:实现语义记忆,跨会话召回用户画像。
技术选型权衡:Redis 保证了低延迟的状态共享;Milvus 提供了高性能的向量检索,适合长期记忆;Token 窗口策略避免了 LLM 调用失败和成本失控。整个系统能够在会话级别快速响应,在跨会话级别提供深度个性化。
Agent Memory 三层模型速查表
| 记忆层 | 定义 | 存储位置 | 大小限制 | 生命周期 | 读写时机 | 管理策略 | 关联系列 |
|---|---|---|---|---|---|---|---|
| 工作记忆 (L1) | LLM 直接看到的上下文窗口 | JVM/Redis List<ChatMessage> | 模型 maxTokens(4K-128K) | 单次循环 | 循环前组装,循环后追加 | 窗口滑动/Token计数/摘要压缩 | 第2篇(四要素循环)、第5篇(Tools结果) |
| 短期记忆 (L2) | 会话内累积的状态缓存 | Redis 键值对 | 内存限制 + TTL | 用户会话 | 每轮异步写入,下一轮读取 | TTL 过期,LRU 淘汰 | 第5篇(Tools参数缓存)、系列四第4篇 |
| 长期记忆 (L3) | 跨会话持久化知识 | 向量库/关系库 | 近乎无限 | 持久化 | 会话结束写入,新会话检索 | 时间衰减,重要性固化,语义检索 | 系列四第4篇(实战实现) |
延伸阅读
- Lilian Weng, “LLM Powered Autonomous Agents” (2023.6) – Memory 章节。
- LangChain 官方文档 Memory 模块。
- LangChain4j 官方文档
ChatMemory章节。 - 《MemGPT: Towards LLMs as Operating Systems》(2023.10) – 虚拟内存管理设计。
本文从 LLM 的无状态本质出发,系统构建了 Agent 记忆的三层模型,深入分析了读写时机、裁剪策略与混合管理模式,并通过 LangChain4j 源码骨架揭示了其工程实现。掌握这套模型,你将能够像设计多级缓存系统一样,从容应对任何复杂 Agent 的记忆需求。在下一篇文章中,我们将聚焦 Agent 的第三个核心要素——Tools,详细拆解 Function Calling 协议、工具抽象与 MCP 实现,敬请期待。