ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

AI Agent记忆系统实战:从架构到遗忘机制

2026/9/10 4:25:00 拓冰建站 浏览量
AI Agent记忆系统实战:从架构到遗忘机制 你有没有过这种体验昨天刚和某个AI助手聊完旅行计划今天再打开它对方一脸无辜地反问“你想去哪儿玩来着”。如果你只是个普通用户顶多吐槽一句“人工智障”但如果你正在做AI Agent开发这种“金鱼记忆”问题几乎躲不掉。这个系列走到第三篇前两篇我们处理了Agent的骨架搭法和任务执行这一篇集中解决一个让Agent真正有“人味”的能力——记忆。这篇文章适合三类人正在用LangChain这类框架搭Agent的开发者、想给个人知识库助手加长期记忆的爱好者、以及刚入行但对“记忆机制”这个概念还一头雾水的初学者。先给个结论Agent的记忆不是一个功能而是一套系统工程只靠“把历史消息塞进prompt”这种歪招做出来的东西离“记住你”差着十万八千里。1. 记忆对Agent意味着什么先想清楚再动手1.1 没有记忆的Agent为什么显得“笨”很多人把Agent的“笨”归咎于模型能力不够其实有一半的锅要记在“失忆”头上。一个没有持久化记忆的Agent每次对话都是从零开始它不知道你是程序员还是牙医不知道你上次聊到一半的项目叫什么也不知道你对答案的偏好是简洁还是详尽。这意味着每一次交互用户都要重新自我介绍、重新描述背景、重新解释上下文。这种体验放在PC时代叫作“重启即空”放在AI应用里叫作“留不住用户”。从技术角度看大模型本身有上下文窗口几千到几万token不等。窗口内的内容模型“看得见”窗口外的东西一律等于不存在。短期会话还好一旦跨天、跨设备、跨会话模型就是彻底失忆。所以我们要做的记忆系统本质上是一个“上下文的外置硬盘”把模型窗口里放不下的历史信息压缩、提炼、存储在需要的时候捞回来再塞回窗口里让模型表现得像什么都记得。1.2 从人脑借鉴的四类记忆在做记忆系统设计时我建议先别急着写代码而是把人脑的记忆机制过一遍。认知科学里通常把记忆分成工作记忆、情景记忆、语义记忆、程序性记忆这套分类放在Agent上非常贴切。工作记忆对应Agent当前会话的上下文也就是窗口里的对话历史它是易失的情景记忆对应“某个时间点发生了什么”的具体事件比如“上周二用户说他的项目下周上线”语义记忆是从事件里提炼出来的事实和偏好比如“用户是后端工程师喜欢用Go语言”程序性记忆则对应“怎么做某件事”的技能和流程比如用户教过Agent如何格式化周报、如何整理报销单Agent学会了下次就照着做。市面上大多数Agent只做到了工作记忆少数做到了情景记忆和语义记忆程序性记忆几乎没有产品做好。但你要知道完整的产品化记忆系统这四个层面都得有只是实现的优先级和成本不同。1.3 开工前必须回答的三个问题别一上来就研究Milvus、Chroma、向量索引先想清楚三个业务问题第一你的Agent需要记住多久只记住当前会话还是跨会话记住一周、一个月、永久记住多久决定了存储方案和数据生命周期管理策略。第二Agent需要记住什么用户的基本信息、偏好、历史决策记录、曾经输入过的文档内容还是它自己执行过的任务轨迹记错比不记更可怕记太多又会导致检索时抓不回重点。第三记忆用在哪里是每次对话前自动调用来润色回答还是只在用户主动询问“你还记得吗”时调取这两者对检索时效和召回精度的要求完全不同。这三个问题来自我实际的踩坑教训。我做第一个带记忆的Agent时根本没想清楚一股脑把所有对话都向量化存进向量库结果对话一长检索结果全是噪音Agent经常张冠李戴把A用户的事安到B用户头上。后来才明白记忆系统的设计决策80%在动手前就定完了不是靠调参调出来的。2. 一张记忆系统架构图分层、存储、技术栈怎么选2.1 四层记忆模型从热数据到冷数据根据前面三个问题的结论我把Agent记忆分成四层来设计第一层是模型上下文层也就是当前会话窗口。这一层不需要我们存储但需要“整理”。对话长了要总结、压缩保证窗口里始终是最关键的信息。第二层是会话缓存层存的是原始对话记录通常放Redis或内存。它有两个用途一是做对话中的快速回溯用户问“我刚才说那个事”Agent能翻出来二是作为提炼记忆的原料每隔一段时间或者会话结束时从缓存里抽取结构化记忆。第三层是关键记忆层存放提炼后的用户画像、偏好、事实、重要事件。这层数据量不大但价值最高适合用SQLite或PostgreSQL这类关系型数据库存储查询快、结构清晰也方便做增删改查。第四层是语义记忆层存放需要模糊检索的文本片段、文档、历史对话摘要。这一层数据量大用途是“语义召回”用向量数据库实现比如Chroma、FAISS、Milvus、pgvector。很多Agent教程只讲这层好像把对话丢进向量库就完事了实际上向量层只是记忆系统的一个组件不是全部。2.2 存储引擎怎么选用表格说话当初我自己选型时看了不少资料也踩过不少坑。不同层级的记忆数据冷热属性、结构化程度、查询方式都不一样不可能用一套存储通吃。我整理了一个选型对照表直接照着选就行。记忆层级典型数据存储选型原因会话缓存层原始对话、未提炼的上下文Redis / Memcached / 内存读写快、支持过期时间适合高吞吐关键记忆层用户画像、偏好、事实SQLite / PostgreSQL结构化强、事务可靠、查询灵活语义记忆层文档、对话摘要、经验文本Chroma / FAISS / Milvus / pgvector适合向量检索支持相似度召回元数据索引记忆来源、时间、置信度、用户ID数据库字段或标签系统用于过滤、排序、权限控制如果你的项目还处于原型阶段SQLite加一个Chroma文件模式就完全够用了不用为了所谓的高并发去上重服务。等到用户量起来、数据量到了千万级别再考虑把SQLite换成PostgreSQL、把Chroma换成Milvus或者使用云厂商的向量数据库。一上来就堆组件只会让排查问题变得痛苦。2.3 一个可以照抄的技术栈组合我现在的个人项目采用的是一个非常朴素的组合FastAPI作为Agent后端服务Redis存会话缓存SQLite存用户画像和结构化记忆Chroma做向量库存语义记忆embedding模型用bge-m3或者OpenAI的text-embedding-3-small。模型调用层用的是开源的LangChain生态但记忆模块我建议你尽量自己写少用框架自带的高层Memory类。为什么建议自己写我遇到过太多次框架升级导致记忆接口行为变化的情况而且框架封装好用的Memory类虽然方便但底层逻辑是个黑盒出了问题你都不知道是存丢了还是查错了。自己写一套记忆模块代码量不大大约几百行但完全可控出了问题能顺着代码查。后面的示例我也都按自定义实现来讲不依赖某个特定框架。3. 第一步让Agent“听进去”——记录层实现细节3.1 会话身份与上下文管理的坑讲存储之前先解决一个最基础也最容易翻车的问题怎么区分不同用户的记忆。很多初学者在开发测试时只用本机user_id写死结果产品上线后用户A看到了用户B的记忆这是数据安全事故级别的bug。我的做法是所有记忆表都带两个字段user_id和session_id。user_id代表用户身份跨会话不变session_id代表一次会话每次对话开始都会生成。检索记忆时所有查询都必须强制带user_id过滤这一步绝对不能省。在设计数据库表时user_id和session_id都要建索引否则数据量上来后查询会非常慢。另外一个容易被忽略的点是session_id的生成时机。不要在用户第一次发消息时才创建而要在应用加载时就生成。不然用户刷新页面就会产生新会话看起来只是丢了一点点上下文实际上会导致会话缓存层频繁失效后续的记忆抽取也会把本来连续的对话切成碎片。3.2 记忆抽取不写流水账只提炼关键信息记录层最核心的工作不是“存原文”而是“提取值得记的东西”。如果你把整个对话原文都塞进向量库那等于没有设计检索时必然噪声爆炸。我一般用LLM执行抽取每次对话结束或者每经过一定轮数跑一次抽取任务。抽取的内容分成三类用户画像相关的事实姓名、职业、城市、技术栈、明确的偏好喜欢详细回答、不要emoji、习惯用Python、重要事件某项目立项、某日期要交报告。注意抽取时不能让模型自由发挥必须用JSON结构约束输出方便后续解析入库。这是我常用的抽取代码结构用pydantic定义结构再让模型填充from pydantic import BaseModel class ExtractedMemory(BaseModel): user_name: str | None None preferences: list[str] [] facts: list[str] [] def extract_memories(conversation: list[dict]) - ExtractedMemory: prompt f 你是用户的私人记忆助手。请仔细阅读以下对话 只抽取值得长期记住的用户信息包括 1. 用户的称呼或身份信息 2. 用户的明确偏好 3. 与用户相关的重要事实或事件 要求 - 不抽取临时性情绪不抽取与用户无关的信息 - 不编造任何对话中不存在的内容 - 每个字段尽量精炼一句话一条 对话内容 {conversation} 请严格按 JSON 格式返回 {{user_name: , preferences: [], facts: []}} resp llm.chat( messages[{role: user, content: prompt}], response_format{type: json_object} ) return ExtractedMemory.model_validate_json(resp)实际运行下来有几个细节值得注意。其一抽取频率不要太高每轮都抽会很费token而且会存大量重复信息。我一般是每5轮对话或者用户主动说“记住这个”的时候才触发一次。其二对话内容在传给抽取模型前要做截断取最近N轮就可以早期的信息要么已经被抽过要么早就不重要了。其三抽取结果入库前要做一次归一化比如用户这次说“我喜欢吃川菜”下次说“我超爱火锅”系统得能把这两条合并成“偏好辣味食物”而不是存两条互相矛盾的记录。3.3 落地示例SQLite存用户事实关键记忆层用SQLite就够建表和执行插入都非常轻量。我习惯把记忆内容分成mem_type字段方便后续按类型查比如profile表示画像、preference表示偏好、event表示事件。import sqlite3 DB_PATH agent_memory.db def init_db(): with sqlite3.connect(DB_PATH) as conn: conn.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, session_id TEXT NOT NULL, mem_type TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) conn.execute(CREATE INDEX IF NOT EXISTS idx_user ON memories (user_id, mem_type)) def upsert_memory(user_id: str, session_id: str, mem_type: str, content: str): with sqlite3.connect(DB_PATH) as conn: conn.execute( INSERT INTO memories (user_id, session_id, mem_type, content) VALUES (?, ?, ?, ?), (user_id, session_id, mem_type, content) )这套表结构看起来非常简单但应付个人项目和中小型产品足够了。当数据量涨到几十万条时你可能会发现SQLite的写入锁是一个瓶颈但那是后话真到了那个规模迁移PostgreSQL也不难因为schema基本不用变。4. 第二步让Agent“想起来”——检索层实现细节4.1 向量化不是万能的先搞懂相似度查询的边界记录存进库里只是第一步真正难的是“在正确的时候把正确的记忆捞出来”。很多人一上来就用向量检索把用户当前问题embedding之后去向量库里查TopK。这个方法对付“语义相似”的场景没问题但它的边界很明显向量相似不等于事实正确语义相近的句子可能内容完全不同。比如用户今天说“我想离婚咨询”明天说“我想离婚后财产怎么分”语义相似度很高但后者对前者并没有参考价值。类似的例子在实际对话里比比皆是。所以我不建议把向量检索当作唯一的召回手段。更稳的做法是混合检索向量检索负责语义召回关键词检索负责精确匹配两者取交集或按权重融合。这样既不会漏掉表达方式不同的句子也不会被“只说了一句话但包含关键人名、项目名”的情况漏掉。4.2 混合检索关键词向量别再二选一关键词检索可以用经典BM25算法也可以用简单的数据库LIKE查询。对于小规模数据SQLite的LIKE配合fts全文搜索就够。对于大规模数据可以考虑用Elasticsearch或者OpenSearch。不过我个人建议在数据量没有大到MySQL扛不住之前先用SQLite FTS5或PostgreSQL自带全文检索不要为了一个关键词检索就引入一套Elasticsearch集群。混合检索的常用融合方法是Reciprocal Rank Fusion原理很简单把两种检索结果分别排序每个结果按排名的倒数算分然后相加排序。这种方法实现简单、效果稳定是RAG领域常用的baseline。def hybrid_search(user_id: str, query: str, top_k: int 5): vec_results vector_recall(user_id, query, top_k * 3) bm25_results bm25_recall(user_id, query, top_k * 3) combined {} for rank, doc_id in enumerate(vec_results): combined[doc_id] combined.get(doc_id, 0) 1 / (60 rank) for rank, doc_id in enumerate(bm25_results): combined[doc_id] combined.get(doc_id, 0) 1 / (60 rank) ranked sorted(combined.items(), keylambda x: -x[1]) return [doc_id for doc_id, _ in ranked[:top_k]]这个代码里的60是经验常数用来平滑排名差异你也可以根据数据情况调整。混合检索不是银弹但它能明显减少“向量召回了一堆相关但不正确内容”的情况。我实测下来在用户画像和事实类记忆的召回准确率上混合检索比纯向量检索提高了大约20到30个百分点数据量越大提升越明显。4.3 排序与相关性门槛宁可少召回不要错召回召回之后还要过一道“相关性门槛”。很多人忽略了这一点把TopK结果一股脑塞给模型。问题是如果Top5里有2条是无关的那模型就会基于错误的上下文回答生成胡说八道的内容。更糟的是这种错召回还会污染对话主题让用户觉得Agent“记性真的好差”。我习惯在召回之后加一个reranking步骤用rerank模型对候选记忆重新打分。轻量做法是直接用LLM做二分类判断给定用户问题和候选记忆判断是否相关。更高效的做法是用cross-encoder模型比如bge-reranker-base它对相关性判断的准确率明显高于纯向量相似度。def rerank(query: str, candidates: list[str]) - list[str]: pairs [[query, c] for c in candidates] scores reranker.predict(pairs) return [c for _, c in sorted(zip(scores, candidates), keylambda x: -x[0])]除了排序我还会设置一个相关性分数的下限阈值。低于阈值的记忆直接丢弃宁可让Agent说“我记不太清了”也不要让它把一个错误记忆当作事实说出来。这个阈值怎么定我一般拿一组人工标注的正负样本跑一遍画出精确率和召回率曲线取曲线拐点对应的分数。听起来复杂其实用几十条样本就够了。4.4 完整示例从query到prompt注入把上面几步串起来完整流程如下用户发来问题先判断是否需要唤起记忆如果需要就取用户ID和问题文本做向量召回和关键词召回融合排序再过rerank和阈值过滤最终把命中的记忆拼进system prompt。def build_system_prompt(user_id: str, query: str) - str: profile load_user_profile(user_id) # 查SQLite关键记忆层 memory_hits recall_relevant_memories(user_id, query) # 混合检索重排 system 你是我的AI助手。关于我的背景信息\n if profile: system json.dumps(profile, ensure_asciiFalse) \n if memory_hits: system 我过去提过相关内容\n system \n.join(f- {m} for m in memory_hits) return system这里有一个细节唤起记忆的时机不是每轮都做。如果用户只是在闲聊你频繁唤起记忆反而显得机械。我现在的策略是先用一个轻量分类器判断当前问题是否需要历史信息。比如“我上次说的那个bug解决了吗”这类含“上次、之前、当时”的句子直接判定为需要唤起比如单纯问“今天天气”就不需要。这个判断可以用规则也可以用LLM数据量大了以后再用模型也不迟。5. 记忆该写什么、又该忘什么写入策略与遗忘机制5.1 写少不写多写稳不写快记忆的写入策略我总结成六个字写少、写稳、写准。宁可少记几条也不要乱记一堆。原因很简单记忆检索是有噪音的你存进去100条垃圾信息最终被召回的概率就越高影响回答质量的风险就越大。我见过很多团队把Agent记忆做成“对话录音机”什么都存结果系统变成一个巨大的垃圾场。一个合理的最小集是用户明确表达过的偏好、用户主动要求记住的事情、跨会话持续出现的高频主题、与任务强相关的关键事实。这四类信息价值密度最高值得写入。其他的比如今天中午吃了什么、随口吐槽了一句什么统统不进长期记忆。你可以把“用户主动要求记住”当作最高优先级把“持续出现3次以上”当作次高优先级其他的都只留在会话缓存层。5.2 冲突与合并旧记忆和新信息打架了怎么办用户的信息是会变化的。用户上个月说“我在北京工作”这个月说“我搬到上海了”。如果系统不做处理两条记忆同时存在Agent检索时会很困惑。处理办法是写入新记忆时先查一下有没有同类型、同主体的旧记忆如果有就标记为“待确认”状态让LLM判断是更新还是新增。这一步需要建立一个实体归一化的映射。最简单的做法是给每条记忆打上实体标签比如user、project、location当新记忆和旧记忆实体相同且内容冲突时触发更新流程。如果实体的技术判断做不好退一步也可以用时间戳覆盖同一实体、同一类型的记忆新写入的覆盖旧的。这个策略虽然粗暴但在大多数场景下够用因为它保证Agent永远优先相信最近的信息。5.3 遗忘机制不能只增不删很多记忆系统设计者把注意力全放在“怎么记住更多”却忽略了“怎么遗忘”。这是一个大坑。记忆只增不删最终会导致两个问题一是检索时干扰项越来越多二是存储成本不断上升。更关键的是用户对Agent的信任建立在“它能忘记不该记的东西”上一个什么都记得、什么都不忘的助手反而会让用户觉得不安。遗忘策略可以从三个维度设计。时间维度超过一定时限未访问的记忆自动降低权重最终进入归档或删除重要性维度低重要度记忆定期清理比如每周清理一次“闲聊类”记忆用户控制维度提供一个接口让用户能查看Agent记住了什么、删除某条记忆、甚至一键清空。最后一个维度往往最容易被开发团队忽略但对产品口碑来说非常重要。我在自己的项目里实现了一个“记忆废纸篓”删除的记忆不会立刻物理删除而是先软删除30天避免用户误操作后无法恢复。这套机制不复杂多一个deleted_at字段就能实现但它带来的用户体验提升非常明显。6. 必须避开的坑我在Agent记忆实战里踩过的雷6.1 记忆串号最严重的数据安全事故我在文章开头提过user_id没隔离用户A看到用户B的记忆这种问题一旦发生轻则产品口碑崩盘重则涉及隐私合规风险。排查这类问题关键是要在存储和检索两个环节都加上强制过滤并且在写代码时约定任何查记忆的SQL或向量查询都必须显式带上user_id条件禁止任何形式的全表扫描。更隐蔽的风险出现在向量库where过滤条件缺失时。Chroma这类向量库如果查询时不加metadata过滤就会检索到所有用户的数据这个bug在开发环境很难发现因为数据量小结果看起来都对。我建议在测试阶段专门写一条“跨用户检索测试用例”明确验证user_a查询时不会返回user_b的内容。6.2 重复写入同一事实被存了几十条重复写入是最常见的记忆质量问题。我一开始没有做去重结果用户说了一次“我喜欢喝美式”后面对话里又提了几次系统每5轮抽取一次一个月下来存了十几条“喜欢美式”。检索时全被召回浪费上下文空间。解决办法有两个层面。写入前先查重同用户、同内容、同类型如果已存在就直接跳过。定期合并用LLM对同一主题的多条记忆做一次归并提炼出一条更精炼的表述。前者适合在线写入后者适合离线任务。两个都做效果最好。6.3 上下文膨胀记忆太多把窗口塞满了很多人给Agent加记忆之后发现对话轮数一长提示词越来越大模型响应越来越慢费用也越来越高。这就是上下文膨胀。解决思路是分层使用记忆检索到的原始记忆只保留当轮需要的最核心部分其他细节转成摘要放到会话缓存层而不是一股脑全部注入system prompt。我现在会控制注入的token预算。比如设定每次最多注入600字记忆超出部分截断或只放摘要。这样既保证核心信息不丢又不会撑爆上下文窗口。6.4 和知识库类应用的结构差异搜索热词里有不少人在关注“obsidian ai agent 知识库”这类应用。这里要提醒一句知识库检索和记忆检索是两套逻辑。知识库是“用户问什么文档里有没有对应内容”它重语义匹配、重来源引用记忆是“用户是谁、之前发生过什么”它重时间线、重用户视角。你可以把知识库当作Agent的外部工具但不要把它塞进记忆模块里混着存否则检索时的目标函数会互相干扰。我见过有人把用户笔记直接向量化存进记忆库结果Agent回答问题时经常引用用户自己的旧笔记看起来像在自言自语体验非常差。7. 从记住一个人到记住一群人多Agent与共享记忆的扩展方向7.1 用户画像中心让多个Agent共享同一份用户档案如果你在做一个平台型产品不同场景下可能有多个Agent客服Agent、推荐Agent、写作助手Agent。如果每个Agent各存一套记忆用户就会觉得这些Agent“不熟”。正确做法是建立独立的用户画像服务所有Agent都通过API读写同一份用户画像再按场景附加各自的专用记忆。这个画像服务本质上就是把我前面讲的关键记忆层抽出来做成一个内部微服务。写接口要保证幂等读接口要带缓存底层直接用PostgreSQL就能扛住大部分场景。7.2 团队记忆一群Agent共享项目上下文再往后扩一步就是多个Agent协作完成复杂任务。这时候记忆的粒度不只有用户还有项目、团队、任务。比如一个项目Agent团队其中一个Agent负责需求分析另一个负责写代码第三个负责测试它们之间得共享项目背景、技术方案、当前进度。这个共享的知识库不是用户画像而是“项目长期记忆”。这里要处理的核心问题是写入冲突。不同Agent可能在同一时间往共享记忆里写内容必须做好乐观锁或版本控制。实际操作中我给每条共享记忆加一个version字段写入时带上期望版本号版本不一致就拒绝写入并返回冲突由上层逻辑决定是覆盖还是合并。7.3 记忆可视化让用户看到Agent记住了什么最后分享一个我觉得极其重要的扩展方向记忆可视化。给用户一个面板展示Agent当前记住了哪些关于他的信息并且提供“删除”“修改”“补充”的操作入口。这件事技术难度不高但对用户信任度的提升非常明显。很多人担心AI记住自己的隐私一旦你给了用户“查看和控制”的权利这个顾虑就会大幅缓解主动使用记忆功能的频率也会更高。我自己的项目里做了一个“记忆卡片”界面按用户画像、偏好、事件三类展示记忆每条旁边都有编辑和删除按钮。上线后大概有三分之一的用户会主动去清理或修改记忆这比任何宣传语都更能说明问题。真要动手做的话我建议从最小可行版本开始SQLite存关键记忆、向量库存语义记忆、单Agent接入。这套东西不用框架自带记忆模块几百行代码就能跑通。跑通之后再逐步加混合检索、重排、遗忘机制、记忆可视化。很多团队一上来就设计一个超级复杂的记忆系统结果迟迟落不了地。先从“能记住”开始再往“记得准”“记得巧”迭代这条路我走过稳。