一个没有记忆的 Agent每轮对话都像得了阿尔茨海默——你上一句刚说我是甲方张总下一句它问你请问怎么称呼您。本文的结论很明确Agent 的记忆不是把聊天记录全塞进 prompt而是按缓冲记忆 / 摘要记忆 / 向量记忆 / 工作记忆四层分层管理——短期靠窗口加摘要长期靠向量库加实体抽取工作记忆靠显式状态。下面我用 LangChain4j Redis 把这套东西跑通。一、为什么不能把历史全塞进 prompt新手最容易犯的错是把ListChatMessage整个往ChatClient里一丢心想有上下文不就完事了。三个坑会把你按在地上摩擦成本爆炸上下文窗口按 Token 计费一个聊了 50 轮的会话光历史就可能吃掉上万 Token而其中 80% 是无效寒暄。信号稀释模型注意力是有限的历史越长真正重要的那条指令越容易被淹没。我做过对比同样一个问题塞 20 条历史和只塞 5 条相关历史后者准确率反而高。窗口硬限制再大的上下文也是有顶的128K、200K 都有物理上限到了上限你连全塞都做不到。所以记忆的本质不是存储而是取舍什么东西该进当前上下文什么东西该沉淀下来什么东西该干脆忘掉。二、四层记忆模型我把 Agent 的记忆拆成四层从当下到永久逐层下沉用户一句话输入 │ ▼ ┌──────────────────────────────────────────────┐ │ ① 工作记忆Working Memory │ │ 当前任务目标 / 步骤 / 中间结果 / 状态 │ │ 存储进程内 Map DB 持久化断点续跑 │ └──────────────────────────────────────────────┘ │ (携带当前任务上下文) ▼ ┌──────────────────────────────────────────────┐ │ ② 短期记忆Short-term / Buffer │ │ 最近 N 条对话滑窗 / Token 窗口 │ │ 存储Redis按 chatId 分区带 TTL │ └──────────────────────────────────────────────┘ │ (摘要压缩后) ▼ ┌──────────────────────────────────────────────┐ │ ③ 摘要记忆Summary │ │ 超出窗口的历史 → 大模型压缩成摘要 │ │ 作用保留来龙去脉不丢主线 │ └──────────────────────────────────────────────┘ │ (用户画像 / 沉淀事实) ▼ ┌──────────────────────────────────────────────┐ │ ④ 长期记忆Long-term / Episodic │ │ 用户偏好 / 关键事实 / 历史结论 │ │ 存储向量库Redis / pgvector 实体抽取 │ └──────────────────────────────────────────────┘一句话区分工作记忆管现在在干啥短期记忆管刚才聊了啥摘要记忆管我们怎么走到这的长期记忆管这个人是谁。这四层职责不同存储不同更新策略也不同混在一起就会乱。三、短期 摘要记忆LangChain4j 的 ChatMemory Redis 持久化LangChain4j 把短期记忆封装成了ChatMemory接口社区版自带两种实现MessageWindowChatMemory按消息条数滑窗和TokenWindowChatMemory按 Token 预算超出自动摘要压缩。但默认的内存实现有个致命问题多实例不共享、重启就丢。生产环境必须用ChatMemoryStore把记忆落到 Redis。下面是我常用的一个实现// RedisChatMemoryStore.java —— LangChain4j 1.x Spring Boot 3.3 Redis // 作用把 Agent 对话记忆从 JVM 内存搬到 Redis支持多实例共享、重启不丢 // 依赖langchain4j、langchain4j-redis、spring-boot-starter-data-redis Component public class RedisChatMemoryStore implements ChatMemoryStore { private final StringRedisTemplate redis; private static final String KEY_PREFIX agent:memory:; private static final String FIELD messages; // TTL30 天没活跃的会话自动清理避免 Redis 无限膨胀 private static final long TTL_SECONDS 30L * 24 * 3600; public RedisChatMemoryStore(StringRedisTemplate redis) { this.redis redis; } /** 按 chatId 读回该会话的全部消息 */ Override public ListChatMessage getMessages(Object chatId) { String raw (String) redis.opsForHash().get(KEY_PREFIX chatId, FIELD); if (raw null || raw.isEmpty()) { return List.of(); } // LangChain4j 自带消息序列化工具比自己拼 JSON 稳得多 return ChatMessageSerializer.messagesFromJson(raw); } /** 每次对话后回写并刷新 TTL */ Override public void updateMessages(Object chatId, ListChatMessage messages) { String json ChatMessageSerializer.messagesToJson(messages); redis.opsForHash().put(KEY_PREFIX chatId, FIELD, json); redis.expire(KEY_PREFIX chatId, Duration.ofSeconds(TTL_SECONDS)); } Override public void deleteMessages(Object chatId) { redis.delete(KEY_PREFIX chatId); } }ChatMemoryStore这个接口就是 LangChain4j 留给你的持久化插槽——你只要实现读、写、删三个方法记忆就自动进 Redis。接下来用两种 Memory 组合短期和摘要// AgentMemoryConfig.java —— LangChain4j 1.x // 短期记忆用滑窗保最近 20 条摘要记忆用 Token 窗口超 2000 Token 自动压缩 Configuration public class AgentMemoryConfig { /** ② 短期记忆消息滑窗超出丢弃最老的成本可控 */ Bean public ChatMemory shortTermMemory(RedisChatMemoryStore store) { return MessageWindowChatMemory.builder() .maxMessages(20) .chatMemoryStore(store) .build(); } /** ③ 摘要记忆按 Token 预算裁剪超出部分交给大模型生成摘要 */ Bean public ChatMemory summaryMemory(RedisChatMemoryStore store, ChatLanguageModel model) { return TokenWindowChatMemory.builder() .maxTokens(2000) .chatLanguageModel(model) // 压缩摘要用的模型建议用便宜的 .chatMemoryStore(store) .build(); } }这里有个实战经验摘要记忆用的模型别用旗舰款。压缩历史摘要是高频调用用 DeepSeek-V3 或 GPT-4o-mini 这类便宜模型就够了我项目里这层单独配了一个轻量模型一个月能省不少钱。四、长期记忆实体抽取 向量库沉淀短期记忆解决刚才聊啥但用户说我司预算 50 万、我是甲方张总这种跨会话也得记住的事实就得进长期记忆。做法是对话中定期抽取用户事实存进向量库我用 Redis 的向量能力省一套独立组件需要时按当前话题召回。// LongTermMemoryService.java —— LangChain4j 1.x RedisEmbeddingStore // ④ 长期记忆把用户说过的事实/偏好固化进向量库对话时检索增强 Service public class LongTermMemoryService { private final EmbeddingStore embeddingStore; private final EmbeddingModel embeddingModel; public LongTermMemoryService(EmbeddingStore embeddingStore, EmbeddingModel embeddingModel) { this.embeddingStore embeddingStore; this.embeddingModel embeddingModel; } /** 把一条用户事实固化进长期记忆比如用户是甲方张总预算 50 万 */ public void remember(String userId, String fact) { Metadata metadata Metadata.from(Map.of( userId, userId, type, fact, ts, String.valueOf(System.currentTimeMillis()) )); TextSegment segment TextSegment.from(fact, metadata); Embedding embedding embeddingModel.embed(segment).content(); embeddingStore.add(embedding, segment); } /** 对话时召回与该用户相关的长期记忆注入 system prompt */ public ListString recall(String userId, String currentTopic) { Embedding query embeddingModel.embed(currentTopic).content(); ListEmbeddingMatchTextSegment matches embeddingStore.findRelevant(query, 5); // top-5 return matches.stream() .filter(m - userId.equals(m.embedded().metadata().getString(userId))) .map(m - m.embedded().text()) .toList(); } }向量库的选型上如果你的系统已经在用 Redis 做缓存直接上RedisEmbeddingStore是最省事的如果是纯 RAG 场景、数据量大再考虑 pgvector 或 Milvus。别为了看起来高级先上 Milvus运维成本不是闹着玩的。// 长期记忆存储的构建Redis 向量能力无需额外中间件 EmbeddingStore longTermStore RedisEmbeddingStore.builder() .host(127.0.0.1) .port(6379) .dimension(1536) // 与你的 Embedding 模型维度一致如 text-embedding-3-small .build();五、工作记忆显式状态别让模型凭记忆猜前三种记忆都是给模型喂上下文但凡是涉及多步任务查订单→比价→生成报告光靠 prompt 里的历史是不可靠的——模型可能跳步、可能重复执行。这类当前任务进行到哪了的信息我主张用一个显式的状态对象来管理而不是指望模型自己记// WorkingMemory.java —— 纯 Java无外部依赖JDK 17 // ① 工作记忆Agent 当前任务的便签本显式记录目标/步骤/中间结果/状态 public class WorkingMemory { private final String taskId; private final String goal; private final MapString, Object scratch new ConcurrentHashMap(); private final ListString steps new CopyOnWriteArrayList(); private volatile TaskStatus status TaskStatus.IN_PROGRESS; public WorkingMemory(String taskId, String goal) { this.taskId taskId; this.goal goal; } /** Agent 每完成一步就记一笔方便断点续跑和人工介入 */ public void recordStep(String step, Object result) { steps.add(step); scratch.put(step, result); } public Object get(String key) { return scratch.get(key); } /** 把当前工作记忆快照序列化给模型或前端便于恢复现场 */ public MapString, Object snapshot() { return Map.of( taskId, taskId, goal, goal, status, status.name(), steps, List.copyOf(steps), scratch, Map.copyOf(scratch) ); } public void done() { this.status TaskStatus.DONE; } public void needHuman(String reason) { this.status TaskStatus.NEED_HUMAN; this.scratch.put(pendingReason, reason); } enum TaskStatus { IN_PROGRESS, DONE, FAILED, NEED_HUMAN } }这个WorkingMemory我一般随任务持久化到数据库按taskId存 JSONAgent 重启后能snapshot()恢复现场——这其实就是 Day 117 要讲的长任务断点续跑的雏形今天先埋个伏笔。六、记忆的失效与清理知道该忘什么比该记什么更难四层记忆搭好了还有两个生产必做项TTL 清理会话记忆、工作记忆一定要带过期时间。我见过一个项目 Redis 里堆了 800 万条agent:memory:*全是半年前的僵尸会话就是因为忘了设 TTL上面代码里的 30 天就是干这个的。隐私与合规长期记忆里可能沉淀用户手机号、身份证号。落地前必须做脱敏并且提供用户要求删除记忆的接口直接调RedisChatMemoryStore.deleteMessages(chatId)和长期库的删除即可。这不是可选项是国内生成式 AI 管理的硬要求。七、三条落地建议先上短期记忆再谈长期记忆。绝大多数业务一个 Redis 持久化的滑窗记忆就能解决 80% 的失忆投诉。长期记忆向量库 实体抽取等你的 Agent 真的需要记住用户是谁时再加别一上来就搞全栈记忆架构。摘要记忆用的模型单独配便宜款。TokenWindowChatMemory的压缩调用很频繁用强模型是浪费。我在配置里给摘要层单独注入一个 mini 模型成本能降一个数量级。工作记忆一定要显式别迷信。凡是多步、有状态、要可恢复的任务用WorkingMemory这种状态对象硬管而不是祈祷模型在 prompt 里记住进度。模型是概率的状态机是确定的关键路径上别把确定性交给概率。记忆不是把过去塞进 prompt而是知道什么该忘、什么该记、什么时候该翻旧账。下篇Day 92我们接着 Agent 工程化往下走聊Tool Calling 工程实践工具定义 / 参数校验 / 并行调用与失败重试——记忆解决了Agent 记得住工具调用要解决的是Agent 手够得着下一篇见。