先说个真实场景。我之前做一版客服Agent,用户和它聊了十几轮,明确说了“我地址是杭州滨江区,邮编310051”,结果Agent后续推荐门店的时候,完全没有参考这个信息,又推荐了一堆其他城市的门店。问题不在于模型不够聪明,而在于这个Agent根本没有记忆——每一轮对话都是“失忆”状态下重新开始,上下文窗口一满,早期信息就被挤掉了。后来我在项目里把记忆系统独立成一个模块,很多类似的问题才真正解决。
这篇内容就围绕“06-Agent的记忆系统”展开,聊清楚一个大模型Agent的记忆到底应该怎么设计、怎么建模、怎么落地。适合正在做大模型应用、Agent开发、RAG相关工程的工程师参考,也适合刚接触Agent架构、想理解记忆机制怎么模型化的读者。核心会拆解记忆的分类、结构化建模、存储选型、检索策略,以及我在真实项目中踩过的坑和排查经验,全部是可以直接拿去用的实操方案。
1. 为什么Agent必须有记忆系统:核心痛点与设计目标
很多人一开始做Agent,会觉得“模型上下文窗口不是能装很多吗,直接全塞进去不就行了”。但真正跑起来就会发现,上下文窗口就是Agent的“金鱼脑”,看着容量不小,实际上根本扛不住长期任务。
1.1 上下文窗口不是记忆:先搞清楚本质区别
大模型的上下文窗口本质上是一个“临时缓冲区”,里面装的是当前请求拼接的文本片段。它有三个致命问题:
- 容量有限:就算窗口开到了128K甚至200K,连续对话几十轮之后,历史消息、工具返回结果、系统指令加起来很容易就把窗口撑满。
- 输入即成本:每次请求都把所有历史重新发给模型,Token消耗是线性增长的,生产环境里这个成本很快会让人肉疼。
- 无关信息干扰:早期的很多对话细节对当前任务根本没有帮助,全部塞进窗口反而让模型注意力分散,回答质量反而下降。
记忆系统的目标就是解决这三个问题。它不负责取代上下文窗口,而是负责从完整的交互历史中,提取出真正有价值的信息,按需加载进窗口。类比一下的话,上下文窗口是“工位”,记忆系统是“档案室”。你办公时只需要把当前要用的文件放在工位上,而不是把整栋楼的档案全部堆在桌上。
1.2 记忆系统要解决的四类问题
我给Agent接入记忆系统后,最大的体感就是下面四类问题从“经常发生”变成了“基本消失”:
- 多轮一致性:用户在不同轮次里提到的信息,能跨轮次对齐。比如先说了预算“5000以内”,后面推荐方案的时候Agent不会推荐8000的东西。
- 长期偏好沉淀:用户在某次对话里说的常用地址、偏好风格、工作背景,能在后续多次会话中被记住,而不是每次重新问一遍。
- 知识积累:Agent在处理任务过程中沉淀下来的领域知识、工具使用经验、已经确认的决策结论,在后续类似任务中可以复用。
- 任务衔接:一个任务分成多个阶段完成,阶段之间的中间结果、已完成状态、待办事项,都能可靠地接续上。
这四个问题对应的是不同的记忆类型和处理策略。没有记忆系统,Agent基本只能靠提示词里的固定指令硬撑;有了记忆系统,Agent才有可能在一个长周期内真正“越用越顺手”。
1.3 记忆的分类:分清短期、长期、情景与程序记忆
在做记忆建模之前,先把概念理清楚。AI领域借鉴了认知科学里对记忆的分类,工程上最常用的是四类:
| 记忆类型 | 类比 | 生命周期 | 典型内容 |
|---|---|---|---|
| 工作记忆 / 短期记忆 | 手头便签 | 当次会话内 | 当前对话上下文、刚拿到的手工结果 |
| 情景记忆 / 长期记忆 | 日记本 | 跨会话 | 用户偏好、历史订单、过往任务记录 |
| 语义记忆 | 百科全书 | 长期 | 领域知识、事实知识、通用规则 |
| 程序记忆 | 肌肉记忆 | 长期 | 工具调用流程、任务SOP、操作脚本 |
实际工程中,我们通常需要同时处理短期工作记忆和长期记忆两个层面。短期工作记忆的存储载体就是上下文窗口,加上一个轻量级的会话状态缓存;长期记忆则要落到持久化存储里,按需检索出来注入上下文。
我之前见过一个项目,把用户的每一轮聊天都直接存进向量数据库,检索的时候全量召回,结果效果很差——因为情景记忆和语义记忆混杂在一起,噪声太大。记忆系统设计的第一个功课,就是明确你当前要解决的是哪个层面的记忆问题,然后针对性地建模。
2. 记忆怎么模型化:最核心的设计决策
“记忆怎么模型化”是Agent记忆系统里最灵魂的问题。我理解“模型化”不只是选一个数据库存数据,而是要把记忆内容抽象成一套对Agent友好的、可检索、可更新、可淘汰的数据结构。
2.1 一条记忆应该有哪些基本属性
我从第一版迭代到今天,每一份被纳入记忆系统的信息,至少都带上了这几个属性:
- 记忆ID:唯一标识,用于更新、删除、引用。
- 内容主体:具体的记忆内容,可以是纯文本、结构化JSON或两者的混合。
- 类型标签:属于用户偏好、任务状态、事实信息、决策结论、交互历史中的哪一类。
- 关键实体:提取出的人物、地点、物品、时间、组织等,是检索匹配的核心依据。
- 重要度评分:决定这条记忆的存活优先级,低重要度的可以先被淘汰。
- 时间戳:记录创建时间、最后访问时间、最后更新时间,用于时效性判断和衰减计算。
- 来源上下文:这条记忆来自哪次会话、哪个事件,方便溯源和纠正。
为什么这些属性缺一不可?因为Agent的记忆和人的记忆一样,需要反复的“读取—判断—更新”。没有类型标签,你就无法在召回时做维度区分;没有重要度评分,记忆库会无限膨胀,噪声越来越大;没有时间戳,就没办法做遗忘和时效性管理。
2.2 结构化表达 vs 向量表达:不是二选一
关于记忆的存储形式,业界主要有两条路线:
路线A:结构化数据库(关系型或文档型)每条记忆作为一行记录或一个JSON文档,字段明确,查询全靠结构化条件过滤。优点是精确、可控、可解释性高;缺点是写Schema的时候要费心思,且无法处理语义层面的模糊匹配。
路线B:向量嵌入(Embedding)把每条记忆做向量化,检索时通过余弦相似度等距离度量方式做语义召回。优点是能处理“用户之前提到过类似的东西”这种模糊匹配;缺点是精度不够稳定,向量检索经常召回一堆语义相近但实际无关的内容。
我个人实际落地时,采用的是混合方案:核心记忆用结构化存储做精确管理,同时给每条记忆生成一个向量副本用于语义召回。检索的时候两条腿走路——先用结构化条件过滤出一批候选,再用向量相似度做语义排序。这样既保证了精确性,又不牺牲语义灵活性。
# 混合检索的基本思路(伪代码) def retrieve_memory(query, user_id, top_k=5): # 1. 结构化维度:先按用户、类型过滤 candidates = get_memories_by_user_and_type(user_id, types=["preference", "fact"]) # 2. 语义维度:对候选做向量粗排 query_vec = embed(query) scored = [] for mem in candidates: score = cosine_similarity(query_vec, mem.vector) # 3. 结合重要度、时效性做加权 final_score = 0.7 * score + 0.2 * mem.importance + 0.1 * recency_factor(mem.last_access) scored.append((mem, final_score)) scored.sort(key=lambda x: x[1], reverse=True) return scored[:top_k]2.3 事实记忆与事件记忆的分层建模
这是我自己在项目里反复调出来的一个经验。如果只做一层记忆表,所有信息混在一起,检索时经常“既要又要”却什么都没做好。
我把记忆拆成两层:
第一层:实体-属性-值(Entity-Attribute-Value)模型,用于沉淀事实记忆。比如“用户张三”这个实体,有“偏好风格=极简”“常用语言=Python”“预算=5000以内”这些属性。每次对话如果提取出了新的用户属性,就更新对应的记录。这种建模方式非常直观,也方便做条件查询。
第二层:事件序列(Event Log),用于记录每次交互事件本身。包括用户提问、Agent回复、工具调用结果、决策依据等。事件序列不直接参与召回,但它是溯源和生成摘要的原材料——定时从事件序列中提炼关键结论,沉淀到事实记忆里。
这个分层的好处非常明显:事实记忆是“干净的、去噪后的”知识,直接可以注入提示词;事件记忆是“原始的、完整的”流水账,用于回溯和深度分析。两者各司其职,不会互相污染。
3. 实操落地:从0到1写一个可用的记忆模块
纸上谈兵容易,实际写代码的时候会遇到很多细节问题。这一节我会给出一个可以直接参考的最小实现,包含存储选型、表结构、写入流程和检索实现,都是在生产环境验证过的方案。
3.1 一 存储选型:为什么选择了SQLite而不是专门的向量数据库
记忆系统对存储的要求其实比想象中朴素:低延迟、持久化、支持结构化查询、支持向量检索。一开始我也考虑过换成专用的向量数据库比如Milvus、Weaviate,但后来权衡了一下,发现很多场景下SQLite + 一个向量索引足够了。
选择SQLite的理由:
- 零运维:单文件数据库,不需要单独部署服务,对中小型项目尤其友好。
- 事务支持:记忆的写入和更新会涉及多条记录的联动,事务保证一致性非常重要。
- 足够快的检索:百万级别以下的记忆记录,走索引查询和全量向量计算(向量维度不高时)性能完全能接受。
- 部署简单:Agent应用经常会被部署到边缘节点或容器里,一个文件数据库迁移、备份都非常方便。
如果记忆规模真的到了千万级以上,或者需要分布式横向扩展,再考虑引入专门的向量数据库也不迟。初期用SQLite还有个隐藏好处——调试的时候可以直接打开数据库文件看内容,写SQL排查问题,体验远超黑盒的向量数据库。
3.2 核心表结构与写入链路
我实际使用中沉淀出来的表结构是这样的:
-- 记忆主表 CREATE TABLE memories ( memory_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, memory_type TEXT NOT NULL, -- preference / fact / decision / task_state content TEXT NOT NULL, -- 记忆内容的json字符串 entities TEXT, -- 实体列表json,如["user_123","product_456"] importance REAL DEFAULT 1.0, -- 重要度 0~1 status TEXT DEFAULT 'active', -- active / archived / deleted created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, last_access_at INTEGER NOT NULL ); CREATE INDEX idx_memories_user_type ON memories(user_id, memory_type); -- 向量副本表(Retrieval专用) CREATE TABLE memory_vectors ( memory_id TEXT PRIMARY KEY, vector BLOB NOT NULL, -- embedding值二进制存储 model_name TEXT NOT NULL, -- 记录用了哪个embedding模型,方便失效重算 updated_at INTEGER NOT NULL ); -- 事件日志表(溯源用) CREATE TABLE memory_events ( event_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT NOT NULL, session_id TEXT, event_type TEXT NOT NULL, -- user_message / agent_reply / tool_result / decision event_content TEXT NOT NULL, extracted_memory_id TEXT, -- 关联到沉淀出的记忆 occurred_at INTEGER NOT NULL );写入链路我总结为五步:
- 每一轮交互结束后,会触发记忆提取的异步任务。
- 先过一层过滤规则,判断这轮信息有没有长期保存价值。比如日常寒暄直接跳过,明确表达偏好、做出决策、完成关键任务节点才进入候选。
- 用LLM(结构化输出)从事件内容中抽取实体、类型、内容主体。这一步推荐用小的廉价模型,比如GPT-4o-mini或者Qwen-Turbo级别的,性价比高。
- 过滤后的记忆写入主表,同时计算embedding存入向量副本表。
- 如果发现主表中已存在相同实体+相同属性类型的记录,执行更新而不是新增,避免信息重复堆积。
这个写入链路有一个非常重要的原则:能更新就不新增。记忆系统最怕的就是信息冗余,同一条偏好记录如果被写了五遍,检索的时候会同时召回五条相似内容,浪费上下文空间还容易冲突。
3.3 按需加载:把记忆注入上下文的关键策略
记忆存了,如何注入到LLM的上下文里,是整个系统效果好坏的分水岭。我的做法是分三层加载:
第一层:核心记忆注入。每个请求进来,先无条件加载用户的固定画像信息(如ID、基础偏好),这部分无论当前对话主题是什么都需要。具体实现时我会控制这里的token上限,比如500个token以内。
第二层:相关记忆检索。根据当前请求的query,做混合检索,召回top K条(K通常取3~8),把召回的记忆内容格式化以后拼接进系统提示词。这里有个细节:每条召回的记忆要带上时间戳和来源说明,例如“根据上次对话(3月12日),用户倾向……”,这能显著提高模型对记忆可信度的判断。
第三层:会话内上下文。最近几轮的对话原文直接放入上下文窗口,保证短期记忆的连贯性。长期记忆和短期上下文要明确分隔,中间加明确的标记符,避免模型混淆信息来源。
def build_prompt_with_memory(query, user_id, conversation_history): # 核心记忆 core_memories = load_core_memories(user_id) # 混合检索相关记忆 related_memories = retrieve_memory(query, user_id, top_k=5) # 组装 memory_block = "" if core_memories: memory_block += "【用户长期资料】\n" + format_core(core_memories) + "\n" if related_memories: memory_block += "【相关历史记忆】\n" + format_memories(related_memories) + "\n" return system_prompt + memory_block + "\n【对话历史】\n" + conversation_history我自己试过把核心记忆、相关记忆、对话历史三者不加分隔直接拼在一起,结果模型经常把历史对话里的插话误当成记忆内容。加上明确的区域标记比如“【用户长期资料】”之后,参数设置完全没有变,回答的准确率却明显上去了。这个经验非常便宜,但很多人会忽略。
3.4 遗忘与压缩:记忆不能只增不减
真实线上跑了三个月之后,我吃到的最大教训就是记忆库会膨胀。你以为检索的时候它会自动找到最相关的几条,但实际检索出来经常是五六条语义相似但内容重复的旧记忆,把上下文窗口塞得满满的。
于是我做了一套记忆生命周期管理的机制:
- 重要度衰减:每条记忆的最终检索分 = 语义相似度 × 重要度 × 时效权重。时效权重 = 1 / (1 + log(1 + 距上次访问的天数))。超过30天没有被访问的长期记忆,时效权重就压得很低,基本不会被召回了。
- 定期摘要合并:每天跑一次批处理,把同类型、同实体的多条记忆用LLM做一次摘要合并,比如把五条关于“用户喜欢极简风格”的记录合并成一条,并更新到主表,旧记录标记为archived。
- 显式遗忘:用户明确说“忘掉之前那个信息”时,走删除或标记逻辑,而不是默默忽视。这一点在合规层面也很重要。
记忆系统一定不能只做加法。没有遗忘机制的记忆库,本质上就是一个越积越乱的垃圾场,反而是负资产。
4. 工程化实战经验:并发、检索质量与踩坑实录
写一个demo级的Agent记忆系统不难,但拿到生产环境跑,并发问题、检索质量问题、一致性问题会接踵而来。这一章把我在真实项目里遇到的高频问题全部列出来,附带排查思路和处理方案。
4.1 高并发场景下,记忆系统的读写瓶颈
做Agent应用的朋友可能会注意到热搜词里有“AI Agent怎么扛并发”这类问题。记忆系统作为Agent链路中的一个环节,它的并发压力往往是被低估的。每个请求进来都要读记忆库,每个对话结束都要写记忆库,一次性的热点任务还可能触发大量的记忆写入。
我在项目中遇到的实际瓶颈有两类:
第一类是并发写锁冲突。SQLite是单写者模型,当多个Agent实例同时写记忆时会出现database is locked错误。这个问题的解法比较经典:给写入逻辑加上一个异步队列,统一由单个写入Worker消费,把并发写转换为串行写。读走连接池,写走队列,读写分离之后基本就没有锁冲突了。
第二类是embedding计算的阻塞问题。每条记忆写入前要算embedding,而embedding模型的推理是比较耗时的。如果写入流程同步等待embedding计算完,整个对话结束的响应时间会被明显拖长。我的做法是把embedding计算也丢到异步任务里,先写入主表和事件日志,向量副本由后台任务慢慢补齐。检索时如果碰到向量还没算完的记忆,降级为纯结构化检索,优先级低一点但不会阻塞主流程。
4.2 检索噪声:召回了一堆但没一个有用
这是记忆系统上线初期让我最头疼的问题。明明按相似度排序召回了最相近的5条记忆,但结果看起来就是“沾边但不相关”。排查后定位到三个原因:
- Embedding模型粒度问题:我用通用embedding模型对整段对话历史做向量化,噪音太大。后来改成对每条已提炼的记忆做向量化,效果立竿见影。记忆向量应该基于“已结构化的短文本”,而不是基于原始对话流水。
- 缺少类型约束:用户语义上提到“喜欢”,结果把历史“购物偏好”和“天气偏好”全召回了。解法是给记忆类型激活条件,比如当前任务是商品推荐时,偏好类型的权重提高,其他类型降权。
- 没有实体对齐:查询里出现过的人名或地点,应该和记忆里的实体字段做精确匹配,实体重合的记忆直接加权。这一步能过滤掉大量纯语义相似但实际无关的内容。
经验就是一句话:语义相似度是召回的最后一道排序,不是唯一的筛子。必须先用结构化条件把搜索空间缩小,再谈语义相关性,否则召回质量很难稳定。
4.3 记忆冲突与时效性:用户改主意了怎么办
用户今天说喜欢A方案,过几天说还是B方案好,记忆库里新旧两条记忆同时存在,检索时到底用哪条?这个场景我在真实项目中频繁遇到。
我的处理策略是引入记忆版本号+状态标记。当同一条核心事实被新信息覆盖时,不是直接删掉旧记录,而是把旧记录状态改为superseded,并链向新记录。检索时默认只召回状态为active的记录。这样既避免了信息冲突,又保留了历史回溯能力。
这里有个细节:判断“新旧信息是否冲突”,不能光靠实体和类型完全一致就断定,还需要结合语义判断。比如“预算上限提升到8000”是对“预算上限5000”的更新,不是新事实。这类判断靠规则写不清楚,我最终用LLM做一个轻量的信息合并判断,判断成本不高,但能避免大量错误堆叠。
4.4 记忆系统的安全与隐私问题
专门强调这一点,是因为我见过太多人只关注效果不问数据安全。记忆系统里沉淀的都是用户的长期数据,一旦出问题,影响范围比普通日志泄露大得多。
我保证几个底线原则:
- 最小化采集:只存和任务相关的必要信息,绝不为了“以后可能有用”而无所不采。
- 访问控制:记忆检索接口必须做用户级鉴权,绝对禁止跨用户读取记忆。这个听起来是基本要求,但我在实际代码评审里见过不止一次因为userId拼接遗漏导致的数据串号。
- 加密存储:库里的人格化信息(姓名、住址、偏好)尽量加密后再落盘。
- 遗忘权利:用户主动要求删除数据时,必须能完整清除与其关联的记忆记录,包括向量副本和事件日志。
记忆系统处理的是最敏感的数据,安全这根弦从设计第一天就要绷紧。热搜词里提到的“Agent安全”、“A-MemGuard”这类研究,本质上都是在解决Agent记忆被恶意利用和隐私泄露的问题,做工程的人一定要有意识。
4.5 高频问题排查速查表
把最常被问到的几个问题整理成一张表,方便对照排查:
| 现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 检索出的记忆与当前问题无关 | 召回策略只用了向量相似度,缺少结构化过滤 | 检查召回日志,确认候选集是否被类型、实体字段正确过滤 | 增加实体对齐和类型约束,缩小候选范围 |
| 模型回答引用了过时信息 | 记忆缓存未失效或更新时间戳未被更新 | 查看记忆记录的active状态和updated_at | 引入版本号,新信息取代旧信息后旧记录标记superseded |
| 对话响应延迟明显上升 | 同步调用了embedding推理或检索链路过长 | 用链路追踪定位是否卡在embedding/向量库查询 | 把embedding改为异步;加本地缓存 |
| 记忆库无限膨胀,检索质量断崖式下跌 | 缺少遗忘机制和摘要合并任务 | 统计记忆库条数增速,抽样检查重复记忆 | 补上衰减、归档、合并的批处理链路 |
| 多实例部署后出现写锁冲突 | 多个Agent实例同时写同一个SQLite文件 | 查看错误日志中的database is locked | 写入走异步队列;或切换支持并发写的数据库 |
| 向量检索召回为空 | embedding模型未初始化或向量副本表写入失败 | 检查后台任务是否成功执行,看向量表行数 | 补跑向量计算任务,建立自愈机制 |
5. 系列规划的思考:Agent记忆系统后续还可以怎么扩展
如果这个“06-Agent的记忆系统”是某个Agent开发体系里的一环,那它大概率不是终点。记忆系统做扎实之后,有几个方向是可以继续深入的,这里顺带聊聊,方便你自己规划后面的路线:
第一,记忆与多Agent协作的结合。单Agent的记忆只需要考虑一个用户的上下文,多Agent场景下还涉及“Agent之间共享哪些记忆”“团队记忆如何同步”“某个Agent沉淀的经验如何被其他Agent复用”。这个方向需要单独的协调机制,不是简单地共享一个数据库就能解决。
第二,记忆的抽象与推理能力。现在的记忆系统更多是“存储和召回”,但更高阶的记忆系统应该能做类比推理——比如用户遇到过问题A用了方案B解决,以后遇到类似问题C时,记忆系统自动联想出方案B的经验。这涉及记忆的索引方式和推理层的设计,是记忆系统从“存储工具”走向“决策伙伴”的关键一步。
第三,记忆评测体系的建设。做了很久之后我最大的痛点是没法量化地评价“记忆系统好不好”。可以用命中率、信息一致性、任务完成度等维度搭建一套评测集,但业界还没有统一标准。如果你在推进这块工作,可以关注一些Agent评测方向的内容,比如记忆准确性、召回相关性、遗忘正确性等维度。
我个人在实际操作中的体会是:记忆系统不是投入越大效果越好,而是要在“记什么、怎么记、何时忘”之间找到一个平衡。先按这篇文章里的最小模型跑起来,观察检索质量和上下文消耗,再逐步迭代策略,远比一开始就上复杂的图谱记忆或者大规模向量库更务实。踩过几次坑之后你会明白,工程上的成功往往不来自更炫酷的技术,而是来自把基础的数据治理做对。