AI Agent 开发实战(三):记忆不是存聊天记录,搞懂这三层记忆架构才算入门

发布时间:2026/7/24 9:37:21
AI Agent 开发实战(三):记忆不是存聊天记录,搞懂这三层记忆架构才算入门 AI Agent 开发实战三记忆不是存聊天记录搞懂这三层记忆架构才算入门这是「AI Agent 开发实战」系列的第 3 篇。上一篇讲了 LLM 调用与 Prompt 工程这一篇继续拆三大基石——记忆系统。很多人的 Agent 跑两轮就失忆或者上下文一长就 Token 爆炸根因都是没搞懂记忆该怎么设计。一、为什么 Agent 需要记忆先说一个反直觉的事实LLM 本身是没有记忆的。你每次调用/chat/completionsLLM 看到的只有你传过去的 messages 数组。它不记得上一次对话说了什么不记得用户的偏好不记得三步之前做了什么。┌──────────────────────────────────────────────────┐ │ 无记忆的 Agent │ │ │ │ 用户我叫张三 │ │ Agent你好张三 │ │ │ │ 用户我叫什么 │ │ Agent抱歉我不知道你叫什么。 │ │ │ │ 原因第二次调用时 messages 里没有第一次的内容 │ └──────────────────────────────────────────────────┘这不是 LLM “笨”而是它根本看不到。记忆系统的本质就是在每次调用 LLM 之前把相关信息塞进 messages 数组里。听起来简单但问题在于上下文窗口有限4K ~ 200K tokens不可能全塞进去什么该塞、什么不该塞、什么时候塞直接决定 Agent 的智商不同类型的记忆存储方式、检索方式、过期策略完全不同二、三层记忆架构工程上Agent 的记忆通常分为三层┌──────────────────────────────────────────────────────────┐ │ Agent 记忆架构 │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第一层短期记忆Working Memory │ │ │ │ 存在 LLM 的上下文窗口里 │ │ │ │ 内容当前对话历史、当前任务的中间状态 │ │ │ │ 生命周期单次会话 │ │ │ │ 特点LLM 直接可见但容量有限 │ │ │ └──────────────────────────────────────────────────┘ │ │ ↑↓ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第二层长期记忆Long-term Memory │ │ │ │ 存在向量数据库 / 关系数据库中 │ │ │ │ 内容用户偏好、历史决策、知识库 │ │ │ │ 生命周期跨会话持久化 │ │ │ │ 特点需要检索才能进入短期记忆 │ │ │ └──────────────────────────────────────────────────┘ │ │ ↑↓ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第三层工作记忆Scratchpad / Task Memory │ │ │ │ 存在内存 / 缓存中 │ │ │ │ 内容当前任务的执行计划、已完成步骤、待办事项 │ │ │ │ 生命周期单次任务 │ │ │ │ 特点结构化存储不直接进 LLM 上下文 │ │ │ └──────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘三层对比维度短期记忆长期记忆工作记忆存储位置LLM 上下文窗口向量数据库 / 关系库内存 / Redis存储内容对话历史用户画像、知识、历史决策执行计划、步骤状态生命周期单次会话永久或手动删除单次任务LLM 可见性直接可见需检索后注入按需注入容量小受 Token 限制大中检索方式不需要全量在上下文向量相似度 / 关键词直接读取三、短期记忆上下文窗口管理短期记忆就是 LLM 的 messages 数组。核心问题是对话越来越长Token 放不下怎么办朴素方案全量保留ListMessagemessagesnewArrayList();messages.add(newSystemMessage(systemPrompt));// 每轮对话都往里塞while(running){messages.add(newUserMessage(userInput));LlmResponserespllm.chat(messages,tools);messages.add(newAssistantMessage(resp.getContent()));}// 问题第 20 轮时messages 可能已经 50K tokens窗口爆了方案一滑动窗口截断最简单粗暴——只保留最近 N 轮对话publicclassSlidingWindowMemory{privatefinalintmaxMessages;// 保留最近多少条消息publicListMessagemanage(ListMessagemessages){if(messages.size()maxMessages){returnmessages;}// 保留 System Prompt第一条 最近 N 条ListMessageresultnewArrayList();result.add(messages.get(0));// System 一定保留result.addAll(messages.subList(messages.size()-maxMessages1,messages.size()));returnresult;}}优点简单、快、可预测。缺点丢掉的对话可能正好是 LLM 需要的。用户第 2 轮说了我偏好高股息股票到第 10 轮 Agent 就忘了。方案二摘要压缩用 LLM 把旧对话总结成一段摘要替换原始消息publicclassSummaryMemory{privateStringsummary;privatefinalintsummarizeThreshold10;// 超过 10 轮触发摘要publicListMessagemanage(ListMessagemessages){if(messages.size()summarizeThreshold){returnmessages;}// 把旧消息交给 LLM 总结ListMessageoldMessagesmessages.subList(1,messages.size()-5);StringnewSummaryllm.chat(List.of(newSystemMessage(请将以下对话总结为关键信息保留用户偏好、重要决策和未完成的事项。),newUserMessage(formatMessages(oldMessages))));// 用摘要替换旧消息summarysummary\nnewSummary;ListMessageresultnewArrayList();result.add(messages.get(0));// Systemresult.add(newSystemMessage(之前的对话摘要\nsummary));result.addAll(messages.subList(messages.size()-5,messages.size()));// 最近 5 轮returnresult;}}优点保留了关键信息Token 占用大幅降低。缺点摘要本身要调一次 LLM有成本和延迟且摘要可能丢失细节。方案三混合策略推荐实际工程中通常组合使用┌─────────────────────────────────────────────────┐ │ 上下文窗口128K │ │ │ │ System Prompt固定 2K │ │ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 对话摘要动态 1K │ │ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 最近 N 轮原文动态 10K │ │ ████████████████████░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 从长期记忆检索的相关信息动态 2K │ │ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 当前用户输入0.5K │ │ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ 预留输出空间4K │ └─────────────────────────────────────────────────┘四、长期记忆跨会话持久化短期记忆解决的是单次会话内的问题。长期记忆解决的是Agent 怎么记住跨会话的信息比如用户上次说我偏好高股息央企股——下次会话 Agent 应该记住Agent 上次帮用户分析过茅台——下次可以引用之前的结论用户问过的问题模式——Agent 可以预判需求存储方案选型方案适用场景优点缺点向量数据库语义记忆用户偏好、知识语义检索强部署复杂关系数据库结构化记忆用户画像、配置查询精确语义检索弱键值存储简单事实记忆读写快无法语义检索图数据库关系型记忆实体关联关联推理强过度设计风险工程建议从向量数据库开始80% 的场景够用了。向量记忆的核心流程写入记忆 读取记忆 ──────── ──────── 用户说了一句话 ┌──────┐ 新一轮对话开始 │ │ │ │ ▼ │ 判断 │ ───────▶│ 提取记忆 worthy │ 是否 │ 是否 │ 的信息 │ 值得 │ 需要 │ │ │ 记住 │ 历史 │ ▼ │ │ 记忆 │ Embedding └──────┘ │ 向量化 │ Yes │ Yes │ ▼ ▼ ▼ 存入向量库 查询向量库 存入向量库 │ ▼ Top-K 相关记忆 │ ▼ 注入到 messages 的 System 部分写入什么值得记住不是每句话都值得存。Agent 需要判断publicclassMemoryWriter{privatefinalLlmClientllm;privatefinalVectorStorevectorStore;publicvoidmaybeSave(StringuserMessage,StringagentResponse){// 用 LLM 判断这条对话是否包含值得记住的信息Stringjudgmentllm.chat(List.of(newSystemMessage( 判断以下对话是否包含值得长期记忆的信息。 值得记忆的用户偏好、个人事实、重要决策、任务结论。 不值得记忆的寒暄、临时性问题、已过期信息。 只回答 YES 或 NO。 ),newUserMessage(用户: userMessage\n助手: agentResponse)));if(judgment.trim().toUpperCase().startsWith(YES)){// 提取结构化记忆Stringmemoryllm.chat(List.of(newSystemMessage(将以下对话中的关键信息提取为一句简洁的记忆陈述。),newUserMessage(用户: userMessage\n助手: agentResponse)));// 向量化并存储vectorStore.add(memory,Map.of(timestamp,Instant.now().toString(),type,user_preference));}}}读取怎么找到相关记忆publicclassMemoryRetriever{privatefinalVectorStorevectorStore;privatestaticfinalintTOP_K5;publicStringretrieve(StringcurrentQuery){// 1. 把当前查询向量化// 2. 在向量库中找 Top-K 最相似的记忆ListMemoryItemmemoriesvectorStore.search(currentQuery,TOP_K);if(memories.isEmpty()){return;}// 2. 拼接成上下文StringBuildersbnewStringBuilder(相关历史记忆\n);for(MemoryItemm:memories){sb.append(- ).append(m.getContent()).append(\n);}returnsb.toString();}}一个完整的记忆增强 Agent 循环publicclassMemoryEnhancedAgent{privatefinalLlmClientllm;privatefinalMemoryWritermemoryWriter;privatefinalMemoryRetrievermemoryRetriever;privatefinalContextManagercontextManager;publicStringrun(StringuserId,StringuserInput){// 1. 从长期记忆中检索相关信息StringrelatedMemoriesmemoryRetriever.retrieve(userInput);// 2. 组装 messagesListMessagemessagesnewArrayList();messages.add(newSystemMessage(systemPrompt));if(!relatedMemories.isEmpty()){messages.add(newSystemMessage(relatedMemories));}messages.addAll(contextManager.manage(sessionHistory));messages.add(newUserMessage(userInput));// 3. 调用 LLMLlmResponserespllm.chat(messages,tools);// 4. 写入长期记忆异步不阻塞响应memoryWriter.maybeSave(userInput,resp.getContent());returnresp.getContent();}}五、工作记忆任务执行的黑板工作记忆是 Agent 在执行单个复杂任务时的草稿纸。和短期记忆的区别短期记忆是对话历史谁说了什么工作记忆是任务状态做了什么、还要做什么。┌──────────────────────────────────────────────┐ │ 工作记忆示例 │ │ │ │ 任务目标分析 600519 是否值得加仓 │ │ │ │ 执行计划 │ │ ✅ Step 1: 查询最近 30 天收盘价 │ │ ✅ Step 2: 计算 20 日均线和 MACD │ │ ⬜ Step 3: 搜索近期公司新闻 │ │ ⬜ Step 4: 综合分析给出建议 │ │ │ │ 中间结果 │ │ - 30 天均价: 1685.3 │ │ - 20 日均线: 拐头向上 │ │ - MACD: 金叉 │ │ │ │ 待办 │ │ - 需要确认近期是否有利空消息 │ └──────────────────────────────────────────────┘工作记忆的工程实现publicclassScratchpad{privateStringgoal;// 任务目标privateListTaskStepplan;// 执行计划privateMapString,Objectresults;// 中间结果privateListStringnotes;// 备注// 序列化为文本注入到 LLM 上下文publicStringtoPromptText(){StringBuildersbnewStringBuilder();sb.append(【当前任务】).append(goal).append(\n\n);sb.append(【执行计划】\n);for(TaskStepstep:plan){sb.append(step.isDone()?✅:⬜).append( Step ).append(step.getIndex()).append(: ).append(step.getDescription()).append(\n);}if(!results.isEmpty()){sb.append(\n【中间结果】\n);results.forEach((k,v)-sb.append(- ).append(k).append(: ).append(v).append(\n));}if(!notes.isEmpty()){sb.append(\n【备注】\n);notes.forEach(n-sb.append(- ).append(n).append(\n));}returnsb.toString();}// 更新步骤状态publicvoidmarkStepDone(intindex,Objectresult){plan.get(index).setDone(true);results.put(plan.get(index).getDescription(),result);}}关键点工作记忆不是全量塞进 LLM 上下文的。聪明的做法是只注入当前步骤 /- 2 步的上下文而不是整个计划。这样既能让 LLM 知道自己在哪一步又不浪费 Token。六、记忆的遗忘策略记忆不是越多越好。过期的、矛盾的、低价值的信息如果不清理反而会让 Agent 越来越蠢。遗忘策略触发条件实现TTL 过期超过设定时间每条记忆带 timestamp定期清理重要性衰减长时间未被检索命中检索时降权长期不命中则删除矛盾覆盖新记忆与旧记忆矛盾用 LLM 判断保留新的删除旧的容量限制超过最大条数按 importance score 淘汰最低的用户显式删除用户说忘掉这个按关键词或时间范围删除publicclassMemoryJanitor{privatefinalVectorStorevectorStore;privatestaticfinalintMAX_MEMORIES1000;privatestaticfinallongTTL_DAYS90;// 定期清理publicvoidcleanup(){// 1. 删除过期记忆longcutoffInstant.now().minus(TTL_DAYS,ChronoUnit.DAYS).toEpochMilli();vectorStore.deleteByMetadata(timestamp,cutoff);// 2. 如果还是太多按重要性淘汰longcountvectorStore.count();if(countMAX_MEMORIES){// 按检索命中次数排序淘汰最少的ListMemoryItemallvectorStore.findAll();all.sort(Comparator.comparingInt(MemoryItem::getHitCount));inttoDelete(int)(count-MAX_MEMORIES);for(inti0;itoDelete;i){vectorStore.delete(all.get(i).getId());}}}}七、三种记忆的协同流程把三层记忆拼在一起一次完整的 Agent 调用流程用户输入 │ ▼ ┌─────────────────────────────────────────┐ │ 1. 检索长期记忆 │ │ 用用户输入查询向量库 │ │ 拿到 Top-K 相关历史记忆 │ └──────────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 2. 读取工作记忆 │ │ 当前任务计划、已完成步骤、中间结果 │ └──────────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 3. 组装上下文 │ │ System Prompt │ │ 长期记忆摘要 │ │ 工作记忆状态 │ │ 短期记忆最近 N 轮对话 │ │ 当前用户输入 │ └──────────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 4. 调用 LLM │ └──────────────────┬──────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ 5. 更新记忆 │ │ 短期追加到对话历史 │ │ 工作更新步骤状态和中间结果 │ │ 长期异步判断是否值得持久化 │ └─────────────────────────────────────────┘八、常见踩坑坑 1把所有对话原样存向量库问题每轮对话都 Embedding 存库结果向量库里全是你好谢谢这种垃圾数据检索时噪声巨大。解法写入前先过一道 LLM 判断见上面的 MemoryWriter只存记忆 worthy的信息。坑 2检索 Top-K 固定不变问题不管什么问题都返回 Top-5简单问题信息过多干扰 LLM复杂问题信息过少不够推理。解法按相似度分数动态调整。分数 0.9 的全要0.7 ~ 0.9 的最多取 3 条 0.7 的不要。坑 3记忆只存不删问题用户三个月前说我看好茅台现在改主意了说茅台太高了要减仓但旧记忆还在Agent 检索到两条矛盾记忆行为混乱。解法写入新记忆时用 LLM 检查是否有矛盾旧记忆有则标记旧记忆为已失效或直接删除。坑 4工作记忆全量注入问题任务执行到第 20 步把前 19 步的所有中间结果全塞进上下文Token 爆炸。解法只注入当前步骤 相关的中间结果。哪些相关按步骤间的依赖关系决定。九、小结记忆系统的核心要点三层架构短期记忆对话历史 长期记忆向量库 工作记忆任务黑板短期记忆靠管理滑动窗口截断 摘要压缩 混合策略长期记忆靠检索写入要过滤不是什么都存读取要排序Top-K 分数阈值工作记忆靠结构化执行计划 步骤状态 中间结果按需注入而非全量遗忘比记忆更重要TTL 过期 重要性衰减 矛盾覆盖 容量淘汰下一篇讲三大基石的最后一块——工具调用。Agent 怎么知道有哪些工具可用、怎么让 LLM 选对工具、怎么处理工具执行失败都会展开讲。这是「AI Agent 开发实战」系列第 3 篇后续会持续更新欢迎关注。如有错误或想法欢迎评论区交流。