AI Agent 开发实战(三):记忆不是存聊天记录,搞懂这三层记忆架构才算入门
2026/7/24 9:37:12 网站建设 项目流程

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

这是「AI Agent 开发实战」系列的第 3 篇。上一篇讲了 LLM 调用与 Prompt 工程,这一篇继续拆三大基石——记忆系统。很多人的 Agent 跑两轮就"失忆",或者上下文一长就 Token 爆炸,根因都是没搞懂记忆该怎么设计。

一、为什么 Agent 需要记忆

先说一个反直觉的事实:LLM 本身是没有记忆的。

你每次调用/chat/completions,LLM 看到的只有你传过去的 messages 数组。它不记得上一次对话说了什么,不记得用户的偏好,不记得三步之前做了什么。

┌──────────────────────────────────────────────────┐ │ 无记忆的 Agent │ │ │ │ 用户:我叫张三 │ │ Agent:你好张三! │ │ │ │ 用户:我叫什么? │ │ Agent:抱歉,我不知道你叫什么。 │ │ │ │ 原因:第二次调用时 messages 里没有第一次的内容 │ └──────────────────────────────────────────────────┘

这不是 LLM “笨”,而是它根本看不到。

记忆系统的本质,就是在每次调用 LLM 之前,把相关信息塞进 messages 数组里。

听起来简单,但问题在于:

  1. 上下文窗口有限(4K ~ 200K tokens),不可能全塞进去
  2. 什么该塞、什么不该塞、什么时候塞,直接决定 Agent 的智商
  3. 不同类型的记忆,存储方式、检索方式、过期策略完全不同

二、三层记忆架构

工程上,Agent 的记忆通常分为三层:

┌──────────────────────────────────────────────────────────┐ │ Agent 记忆架构 │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第一层:短期记忆(Working Memory) │ │ │ │ 存在 LLM 的上下文窗口里 │ │ │ │ 内容:当前对话历史、当前任务的中间状态 │ │ │ │ 生命周期:单次会话 │ │ │ │ 特点:LLM 直接可见,但容量有限 │ │ │ └──────────────────────────────────────────────────┘ │ │ ↑↓ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第二层:长期记忆(Long-term Memory) │ │ │ │ 存在向量数据库 / 关系数据库中 │ │ │ │ 内容:用户偏好、历史决策、知识库 │ │ │ │ 生命周期:跨会话持久化 │ │ │ │ 特点:需要检索才能进入短期记忆 │ │ │ └──────────────────────────────────────────────────┘ │ │ ↑↓ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ 第三层:工作记忆(Scratchpad / Task Memory) │ │ │ │ 存在内存 / 缓存中 │ │ │ │ 内容:当前任务的执行计划、已完成步骤、待办事项 │ │ │ │ 生命周期:单次任务 │ │ │ │ 特点:结构化存储,不直接进 LLM 上下文 │ │ │ └──────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────┘

三层对比:

维度短期记忆长期记忆工作记忆
存储位置LLM 上下文窗口向量数据库 / 关系库内存 / Redis
存储内容对话历史用户画像、知识、历史决策执行计划、步骤状态
生命周期单次会话永久(或手动删除)单次任务
LLM 可见性直接可见需检索后注入按需注入
容量小(受 Token 限制)
检索方式不需要(全量在上下文)向量相似度 / 关键词直接读取

三、短期记忆:上下文窗口管理

短期记忆就是 LLM 的 messages 数组。核心问题是:对话越来越长,Token 放不下怎么办?

朴素方案:全量保留

List<Message>messages=newArrayList<>();messages.add(newSystemMessage(systemPrompt));// 每轮对话都往里塞while(running){messages.add(newUserMessage(userInput));LlmResponseresp=llm.chat(messages,tools);messages.add(newAssistantMessage(resp.getContent()));}// 问题:第 20 轮时,messages 可能已经 50K tokens,窗口爆了

方案一:滑动窗口截断

最简单粗暴——只保留最近 N 轮对话:

publicclassSlidingWindowMemory{privatefinalintmaxMessages;// 保留最近多少条消息publicList<Message>manage(List<Message>messages){if(messages.size()<=maxMessages){returnmessages;}// 保留 System Prompt(第一条)+ 最近 N 条List<Message>result=newArrayList<>();result.add(messages.get(0));// System 一定保留result.addAll(messages.subList(messages.size()-maxMessages+1,messages.size()));returnresult;}}

优点:简单、快、可预测。

缺点:丢掉的对话可能正好是 LLM 需要的。用户第 2 轮说了"我偏好高股息股票",到第 10 轮 Agent 就忘了。

方案二:摘要压缩

用 LLM 把旧对话总结成一段摘要,替换原始消息:

publicclassSummaryMemory{privateStringsummary="";privatefinalintsummarizeThreshold=10;// 超过 10 轮触发摘要publicList<Message>manage(List<Message>messages){if(messages.size()<=summarizeThreshold){returnmessages;}// 把旧消息交给 LLM 总结List<Message>oldMessages=messages.subList(1,messages.size()-5);StringnewSummary=llm.chat(List.of(newSystemMessage("请将以下对话总结为关键信息,保留用户偏好、重要决策和未完成的事项。"),newUserMessage(formatMessages(oldMessages))));// 用摘要替换旧消息summary=summary+"\n"+newSummary;List<Message>result=newArrayList<>();result.add(messages.get(0));// Systemresult.add(newSystemMessage("之前的对话摘要:\n"+summary));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 判断这条对话是否包含值得记住的信息Stringjudgment=llm.chat(List.of(newSystemMessage(""" 判断以下对话是否包含值得长期记忆的信息。 值得记忆的:用户偏好、个人事实、重要决策、任务结论。 不值得记忆的:寒暄、临时性问题、已过期信息。 只回答 YES 或 NO。 """),newUserMessage("用户: "+userMessage+"\n助手: "+agentResponse)));if(judgment.trim().toUpperCase().startsWith("YES")){// 提取结构化记忆Stringmemory=llm.chat(List.of(newSystemMessage("将以下对话中的关键信息提取为一句简洁的记忆陈述。"),newUserMessage("用户: "+userMessage+"\n助手: "+agentResponse)));// 向量化并存储vectorStore.add(memory,Map.of("timestamp",Instant.now().toString(),"type","user_preference"));}}}

读取:怎么找到相关记忆

publicclassMemoryRetriever{privatefinalVectorStorevectorStore;privatestaticfinalintTOP_K=5;publicStringretrieve(StringcurrentQuery){// 1. 把当前查询向量化// 2. 在向量库中找 Top-K 最相似的记忆List<MemoryItem>memories=vectorStore.search(currentQuery,TOP_K);if(memories.isEmpty()){return"";}// 2. 拼接成上下文StringBuildersb=newStringBuilder("相关历史记忆:\n");for(MemoryItemm:memories){sb.append("- ").append(m.getContent()).append("\n");}returnsb.toString();}}

一个完整的记忆增强 Agent 循环

publicclassMemoryEnhancedAgent{privatefinalLlmClientllm;privatefinalMemoryWritermemoryWriter;privatefinalMemoryRetrievermemoryRetriever;privatefinalContextManagercontextManager;publicStringrun(StringuserId,StringuserInput){// 1. 从长期记忆中检索相关信息StringrelatedMemories=memoryRetriever.retrieve(userInput);// 2. 组装 messagesList<Message>messages=newArrayList<>();messages.add(newSystemMessage(systemPrompt));if(!relatedMemories.isEmpty()){messages.add(newSystemMessage(relatedMemories));}messages.addAll(contextManager.manage(sessionHistory));messages.add(newUserMessage(userInput));// 3. 调用 LLMLlmResponseresp=llm.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;// 任务目标privateList<TaskStep>plan;// 执行计划privateMap<String,Object>results;// 中间结果privateList<String>notes;// 备注// 序列化为文本,注入到 LLM 上下文publicStringtoPromptText(){StringBuildersb=newStringBuilder();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_MEMORIES=1000;privatestaticfinallongTTL_DAYS=90;// 定期清理publicvoidcleanup(){// 1. 删除过期记忆longcutoff=Instant.now().minus(TTL_DAYS,ChronoUnit.DAYS).toEpochMilli();vectorStore.deleteByMetadata("timestamp",cutoff);// 2. 如果还是太多,按重要性淘汰longcount=vectorStore.count();if(count>MAX_MEMORIES){// 按检索命中次数排序,淘汰最少的List<MemoryItem>all=vectorStore.findAll();all.sort(Comparator.comparingInt(MemoryItem::getHitCount));inttoDelete=(int)(count-MAX_MEMORIES);for(inti=0;i<toDelete;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 爆炸。

解法:只注入"当前步骤 + 相关的中间结果"。哪些相关?按步骤间的依赖关系决定。

九、小结

记忆系统的核心要点:

  1. 三层架构:短期记忆(对话历史)+ 长期记忆(向量库)+ 工作记忆(任务黑板)
  2. 短期记忆靠管理:滑动窗口截断 + 摘要压缩 + 混合策略
  3. 长期记忆靠检索:写入要过滤(不是什么都存),读取要排序(Top-K + 分数阈值)
  4. 工作记忆靠结构化:执行计划 + 步骤状态 + 中间结果,按需注入而非全量
  5. 遗忘比记忆更重要:TTL 过期 + 重要性衰减 + 矛盾覆盖 + 容量淘汰

下一篇讲三大基石的最后一块——工具调用。Agent 怎么知道有哪些工具可用、怎么让 LLM 选对工具、怎么处理工具执行失败,都会展开讲。


这是「AI Agent 开发实战」系列第 3 篇,后续会持续更新,欢迎关注。如有错误或想法,欢迎评论区交流。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询