Agent 长期记忆架构不是一个新概念但真正把它从“知道要分层”落到“能跑通、能检索、能遗忘”的工程实现很多人在第一步就卡住了。这篇文章围绕 Agent 长期记忆架构把短时记忆与长期记忆的边界、记忆分层、向量检索、混合召回、遗忘与压缩机制、以及完整落地流程拆开讲一遍。适合正在做大模型应用、Agent 框架开发、知识库问答和对话系统的人看。最值得关注的点是记忆系统不是加一个向量库就完事它需要存储、检索、更新、遗忘四个环节互相配合缺一个都会在实际使用时暴露出上下文混乱、历史遗漏、记忆膨胀这些很难受的问题。1. 先搞清楚 Agent 记忆到底在解决什么问题Agent 记忆问题的本质是大模型本身没有跨会话记忆。模型每次接收的都是独立请求对话一结束上一轮的信息就丢掉了。你可以把模型本身当作一个“每次重新开机”的处理器而 Agent 要做的是在模型外部补一套持久化的记忆系统让它在多轮对话、多个任务、长期运行过程中知道自己做过什么、用户说过什么、哪些结论需要保留。1.1 没有记忆的 Agent 会有哪些具体表现最常见的现象是三个用户在多轮对话中已经提供了自己的偏好比如“不要用表格输出”下一轮 Agent 又用表格了。Agent 在某个任务里生成了中间结果新任务开始后完全忘了重新从零推理导致重复劳动。多用户场景下不同用户的上下文互相干扰A 用户的数据被模型误当作 B 用户的数据。这些问题不是模型能力不够而是缺少一个“状态保存与恢复”的机制。短期看加长上下文窗口能缓解一点但上下文窗口有上限成本也会随 token 数量快速增长真正要解决长期运行的问题必须做记忆系统。1.2 为什么不能把所有内容都塞进上下文有人会问大模型上下文窗口已经做到几十万甚至上百万 token 了是不是就不需要记忆系统了这里有个误区。上下文窗口大只代表“能塞进去”不代表“应该塞进去”。原因有三个第一成本问题。每轮请求都会把上下文里的所有 token 重新编码一次内容越多延迟越高费用越大。第二注意力稀释问题。几百条历史消息堆在上下文中模型很容易被无关内容干扰反而找不到当前任务真正需要的那几段关键信息。第三维护复杂性。如果每个会话都把全部历史拼进去会话长度会无限制膨胀迟早触顶。所以正确思路是把记忆当作一个独立的数据层必要时通过检索把最相关的片段放回上下文而不是让所有历史常驻上下文。2. 记忆分层短时记忆与长期记忆的边界到底怎么划很多人把短时记忆理解为“最近几轮对话”把长期记忆理解为“全部历史”。这个理解太粗糙。真正的分层设计要考虑的是记忆的生命周期、访问频率、抽象程度和保存优先级。2.1 工作记忆与短期上下文工作记忆Working Memory是当前任务真正在用的那部分内容相当于 Agent 当前推理过程中的临时草稿。它可以表现为当前对话最近几轮的原始交互记录。当前任务正在处理的中间变量和结果。本次执行链路的步骤状态。这部分记忆的特点是生命周期短、与当前任务强绑定、访问频率最高。一般存放在 Redis 或者内存里也可以直接放在上下文里。它不需要持久化任务结束就清理。短期上下文可以看作是工作记忆的扩展一般保存最近 N 轮对话。它的作用是让模型能够理解“当前正在聊什么”。实现上可以简单用消息列表拼接也可以设置 token 上限超出后丢弃最旧的消息。这里不需要做复杂索引因为访问模式是顺序读取按时间倒序即可。2.2 长期记忆的三种类型长期记忆不等于“所有历史消息的堆积”。在实际系统中我更建议把长期记忆拆成三种类型分别采用不同的存储和检索方式。情景记忆Episodic Memory记录“过去发生了什么”。比如用户说过的需求、Agent 完成过的任务、某个决策的前因后果。它是有时间戳的事件记录检索时通常按时间、按实体、按语义相关性召回。语义记忆Semantic Memory记录“从经历中抽象出的知识”。比如用户的偏好、项目的固定规则、领域术语解释。它不绑定具体时间属于可以长期复用的结论。它通常由情景记忆经过摘要、提炼后生成。程序记忆Procedural Memory记录“怎么做”。比如某个任务的标准流程、某个工具的正确调用方式、某个 Bug 的解决方案。它像一套技能库Agent 遇到相同场景时直接调用而不是重新推理。这三种记忆在工程实现上可以共用一套存储基础设施但在数据结构、写入流程和检索策略上要区分。否则就会出现“把用户偏好当成历史消息搜索”“把任务流程当知识片段引用”这种错位。2.3 记忆分层的核心原则我自己在落地时会坚持几个原则。第一写入时分类不要等检索时再猜。每次写入记忆时明确标注这条记忆的类型是事件、是偏好、还是技能。检索时才能按类型过滤。第二短期记忆负责“连续性”长期记忆负责“复用性”。短期记忆可以粗糙、完整、按时间顺序存长期记忆一定要加工过要摘要、去重、结构化不能直接丢原文。第三记忆要有多级抽象。原始事件是最底层向上可以聚合成摘要再向上可以提炼成规则。层级越高数量越少重用价值越大。3. 存储层设计向量库、关系库还是混合存储记忆系统的存储层是整个架构的地基。选型做对了后面检索和遗忘都好做选型做错了索引混乱、数据冗余、更新困难会轮着来。3.1 不同存储介质适合什么内容没有一种存储能解决所有记忆问题。常见选择是这样的存储介质适合内容优点需要注意的问题内存 / Redis工作记忆、短期会话状态访问快、支持过期重启丢失不适合长期保存关系型数据库SQLite / PostgreSQL事件记录、用户档案、技能条目查询灵活、事务保障、结构化程度高语义检索弱需要配合向量字段向量数据库Chroma / FAISS / Qdrant / Milvus语义检索场景匹配相似片段支持 embedding 相似度搜索精确过滤弱元数据管理能力一般日志 / 对象存储原始记忆快照、中间结果成本低、容量大不适合在线检索只做存档实际项目里我推荐至少组合两种关系库保存结构化记忆向量库负责语义检索。关系库里的记录用唯一 ID 和向量库里的向量条目对应两边通过记忆 ID 关联。3.2 记忆条目的数据结构一条记忆的存储结构至少应该包含这些字段{ memory_id: mem_8f2a31c0, type: episodic, content: 用户在 2025-06-01 提出需要把周报改成 Markdown 格式发送, summary: 用户偏好 Markdown 格式周报, embedding_vector: [0.012, -0.047, ...], entities: [周报, Markdown, 工作日], importance: 0.8, access_count: 3, last_access_at: 2025-06-10T09:30:00Z, created_at: 2025-06-01T18:00:00Z, expire_at: null, source_session_id: session_88d1 }字段含义可以这样理解type记忆类型用于检索时按类型过滤。content记忆原文是处理当前任务时回填给模型的关键内容。summary摘要用于上层快速浏览和后续压缩。embedding_vector语义向量用于相似度检索。entities抽取出的关键实体用于关键词召回和关系查询。importance重要度分数遗忘机制的核心依据。access_count 和 last_access_at访问频率和时间用于计算热度。expire_at过期时间可选字段。3.3 写入记忆的流程记忆不是“对话结束后顺手存一下”这么简单。我在项目里会走一条固定写入链路从当前 Agent 的执行结果中抽取记忆候选。可能是关键事件、用户明确表达的偏好、或者一个成功执行过的技能流程。对候选内容做清洗和格式化。去掉无意义的口头语补全上下文保证内容可以独立阅读。给记忆打标签。判断类型抽取实体评估重要度。生成 embedding 向量并写入存储。检查是否已有相同或相似记忆。相似度超过阈值时选择更新旧记录而不是新增避免重复膨胀。第 5 步容易被忽略但长期运行之后重复记忆会让检索质量急剧下降。同一个偏好存了 20 遍检索时返回 20 条相似内容既浪费 token 又干扰模型判断。4. 检索机制从单路召回走向多路融合记忆系统能不能用检索质量说了算。检索不好的时候最典型的表现是明明存了相关信息Agent 却“想不起来”或者检索出一堆不相关的内容干扰回答。4.1 单靠向量检索的局限向量检索的思路是把文本转成 embedding 向量然后计算余弦相似度或内积返回最相似的 Top N。它擅长处理语义相近但表达不同的情况。比如用户之前说“把报告改成 PDF 发我”现在说“还是用 PDF 方式给我”向量检索能匹配上。但它有几个明显局限对关键词命中的场景不敏感。比如用户搜索“预算表”如果历史记录里只有“财务报表”和“季度费用汇总表”向量相似度可能偏低。对数字、名称、代码这种精确信息不友好。embedding 在表示数字、版本号、文件路径时经常把“v2.1”和“v2.3”当成相近内容。对时间顺序无能为力。向量检索无法表达“最近三个月”这种时间过滤条件需要额外用元数据过滤。4.2 混合检索向量 BM25 多路召回更稳的做法是混合检索。向量召回和关键词召回并行执行然后做结果融合。BM25 是传统的关键词检索算法适合处理精确匹配、命名实体、编号和代码片段。它和向量检索正好互补def hybrid_search(query, memory_store, top_k10): # 第一路向量检索 query_vector embed(query) vector_results memory_store.vector_search(query_vector, top_ktop_k) # 第二路BM25 关键词检索 keyword_results memory_store.bm25_search(query, top_ktop_k) # 结果融合按分数加权 merged fuse_results(vector_results, keyword_results, vector_weight0.7, keyword_weight0.3) return merged[:top_k]融合方式可以从简单到复杂选择简单加权向量分数和 BM25 分数做加权平均适合快速验证。RRFReciprocal Rank Fusion不关注具体分数按排名倒数的融合方式稳定性好适合不同分数体系差异大的情况。Rerank 模型先用多路召回取回几十条候选再用一个交叉编码器精排准确率最高但会多一次模型推理延迟和成本更高。我自己的建议是第一版先用 RRF结构简单、换算法影响小。等数据量上来、检索瓶颈明确之后再考虑引入 rerank。不要一上来就上重排序模型很多场景根本不需要。4.3 检索前处理先过滤再召回检索不应该直接对全量记忆做相似度搜索。更合理的顺序是先根据记忆类型过滤。只搜事件类型还是事件加语义由任务场景决定。再根据时间、用户、会话维度过滤。防止跨用户、跨项目的数据串扰。最后做向量检索和关键词检索。检索后进行时间衰减调整。同样相似度的内容更近的优先级更高。这套流程看起来多实际一次性写完并不复杂。关键是顺序不能反。如果先做相似度搜索再做过滤数据量大了之后很多被过滤掉的结果会浪费计算而且可能把真正相关的记忆挤出不进 Top N。注意检索结果回填到上下文时必须在每条记忆前标注来源字段例如“来自用户 2025-06-01 的反馈”或“来自 2025-05-20 的任务记录”。这样模型能区分记忆事实和当前对话内容减少事实性混淆。5. 遗忘机制不是简单删除而是压缩、降级与归档遗忘机制是长期记忆架构里最容易被忽视、却最影响系统长期稳定性的部分。没有遗忘机制的 Agent运行一段时间后记忆库就会膨胀检索变慢、干扰变多、成本变高。5.1 遗忘的必要性我们可以把记忆库想象成一个文档库。如果所有文档都永久保留一是不好找二是找到的文档里有很多过期内容。对 Agent 来说不遗忘的表现是旧需求覆盖了新需求。用户已经改过三次要求老的偏好记录还在最高权重位置每次检索都把旧要求带回来。记忆条数过多向量检索耗时增长检索结果噪声变大。存储成本持续上升。embedding 向量本身不小大量过期记忆是纯浪费。5.2 遗忘设计的三个层级遗忘不能设计成“到期直接删”。更好的方案是分三层处理。第一层归一化压缩。对于高度相似或重复的记忆合并成一条摘要。比如用户在过去十天里说过五次“邮件用中文写”系统应该维护一条“邮件语言偏好中文”而不是保存五条原始记录。压缩过后记忆数量下降检索精度反而上升。第二层重要度衰减。每个记忆维护一个综合分数分数由三部分构成final_score initial_importance * decay_factor access_bonus - time_penalty其中 decay_factor 随时间衰减时间越久初始重要度的影响越低。access_bonus 是每次被检索命中时增加的分数。time_penalty 是一个持续累计的惩罚项防止某些记忆因为初始重要度高就永远霸占前排。这个分数不需要太复杂重点是让“经常被动用”“最近还在使用”“被用户明确标记”的记忆保持靠前。第三层归档或删除。当记忆分数低于阈值且超过一定时间未访问时可以转移到冷存储目录保留原始记录但退出在线检索。如果冷存储也超过保存周期再清理删除。这样做的价值在于你既能保住长尾数据用于离线分析又不让它们干扰在线检索。5.3 遗忘触发时机遗忘不应该是实时轮询更合理的做法是写入记忆时触发检查当记忆总数超过阈值执行一次批量压缩。定期任务每天或每周执行一次衰减计算和降级归档。检索返回空结果时触发如果检索不到内容可能说明记忆已经过度压缩需要从更底层恢复摘要或原文。我见过不少项目把遗忘做成“满了就删最旧的”这种方案太粗。丢掉的往往不是过期数据而是低频但重要的长期偏好。正确姿势一定是先压缩再衰减最后才删。6. 完整落地流程从单会话到多用户长期运行前面几节讲的是组件这一节把它们串成一个可运行的完整流程。6.1 最小可用架构一个能跑起来的 Agent 记忆系统至少需要这些模块记忆写入模块从对话或任务结果中提取候选记忆。记忆存储模块包含向量库、关系库、内存缓存。记忆检索模块执行混合召回、评分、过滤。记忆遗忘模块执行压缩、衰减、归档。上下文组装模块把检索结果按要求格式拼装成模型可用的上下文。这五个模块可以全部写在同一个项目里也可以拆成独立服务。初期建议先放在同一个进程里靠函数调用串联降低运维成本。等数据量和并发上来了再把向量检索和遗忘任务拆出去。6.2 单条任务的处理时序一次带记忆的 Agent 调用时序大致是这样用户输入问题。Agent 从短期会话上下文中取最近几轮消息。根据当前任务意图抽取检索关键词和过滤条件。执行混合检索从长期记忆库召回 Top N 相关记忆。将短期上下文和长期记忆结果按角色标签拼接构造完整 prompt。模型推理生成回答。执行完成后从本轮交互中提取值得记忆的内容写入记忆库。更新相关记忆的访问时间和访问次数。第 7 步非常关键。很多 Agent 只做前 6 步结果是每轮都能查但从不新增记忆长期记忆库永远是空的。这就等于只做了一个“历史数据库只读接口”没有真正实现记忆。6.3 多用户场景下的隔离策略如果你的 Agent 要服务多个用户或项目必须在记忆库层面做隔离。最简单的方式是所有记忆记录都带 owner_id 和 project_id 字段检索时强制过滤这两个维度。-- 示例只检索当前用户在当前项目的记忆 WHERE owner_id user_1001 AND project_id proj_88 AND status active不要指望靠向量相似度区分用户。不同用户的偏好可能高度相似必须用字段过滤。这也是为什么前面强调关系库和向量库配合使用而不是只用向量库。7. 常见坑点与排查顺序这部分是我自己踩过、也帮别人排查过的高频问题。遇到记忆系统“看起来不工作”时先不要怀疑模型按下面顺序看。7.1 记忆检索不到内容先看这几处基础数据是否存在查记忆库记录数。记录数是零说明写入环节就没工作。输入输出路径检查 query embedding 是否和写入时用同一个模型。embedding 模型不一致检索结果会完全不对。过滤条件检查 owner_id、project_id、type 过滤条件是否过严。相似度阈值阈值设太高会导致召回为空建议先从 0.6 或更低起步调试。7.2 记忆检索到了但回答不用这是更隐蔽的问题。检索结果存在但模型回答里没体现。常见原因是prompt 里没有明确指出记忆内容的角色和出处。模型分不清哪些是记忆、哪些是当前对话。记忆内容太多挤占了模型注意力。Top N 取太多反而把关键信息淹没。记忆内容本身残缺。存入时没有补全上下文模型看到一条孤立的“用户偏好中文”不知道它属于哪个场景。解决办法是严格控制回填数量一般 3 到 5 条足够每条记忆用“【记忆】来源xxx”这种格式标识写入时保证内容在脱离原上下文后依然可读。7.3 记忆库膨胀导致检索变慢排查顺序是先看重复率。有没有大量相似记忆这是最常见的原因。再看有无清理任务。检查遗忘机制是否真的在执行还是只写了代码没跑定时任务。最后看索引。向量库有没有建索引关系库有没有按 owner_id 和时间建联合索引。这里要给一个判断标准如果你的记忆库预期有百万级条目向量检索必须用 ANN 索引不能用暴力全量比对如果只有几万条用暴力比对也还行不要提前优化。# 示例驱动遗忘任务的伪代码 def run_forget_cycle(memory_store): # 1. 合并重复记忆 duplicates memory_store.find_similar_groups(threshold0.92) for group in duplicates: merged merge_memories(group) memory_store.replace_group(group, merged) # 2. 更新重要度分数 memory_store.decay_importance(rate0.95) # 3. 归档低频记忆 archive_list memory_store.find_low_score_memories(score_threshold0.3) memory_store.archive(archive_list) # 4. 清理过期归档 memory_store.purge_archived_overdue(days180)7.4 环境与调试建议如果你还没有跑过记忆系统强烈建议先用 SQLite 加一个小型向量库起步不要一上来就上分布式存储。环境配置方面注意三点embedding 模型版本要固定。不要今天用模型 A 写入明天用模型 B 检索维度不同直接报错或全都不匹配。临时文件和数据库目录权限要先确认。很多启动失败都是路径或写入权限导致的不是算法问题。单条任务先验证再开并发。先跑一条对话确认记忆写入、检索、回填都正常再上批量和多用户。注意不要急着调遗忘参数。先让系统跑上一周积累真实记忆数据看哪些记忆被反复访问、哪些从未被命中再决定衰减速率和归档阈值。用假数据调参调出来的数字没有参考意义。8. 从 Demo 到生产的关键分界线最后说几句关于“能用”和“能上线”的区别。一个 Demo 级记忆系统能存、能查、能回填已经很有意思了。但生产环境的要求不一样。生产系统必须回答这几个问题重复记忆谁负责合并检索失败时怎么降级记忆误召回导致模型回答错误能不能追溯记忆数据怎么备份和迁移遗忘任务挂了有没有告警这些问题的答案会逼着你把记忆架构做得更细写入链路需要加消息队列解耦检索链路需要加超时控制在几十毫秒内遗忘任务需要独立的调度器。如果你现在还在学习阶段我建议按这样一个路径走先用 SQLite 加一个本地向量库做一个最小闭环目标是跑通“写入-检索-回填”。手动往记忆库里插一批精心构造的测试数据验证不同类型的记忆能不能被正确分类和召回。让一个 Agent 连续跑 50 轮对话观察记忆库增长速度和检索命中率。再加遗忘机制对比加之前和之后的检索质量。这个路径不需要大量算力也不需要一开始就选对全部技术栈。真正的难点不在选型而是把记忆从“一个可以存起来的数据结构”变成“一个能在关键时刻被找回来并正确使用的系统”。按分层思路先把骨架搭起来再逐步填充检索、压缩、降级和归档逻辑是比较稳的做法。