1. 定制记忆机制:Agent 为什么必须“记住你”
如果你玩过几款对话式 AI 工具,大概会有一种很典型的感觉:刚聊完一个复杂需求,换个会话再问,它就像失忆了一样,又得从头交代背景。最开始我以为是产品设计缺陷,后来自己动手写 Agent 才明白,这恰恰是当前大多数 Agent 的基础形态——无状态推理。每一次请求进来,模型看到的就是一段独立的上下文,聊完即焚,什么都不留。
这就带来了一个很实际的问题:我们想要的 Agent,应该是越用越懂我的助手,而不是每次见面都要重新自我介绍的陌生人。所以当我在搭建自己的 Agent 项目时,记忆模块就成了优先级最高的事情之一。这篇博文我用实际项目的经验,把 Agent 记忆从概念到落地完整拆一遍,包括我踩过的坑、改过的设计、实测下来的方案,尽量讲得直白可复现。
先说清楚一个容易混淆的点:记忆不是简单地“把聊天记录存下来”然后一股脑塞回提示词里。如果真是这么干,上下文窗口再大也会被撑爆,而且模型会分不清哪些信息是当前任务需要的、哪些只是历史噪音。真正可用的 Agent 记忆,是一个有分层、有写入策略、有召回机制的系统。好的记忆设计,直接影响 Agent 在复杂任务里的连贯性和个性化表现,而这两点恰恰是 Agent 从“玩具”走向“生产力工具”的关键。
这篇文章适合这几类人看:
- 正在自己搭 Agent、但对记忆模块无从下手的开发者;
- 使用 Claude、开源编程助手等工具时,发现“换会话就失忆”、想理解背后原理的进阶用户;
- 准备面试 AI Agent 相关岗位,需要系统梳理记忆系统设计的人。
我会从最基础的设计思路开始讲,然后落地到存储、写入、召回、清理这几个核心环节,最后分享一套我在实际项目里验证过的方案和踩坑记录。
2. 先想清楚:短期记忆与长期记忆到底该怎么分层
2.1 双网络记忆模型的启发性思路
在设计记忆系统之前,建议先了解一下“双网络记忆模型”这个思路。它不是某个产品的专利,而是一种很有参考价值的架构划分:把记忆分为快速更新的短期网络和稳定积累的长期网络。短期网络负责当前任务上下文里的即时信息,长期网络负责跨会话、跨任务持续保留的用户偏好与事实。
放在 Agent 场景里,这套思路刚好和人类的记忆机制形成类比。你和一个同事协作:短期记忆是你俩正在讨论的这份文档的细节,长期记忆是你知道他习惯用 Python、讨厌冗长邮件、项目背景是金融风控。同事之所以好用,是因为他两套记忆都在线。Agent 要成为好用的数字同事,也一样。
我之前看过一份记忆系统设计文档,里面有一句话我特别认同:短期记忆要“快进快出”,长期记忆要“精挑细选”。什么意思呢?短期记忆追求的是准确和时效,当前对话里用户刚提到的需求、刚修改的文件,必须原样保留,不能丢;长期记忆追求的是稳定和泛化,得从多次交互里提炼出真正值得跨会话保留的东西,而不是把流水账全塞进去。
2.2 三层结构:工作记忆、短时记忆、长时记忆
我在实际项目里,把记忆划分成了三层,比“短/长”两分法多了一层工作记忆,用起来更顺手:
工作记忆(Working Memory):当前这一次任务执行过程中的动态信息。比如 Agent 正在读的代码文件内容、正在处理的用户指令、上一次工具调用的返回结果。这层记忆生命周期极短,任务结束就基本作废,但它决定了一次任务内的推理质量。
短时记忆(Short-term Memory):最近几次会话里的对话摘要和关键上下文。比如用户昨天让我写了个爬虫脚本,今天说“那个脚本再加个重试机制”,短时记忆要能帮 Agent 知道“那个脚本”指的是什么。这层记忆通常以会话摘要或最近 N 轮对话的形式存在。
长时记忆(Long-term Memory):跨天、跨月仍然有效的稳定信息。用户的工作领域、常用技术栈、代码风格偏好、项目的业务背景、曾经明确纠正过的错误。这层记忆要求最高,需要抽取、去重、验证,不能随便什么东西都往里面写。
这三层的关系,我自己的理解是:工作记忆是“寄存器”,短时记忆是“内存”,长时记忆是“磁盘”。寄存器数据丢了无所谓,内存丢了影响当前会话,磁盘丢了相当于失忆。
2.3 分层设计带来的实际收益
把记忆做成分层之后,最直接的收益是提示词成本的下降。以前我每开一个新会话,都得把项目背景、技术栈、用户偏好写一大段塞进 system prompt,现在这部分可以完全交给长期记忆自动注入。实测下来,新会话的“冷启动”时间缩短了一大截,用户不需要重复交代背景,Agent 也能给出更贴合个人习惯的回答。
另一个收益是降低了记忆相互污染的风险。如果不分层,把所有历史记录混在一个池子里,很容易出现一种尴尬情况:Agent 在当前这个编程任务里,突然想起了用户上个月聊的旅游计划,然后给出了一个莫名其妙、混杂了无关信息的回答。分层之后,召回时按场景和类型过滤,这种串味问题基本杜绝了。
注意:分层不是越细越好。我最初设计过四层、五层的方案,加了“技能记忆”“事实记忆”“偏好记忆”之类的细分,结果维护成本爆炸,召回逻辑复杂到我自己都调试不过来。三轮迭代之后,稳定在三层结构,简单、够用、好维护。
3. 存储与写入:让记忆真正沉淀下来
3.1 向量库选型:不要一上来就上重型武器
记忆大概率要用向量检索来找回最相关的内容,所以向量数据库是绕不开的一环。市面上可选的东西不少,有专业的 Milvus、Qdrant、Weaviate,也有轻量的 Chroma、LanceDB,还有各云厂商的向量检索服务。很多人一上来就选功能最全的,我觉得没必要,得看你自己的场景和规模。
我自己在个人项目和中小型项目里,用得最顺手的是 Chroma,原因很实际:零配置、嵌入式运行、API 简洁。对于一个会话量不大的 Agent 来说,Chroma 的单机性能完全够用,数据存在本地,不用额外维护一套数据库服务。等规模真的大到单机扛不住了,再迁到 Qdrant 或者 Milvus,那时候你的数据模型和召回逻辑已经成熟了,迁移只是换 client 的事。
如果你的场景是团队协作、数据量很大、需要高并发检索,那可以直接上专业的向量库。但注意,选型之前先想清楚三个问题:你的数据量级有多大?检索延迟要求多高?运维人力够不够?这三个问题没想清楚就选型,容易陷入“用大炮打蚊子”的尴尬。
3.2 结构化记忆与向量记忆的配合使用
纯靠向量检索做记忆,有一个隐藏的问题:向量检索擅长找“相似”,但不太擅长找“精确”。比如用户明确说过“我的项目部署在 8080 端口”,这是个精确事实,如果把它向量化存起来,召回时靠相似度匹配,很可能召回的是一堆“端口”“部署”相关的相似内容,真正的答案反而被淹没在排序结果里。
所以我的方案是双轨制:结构化记忆 + 向量记忆。
结构化记忆:用 JSON 或数据库表存用户偏好、项目配置、明确的键值对信息。查询时走精确匹配,比如获取用户的技术栈、常用路径、历史纠错记录。
向量记忆:存对话摘要、经验教训、文本片段这类“语义型”的信息。查询时走相似度检索。
在实际调用时,我会先查结构化记忆,把确定的硬事实注入提示词,然后再用向量检索补充相关内容。两条路并行,互补性很强。这里有一个很实用的做法:给每条记忆打上类型标签,比如fact、preference、summary、correction,召回时按类型过滤。这样既能保证精确信息不走偏,也能让语义内容有足够的召回空间。
3.3 写入策略:什么值得记,什么不值得记
记忆系统的关键其实不在存储,而在写入。存什么、不存什么,直接决定了记忆的质量。如果你把每轮对话都原样存进长期记忆,过不了一周,记忆库就全是噪音,召回质量迅速恶化。我自己用了一套写入规则,实测下来效果很好:
值得记:用户明确表达过两次以上的偏好;用户纠正过 Agent 的说法;项目的核心背景信息;用户主动介绍的个人信息和工作习惯;任务完成后的关键结论。
不值得记:一次性的问候和闲聊;临时性的调试过程;当前任务中的过程性噪音;已经过时的临时信息。
需要经过摘要再记:长对话的关键结论。这里我用摘要模型做一个信息压缩,把一轮长对话提炼成 3-5 句话,只保留关键事件、结论、用户偏好变化,再写入长期记忆。
这个筛选过程,我一开始是靠手工设计的规则,后来逐步升级成“两条路并行”的写法:一条规则路径处理精确信息,一条模型路径处理需要理解和提炼的内容。规则路径用正则和简单的条件判断,保证可靠;模型路径用大模型做摘要和实体抽取,保证灵活。
实操心得:在写入长期记忆之前,一定要做一次“相似度去重”检查。先检索一下库里有没有意思相近的记录,如果相似度超过阈值,要么跳过,要么合并后覆盖旧记录。不然同一个信息可能会以不同措辞存上七八条,将来归回时会出现内容重复、互相矛盾的情况。
4. 召回与注入:记忆怎么在对话里“活”起来
4.1 召回策略:按场景、时间、相关性三维过滤
记忆存进去了,怎么取出来用才是难点。如果每次对话都把库里所有记忆全塞进上下文,一是浪费 token,二是引入噪音。我的召回策略是三维过滤:
第一维是场景过滤。你当前在做什么任务?写代码、查资料、聊天?按记忆的scene标签过滤,比如编程任务只召回编程相关的历史记忆,不把用户聊旅行时的偏好翻出来。
第二维是时间衰减。记忆不是越久越重要。我给每条记忆打了一个last_accessed时间戳,在相关性计算时叠加一个时间衰减因子。很久没用的记忆,相关性会被适当降低;最近反复用到的高频记忆,权重会提高。
第三维才是相关性检索。经过前两轮粗过滤,再进向量库做相似度检索,取 top K 条。我一般取 5-10 条,根据任务复杂度动态调整。
召回的时间点也很有讲究。我一开始是在每次用户发消息时都做召回,后来发现很多轮对话根本不需要新的记忆注入——比如用户只说了一句“继续”。后来改成:每次新会话开始、用户提到历史相关内容、任务上下文发生明显切换时,才触发召回。这样既省 token,也避免提示词太臃肿。
4.2 上下文注入的位置与格式
召回出来的记忆,放在提示词哪里是有讲究的。我自己常用的套路是分两块放:
把稳定的长期记忆(用户偏好、项目背景、纠错记录)放在 system prompt 里,作为 Agent 的“人设底座”。
把当前任务相关的短时记忆和召回结果,插入到对话历史的开头,作为背景参考。
格式上,我用了一个很简单的 XML 标签包裹记忆块,模型理解起来非常干净:
<memory> <user_profile> 技术栈:Python, TypeScript;偏好简洁风格;用过 FastAPI 和 Next.js </user_profile> <project_context> 正在开发一个个人博客系统,部署在 8080 端口 </project_context> <relevant_history> 用户不喜欢冗长的代码注释,偏好自解释的命名 </relevant_history> </memory>实测下来,这种带标签的记忆块比纯文本拼接更容易让模型区分“哪些是背景记忆、哪些是当前上下文”,回答时也更不容易把记忆内容当成用户当前的指令。如果你在用 Claude 或 GPT 系列模型,这个格式基本都是兼容的。
4.3 防止“记忆中毒”的隔离设计
记忆系统最怕的一类问题是“记忆中毒”——即某一次对话里的错误信息,被当成长期事实写入记忆库,从此每一次对话都被污染。这种情况在 Agent 场景里很容易发生,毕竟大模型也会犯错,如果它犯的错被当成“事实”存了下来,后续所有任务都会基于这个错误事实推理。
我在项目里加了两层防线。第一层是写入前的置信度校验:不是所有模型抽取出的“事实”都允许直接入长期库,只有满足以下条件之一的才写入——用户明确确认过的信息、规则路径抽取的结构化硬数据、已经被验证多次的模式。第二层是定期的人工/模型复核:我用一个独立的审计任务,每周扫描一次长期记忆库,把可疑的、矛盾的、过时的记录清理掉。
这个隔离设计一开始被我想得过于简单,觉得“抽取出事实、存进去、召回”三步就完了。实际跑了两周之后发现问题比预想的多得多:模型会把“我觉得”“可能”这类猜测性表达直接抽成事实;会把某个单次任务里的临时上下文写进长期库;甚至会把用户的一句反话当成偏好记下来。加了审计之后,记忆库的“纯度”才真正可控。
5. 实操案例:借助 Hindsight 式经验库和双网络模型做升级
5.1 从工具型 Agent 到“经验增长型” Agent
前面讲的这套短时 + 长时记忆方案,解决的是“记住用户”的基本问题。但如果你想往更高级的方向做,就要开始思考另一件事:Agent 能不能记住“经验”?
这里说的经验,不是用户的偏好,而是 Agent 自己踩过的坑和总结出的方法。比如在编程场景里,Agent 上次改一个模块时发现某种写法会导致报错,它下次遇到类似任务时能不能自动避开这个坑?这种能力,就是所谓的“经验增长型” Agent。Hindsight 记忆库(事后复盘式记忆)就是解决这个问题的思路:每次任务结束后,让 Agent 做一次回顾,提炼出“什么做法有效、什么做法有问题、下次怎么做更好”这类元经验,存入独立记忆库。下次遇到相似任务时,这个经验库会参与召回,让 Agent 的表现超出单个模型的基础能力。
我实测过这个机制,效果确实明显。一个多轮调试任务里,第一次跑的时候 Agent 绕了三个弯才找到问题根因;跑了七八个任务、积累了复盘经验之后,再遇到同类问题时,它往往第一轮就能直接命中正确的排查方向。这个提升不是来自模型本身变聪明了,而是记忆系统把它过去的经验“喂”给了它。
Hindsight 式经验库的关键在于复盘触发机制。不能每个任务都复盘,那样浪费 token 且噪音多;也不能完全不复盘。我的做法是设定触发规则:任务失败或做了多次尝试才成功的时候,强制复盘;任务顺利完成的,由模型快速判断是否有值得沉淀的经验。复盘输出统一格式:目标、尝试过的方案、失败原因、有效方案、可复用的经验。
5.2 双网络记忆模型在长对话 Mult-Agent 场景里的落地
前面提到过双网络记忆模型。在简单的单 Agent 场景里,这个模型的实现相对直接:一个短期存储 + 一个长期存储。但如果你做的是 Multi-Agent(多智能体)系统,比如用 Spring AI Multi-Agent 的架构编排多个角色协作,记忆的复杂度会上一个新台阶:每个子 Agent 有没有自己的独立记忆?它们之间要不要共享记忆?共享到什么程度?
我的项目里有三个角色 Agent:一个负责用户需求理解,一个负责技术方案设计,一个负责最终代码实现。最开始我让三者共享同一个长期记忆库,结果很糟糕:需求理解 Agent 写入的用户偏好,被技术方案 Agent 当成了技术约束;技术方案 Agent 写的讨论过程,被代码实现 Agent 当成了最终决策。各个角色的记忆互相串味。
后来我改成了“分工独立 + 共享摘要”的模式:每个 Agent 有自己的私有短期记忆和私有长期记忆,只在必要节点向共享记忆层写入经过摘要的结论。比如需求理解 Agent 最终确认的用户意图,写入共享层;技术方案 Agent 的选择和理由,写入共享层;代码实现 Agent 的过程性探索,只留在私有层,不进共享。这套设计和双网络记忆模型的结合点在于:每个 Agent 内部是双网络结构,多 Agent 之间则通过共享摘要形成第三层“团队记忆”。
5.3 一个可复制的本地记忆迁移思路
最后分享一个很多人在实际使用中都会遇到的场景:本地已有的历史对话记录,怎么迁移到自己的 Agent 记忆系统里,让之前积累的信息不白费。
我之前的工具里存了大概几个月的对话记录,有大量的项目讨论、代码方案、用户偏好,这些都是宝贵的记忆素材。迁移思路分三步:
第一步是数据导出。先把历史对话按会话维度导出成 JSON 格式,每条消息保留 role、content、timestamp 字段。
第二步是批量清洗和结构化。这一步是关键。原始对话里有大量寒暄、中断、试错过程,直接入库就是灾难。我给这个过程写了一个批处理脚本,用模型逐条判断:这条消息里有没有值得长期保留的用户信息、项目事实、方案决策?有的话,抽取提炼后生成结构化记忆条目,打上类型和场景标签;没有价值就当噪声丢弃。
第三步是去重验证后批量写入。写入前先向量化所有现有记忆,对每条新条目做相似度检查,高于阈值就跳过或者合并。全部过完一遍后,再随机抽几条做人工验证,确认迁移质量没问题。
我在实际迁移中,几个月的历史对话,最终提炼出大概几十条干净的长期记忆条目,量不大,但每条都是精品。后续对话里,Agent 能准确说出我之前的项目背景和技术选型,让它的回答明显“更懂我”了。
6. 踩坑记录:记忆系统常见的四个大坑与排查方案
6.1 幻觉记忆覆盖:正确的信息被错误覆盖
这是我在长期记忆系统里遇过的最隐蔽的问题之一。场景是这样的:用户在 12 月说“部署在 8080 端口”,后来项目调整,又改口说“现在用 80 端口了”。新记忆写入时检测到“端口”信息相似,直接覆盖了旧记录。表面上看没问题,但如果 Agent 在回答“项目之前部署在哪里”这样的历史问题时,正确答案已经在覆盖过程中丢失了。
排查思路:覆盖策略不能是“见新就覆盖旧的”。我后来加了版本化机制,旧记录不是直接删除,而是标记为superseded并存到一个归档区。召回时默认用最新版,但如果问题明显是询问历史情况,则可以在摘要层看到变更轨迹。
6.2 上下文膨胀:记忆太多导致模型分心
还有一个高频问题:召回逻辑太激进,每次都注入十几条记忆,提示词越来越长,模型的注意力反而被稀释。最典型的表现为:用户问一个简单问题,Agent 却在回答里塞了很多记忆里的背景信息,答非所问,对话变得笨重。
排查方案:严格控制注入条数,并且引入一个“记忆必要性判断”。不是所有问题都需要记忆加持。简单的事实问答、闲聊、当前上下文中已经有答案的问题,都不触发记忆召回。只有用户的问题里出现了历史相关内容的关键词,或者系统判断当前任务与长期记忆中的某个场景强相关时,才做召回注入。
6.3 召回失效:看起来存了,但用的时候想不起来
另一种常见问题是“存了但召回不出来”。我在早期排查时发现,很多情况下不是向量检索本身有问题,而是存储时的分块策略和召回时的查询语句不匹配。比如存的时候是整段长文本,召回的 query 是一句简短的话,语义距离可能超过阈值,导致该命中的没命中。
排查方案:一是调整分块长度,建议先按 300-500 字一个块做实验,比较召回效果后再定;二是召回时不只用用户原始的问题去做向量检索,还可以配合关键词匹配作为补充通道;三是对已经确认有价值、但召回不稳定的记忆,额外加一条结构化标签路径,保证精确命中。
6.4 敏感信息与隐私边界的处理
记忆系统越智能,越要重视隐私。如果 Agent 记住了用户的银行卡号、密码、身份证信息,一旦记忆库泄露,后果不堪设想。我在设计时就定了一条规则:长期记忆库只允许存“信息”和“偏好”,不允许存“凭证”和“隐私”。
具体做法是,写入前有一个敏感信息检测过滤层,用正则和模型双重判断,包含手机号、邮箱、地址、账号密码类的信息直接拦截,不进入记忆库。如果用户确实需要在上下文里使用这些信息,可以用临时的环境变量或专用配置注入,而不是沉淀为长期记忆。
注意:这条红线建议所有做记忆系统的人都严格遵守。技术能力越强,越要在数据边界上保持克制。
7. 一套经过验证的最小可落地记忆方案参考
说了这么多,最后给出一套最小的落地参考,方便你先跑起来再迭代。
首先是记忆存储层的组件选型,我的组合是这样:
| 功能 | 选型 | 理由 |
|---|---|---|
| 向量存储 | Chroma(单机)/ Qdrant(规模化) | 起步零配置,后期可迁移 |
| 结构化存储 | SQLite 或 JSON 文件 | 简单、可靠,适合个人项目 |
| 摘要生成 | 大模型二次调用 | 利用模型做信息压缩和提炼 |
| 记忆去重 | 向量相似度计算 | 检索历史记忆,判断是否重复 |
然后是写入流程,按照前面的规则整理成四条执行步骤:
- 对话结束后,先判断这条消息是否包含值得保留的信息;
- 规则路径抽取结构化数据(用户偏好、明确事实、纠错记录);
- 模型路径生成摘要和语义向量,用于语义型记忆;
- 写入前做去重检查,超过相似度阈值就跳过或合并。
召回流程则按照这个顺序执行:
- 判断当前任务是否需要调用记忆,不需要就跳过;
- 按场景标签做初步过滤;
- 对候选记忆做时间衰减加权;
- 进入向量库做相似度检索,取 Top 5 条左右;
- 格式化后注入提示词的 system prompt 和背景区。
再给一个简化版的代码思路,方便理解流程:
# 简化的记忆召回逻辑 def recall_memories(user_query, scene): # 1. 场景过滤 candidates = memory_store.filter(scene=scene) # 2. 时间衰减加权 candidates = [ {**mem, "weight": mem["relevance"] * decay(mem["last_accessed"])} for mem in candidates ] # 3. 相似度检索 query_embedding = embed(user_query) results = vector_store.query(query_embedding, filter=candidates) # 4. 返回 Top K 条 return sorted(results[:5], key=lambda x: x["weight"], reverse=True)这套方案不可能适用所有场景,但它最大的优点是好调试、好扩展。你先跑通这套最小闭环,再来按自己项目的复杂程度逐步升级。我在实践里也是从这套方案起步,慢慢迭代成了前面说的多层结构。
根据我个人的实操体会,Agent 记忆系统最需要耐心的地方不在存储选型,也不在提示词技巧,而在“取舍”:什么值得记、什么不该记、什么该忘。把这三件事想清楚了,你的 Agent 就会真正越来越懂你。