news 2026/10/3 7:13:35

AI Agent生产级记忆系统:踩坑总结与四层金字塔架构

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
AI Agent生产级记忆系统:踩坑总结与四层金字塔架构

文章目录

  • 1 为啥AI Agent一定要搞记忆?现实坑多到离谱
  • 2 整体架构:解耦独立记忆子系统
  • 3 四层金字塔记忆模型 L0‑L3
  • 4 双存储引擎,SPI插拔式设计
  • 5 写入主链路,踩坑踩出来的代码逻辑
  • 6 主动Push召回,别搞被动Pull模式
    • 6.1 召回流水线
    • 6.2 异步回写队列MemoryWritebackQueue
  • 7 S1可解释混合打分:为啥叫match,不叫similarity
  • 8 记忆治理闭环:抽取、合并、遗忘淘汰
  • 9 和对话链路怎么集成?MCP接入层
  • 10 12条工程设计原则,血泪总结
  • 11 最后唠两句

P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

1 为啥AI Agent一定要搞记忆?现实坑多到离谱

大模型本身天生就是无状态选手,很多人没意识到这点。

你跟它聊天,它能记住东西,完全是靠把所有历史对话一股脑塞进上下文窗口。

会话一关,或者对话太长撑爆窗口,直接原地失忆,跟鱼的记忆一模一样,七秒过后啥都忘干净。

网上一堆demo做Agent记忆,简单到离谱:拿个向量库,文本丢进去完事,就号称搞定长期记忆。

上线跑两天直接傻眼。就跟你往衣柜疯狂塞衣服,只管塞不管整理,最后找一件卫衣得扒拉半小时。

真实业务场景,至少要搞定三类头疼问题:

  1. 1.1 跨会话长期记忆。用户喜好、之前说好的约定,新开对话还得能用,不能每次聊天都要重新自我介绍一遍。谁聊天也不想每次上来都重复“我不吃香菜”。
  2. 1.2 推理主动召回。该想起来的信息,Agent自己主动捞出来,别等着用户反复提醒。总不能每次都要用户说“还记得我上次跟你说的吗”。
  3. 1.3 记忆腐烂治理。会记错、会重复存、混进去敏感信息,还有一大堆一辈子都用不上的垃圾记忆。这些脏东西不收拾,Agent越跑越蠢。就跟浏览器缓存,堆多了干啥都卡。

市面上很多方案,把召回完全甩给大模型自己处理。模型想起来就看,想不起来直接忽略。这能叫记忆吗?这叫开盲盒。

我做这套agent‑memory模块,核心目标不是单纯把数据存下来。

重点是召回准、能治理、出错不会把整个对话搞崩,出问题还能溯源查清楚。线上开发,能出问题之后查得到原因,比啥都香。

2 整体架构:解耦独立记忆子系统

先给大家捋清楚整体链路。

上层调用方五花八门:对话链路、管理后台UI、治理API全都可以来调用记忆模块。

但是所有请求,全部打到agent‑memory核心模块,绝不允许到处写读写逻辑。

很多新手写项目,到处散落写DB的代码,后面改存储,要改几十处文件,改到怀疑人生。

模块内部职责分得明明白白:接入层、写入服务、治理门面、召回构建器、异步回写队列、打分器、审计埋点组件。

存储搞了双库设计,H2 OLTP做主库,Arrow OLAP当副库。

  • 运行时读写操作全部走H2主库,追求响应速度。
  • Arrow副库只拿来做后台离线分析,每分钟靠增量ETTL同步数据过去。

读写严格分离,线上推理流量和离线分析互不打架。

业务代码只依赖门面接口MemoryStorageFacade。底层换成MySQL、RocksDB,业务代码一行不用改。SPI扩展是真的香,不用到处硬编码存储实现。

划重点:全部落库操作,只能走AiMemoryService这唯一写入出口。脱敏、白名单、重要性评估全部集中在这里处理。不要到处复制粘贴脱敏逻辑,复制代码就是bug的温床。

3 四层金字塔记忆模型 L0‑L3

记忆不要平铺一堆记录堆在表里。搞成分层金字塔,越往上信息密度越高,召回优先级也越高。

  1. 3.1 L0 RawLog原始对话日志

完整原始会话痕迹,带上traceId用来全局溯源回放。

**这一层不会参与召回!**只用来留痕排错。别啥都往模型塞,原始日志token爆炸能把钱包烧穿。线上排查bug的时候,这一层就是救命稻草。

  1. 3.2 L1 Atomic原子记忆

最小不可拆分记忆单元,分三类:session_var会话变量、user用户记忆、global租户全局记忆。

会参与召回打分,也支持遗忘淘汰。这里踩过大坑:global类型记录,存的不是userId,实际存的tenantId租户ID。

写查询的时候,如果不加过滤直接按userId查全部,租户之间数据直接串。线上一出这种跨租户泄露bug,年终奖原地拜拜。

  1. 3.3 L2 Scene场景块

会话摘要块,附带关联一批L1的id列表。每次会话启动优先加载,充当对话骨架。相当于给这一次聊天做的读书笔记。

  1. 3.4 L3 Persona用户画像

存放用户人设、偏好、生活习惯,跨会话稳定,支持版本迭代演进。召回优先级最高。

相当于我们系统里面的用户档案,这部分内容不能随便淘汰丢掉。

4 双存储引擎,SPI插拔式设计

很多人一上来就各种猛上重量级数据库,其实没必要。

业务层 (AiMemoryService / MemoryManager / Analytics) │ 只依赖门面 ▼ MemoryStorageFacade ├── oltp() ──▶ OltpMemoryRepository ──▶ H2OltpStorageProvider ──▶ H2 MVStore 文件库 └── olap() ──▶ OlapAnalyticsRepository ─▶ ArrowOlapStorageProvider ─▶ Arrow IPC 文件

这套SPI抽象是通用组件,别的模块也能拿来复用。想切换存储,只需要实现Provider接口,注册Bean,改一行配置,业务完全不用动。

副库选Arrow加Calcite纯Java实现,没有JNI依赖。用过DuckDB JNI的同学应该懂痛,版本稍微不对,环境直接炸,打包部署各种玄学问题。

主库扛线上实时CRUD召回;副库专门跑离线时序、溯源分析。两边负载彻底隔开,离线跑大查询不会把线上接口拖垮。

5 写入主链路,踩坑踩出来的代码逻辑

所有写记忆统一入口saveUserMemory,这里坑点密度堪称全项目天花板,一步顺序写错,线上悄悄出bug,还很难复现。

publicMap<String,Object>saveUserMemory(StringtenantId,StringuserId,Stringcategory,Stringcontent,Doubleimportance){requireText(tenantId,"租户标识不能为空");requireText(userId,"用户标识不能为空");StringnormalizedCategory=hasText(category)?category.trim():"custom";if(!isCategoryAllowed(normalizedCategory)){// ① 白名单校验returnMap.of("allowed",false,"reason","记忆类别不在白名单内...");}ImportanceScoreassessed=importanceScorer.score(content,normalizedCategory);// ② 重要性的评估【脱敏前】StringsafeValue=sanitizeIfEnabled(content);// ③ 敏感脱敏if(!hasText(safeValue)){returnMap.of("allowed",false,"reason","内容为空或全部为敏感信息...");}Stringid=IdUtil.fastSimpleUUID().substring(0,12);StringtraceId="trace-"+id;longts=System.currentTimeMillis();doublevalue=resolveImportance(importance,assessed);oltp.saveRawLog(L0RawLog.forInsert(traceId,"user-"+userId,tenantId,ts,"system","USER_MEMORY:"+normalizedCategory+"="+safeValue,null,null));// ④ L0 溯源留痕if("persona".equals(normalizedCategory)||"preference".equals(normalizedCategory)){oltp.savePersona(L3Persona.create(id,userId,normalizedCategory,safeValue,1,ts,tenantId,value));}else{oltp.saveAtomicMemory(L1AtomicMemory.create(id,traceId,"user-"+userId,userId,normalizedCategory,safeValue,ts,tenantId,value));}// 返回体诚实:importanceSource = scored | explicitMap<String,Object>recordMap=record(id,safeValue,ts,Map.of("userId",userId,"category",normalizedCategory));returnresult(applyImportance(recordMap,importance,assessed),content,safeValue);}

有一个千万不能颠倒的执行顺序:重要性打分,必须拿脱敏之前的原文运算。

如果你先脱敏再打分,手机号变成138****8000,规则识别直接失效。敏感信息会被系统当成高权重记忆存进去。bug安安静静潜伏,你测半年都测不出来。

返回结果也不要糊弄调用方,返回importanceSource标记,区分是系统自动算出来分数,还是外部调用方显式传入。

不然调用者分不清,到底自己传的参数有没有生效,排错直接两眼一抹黑。就跟接口返回永远“成功”,实际啥都没干,纯纯害人。

6 主动Push召回,别搞被动Pull模式

很多记忆库是Pull模式,模型想读才去读。模型脑子一“走神”就忘掉关键记忆。

我们这里玩Push模式,推理之前主动把合适记忆注入进去。

6.1 召回流水线

recallFragment 作为入口,调用MemoryManager.recall。

  1. SQL下推查询当前用户L3画像加上L1原子记忆数据;
  2. 逐条跑MemoryScorer打分;
  3. 按照分数、最近访问时间、时间戳做稳定排序,截取topK;
  4. 最后过滤掉低于minScore阈值的记录,组装片段注入prompt,同时写L0留痕。

三条血的教训,三条注入铁律:

  • ① 必须加冲突声明:仅供参考,若与用户当前陈述冲突,以用户当前陈述为准。用户改了喜好,旧记忆不能还在那瞎指挥。
  • ② 如果啥记忆都没召回,什么文本都不要注入。千万别写个“暂无记忆”占位,凭空污染上下文token,还搞坏prompt缓存。写demo无所谓,线上token钱一分一分烧。
  • ③ 记忆内容注入到系统提示词,不要直接往messages数组塞SYSTEM消息。框架会直接拒绝,一命中记忆对话直接报错失败,复现还时有时无,调得人心态爆炸。

6.2 异步回写队列MemoryWritebackQueue

大模型把回答返回给用户之后,整个http链路就结束了。事实抽取更新记忆这种重活,放到后台异步队列处理。

publicclassMemoryWritebackQueueimplementsAutoCloseable{publicstaticfinalintMAX_PENDING=1000;// 队满即拒绝新任务privatefinalBlockingQueue<Task>queue=newLinkedBlockingQueue<>(MAX_PENDING);privatefinalExecutorServiceworker;publicMemoryWritebackQueue(MemoryManagermemoryManager,MemorySpanRecorderrecorder){this.worker=Executors.newSingleThreadExecutor(r->{Threadt=newThread(r,"agent-memory-writeback");t.setDaemon(true);// 守护线程:退出不卡进程returnt;});this.worker.submit(this::consume);}publicintsubmit(Tasktask){booleanaccepted=queue.offer(task);if(!accepted){dropped.incrementAndGet();log.warn("[memory-writeback] 队列积压已达上限 {}, 丢弃本次回写 traceId={}",MAX_PENDING,task.traceId());recorder.recordDroppedWriteback(...);// 丢弃也必须可归因return-1;}returnqueue.size();}}

这里有几个取舍,都是踩坑总结:

任务对象直接带上本轮完整消息体,不要只存traceId。会话侧traceId和链路traceId两套体系,如果只存id,后台查原始记录查不到,回写直接静默空转。日志啥错都不报,功能就是不生效,阴间bug天花板。

队列满了拒绝新任务,而不是丢掉老任务。老任务已经排队很久,扔了等于前面等待全部白费。宁可丢掉最新几轮记忆,也不能让队列无限膨胀把服务拖垮。

异步任务就算抽取逻辑抛异常,只打印警告日志,绝对不能反向影响已经返回给用户的对话结果。记忆只是附加增强功能,不能让记忆模块故障搞崩整个主业务。就好比浏览器插件崩了,不能让浏览器直接闪退。

7 S1可解释混合打分:为啥叫match,不叫similarity

很多项目直接上向量相似度做召回,中文场景坑巨多。一堆“的、了、是”高频虚词疯狂干扰结果。随便一句查询,跟一堆八竿子打不着的记忆算出高相似度。

我们这套打分是纯函数,全部数值归一化0‑1区间。

score = 0.5 × match + 0.3 × decay + 0.2 × importance match = 0.6 × bigramOverlap(query, content) + 0.4 × (content 含 query ? 1 : 0) decay = 0.5 ^ (Δt / halfLife)

match用二元字符bigram覆盖率,不依赖分词,不用大模型,结果完全可复现。命名老老实实叫match,不吹语义相似度,诚实命名,出问题才好排查。

decay代表记忆衰减,记忆越久没被访问,分数慢慢往下掉。时间差值统一量化到整小时。

为啥要量化?如果拿原始毫秒时间戳参与计算,毫秒微小抖动,两次运行同一份数据,算出来分数小数点后十几位飘来飘去,单元测试永远不稳定,你跑一遍过,CI流水线跑一遍失败,能把测试工程师逼疯。一小时带来的衰减误差几乎可以忽略不计。

MemoryScorer设计成纯函数,不持有可变状态,不内部读系统时钟,时间参数全部由调用方传入。同一套输入,输出结果必须完全一模一样,这叫可回归。单元测试才能写得住。

权重配置在服务启动阶段做强校验。权重相加不等于1,直接抛出异常终止启动。绝不允许服务成功启动,但打分逻辑完全失效。很多项目配置写错,服务还开开心心跑,业务全错,等到上线才发现。配置校验越早做越好。

召回、记忆合并、淘汰清理,全部共用同一个Scorer实例。不要搞多套打分逻辑。不然出现:低分记录不会被注入prompt,但又不会被淘汰清理,脏数据越堆越多。两套标准打架,谁来都修不动。

8 记忆治理闭环:抽取、合并、遗忘淘汰

记忆不能只写不删不整理,跟家里房间一样,只买东西不扔垃圾,最后下不去脚。

MemoryManager在基础增删改查之上编排5个核心动作,完成记忆新陈代谢。

动作组件简单说明
召回MemoryScorer前面讲的打分召回逻辑
事实抽取FactExtractor规则模式默认开启,LLM抽取模式默认关闭
记忆合并MemoryMerger复用match算法,同一用户同一类别,内容近似就合并,减少重复记忆
淘汰清理MemoryEvictor复用同一打分器,低分并且过期记忆清理掉,带安全阀保护
访问统计AccessCounter进程内计数,定时批量落库,不阻塞查询接口

事务边界要拎清楚:召回只读;抽取合并淘汰逐条处理,允许部分成功。不要给一堆记录套大事务。一部分处理成功一部分失败,大事务一回滚,全部白干,放大数据损失。线上千万不要迷信大事务万能。

加安全阀:重要性≥0.9或者显式标记的记忆,不会被淘汰清理。核心用户画像不能被自动清理逻辑误删掉。

9 和对话链路怎么集成?MCP接入层

记忆模块和对话模块是平级兄弟组件,没有编译依赖。通过HTTP JSON‑RPC 2.0的MCP协议交互。

对话模块调用MemoryMcpClient,发起memory_write、memory_read、memory_delete、memory_search工具调用。

MCP接入层会走MemoryMcpAccessGuard做鉴权。配置token就校验Authorization头;不配置token,就只允许本机回环访问。

会话侧每一轮对话,会调用appendTurn保存会话消息,写入L0事件流,支持服务重启会话回放。

publicsynchronizedvoidappendTurn(StringsessionId,StringuserMessage,StringassistantMessage,intmaxMessages){if(!hasText(sessionId)||maxMessages<=0)return;SessionMemorysession=loadSession(sessionId);if(session==null)session=newSessionMemory();session.messages.addLast(newAiChatMessage("user",userMessage));session.messages.addLast(newAiChatMessage("assistant",assistantMessage));trimMessages(session.messages,maxMessages);session.revision++;storeSession(sessionId,session);recordTurnEvent(sessionId,userMessage,assistantMessage);// 落 L0, 可回放}privatevoidrecordTurnEvent(StringsessionId,StringuserMsg,StringassistantMsg){longts=System.currentTimeMillis();StringtraceId="convo-"+IdUtil.fastSimpleUUID().substring(0,12);if(hasText(userMsg))eventLog.saveRawLog(L0RawLog.forInsert(traceId,sessionId,null,ts,"user",userMsg,null,null));if(hasText(assistantMsg))eventLog.saveRawLog(L0RawLog.forInsert(traceId,sessionId,null,ts,"assistant",assistantMsg,null,null));}

这里又一个大坑:写记忆时候填的tenantId、sessionId,读记忆的时候必须完全一模一样。

如果两边参数不一样,管理面板看着永远是空数据,还不会抛出任何异常。这种静默错误,排查能耗掉你一整天时间。最好写自动化校验脚本提前发现。

10 12条工程设计原则,血泪总结

很多看起来非常啰嗦、多余的约束,都是为了解决线上记忆慢慢腐坏的问题。很多人写demo全部忽略这些,跑demo美滋滋,上线就翻车。

  1. 10.1 强制隔离维度编译期约束:仓储方法tenantId、userId必须入参,不提供只有userId的重载,漏传直接编译报错,不要留运行时才炸的口子。
  2. 10.2 唯一写入出口,脱敏白名单重要评估全部收拢一处,拒绝到处复制粘贴逻辑。复制一次代码,复制一堆潜在bug。
  3. 10.3 记忆属于增强附加能力。记忆逻辑不管哪里失败,只打警告日志,不能阻断主对话链路。不能辅助组件把主业务搞挂。
  4. 10.4 配置阈值启动期校验。权重总和、半衰期、topK参数非法直接终止服务启动。不要让服务带着错误配置跑业务流量。
  5. 10.5 打分器纯函数,无状态,时钟时间由调用方传入,保障打分结果可回归,单元测试能稳定跑。
  6. 10.6 区分同步异步相位。链路结束之后的异步任务,不要再写链路span,只允许追加写审计表。
  7. 10.7 审计表append‑only,不支持update、delete。事件记录只追加不改,方便事后审计溯源。
  8. 10.8 召回、合并、淘汰,全部使用同一个打分器实例,杜绝多套标准打架。
  9. 10.9 三条注入铁律严格执行:冲突声明、空召回不注入、注入内容落L0留痕。
  10. 10.10 读写两边参数同源。写入、读取sessionId、tenantId必须保持一致。
  11. 10.11 删除接口返回真实影响行数,同时操作L1、L3多张表。
  12. 10.12 访问计数不要同步写库,进程内存先累加,定时批量落库,不拖慢查询接口响应。

11 最后唠两句

实现一个能存记忆的模块,网上随便抄抄代码半天就能写出来。

但是做一套长期稳定、不会越跑越乱的Agent记忆系统,难度直接上一个大台阶。

千万不要把记忆当成简单的存储桶。要当做一套独立子系统看待,强调隔离、可解释、故障隔离、可审计溯源。

存进去只是入门,召回准确、可控治理、故障不会雪崩、出问题能够查清楚,这才是生产环境真正刚需。

很多demo给你展示的效果都特别漂亮,但是没有处理线上的脏数据、异常边界、数据污染。真要上生产,这些啰嗦的约束一个都省不掉。

P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 7:13:33

电流采样选型指南:分流器与霍尔传感器的物理本质与工程决策

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/3 7:12:40

2026年字节跳动嵌入式笔试试卷带答案

2026年字节跳动嵌入式笔试试卷带答案 满分:100分 时间:90分钟 一、单选题(每题3分,共30分) 1. 在智能硬件/IoT边缘设备上,应用程序常跑在嵌入式Linux用户空间。下列关于进程与线程的说法,正确的是( ) A. 进程间不共享地址空间,线程间共享同一进程地址空间 B. 进…

作者头像 李华
网站建设 2026/10/3 7:12:39

门窗玻璃的质量检查

门窗玻璃的质量检查 门窗上的玻璃进行质量检查时, 并不是对整个建筑物所有门窗上的玻璃都需要进行检查。而应首先划定检验批, 检验批的确定为: 以同一品种、类型和规格门窗的樘数为单位, 每100 樘作为一个检验批, 如果不足100 樘时也应当划为一个检验批。有些特种门上不要求安装…

作者头像 李华
网站建设 2026/10/3 7:12:05

基于springboot + vue美食商城系统(源码+数据库+文档)

美食商城系统 目录 基于springboot vue美食商城系统 一、前言 二、系统功能演示 三、技术选型 四、其他项目参考 五、代码参考 六、测试参考 七、最新计算机毕设选题推荐 八、源码获取&#xff1a; 基于springboot vue美食商城系统 一、前言 博主介绍&#xff1a;✌…

作者头像 李华
网站建设 2026/10/3 7:12:02

fhb - 青龙面板自动签到脚本

自动签到、自动观看、消息推送。这款自动化脚本帮你完成平台的日常签到和任务&#xff0c;解放双手&#xff0c;再也不用每天手动操作。功能介绍 「fhb」脚本支持以下功能&#xff1a; • 自动完成每日签到 • 自动领取奖励 • 支持多账号 • 签到结果通知 使用方法 1. 下载脚本…

作者头像 李华