Memory:Agent 的“记忆”——三层记忆模型详解

0 阅读40分钟

概述

系列定位与引言

本文是 “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&lt;ChatMessage&gt;"]
        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>。这个组装过程遵循严格的顺序:

  1. 用户输入 UserMessage 到达
  2. 从工作记忆读取当前消息列表:包含最近的对话历史、前序工具调用结果。这部分直接通过 ChatMemory.messages() 获得。
  3. 从短期记忆读取会话累积信息:根据 sessionId 从 Redis 中获取已缓存的用户偏好、临时实体。例如,用户在前一轮中提到“我的订单 12345”,这个信息可能作为字符串缓存,此时提取并拼接到系统消息中。
  4. 从长期记忆语义检索相关记忆:将当前用户意图(或最近几轮对话摘要)通过 Embedding 模型向量化,在向量数据库中执行 ANN(近似最近邻)检索,返回 Top-K 相关的长期记忆(如“用户偏好:喜欢靠窗座位”、“历史决策:上次使用微信支付”)。
  5. 组装完整消息列表:顺序拼接 SystemMessage(包含角色定义、工具列表、以及步骤 3 和 4 检索到的记忆上下文)+ 步骤 2 的历史消息 + 当前 UserMessage
  6. 传入 LLM:调用 model.generate(assembledMessages)

3.2 写入时机(每轮 LLM 调用结束后)

LLM 返回响应后,Memory 系统需要将新信息分层写入:

  1. LLM 返回 AiMessage(可能包含文本回复或 ToolCall 请求)。
  2. 追加到工作记忆:调用 ChatMemory.add(aiMessage),将 AI 回复加入消息列表。这是保持对话连续性的最基础动作。
  3. 工具执行结果追加:若 AiMessage 包含工具调用,ToolExecutor 执行完毕后生成 ToolExecutionResultMessage,同样调用 ChatMemory.add(toolResultMessage)
  4. 异步提取关键实体到短期记忆:通过一个轻量级的实体提取器(可以是 LLM 调用,也可以是规则解析),从本轮 UserMessageAiMessage 中提取关键信息(如偏好、决策、新实体),并写入 Redis 短期记忆,刷新 TTL。
  5. 异步写入长期记忆:当会话结束(或达到一定轮次阈值),将短期记忆中累积的有价值实体 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 实现中,长期记忆的写入可借助 @AsyncCompletableFuture 实现。

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 中,可以将 weightimportance 作为标量字段存储,在搜索时结合向量相似度与标量过滤进行混合排序。

这整套机制——窗口+摘要保障近期细节,实体+向量保障长期相关性,时间衰减管理信息生命周期——构成了一个健壮且可扩展的 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()MessageWindowChatMemoryTokenWindowChatMemory 分别是窗口滑动与 Token 计数裁剪的具体实现。ChatMemoryStore 负责将消息列表持久化到外部存储(如 Redis)。
  • 设计原理映射:这是一种典型的 策略模式 + 桥接模式 的应用。ChatMemory 是抽象策略,不同的裁剪方式是具体策略;ChatMemoryStore 作为桥接,将记忆内容与存储介质解耦,开发者可以选择内存存储或 Redis 持久化。
  • 工程联系与关键结论理解这个类图,就能快速在代码中落地任何记忆策略。 你可以组合 TokenWindowChatMemory 与 Redis ChatMemoryStore,获得一个生产可用的、支持持久化与精确 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 的 MessageWindowChatMemoryTokenWindowChatMemory 有什么区别?源码行为是怎样的?

① 一句话回答 前者按消息条数限制(maxMessages),后者按 Token 总数限制(maxTokens);MessageWindowChatMemory 直接操作 LinkedListremoveFirst()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)循环中:

  1. 用户输入→读取 Memory 组装上下文。
  2. LLM 思考(Thought)并决定行动(Action,工具调用)。
  3. 工具执行,返回结果(Observation)。
  4. Memory 将工具调用与结果成对写入工作记忆
  5. 下一轮循环开始时,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 的记忆系统

需求

  1. 记住当前会话的对话上下文(最近 10 轮)。
  2. 记住用户在本次会话中提到的所有诉求(如“我的订单 12345 还没发货”)。
  3. 跨会话记住用户的偏好(如“我喜欢微信支付”“我的常用地址是北京”)。

请给出: ① 三层记忆的选型与配置。 ② 一次“查询订单状态”的完整 Memory 读写时序图。 ③ 记忆裁剪策略的选择与理由。 ④ 跨会话偏好记忆的写入与检索流程。 ⑤ 架构图。


答案

① 三层记忆选型与配置

记忆层存储介质与配置参数
工作记忆TokenWindowChatMemory + Redis ChatMemoryStoremaxTokens=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 实现,敬请期待。