AI Agent开发中的记忆管理:构建长期记忆与向量检索的完整方案
发布时间:2026/10/13 12:14:59 锦皓数字建站

做 Agent 开发的时候最容易翻车的部分往往不是模型本身也不是那几套提示词模板而是“失忆”。我手头有个知识管理方向的小助手项目原型阶段用户问得最多的一句话就是它能不能记住我上个月说过的那堆需求很遗憾底层模型天生没有记忆关掉会话一切归零。这也是我把“AI Agent教程”系列第 04 篇专门留给记忆管理的原因。这一篇我会从记忆到底缺在哪讲起覆盖三种记忆的拆分、向量化存储选型、写入和检索的完整链路、记忆淘汰与更新机制最后附上我实际调试中踩过的坑和一套问题速查表给正在做智能体的读者一份可以直接参考落地的方案。1. 先理清楚Agent 最缺的到底是哪种记忆1.1 模型自带的“短期记忆”只有一张便签纸大模型本身并没有记忆能力它每次收到请求输入给它的是一大段拼接好的文本输出则是基于这段文本的续写或补全。所谓的上下文窗口本质上是临时借来的一叠便签纸窗口多大能摊开的纸就多大窗口一关纸上的字全部消失。很多人刚接触 Agent 时会误以为“模型能记住我说的话”其实那只是同一次会话里代码把历史对话不断拼接进新的请求而已。一旦换了会话、换了用户或者程序重启模型对你是谁、你关心什么、你之前做过什么完全一无所知。更麻烦的是即便是在单次会话内如果中间穿插了太长的工具返回结果、太长的文档片段模型照样会把最初的用户意图“忘”得七七八八。所以记忆管理要解决的第一件事不是“怎么让记忆变大”而是“怎么在模型有限的便签纸之外再给它准备一个真正可以长期存放、按需取用的仓库”。1.2 三类记忆分对了才谈得上管理做记忆系统之前我习惯把 Agent 的记忆拆成三层短期记忆、工作记忆、长期记忆。分类标准不是我拍脑袋定的而是从实际使用场景里反推出来的。短期记忆直接对应模型的上下文窗口存放的是当前会话里的多轮对话、临时事实特点是生命周期短、读取频繁、时效性高。它的管理重点是“控制规模”不能无限堆积否则便签纸会被无关内容占满。工作记忆对应当前任务运行过程中的中间状态例如多步 Agent 里已经完成的子步骤、某个工具返回的临时数据、等待下一步使用的变量。这部分往往不需要持久化任务结束就清理但设计不好会直接导致 Agent“走两步忘了第一步”。长期记忆则是真正值得沉淀的部分包括用户偏好、历史结论、领域知识、常用术语、之前任务的经验教训。它需要持久化存储并且能够在大批数据中快速检索到与当前问题相关的片段。我们通常说的“记忆管理”大不多都落在这一层。记忆类型生命周期典型载体管理重点丢失后的表现短期记忆一次会话内上下文窗口、消息列表控制规模、防止稀释对话前言不搭后语工作记忆单次任务内变量、状态对象、栈结构明确生命周期、及时清理多步任务中途找不到依据长期记忆跨会话、跨天向量库、关系表、文件索引命中率、时效性、防污染用户需求重复确认知识无法复用三层记忆不是独立存在的。短期记忆的数据经过提炼后可以写入长期记忆长期记忆被检索出来后会拼进短期记忆一起输入模型工作记忆则负责在它们之间传递当前任务的上下文。搞清楚这个闭环后面所有设计都有章可循。1.3 记忆模块在 Agent 里的位置夹在调用链中间的那个状态层Agent 的典型工作循环是“感知输入 → 规划动作 → 调用工具/模型 → 汇总输出 → 更新状态”。记忆模块就落在“更新状态”这一层同时反向干预“感知输入”时的内容组装。我见过不少项目把记忆做成一个散落各处的工具函数哪里需要就哪里塞一段结果一个问题引发连锁反应会话切换时记忆没带上多轮任务中间状态被覆盖长期记忆里写满了没有去重的重复条目。比较稳妥的做法是把记忆模块放到一个独立的状态管理层向上对 Agent 主流程只暴露两个能力读取记忆和写入记忆。读取记忆发生在模型调用之前负责把当前问题和相关记忆拼成供模型使用的输入写入记忆发生在模型调用之后负责从这次交互中提炼值得保存的信息。其余细节比如存到哪、怎么过滤、多久清理都被封装在模块内部主流程不需要关心。这样的结构也让记忆模块可以单独测试换存储后端、换向量库时不影响 Agent 主逻辑。2. 存储方案怎么选三个选项一次看清2.1 方案一硬塞上下文最省事但最不经用如果你只是做一个十几轮对话的小玩具最粗暴的方案就是不加任何记忆存储直接把历史消息拼进请求里。这种方案实现成本几乎为零效果在会话早期也还能看因为它天然符合模型的输入习惯。但它的瓶颈非常明显上下文窗口是有限的几十轮之后要么超额报错要么只能做“截断尾巴”的丑陋处理。更致命的是它做不了任何跨会话复用用户这次问的和上次问的完全割裂。基于我的经验一旦 Agent 需要面对十个以上真实问题场景就不要指望这个方案硬扛它更适合作为其他记忆方案之上的“最终输出缓冲区”而不是唯一的记忆机制。2.2 方案二关系表与关键词索引规则型任务的可靠底座对于偏好、日程、订单状态这类有明确结构的记忆关系型数据库配合关键词索引非常合适。比如用户设置了“每周五下午提醒我整理周报”这种记忆就是典型的键值结构时间规则是周五动作是整理周报状态是启用。用固定的字段存下来查询时精确匹配准确率接近百分之百且不消耗任何模型调用。关键词索引方案适合记忆内容本身能用词频和倒排索引覆盖的场景比如查找“上个月做过的那份市场分析”只要标题和正文里出现过“市场分析”查询时就能命中。这个方案的短板是不能理解语义。用户问“我上周研究过什么来着”如果记忆里只存了“竞品调研”而没有存“研究”关键词匹配就会落空。所以它和向量方案不是替代关系而是配合关系结构化程度高的记忆进关系表非结构化的文本进向量库查询时两路并查再融合结果。2.3 方案三向量数据库语义召回的首选当记忆内容以自然语言为主且查询时表达方式可能和原文差别很大就该上向量数据库了。核心原理并不神秘把文本经过嵌入模型转成一串高维向量让语义相近的文本在向量空间里距离更近。查询时把用户问题也转成向量在库里搜索最近邻的若干条。我实际项目里的典型案例如下用户当初说的是“帮我留意行业里有哪些新冒出来的头部工具”过几天他来问“最近有没有值得试的新软件”字面几乎碰不上但语义高度重合。关键词方案大概率返回空结果向量方案能稳定召回原文之后再由大模型把召回内容组织成回答。选型上不需要盲目追求大规模。个人项目和小团队项目的记忆量通常远没到海量级别社区里常见的几款开源向量库完全够用优先考虑部署难度、内存占用和客户端语言的成熟度。生产环境如果数据量大再考虑服务化方案通过多副本和水平扩展解决性能问题。2.4 分块、嵌入、阈值向量化落地的三组关键参数把文本塞进向量库之前有三个参数直接影响最终效果我第一次做的时候全靠拍脑袋后来逐个调过才摸到规律。第一是分块大小。把一篇长文整段塞进向量库查询时返回的是一大坨可能只有一句相关的文本既浪费上下文空间又拉低准确度。按语义边界切分比如一个段落或连续的二到三句话作为一块每块控制在二百到五百个字符之间效果相对稳妥。过长会引入噪声过短则丢失上下文。涉及长文档时我会让相邻分块保留百分之十到二十的重叠保证跨块信息不丢。第二是嵌入模型的选择。不要执着于“越大越好”而是要跟你的文本语言、领域风格匹配。中文场景下选择对中文支持较好的语义模型更关键。向量维度决定存储开销和检索速度百维级别在很多场景已经够用不必一味追求高维。入库前记得做归一化这样用内积和余弦计算相似度时结果一致避免后续排查问题时被这一处细节卡住。第三是相似度阈值。阈值太低检索结果里混入大量无关内容阈值太高召回率又不够很多有用的记忆被过滤掉。我的起步经验值是零点七五左右先跑一批真实查询观察返回结果里“真正相关”的比例再上下微调。低于零点七的记忆基本属于弱相关除非它是用户特定标签下的唯一内容否则默认不入上下文。3. 完整实现一个能跑起来的长期记忆模块3.1 模块结构原文库、向量索引、概述库三层拆分我在项目里把长期记忆模块拆成三部分而不是只用一个向量库打天下。原文库保存记忆的完整原始内容一般落在关系表或文档数据库里字段包括内容、用户标识、会话标识、标签、重要度、时间戳。向量索引库里只存向量和轻量载荷载荷是原文的索引标识避免把大段文本重复塞进每一条向量记录。概述库保存的是对长对话或长文档做摘要后的浓缩记忆比如用户聊了半小时项目需求最终沉淀的是几段结构化摘要而不是半小时的聊天原文。这样拆的好处是职责清楚向量库负责“快速召回候选”原文负责“取全内容和元数据”概述负责“在召回结果字数超预算时提供压缩替换”。如果只有向量库会面临一个尴尬局面召回结果很长要么硬塞导致上下文溢出要么丢弃导致信息缺失。有了概述库可以在召回后用摘要版本替代原文兼顾信息量和预算。3.2 写入流程从文本到记忆条目的四步走写入记忆不是简单地把模型输出原样存下来而是先经过过滤、分块、向量化、落库四个环节。过滤是最容易被省略却最值得做的一步。多轮对话里有大量寒暄、重复确认、临时口误这些如果全部入库后期检索噪音会非常大。我给写入环节加了一个规则只有包含明确事实、偏好、任务状态或者可复用结论的内容才允许进入候选池。代码结构上我用一个统一的封装类来承载记忆条目所有写入操作都走同一个入口class MemoryItem: def __init__(self, content, user_id, session_id, tagsNone, importance0.5): self.content content self.user_id user_id self.session_id session_id self.tags tags or [] self.importance importance self.timestamp int(time.time()) def add_memory(item: MemoryItem, embed_func): # 第一步分块这里按句子组块每块控制长度 chunks split_text(item.content, max_len400, overlap50) for chunk in chunks: # 第二步向量化 embedding embed_func(chunk) # 第三步向量入库payload只保留定位信息与元数据 vector_index.insert( embeddingembedding, payload{ user_id: item.user_id, session_id: item.session_id, tags: item.tags, importance: item.importance, timestamp: item.timestamp, } ) # 第四步原文落库 raw_store.insert(item)整个流程里分块是细节最多的环节。我习惯先按换行和句号切句再把相邻句子合并到目标长度内遇到标题、列表这类结构性内容自动单独成块。长段落如果硬按固定字符切容易把一句话从中间劈开导致向量化后的语义失真。3.3 检索流程召回、过滤、排序、组装的完整链路读取记忆时完整链路应该是“问题向量化 → 向量召回 → 元数据过滤 → 排序 → 组装成上下文”缺一环效果都会打折。先看召回和过滤def recall(query, user_id, embed_func, top_k5, threshold0.72): query_embedding embed_func(query) candidates vector_index.search( query_embedding, top_ktop_k * 3, thresholdthreshold ) # 只保留当前用户的数据避免跨用户记忆泄漏 candidates [c for c in candidates if c.payload[user_id] user_id] # 二次排序重要度加权 时间衰减 candidates.sort( keylambda c: c.payload[importance] recency_score(c.payload[timestamp]), reverseTrue, ) return candidates[:top_k]召回时故意多取三倍候选是因为向量搜索的第一轮结果往往混有主题相近但实际无关的内容。先放宽数量再靠元数据过滤和二次排序收紧比直接依赖向量阈值更稳。组装上下文是容易被忽略的部分。记忆只是上下文的一部分不是全部我给记忆、会话历史、系统提示安排了一个预算比例。比如一个允许八千 token 的模型系统提示占一千五最近五轮会话历史占三千长期记忆最多占两千五剩下留给当前问题和新生成内容。组装前先对每段做 token 估算超预算就优先裁掉最旧、最低分的内容而不是笼统地整体截断。3.4 更新与遗忘机制记忆不是只进不出的仓库记忆系统的健康程度取决于“遗忘”设计得怎么样而不是“记住”了多少。只进不出的记忆库要不了两周就会堆满过时信息检索结果里权重常年被旧内容占据。我给记忆条目维护了一个活跃度得分三类因素加权最近被访问的时间、重要度标注、被命中的频率。分数可以简单表示为score 0.5 * recency_norm(timestamp) 0.3 * importance 0.2 * hit_frequency定期清理任务会对低分条目做处理三个月没被命中的条目自动归档归档后不参与常规召回只有显式查询旧历史时才被翻出来超过一年且非重要条目直接删除。写入新记忆时如果发现同一主题已有非常相似的旧条目做一次合并更新保留新时间戳和部分旧细节而不是让重复内容在库里越积越多。另一个实用机制是“记忆覆盖”。用户明确修正了之前的说法比如“把会议时间改成周四”旧记忆不能原样留着否则模型下次还是告诉你周三开会。我在更新流程里会先按关键词和标签定位旧条目标记为“已覆盖”再写入新条目。这样即使语义检索时同时召回新旧两条模型也能通过元数据里的状态字段识别出该以哪条为准。4. 真实项目里踩过的坑和一份问题速查表4.1 检索命中率很高回答质量反而直线下降我第一次调通向量召回时很兴奋因为检索结果的相似度分数都非常高几乎条条命中主题。结果真正放到 Agent 里跑回答质量却下降了。排查后发现问题出在召回内容之间的重复度上十条候选里有五条说的是同一件事的不同表述模型被大量冗余信息包围等于在答案里反复绕圈。解决方案有两个。第一召回后做一次去重按内容哈希或向量距离判断相似只保留一条第二如果检索结果还是偏多引入一轮轻量级精筛用模型对候选做一次十选三的过滤把明显不相关的排除掉。这一轮精筛只处理候选文本量小成本可控但对最终回答质量的提升非常明显。4.2 上下文越拖越长模型越往后越“老糊涂”会话进行到中后期历史消息加上不断召回的长期记忆上下文常常逼近窗口上限。这时模型不是“更加聪明”而是开始忽略靠前的指令把用户早期的明确要求丢掉回答显得越来越散。针对这个问题我给会话做了两级压缩。第一级是滑动窗口只保留最近五轮原文和更早内容的自动摘要第二级是“摘要记忆”当检测到对话轮次超过阈值或累计 token 接近预算的七成时主动触发一次总结调用把本轮的核心事实、未完成任务、用户偏好浓缩成两百字以内的摘要存入概述库作为后续会话的记忆来源。4.3 错误记忆一旦入库会被一遍遍放大记忆系统最怕的是“污染”。模型在一次回答里给出错误结论随后这段结论被当成记忆写入长期库之后每一次相关问题的检索都会把这条错误记忆召回模型基于错误前提继续推导错误的可信度反而被一遍遍增强。我现在给写入环节加了一道低成本的校验只有当当前输入里有明确的用户表述、工具返回结构化数据、或者模型输出经过了用户确认时才允许直接入库模型自身的推测内容默认降低重要度并且带上“待确认”标签。后续如果用户在下一次对话中表达出质疑或纠正系统优先更新这些“待确认”条目。4.4 成本、隐私和性能容易被低估的三座山向量化调用是有成本的尤其是对话文本量大时每次追加记忆和每次查询都要调用嵌入接口。我后来给高频写入加了批处理会话结束或积累到一定条数再统一批量向量化而不是每一轮对话都实时嵌入。存储侧向量库索引会随记忆量增长明显变大内存占用也随之上升。长期不清理会让查询延迟从几十毫秒涨到几百毫秒。定时清理和归档在这里不仅是质量需要也是性能需要。隐私方面我的原则是敏感信息不落长期库。对用户ID、会话ID这类标识做哈希处理对文本里的手机号、地址等个人信息先做脱敏再入库。记忆模块在日志里也不打印原始内容只打印元数据避免调试时无意间把用户隐私写进日志文件。4.5 常见问题速查表症状可能原因解决思路检索结果全是无关内容相似度阈值过低或分块过大提高阈值缩小分块增加返回结果去重明明记得的内容却查不到阈值过高或嵌入模型与语言不匹配下调阈值改用领域匹配的嵌入模型回答引用过时信息旧记忆未标记覆盖清理机制缺失增加更新与覆盖逻辑定期归档过期条目上下文很快超限记忆和会话历史无预算控制按比例分配预算超限时裁掉低优内容多次出现互相矛盾的记忆召回结果里新旧条目并存用状态字段标识已覆盖召回时优先过滤记忆库增长过快所有模型输出都被写入增加写入过滤只留事实、偏好、结论换用户后串记忆没有按 user_id 隔离过滤写入与检索时都强制绑定用户标识如果你正在做自己的 Agent我最想强调的一点是记忆管理不是挑一个漂亮的技术方案就完事它更像一门“取舍”的手艺。你的每一个检索参数、每一轮写入过滤、每一条淘汰规则最终都会反映在模型回答的稳定性和可信度上。我后来把所有记忆策略都做成可配置项跑测试集时反复调参才真正体会到这套系统对 Agent 整体体验的影响有多深。我自己的习惯是给每条记忆打上一个重要度标签重要度高的内容在检索排序里永远优先。如果你刚开始给 Agent 加记忆不妨从这一条做起。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。