ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

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

AI Agent生产级记忆系统:踩坑总结与四层金字塔架构 文章目录1 为啥AI Agent一定要搞记忆现实坑多到离谱2 整体架构解耦独立记忆子系统3 四层金字塔记忆模型 L0‑L34 双存储引擎SPI插拔式设计5 写入主链路踩坑踩出来的代码逻辑6 主动Push召回别搞被动Pull模式6.1 召回流水线6.2 异步回写队列MemoryWritebackQueue7 S1可解释混合打分为啥叫match不叫similarity8 记忆治理闭环抽取、合并、遗忘淘汰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 推理主动召回。该想起来的信息Agent自己主动捞出来别等着用户反复提醒。总不能每次都要用户说“还记得我上次跟你说的吗”。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记忆不要平铺一堆记录堆在表里。搞成分层金字塔越往上信息密度越高召回优先级也越高。3.1 L0 RawLog原始对话日志完整原始会话痕迹带上traceId用来全局溯源回放。**这一层不会参与召回**只用来留痕排错。别啥都往模型塞原始日志token爆炸能把钱包烧穿。线上排查bug的时候这一层就是救命稻草。3.2 L1 Atomic原子记忆最小不可拆分记忆单元分三类session_var会话变量、user用户记忆、global租户全局记忆。会参与召回打分也支持遗忘淘汰。这里踩过大坑global类型记录存的不是userId实际存的tenantId租户ID。写查询的时候如果不加过滤直接按userId查全部租户之间数据直接串。线上一出这种跨租户泄露bug年终奖原地拜拜。3.3 L2 Scene场景块会话摘要块附带关联一批L1的id列表。每次会话启动优先加载充当对话骨架。相当于给这一次聊天做的读书笔记。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还很难复现。publicMapString,ObjectsaveUserMemory(StringtenantId,StringuserId,Stringcategory,Stringcontent,Doubleimportance){requireText(tenantId,租户标识不能为空);requireText(userId,用户标识不能为空);StringnormalizedCategoryhasText(category)?category.trim():custom;if(!isCategoryAllowed(normalizedCategory)){// ① 白名单校验returnMap.of(allowed,false,reason,记忆类别不在白名单内...);}ImportanceScoreassessedimportanceScorer.score(content,normalizedCategory);// ② 重要性的评估【脱敏前】StringsafeValuesanitizeIfEnabled(content);// ③ 敏感脱敏if(!hasText(safeValue)){returnMap.of(allowed,false,reason,内容为空或全部为敏感信息...);}StringidIdUtil.fastSimpleUUID().substring(0,12);StringtraceIdtrace-id;longtsSystem.currentTimeMillis();doublevalueresolveImportance(importance,assessed);oltp.saveRawLog(L0RawLog.forInsert(traceId,user-userId,tenantId,ts,system,USER_MEMORY:normalizedCategorysafeValue,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 | explicitMapString,ObjectrecordMaprecord(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。SQL下推查询当前用户L3画像加上L1原子记忆数据逐条跑MemoryScorer打分按照分数、最近访问时间、时间戳做稳定排序截取topK最后过滤掉低于minScore阈值的记录组装片段注入prompt同时写L0留痕。三条血的教训三条注入铁律① 必须加冲突声明仅供参考若与用户当前陈述冲突以用户当前陈述为准。用户改了喜好旧记忆不能还在那瞎指挥。② 如果啥记忆都没召回什么文本都不要注入。千万别写个“暂无记忆”占位凭空污染上下文token还搞坏prompt缓存。写demo无所谓线上token钱一分一分烧。③ 记忆内容注入到系统提示词不要直接往messages数组塞SYSTEM消息。框架会直接拒绝一命中记忆对话直接报错失败复现还时有时无调得人心态爆炸。6.2 异步回写队列MemoryWritebackQueue大模型把回答返回给用户之后整个http链路就结束了。事实抽取更新记忆这种重活放到后台异步队列处理。publicclassMemoryWritebackQueueimplementsAutoCloseable{publicstaticfinalintMAX_PENDING1000;// 队满即拒绝新任务privatefinalBlockingQueueTaskqueuenewLinkedBlockingQueue(MAX_PENDING);privatefinalExecutorServiceworker;publicMemoryWritebackQueue(MemoryManagermemoryManager,MemorySpanRecorderrecorder){this.workerExecutors.newSingleThreadExecutor(r-{ThreadtnewThread(r,agent-memory-writeback);t.setDaemon(true);// 守护线程退出不卡进程returnt;});this.worker.submit(this::consume);}publicintsubmit(Tasktask){booleanacceptedqueue.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)||maxMessages0)return;SessionMemorysessionloadSession(sessionId);if(sessionnull)sessionnewSessionMemory();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){longtsSystem.currentTimeMillis();StringtraceIdconvo-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美滋滋上线就翻车。10.1 强制隔离维度编译期约束仓储方法tenantId、userId必须入参不提供只有userId的重载漏传直接编译报错不要留运行时才炸的口子。10.2 唯一写入出口脱敏白名单重要评估全部收拢一处拒绝到处复制粘贴逻辑。复制一次代码复制一堆潜在bug。10.3 记忆属于增强附加能力。记忆逻辑不管哪里失败只打警告日志不能阻断主对话链路。不能辅助组件把主业务搞挂。10.4 配置阈值启动期校验。权重总和、半衰期、topK参数非法直接终止服务启动。不要让服务带着错误配置跑业务流量。10.5 打分器纯函数无状态时钟时间由调用方传入保障打分结果可回归单元测试能稳定跑。10.6 区分同步异步相位。链路结束之后的异步任务不要再写链路span只允许追加写审计表。10.7 审计表append‑only不支持update、delete。事件记录只追加不改方便事后审计溯源。10.8 召回、合并、淘汰全部使用同一个打分器实例杜绝多套标准打架。10.9 三条注入铁律严格执行冲突声明、空召回不注入、注入内容落L0留痕。10.10 读写两边参数同源。写入、读取sessionId、tenantId必须保持一致。10.11 删除接口返回真实影响行数同时操作L1、L3多张表。10.12 访问计数不要同步写库进程内存先累加定时批量落库不拖慢查询接口响应。11 最后唠两句实现一个能存记忆的模块网上随便抄抄代码半天就能写出来。但是做一套长期稳定、不会越跑越乱的Agent记忆系统难度直接上一个大台阶。千万不要把记忆当成简单的存储桶。要当做一套独立子系统看待强调隔离、可解释、故障隔离、可审计溯源。存进去只是入门召回准确、可控治理、故障不会雪崩、出问题能够查清楚这才是生产环境真正刚需。很多demo给你展示的效果都特别漂亮但是没有处理线上的脏数据、异常边界、数据污染。真要上生产这些啰嗦的约束一个都省不掉。P.S. 目前国内还是很缺AI人才的希望更多人能真正加入到AI行业共同促进行业进步增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 传送门http://blog.csdn.net/jiangjunshow教程通俗易懂高中生都能看懂还有各种段子风趣幽默从深度学习基础原理到各领域实战应用都有讲解我22年的AI积累全在里面了。注意教程仅限真正想入门AI的朋友否则看看零散的博文就够了。
返回列表