最近几周我一直在调一个多轮任务的Agent项目,排障排到怀疑人生:明明单轮对话都很稳,一旦任务超过五六个步骤,Agent就开始“失忆”——前面已经确认过的信息,后面又反复确认;用户早就说过的偏好,完全没被记住;工具调用返回的结果,用着用着就串了。锅甩来甩去,最后发现根子全在记忆上。Agent记忆组件,说白了就是让AI Agent能“记住”东西的模块,也是现在Agent开发里最容易拖垮整个系统的一环。这篇文章我想把记忆组件的设计思路、分类、实现方案和踩坑经验完整梳理一遍,尤其适合正在做Agent落地、被多轮对话和长任务搞得焦头烂额的朋友。
先说一个观察:现在市面上的Agent框架,规划能力、工具调用能力都已经很成熟了,但记忆这块普遍做得糙。很多框架所谓的记忆就是一个messages数组,把所有历史对话一股脑塞进上下文。这在Demo阶段没问题,一旦你的Agent要处理真实业务——比如帮用户做跨周的项目管理、维护客户档案、在几百个文档的基础上做分析——这种“伪记忆”马上就崩。接下来我按我实际开发的经验,把这套东西拆开讲。
1. Agent记忆组件到底是什么,为什么突然这么火
1.1 记忆组件在Agent架构里的位置
但凡画过Agent架构图的人都知道,一个完整的Agent通常由四块组成:感知层(接收用户输入和环境反馈)、决策层(LLM推理、规划器、ReAct循环)、行动层(工具调用、API交互)、记忆层(历史状态、知识、偏好的持久化)。很多人画图的时候把记忆层画成一个挂在边上的小方块,实际开发中它才是真正决定Agent“智商上限”的部分。
我习惯把记忆组件理解成Agent的“数据库+缓存+索引”三合一。它不直接产生推理结果,但它的读写质量直接决定LLM看到的上下文是什么。同样一个用户问题,记忆组件给LLM喂了精准的历史摘要和用户偏好,LLM就能给出正确决策;喂了一堆无关历史甚至互相矛盾的记录,LLM再强也会被带偏。这也是为什么现在头部Agent项目都在重做记忆层。
1.2 为什么“没有记忆”的Agent走不远
说句得罪人的话:一个没有记忆组件的Agent,本质就是个套了壳的ChatGPT。它能完成单步任务,但做不了持续性的工作,因为它每次都是“从零开始”理解用户。
举一个我实际遇到的场景。用户想让Agent帮忙整理一个月的项目周报,要求“每周五下午发到邮箱,格式参照上周那份”。没有记忆的Agent会怎么样?第一次它能做,第二次它就忘了格式参照的是哪份,第三次它连“用户要求周五发送”这个偏好都丢了。每轮对话都得重新交代背景,体验极其割裂。
加上记忆组件之后,这类问题就变成了纯粹的检索问题:Agent把用户偏好写进长期记忆,下次任务开始时通过语义检索把“周五发送”“参照上周周报格式”这些关键约束拉回上下文。用户不需要重复表达需求,Agent的表现就会从“迟钝的客服”变成“默契的助手”。现在行业里都在喊记忆是Agent的“第二大脑”,原因就在这里。
2. 先分清记忆的类型,再谈实现
2.1 短期工作记忆:会话上下文
认知科学里把人脑记忆分成感觉记忆、工作记忆、长期记忆。Agent的记忆也类似,只不过实现方式完全不同。先说最基础的短期工作记忆,对应的是一个会话内正在处理的信息——用户刚说的话、工具返回的结果、当前任务的中间状态。
技术实现上,短期工作记忆最直接的形式就是LLM的context window,也就是我们把历史消息拼接成一个列表塞给模型。这里有个关键决策:是全部塞,还是摘要后塞?我的经验是分阶段处理。如果上下文总量在模型context的60%以内,直接全量塞;超过60%,就必须做摘要。摘要策略我后面详细讲,这里先记住一个原则:短期记忆的维护目标是“不丢关键信息、不超长度限制”,优先级高于检索优化。
2.2 长期记忆:向量数据库与语义检索
长期记忆要解决的是跨会话、跨任务的信息复用。用户偏好、历史项目总结、领域知识、过去的决策记录,这些都属于长期记忆的范畴。当前主流的实现方式是向量数据库 + 语义检索。
流程不复杂:把文本内容切块,用Embedding模型转成向量,写入向量数据库;查询时把用户的问题也转成向量,用余弦相似度或内积检索出最相关的记忆片段,注入提示词。这个方案的好处是能处理海量历史信息——你可能存了几十万条用户交互记录,但真正需要LLM看到的只有最相关的三五条,向量检索就是用来做这个筛选的。
需要提醒的是,很多人觉得向量检索是万能的,其实不是。它本质上是基于文本语义相似度的匹配,如果你的业务需要精确记忆(比如“用户地址是某某路某号”),纯向量检索很容易漏。我现在做长期记忆都是向量检索 + 关键词/结构化字段混合检索,双路召回后再融合,效果好很多。
2.3 情景记忆、语义记忆与程序性记忆的Agent映射
人脑记忆分三类,Agent记忆也可以照这个框架去分类,而且这个分类对设计存储结构非常有用:
| 人脑记忆类型 | Agent对应含义 | 存储方案 | 典型例子 |
|---|---|---|---|
| 情景记忆(Episodic) | 特定时间、地点发生的事件 | 事件日志 + 向量索引 | 用户上周二要求改过报价 |
| 语义记忆(Semantic) | 事实性知识、概念 | 知识库 + 结构化存储 | 用户所在行业是电商 |
| 程序性记忆(Procedural) | 做事的流程、技能 | 工作流模板 + 策略池 | 生成周报的固定步骤 |
最常见的错误是只做语义记忆,把情景和程序都当成普通文本处理。结果就是Agent能答上来“电商行业有哪些流量玩法”,却想不起来“用户上周提过竞品涨价的细节”。我的做法是:存储时给每条记忆打类型标签,检索时根据当前任务类型加权。比如用户在做咨询类任务时,情景记忆权重拉高;在做知识问答时,语义记忆权重拉高;在执行复杂工作流时,程序记忆权重拉高。这个加权检索的改造,比想象中管用得多。
3. 核心实现方案拆解:向量存储、压缩与协作
3.1 基于向量数据库的记忆存取全流程
我现在项目里用的是一套相对标准的记忆存取管线。写入侧分四步:预处理 -> 分段 -> Embedding -> 存储。预处理阶段我坚持先做实体抽取和意图标记——也就是先用LLM或者规则引擎提取出这段记忆涉及的人物、时间、事件、偏好等关键实体。这么做看起来多花了一次LLM调用,但后续检索质量的提升是质的飞跃。分段长度我一般控制在300-500字,太短会丢失语义完整性,太长则降低向量检索精度。
存储时每条记忆片段会带上结构化元数据:产生时间、来源会话ID、实体列表、记忆类型、重要度评分。这个重要度评分是后面做记忆压缩和遗忘的关键,我放在3.2节专门说。
检索侧则是查询扩展 -> 双路召回 -> 重排 -> 注入。查询扩展是我比较推荐大家做的一步,因为用户原始问题往往太短,直接检索效果一般。我会先用LLM把用户问题扩展成2-3个相关联的查询词,比如用户说“帮我准备季度汇报”,我会扩展出“季度数据总结”“汇报PPT结构”“竞品对标信息”。然后每一路做向量检索和关键词检索,合并候选集后交给一个重排模型(最简单的可以用LLM打分),最后根据token预算截断并拼装进上下文。
3.2 记忆压缩与遗忘机制:不让上下文无限膨胀
记忆组件做得越久,积累的长期记忆就越多。如果只写不删,最后必然导致检索变慢、上下文被无关记忆淹没。所以我几乎是强制要求项目里实现记忆压缩与遗忘机制。
我的核心思路是给每条记忆算一个“综合保留分”。公式很简单,就三个要素相乘:
保留分 = 重要度评分 × 时间衰减系数 + 访问频率加权重要度评分在写入时由LLM给出,比如用户明确表达的偏好、涉及钱和截止日期的关键决策,评分就高;随口闲聊的分就低。时间衰减系数按指数递减——我用的半衰期是30天,也就是30天后这条记忆的重要度打对折。但如果用户反复触发、访问频率高,加权会把它“捞回来”。
实际操作中,我建议每天跑一次批量清理任务:把综合保留分低于阈值的记忆降级,先不是物理删除,而是挪到归档表。如果之后用户又提到相关内容,检索到归档记录时再激活恢复。这个遗忘机制的好处是让记忆组件保持“鲜活”,避免无效信息把真正重要的记忆挤掉。你要是跳过这步,用不了几个月数据库里就全是垃圾。
3.3 记忆组件与规划器、工具链的协作方式
记忆组件不是孤立存在的,它要跟Agent的其他模块咬合。在ReAct架构里,记忆组件的调用通常发生在两个节点:规划前和动作后。
规划前调用记忆,是为了获取任务所需的背景信息。我现在会让Agent先做一次“记忆召回”,把检索到的关键记忆拼在一个专门的Memory Context区块里,再让LLM开始规划。注意这个区块和对话历史是分开的,这样可以让模型清楚地知道哪些是事实性记忆、哪些是即时对话。动作后调用记忆,是为了更新状态——工具调用返回了什么新信息、完成了什么子任务,都要实时写入短期记忆并异步同步到长期记忆。
这里有个容易踩坑的点:工具返回的结果别一股脑塞进记忆。我项目里之前出现过一次事故,原因是某次API返回了超长JSON,Agent把整段内容当记忆写进去了,导致后续每次检索都被这段垃圾数据污染。后来我在写入管线里加了一道过滤规则——超过一定大小、且经LLM判断无保留价值的内容直接丢弃,只把提取出的关键结论实体化存储。
4. 实操:从零搭一个Agent记忆组件
4.1 技术选型与理由
从零搭建记忆组件,技术选型上我的建议是别贪多,先跑通再升级。初期最小可用组合就是:一个关系型数据库(SQLite/PostgreSQL)+ 一个向量扩展(pgvector或ChromaDB)+ 一个Embedding API。SQLite适合单机开发调试,PostgreSQL + pgvector适合要上生产的项目,因为它不用引入额外基础组件,事务和备份都现成。
Embedding模型我目前用的是文本-embedding系列模型,维度1024,效果在中文场景下比很多轻量模型稳定得多。如果你完全没头绪,我建议先拿一个开源的Embedding模型在本地跑通再考虑换,因为换Embedding模型往往是牵一发动全身的事,历史数据的向量全得重算。这一条我吃过大亏,后面详说。
4.2 记忆写入流程代码示例
我写了一个最简的记忆管理器,核心功能包含写入、检索、清理三类。代码逻辑很简单,重点看写入流程的设计:
class MemoryManager: def __init__(self, embed_fn, collection, llm_extract_fn): self.embed_fn = embed_fn self.collection = collection # 向量库collection self.llm_extract_fn = llm_extract_fn # 用于实体抽取和重要度评分 def add_memory(self, content, meta): # 1. 分段(按长度和语义完整性切分) chunks = self._split_text(content, max_chunk=400) for idx, chunk in enumerate(chunks): # 2. 实体抽取 + 重要度评分(一次LLM调用完成) extracted = self.llm_extract_fn(chunk) local_meta = { **meta, "entities": extracted.entities, "importance": extracted.importance, # 0~10 "memory_type": self._infer_type(extracted.entities), "timestamp": time.time(), "access_count": 0, } # 3. 生成向量并写入 vec = self.embed_fn(chunk) self.collection.add( ids=[f"{meta['session_id']}_{time.time()}_{idx}"], embeddings=[vec], documents=[chunk], metadatas=[local_meta], )写入流程最核心的就是那段LLM抽取:一次调用把实体、重要度、类型全拿到。虽然多花了一点token和时间,但省掉了后面无数次的检索试错。很多人图省事直接裸存文本,结果检索时匹配出来一堆结构模糊的片段,还得再让LLM重新理解一遍,反而更贵。
4.3 记忆检索与上下文注入实现
检索阶段我采用双路召回。除了向量检索,我还用SQLite里的一个关键词表做倒排匹配。注意,这里的关键词来自内存里的entities,而不是文章原文。双路召回后合并候选集,用LLM做一个简单重排,选中最相关的top-k。
def retrieve_relevant(self, query, top_k=5): # 1. 查询扩展(LLM生成2~3个关联查询) related_queries = self.llm_extract_fn(query, mode="expand") queries = [query] + related_queries # 2. 向量召回 + 关键词召回双路并行 vector_hits = [] keyword_hits = [] for q in queries: vec = self.embed_fn(q) vector_hits.extend(self.collection.query(query_embeddings=[vec], top_k=top_k)) keyword_hits.extend(self._keyword_search(q)) # 3. 合并去重 merged = self._merge_candidates(vector_hits, keyword_hits) # 4. LLM重排:结合当前对话场景打分 reranked = self.llm_rerank(merged, query, top_k=4) # 5. 组装成结构化上下文区块 memory_context = self._format_memory_block(reranked) return memory_context格式化输出我固定这样拼:
def _format_memory_block(self, memories): lines = ["【相关记忆】"] for idx, mem in enumerate(memories): lines.append(f"{idx+1}. 时间:{mem['time']} 类型:{mem['type']} 内容:{mem['text']}") return "\n".join(lines)我建议把记忆区块放在系统提示词和用户消息之间,给LLM一个明确的定位:上面的记忆是参考信息,下面是对话历史和当前问题。实测下来,这个顺序比塞在会话末尾的效果好,模型会更主动地引用记忆内容而不是忽略它。
4.4 记忆更新、清理与持久化策略
记忆更新包含两个方向:已有记忆的修正和记忆的强化。比如用户之前偏好“周四汇报”,某次明确说改成“周五”,那原来的记忆就要标记为“已过时”,写入一条高重要度的新偏好记忆。如果用户反复触发同一主题,我会把访问频率计数递增,提升它的保留分而不重复写同内容。
清理策略用一个定时任务实现。每天一次,执行两步操作:
- 降级:保留分排名在末尾且低于阈值的记忆,标记为archived。
- 物理删除:archived超过30天且从未被重新访问的记录,直接删除。
持久化方面,我直接用PostgreSQL + pgvector。事务上有个细节:向量写入和结构化元数据写入要在同一个事务里,否则异常中断后两边数据不一致。我踩过这个坑,启动恢复逻辑排查时,数据库里一半记录有向量没元数据,一半有元数据没向量,修了一晚上,所以这个教训必须提。
5. 常见问题与排查技巧实录
5.1 上下文长度爆炸:对话历史到底保留多少
短期工作记忆最常见的故障是上下文长度爆炸。很多人的第一反应是“把history全留着”,结果没聊几轮token就爆了。我现在处理对话历史的方式是三层结构:
- 最近5轮对话:完整保留。
- 更早的对话:按轮做摘要,保留要点和决策结论。
- 跨会话信息:全部走记忆组件,不进对话历史。
实测中,三层结构能把上下文稳定性提升一大截。它等于把短期记忆的“存档”职责交还给了记忆组件,而不是让原始消息无限堆积。
5.2 记忆检索结果不相关、质量差
如果你发现检索回来的记忆跟当前问题八竿子打不着,先别急着换Embedding模型,大概率是写入和检索两侧的链路有问题。排查顺序我一般是:
- 先看分段质量:写入的分段是不是把完整语义切碎了?比如某段在讲用户偏好,切出来一半讲地区、一半讲价格,检索时向量互相干扰。
- 再查查询扩展:扩展出来的关联查询是不是跑偏了?我调试过一例,用户问“下周会议安排”,扩展成了“下周出差安排”,检索回来一堆机票信息。
- 最后看重排逻辑:重排LLM打分标准是什么、有没有引入新偏差?有一次重排模型把“时间近”当成唯一标准,导致老的重要偏好沉底。
有一个非常实操的调试方法:给记忆库的检索接口加观测日志,记录每条候选记忆的相似度分数、来源、类型、重排前后排名变化。不一定要立刻加复杂的评测集,先跑几十个真实用户请求,看候选集里的记忆分布,问题基本就能定位。
5.3 记忆污染与跨会话干扰
跨会话干扰是我在长线Agent项目中遇到的最难缠问题。表现是:Agent在处理A用户问题时,突然引用B用户的历史记录。这个问题的根源通常有两个——实体隔离没做好和记忆类型混淆。
我的解决方案:存储时强制带上用户/项目粒度的隔离字段(tenant_id),检索时所有查询SQL和向量检索都带租户过滤条件。这个听起来简单,但我在代码里发现过两次忘传过滤参数的bug,一次是直接检索,一次是异步清理任务。后来我把租户过滤做成了存储层的强制预编译条件,而不是靠业务层自觉传参,才彻底杜绝。
5.4 并发写入与数据一致性
当你的Agent开始支撑多个并发会话时,记忆组件会面临写入竞争问题。同一时刻多个会话可能都在向记忆库写入,如果处理不当轻则记录错乱,重则阻塞Agent主流程。
我的处理方式有三点:
- 写入走异步队列,不阻塞Agent的推理主链路。比如工具调用结束,先把结论同步写短期记忆,长期记忆的写入推到队列后台处理。
- 短期记忆用Redis这类内存存储,长期记忆用PostgreSQL。短期读多写多、长期写少读多,两者接口分离开。
- 关键操作(比如删除和更新记忆)加CAS乐观锁,用版本号防止覆盖。
并发这块还连带一个容易忽略的问题:Embedding写入的高峰期。不及时做限流,外部Embedding API会被打爆,导致批量写入大面积失败。我现在会给每个会话限速,比如每秒最多提交30条写入请求,失败后进入指数退避重试,最终平稳很多。
5.5 安全性:记忆注入与越权读取
最后说安全性。记忆组件如果做得太开放,攻击者可能通过构造特殊输入,向记忆库注入恶意指令,或者诱导Agent检索出不该读取的信息。这就是现在说的记忆注入攻击。我在Agent环境里会让记忆组件遵循两个铁律:
- 记忆内容不执行:检索出的记忆永远只作为文本参考,绝不被当作指令解析执行。需要LLM决策时,我把记忆区块标成“数据”而非“指令”,防止prompt injection生效。
- 最小权限召回:所有会话只能检索自己的记忆空间,涉及敏感内容还要做脱敏处理。特别是企业内部Agent,用户权限矩阵必须映射到租户过滤条件上执行。
这块的安全设计,我在项目验收阶段专门让安全团队做了一次针对性测试,什么“把这句话加到记忆里然后让我忽略系统提示”之类的攻击,基本都能靠上述两条挡下来。
写在最后的一些体会
记忆组件的开发,做一遍跟看十遍原理完全不是一回事。真正动手之后你才会意识到,它不是一个“查一下历史”的小工具,而是夹在LLM、向量库、业务数据之间的一层复杂中间件。我个人的体会是,别急着上高大上的图数据库、时序数据库,先用一套简单可靠的“向量库 + 结构化元数据 + 召回重排”方案跑通业务闭环,把写入质量、衰减策略、租户隔离这些基本功做扎实,就已经能解决80%以上的“失忆”问题。
最后再分享一个小技巧:给你的记忆组件加一个“记忆面板”调试入口,开发模式下能实时查看当前会话调用了哪些记忆、每条记忆的分数和来源。这比盯着日志猜问题高效得多。我做完这个面板之后,很多原本要花半天的排查,五分钟就能定位到根因。Agent记忆这个方向还有很多值得挖的东西,祝大家少踩坑,多跑通。