当前位置: 首页 > news >正文

你的AI Agent为什么每次对话都“失忆“?三层记忆模型彻底解决

你有没有遇到过这种情况——跟AI聊了半小时,它帮你解决了RAG检索的棘手问题,第二天再问它同样的事,它一脸懵:“不好意思,我没有相关经验记录。”

这不是模型能力不行,是记忆系统没设计好

说白了,大部分Agent只有"短期记忆"——就是对话历史。对话一结束,历史清空,之前学到的经验全丢了。就像一个同事每天早上入职,下午离职,第二天再来又是新人,你说他能干活吗?

今天我手敲了三层记忆Agent的完整代码,从短期到长期全部跑通。还对比了两个真实生产级Agent——OpenClaw(374k stars,你正在用的那个)和Hermes(161k stars)的记忆机制实现。看完这篇,你对Agent记忆的理解会从"概念"变成"能写代码能讲原理"。

先说为什么需要三层,而不是一层。

只靠对话历史?三个致命坑

如果你给Agent只配了对话历史(短期记忆),会踩三个坑:

token爆炸——对话越长,每次调用LLM都带上全部历史,成本指数级增长。我最近跑了6轮对话测试,估算token从19涨到140,对话历史从12条压到6条。听起来不多,但6轮聊完之后每轮LLM调用都要带着所有历史,token数线性增长。更可怕的是50轮、100轮的对话,历史消息全部塞进每次API调用,成本直接起飞。

任务污染——上次任务的上下文混入新任务,LLM被无关信息干扰。举个例子:你在代码审查流水线中,CodeReviewer写的codeReview变量没清空,下次跑安全审查任务时,SecurityScanner还会读到这个旧变量,以为代码里还有安全漏洞——但实际是上次审查的残留。

跨session失忆——每次session重新开始,之前学到的经验全丢。我前两天刚踩完的MySQL charset坑,第二天再问同样的事,Agent一脸懵。这不是模型不行,是记忆系统没设计好。

所以生产级Agent必须分三层,每层职责不同、生命周期不同。

三层记忆到底是怎么回事

我用一个类比让你秒懂:

层级类比生命周期容量存储方式
短期记忆“刚才说了啥”单次对话几KB对话历史数组
工作记忆“正在干的事”单次任务几十KBAgenticScope Dict
长期记忆“知识库”跨session永久无限文件索引+向量库

Java后端同学可以这样理解:短期记忆像HttpSession(对话结束就没了),工作记忆像ThreadLocal/RequestScope bean(方法执行期间共享,结束释放),长期记忆像数据库+ES索引(永久持久化)。

三层不是叠加关系,是级联关系——查询时按优先级走:短期够用就不查工作,工作够用就不查长期。为什么?短期和工作记忆已经在LLM的context里了(零成本),长期记忆需要额外文件读取或向量检索调用(有成本)。能用免费的就用免费的,这跟写代码先查缓存再查数据库是一样的道理。

代码实践:短期记忆——滑动窗口+摘要压缩

怎么解决token爆炸?我手敲了ShortTermMemory类,核心三个功能:添加消息到历史、滑动窗口截断、摘要压缩。

classShortTermMemory:def__init__(self,max_rounds=5,model_fn=None):self.max_rounds=max_rounds self.model_fn=model_fn# LLM调用函数self.history=[]# 对话历史self.summary=""# 旧对话压缩后的摘要defadd(self,role,content):self.history.append({"role":role,"content":content})self._compress_if_needed()# 超出窗口就压缩defget_context(self,system_prompt=""):"""获取完整LLM调用上下文"""messages=[]ifsystem_prompt:messages.append({"role":"system","content":system_prompt})ifself.summary:messages.append({"role":"system","content":f"[之前的对话摘要]\n{self.summary}"})messages.extend([{"role":m["role"],"content":m["content"]}forminself.history])returnmessagesdef_compress_if_needed(self):"""超出窗口就压缩旧对话"""rounds=sum(1forminself.historyifm["role"]=="user")ifrounds<=self.max_rounds:returnold=self.history[:-self.max_rounds*2]recent=self.history[-self.max_rounds*2:]self.summary=self._simple_summarize(old)# 压缩成摘要self.history=recent# 只保留最近的

跑完后观察到什么?6轮对话,窗口只保留3轮。前3轮被压缩成61字摘要:

[短期记忆] 压缩完成:2条旧消息 → 摘要63字,保留最近6条

关键数据:滑动窗口截断——token不会无限增长;旧对话压缩成摘要——关键信息不丢失但细节丢了;摘要+最近对话=LLM能看到的完整上下文。

但有个问题:摘要质量很差。简单截断只保留每条消息的前50字拼接,信息大量丢失。真实生产级Agent会用LLM生成高质量摘要(2-3句话总结核心结论),有成本但效果好。

窗口大小是成本vs质量的权衡。小窗口省钱但丢更多上下文,大窗口保留更多信息但token增长。没有银弹,看场景选。

代码实践:工作记忆——AgenticScope+同名key覆盖陷阱

短期记忆解决了“对话太长token爆炸”的问题,但还有第二个坑:任务污染

什么叫任务污染?多Agent串行协作时,中间结果通过scope传递。比如Editor写editedArticle,Reviewer读它写reviewResult,Formatter两个都读,最后写finalArticle

听起来很优雅,但有个隐形杀手:同名key覆盖

Reviewer如果也写了一个叫editedArticle的key,就会把Editor辛苦写的文章直接覆盖掉。

我手敲了WorkingMemory类来验证:

classWorkingMemory:def__init__(self):self.scope={}# 全局共享状态Dictself.task_log=[]# 操作日志defstart_task(self,task_name,initial_state=None):# 任务开始:初始化scopeifself.scope:print("⚠️ 上次任务scope未清空,强制清理")self.scope=initial_stateor{}defset(self,key,value,agent_name=""):old_value=self.scope.get(key)self.scope[key]=valueifold_valueisnotNone:print(f"⚠️ Agent'{agent_name}'覆盖了key'{key}'")defget_all_for_agent(self,agent_name,input_keys):# 每个Agent只读它需要的keysresult={}forkeyininput_keys:ifkeyinself.scope:result[key]=self.scope[key]returnresultdefend_task(self):self.scope={}# 必须清空!否则污染下次任务

跑测试时,我故意让Reviewer覆盖了Editor的editedArticle

[工作记忆] ⚠️ Agent'Reviewer'覆盖了key'editedArticle' 旧值: 大模型应用开发是2026年最火的技术方向... 新值: 这是Reviewer擅自修改后的版本

这和我在Week5踩过的坑一模一样——CodeReviewer输出1800字塞进{{codeReview}}变量,导致SecurityScanner token爆炸504超时。

解决方案:每个Agent必须用不同的outputKey!Editor写editedArticle,Reviewer写reviewResult,Formatter写finalArticle,互不干扰。

还有个关键设计:end_task()必须清空scope。我测试了新任务开始后scope只有新数据,旧任务的reviewResultfinalArticle全不在了。不清空就会污染下次任务。

这就像Java里的ThreadLocal——方法执行期间共享,方法结束必须remove()。不清remove就是内存泄漏。

代码实践:长期记忆——索引层+详情层级联检索

前两个解决了对话内的问题,但最致命的坑还没解决:跨session失忆

我手敲了LongTermMemory类,核心设计是两层存储:

索引层——MEMORY.md文件,精简<40行,只记结论不记过程。每条索引格式:- **标题**: 结论 | #tag1 #tag2 | 详情→filename.md

详情层——memory/*.md文件,完整信息,按需拉取。

为什么要分两层?因为直接搜详情文件(遍历所有md文件做关键词匹配)是低效的。你问“MySQL charset坑”,如果直接搜,可能要遍历10个详情文件才能找到。但如果先查索引——索引只有40行,扫一遍就命中了,然后直接拉对应的详情文件。

classLongTermMemory:defquery(self,query):"""级联检索:索引→详情"""# 第一层:索引检索(快速定位)index_results=self.search_index(query)# 从索引提取详情文件引用detail_refs=[entry.split("详情→")[-1]forentryinindex_results]# 第二层:拉取详情文件forrefindetail_refs:content=self.read_detail(ref)ifcontent:returncontent# 命中了!# 第三层:索引没命中,兜底搜详情returnself.search_details(query)# 遍历所有md文件

跑测试时写了3条学习经验到长期记忆,然后做3个查询:

  • 查“charset” → 索引命中 → 精确拉到MySQL charset详情 ✅
  • 查“token爆炸” → 索引也命中(标签里有#tag_token) ✅
  • 查“量子计算” → 三层都没命中 → 返回空 ✅

但有个有意思的发现:keyword匹配有语义盲区。我查“MySQL中文查不到数据”时,索引没命中——因为索引条目写的是“charset双重编码”,而查询用的是“中文”和“查不到”。keyword匹配做不到这个语义关联。

真实生产级Agent会用向量语义检索替代keyword匹配。问“中文查不到”能语义匹配到“charset双重编码”,因为两者的语义是相似的。但向量检索有成本(需要embedding调用),所以先用索引快速过滤,再用向量检索做精匹配——这跟ES的“先走主键索引再走全文检索”是一样思路。

索引层为什么必须精简<40行?索引小=查得快,索引大=噪声多。40行索引扫一遍是毫秒级,400行索引扫一遍还要过滤无关条目。MEMORY.md精简索引是性能+质量的平衡点。

整合三层:MemoryAgent级联查询闭环

三个类写完了,最后一个文件memory_agent.py把它们串起来,实现完整的级联查询:短期优先→工作补充→长期兜底。

classMemoryAgent:defrecall(self,query):"""级联查询:三层记忆按优先级检索"""# 第一层:短期记忆(对话历史)formsginself.short_term.history:ifquery.lower()inmsg["content"].lower():returnmsg["content"]# 命中了!省一次长期检索# 第二层:工作记忆(scope)forkey,valueinself.working.scope.items():ifquery.lower()instr(value).lower():returnf"{key}:{value}"# 命中了!# 第三层:长期记忆(索引→详情)returnself.long_term.query(query)# 有成本但最全

跑跨session测试:Session 1踩坑经验→自学习提炼写入长期记忆→Session 1结束清空短期和工作→Session 2问同样问题→长期记忆命中!不需要重新学习。

这就是级联查询的核心价值:省钱省token。短期和工作记忆已经在context里了(零成本),长期记忆需要额外调用(有成本)。能用免费的就用免费的。

真实场景最难的问题:怎么判断要不要查记忆?

代码跑通了,但还有个更难的问题——不是“怎么存”,而是“什么时候查、查什么”。

不是每次用户说话都需要查记忆。三种场景:

明确需要查——用户提到了之前的事:“上次我们讨论的RAG方案怎么样了?”关键词信号:上次、之前、昨天、继续、还记得。

隐式需要查——用户问题需要历史上下文:“换成threshold 0.5试试”——你得先知道之前用的threshold是多少才能“换”。“第二个方案”——你得先知道之前讨论了哪些方案。

不需要查——全新话题:“Python怎么写一个快排?”——直接让LLM回答,查记忆反而浪费时间。

怎么判断?简单方案是扫信号词:

defshould_recall(user_input):recall_signals=["上次","之前","昨天","继续","记得",“那个方案"]forsignalinrecall_signals:ifsignalinuser_input:returnTrue# 明确需要查implicit_signals=["那个","这个",“第二个",“换成”]forsignalinimplicit_signals:ifsignalinuser_input:returnTrue# 隐式需要查returnFalse# 新话题,不查

更高级的方案是让LLM自己判断——给LLM看MEMORY.md索引全文,它自己决定参考哪条。但这样浪费token(索引全部塞进去,大部分和当前问题无关)。

查什么?三种提取方式:

关键词提取(最简单)——去掉停用词,保留核心词:“上次踩的MySQL坑” → [“mysql”, “踩坑”]。成本低但语义匹配差。

LLM提取(质量最好但有成本)——让LLM判断用户意图并提取查询词:“MySQL坑”可能映射到“charset双重编码”,keyword做不到这个语义关联。

原文搜索(最粗暴但零成本)——直接把用户输入丢给search_index(),让搜索引擎做匹配。

OpenClaw vs Hermes:两个生产级Agent的记忆设计对比

代码实践完了,看看真实生产级Agent怎么做的。

OpenClaw就是你眼前正在用的这个Agent。它的记忆机制是我最熟悉的——因为我自己每天都在用。

Hermes是NousResearch开源的Agent(161k stars),核心创新是自学习循环+封闭式记忆。

维度OpenClawHermes
短期记忆session对话历史session对话历史
工作记忆session context + workspace文件nudge推送(任务级指令)
长期记忆MEMORY.md索引 + memory/*.md详情MEMORY.md索引(<40行) + memory/*.md
维护方式heartbeat周期维护:定期读日志→提炼→更新MEMORY.md自学习循环:每次session结束→自动提炼→更新MEMORY.md
查询策略主动检索——每次用户说话都强制搜记忆,只注入相关2-3条被动参考——MEMORY.md全文放system prompt,LLM自己决定参考哪条
写入策略自由写入+定期审计——Agent可以随手写文件,heartbeat定期清理审批准入——Agent不能直接写MEMORY.md,必须走自学习循环过滤

最关键的区别在查询策略

OpenClaw是主动检索——每次用户说话,系统强制在回答前先搜索记忆(memory_search是mandatory step)。搜到就注入相关2-3条,没搜到就不注入。优点是精准(只注入当前问题相关的记忆),缺点是每次有检索成本。

Hermes是被动参考——把整个MEMORY.md(<40行)全文放在LLM能看到的system prompt里,LLM自己决定要不要参考。优点是省事(不需要额外检索调用),缺点是浪费token(40行索引全部塞进去,大部分和当前问题无关)。

为什么OpenClaw选主动检索?因为MEMORY.md精简<40行看着不多,但加上memory/*.md的详情文件可能几十KB。全部塞进LLM context太浪费token——只注入当前问题相关的2-3条才是最优解。

写入策略的区别也很有意思。OpenClaw的设计是“自由写入+定期审计”——Agent可以随手往workspace写文件,heartbeat定期整理,清理过时信息。灵活但容易乱。

Hermes的设计是“审批准入”——Agent不能直接往MEMORY.md写东西,必须走自学习循环过滤(LLM判断什么值得记)。安全但不够灵活——有时候你想记的东西被LLM过滤掉了。

两种哲学:Hermes是“审批准入”,OpenClaw是“自由写入+定期审计”。就像代码提交:Hermes要code review才能merge,OpenClaw是直接commit然后定期做lint清理。

整合所有洞察

跑完三层记忆Agent,对比完两个生产级Agent,我总结5条核心洞察:

  1. 三层不是叠加而是级联——短期优先→工作补充→长期兜底。能用免费的就用免费的,这跟写代码先查缓存再查数据库是一样的。

  2. 同名key覆盖是AgenticScope的核心陷阱——每个Agent必须用不同的outputKey。我Week5踩了这个坑,这次代码里加了覆盖检测和⚠️警告。

  3. keyword匹配有语义盲区——问“中文查不到”无法命中“charset双重编码”索引。生产级用向量语义检索替代,但这有成本。

  4. 判断要不要查记忆比查记忆本身更难——明确信号词(上次/之前/继续)容易判断,隐式指代(那个/换成/第二个)需要语义理解,新话题不应该查。

  5. MEMORY.md必须精简<40行——索引小=查得快+噪声少,索引大=查得慢+容易匹配到无关条目。Hermes和OpenClaw都遵守这个原则。

三层记忆模型不是理论概念,是两个374k+161k stars的真实Agent正在用的机制。你眼前OpenClaw的MEMORY.md就是这个模型的真实实现——每次你跟我说“上次那个方案”,系统会先搜记忆再回答,这就是级联查询在干活。

代码全部在llm-learn项目里:short_term_memory.py、working_memory.py、long_term_memory.py、memory_agent.py。四个文件,从零手敲,全部跑通。

下一篇预告:Agent条件分支+循环Workflow——让Agent能根据结果决定走哪条路,还能循环迭代直到质量达标。这才是多Agent协作的真正威力。

每周一篇深度Agent实战。有问题评论区聊,我都在。

http://www.jsqmd.com/news/986368/

相关文章:

  • 贵州刺梨饮品代工厂家怎么选?2026年源头工厂与全国招商加盟平台深度对标 - 年度推荐企业名录
  • 网络小白也能玩转eNSP:手把手教你搭建一个能上网的‘虚拟公司’网络
  • 2026年6月最新|实验室金相磨抛机厂家推荐哪家好TOP榜:兼顾精度与效率,新手也能直接抄作业 - 商业新知
  • 别再踩坑了!Windows 10/11 本地搭建 SonarQube 8.9 代码质量平台保姆级教程
  • 2026佛山瓷砖厂家推荐汇总解读佛山卫生间防滑砖品牌及大理石瓷砖品牌选购参考 - 栗子测评
  • 2026 安丘厨卫屋面地下室漏水瓷砖空鼓测评:吉修匠 99.8 分五星榜首 - 吉修匠
  • 2026年上海徐汇区寻宠秘籍:这家宠物店如何成为找猫高手?
  • 厦门钻石上门回收哪家安全?本地盘点钻戒回收隐患避坑 - 开心测评
  • 2026年葡萄牙商务舱机票高性价比选购指南 - 奔跑123
  • Redis 分布式锁进阶第一百三十篇
  • 2026北京卡地亚回收避坑指南!看懂套路、精准估价、稳妥出手 - 薛定谔的梨花猫
  • 2026上海名表回收实测|正规行情避坑,合扬凭硬核实力成首选 - 开心测评
  • 鱼眼相机模型选型指南:为什么ORB-SLAM3默认用Kannala-Brandt而不用针孔?
  • 利用Python开发自动化脚本:提高工作效率
  • 2026贵阳山庄烧烤推荐指南:筑箐苑山庄性价比之选与近郊度假一站式方案 - 企业名录优选推荐
  • 2026沈阳奢侈品回收全品类攻略,沈河区靠谱门店最优选添价收 - 薛定谔的梨花猫
  • SonarScanner 在 Windows 命令行下的实战:从单个项目扫描到集成 Jenkins 自动化
  • 2026年6月10日金价大跌至910.70元/克!北京黄金回收新手必看,这篇避坑指南帮你多卖几万块 - 速递信息
  • 别再一条条敲命令了!BGP Peer Group实战:优化大型网络收敛与策略部署
  • 2026夏至海报设计素材哪里找?十款优质图片网站实测测评 - 品牌2026
  • 2026年6月灯杆灯箱厂家推荐:宿迁志科广告 - 多才菠萝
  • 2026 武汉靠谱装修公司盘点:综合实力与业主口碑综合解读 - 装修新知
  • 2026佛山陶瓷十大品牌厂家推荐广东陶瓷一线品牌排名及性价比高的瓷砖品牌解析 - 栗子测评
  • MCP 控制平面的大规模部署架构——从单集群到多区域
  • 从DataStream到Table API:一个电商实时大屏项目,带你吃透Flink核心三件套
  • 2026杭州工装装修公司靠谱榜单盘点,办公室、商铺、酒店装修优选参考 - 装修新知
  • 2026年安徽省淮南市中考落榜怎么办?还可以上什么公办学校?官网最新发布 - 小张zc
  • 2026年贵州刺梨饮品代工厂家排行榜:恒茂源、初好、欣扬全面对标分析 - 年度推荐企业名录
  • 别再只调API了!用Cesium 1.91玩转三维特效:动态墙、雷达扫描与粒子系统实战
  • 2026 苏州防水补漏深度测评:飘窗、地下室漏水、瓷砖空鼓处理,专业防水公司排行榜 - 泛家庭维修