一个典型的尴尬场景:用户昨天刚在自动化助手对话中详细描述了自己的技术栈、希望每天几点收到日报、当前项目的核心指标。今天打开新会话,助手像第一次见面一样,把同样的问题又问了一遍。
这类问题的根源不是模型不够聪明,而是 Agent 缺少“长期记忆”。大模型的上下文窗口再大,也只相当于工作记忆;一旦会话结束,这些信息就会丢失。真正让 Agent 从“会聊天”走向“能办事”的,是 Augmenting Long-Term Memory——把关键信息从对话中抽取出来,存进可以跨会话检索的记忆系统,并在合适时机自动更新和遗忘。
这篇文章不打算停留在概念层面。我会从开发者视角拆解:Agent 长期记忆到底解决什么问题,主流方案 Mem0、Letta、Zep 分别是什么思路;并用一个可运行的 Mem0 示例,演示如何给 Agent 接入最基础也最常用的长期记忆能力。如果你正在做 LLM 应用、智能助手、客服机器人或任何需要跨会话保持状态的系统,这篇文章值得收藏备用。
1. 这篇文章真正要解决的问题
1.1 三种典型的“失忆”场景
第一类是客服与工单场景。用户在周一提交了一个技术工单,周二回来问“上个工单处理到哪一步了”。如果 Agent 没有长期记忆,它需要用户重新描述一遍问题、重复提供订单号、再次说明之前已经确认过的信息。这种体验不是“智能”,而是“健忘”。
第二类是个人助手场景。用户说过“我喜欢简洁的回答”“我主要用 Python 和 PyTorch”“我周五下午通常有会”。这些偏好如果无法跨会话保留,用户每次使用都需要重新交代背景,助手永远停留在“初次见面”的状态,价值大打折扣。
第三类是长期项目场景。Agent 负责一个持续时间很长的任务,比如素材调研、竞品跟踪、代码库维护。项目进行到第 N 轮对话时,早期已经确定的技术选型、决策理由、边界条件,会随着上下文滚动而慢慢被挤出窗口。最后模型给出的结论可能和几天前自己确认过的方案互相矛盾。
这三个场景的共同点是什么?它们需要记住的不是一段临时聊天记录,而是可以从交互历史中提取出来、在未来继续发挥作用的信息单元。
1.2 传统方案的边界在哪里
很多人第一反应是“把对话历史都存下来,下一次塞进 prompt”。这个方案在轮次少的时候能用,但存在三个问题:token 成本快速上升、长历史中关键信息被淹没、跨会话数据没有结构化的读取方式。
第二种做法是给每个用户缓存最近 N 轮对话。这能解决同一个会话里面上下文丢失的问题,却解决不了跨 session 的偏好、事实和项目状态。缓存本质上是“短期记忆”的扩展,不是“长期记忆”。
第三种做法是 RAG。RAG 适合检索外部知识库、文档、网页内容,它解决的是“模型不知道的信息”问题,而长期记忆要解决的是“模型曾经知道但后来忘了”的问题。两者有关联,但不能画等号。
1.3 适合阅读本文的读者
如果你正在做以下事情,这篇文章对你会很有帮助:开发智能客服、搭建个人知识助手、实现带记忆的 Agent 工作流、设计多轮对话系统,或者只是好奇“Agent 怎么记住用户”。我默认你了解基本的 Python 编程和大模型 API 调用,但不要求你之前接触过专门记忆框架。
2. 长期记忆的基础概念与记忆周期
2.1 三个记忆层级
可以把 Agent 的记忆理解为一个三层结构。
第一层是工作记忆,也就是模型当前上下文窗口里能看到的内容。窗口越大,能同时处理的临时信息越多,但它是易失的,一旦请求结束或被弹出窗口就会丢失。
第二层是会话记忆,它服务于同一个会话内部的连续性,通常用滑动窗口、摘要压缩或短期缓存实现。当前大多数对话应用的“记忆增强”,其实只做到了这一层。
第三层是长期记忆,它负责保存跨会话仍然有效的用户偏好、事实、事件和状态。长期记忆通常存储在外部的向量数据库、键值存储或图数据库中,需要时再检索回上下文。本文说的 Augmenting Long-Term Memory,指的就是这个层级的构建与增强。
2.2 长期记忆保存什么
应用开发中,长期记忆可以进一步细分:
| 记忆类型 | 说明 | 示例 |
|---|---|---|
| 事实记忆 | 用户提供的确定性偏好或属性 | 用户使用 Java 开发,工作语言是中文 |
| 情景记忆 | 发生过的事件或交互历史 | 上周三用户提交了订单退款申请 |
| 程序记忆 | 任务执行流程和约束 | 项目要求先写测试再提交代码 |
| 状态记忆 | 正在进行中的任务状态 | 推荐系统开发进度为 70%,正在做 A/B 测试 |
不同记忆类型的存储方式不同。事实记忆适合放入结构化或向量化记录;情景记忆往往带时间属性;程序记忆可能更接近一套规则;状态记忆则需要支持反复更新。一个合格的长期记忆框架,不应该把所有内容一股脑塞进同一个文本字段。
2.3 记忆的生命周期
长期记忆不是“写进去就完事”,它有完整的生命周期。
编码阶段,系统从原始对话中抽取值得长期保留的信息。存储阶段,为这些信息建立索引、元数据和向量表示。检索阶段,在需要时根据当前用户问题召回相关记忆。更新与遗忘阶段,当事实发生变化或用户明确要求删除时,修正或移除旧记忆。
记忆工程中大量的问题不在“存储”而在“编码”和“更新”:什么信息值得存、什么时候该更新旧记忆、什么时候该遗忘,这些判断直接影响记忆系统的质量。如果任何一句闲聊都被记住,记忆库很快就会充满噪音,检索阶段反而召不回真正重要的内容。
3. 三种主流技术方案:Mem0、Letta、Zep
3.1 Mem0:面向开发者的记忆层
Mem0 的定位是给 Agent 提供一个“记忆层”。它的核心思路是:从用户对话中提取可以长期保存的事实性记忆,写入向量存储,并通过 LLM 判断新增、更新或删除。
它的工作方式大致可以概括为:当用户说了一句话,Mem0 会尝试从里面抽取可记忆的信息,然后在已有记忆中查找是否信息冲突或重复。如果存在相似旧记忆,就触发更新;如果没有,就新增一条记忆。查询时,用户的一句话会被转换成向量,去向量库中召回语义相近的记忆条目。
Mem0 对开发者的价值在于接入简单。它有比较清晰 Python API,可以按 user 或 agent 维度隔离记忆,也支持 Qdrant、pgvector 等多种向量存储。它比较适合希望快速给 Agent 加上记忆能力,又不打算从零设计抽取逻辑的团队。
3.2 Letta(MemGPT):模拟操作系统的虚拟上下文
Letta 的前身是 MemGPT,它的设计灵感来自操作系统虚拟内存。模型上下文窗口很小,那就把记忆分成“主存”和“外部存储”:主存里放 core memory,包含 persona、human 等固定模块;外部存储放 archival memory 和 recall memory,也就是可以无限增长的历史记录和知识。
Letta 的特别之处在于,模型自己通过函数调用管理记忆。系统根据 token 压力或任务需求,触发“把旧记忆写入外部存储”“把重要记忆加载回上下文”“更新 core memory 中的某一段信息”等操作。这种设计把记忆管理变成了 Agent 的一项自主能力,而不是由外部脚本固定执行。
它更适合想深入探索 Agent 自主记忆行为、研究上下文自动化管理的开发者。代价是概念和配置更复杂,学习曲线比 Mem0 陡峭。
3.3 Zep:企业级时间感知记忆
Zep 的特点是面向生产环境,把时序信息和知识图谱结合。它能把一段对话转成易于检索的记忆事实,同时保留事件发生的时间顺序,让 Agent 可以询问“用户上个月提过哪些需求”这类带时间约束的问题。
Zep 在事件类记忆上更自然,因为它有明确的 graph memory 概念,可以捕捉实体之间的关系。对于企业知识助手、客户服务系统这类需要追溯历史、分析关系、做时间过滤的场景,Zep 的方向更合适。当然,它的部署和运维成本也高于 Mem0 的最小接入方案。
3.4 怎么选
从公开资料和社区使用情况看,方案选择主要取决于场景复杂度。
| 方案 | 记忆组织方式 | 突出能力 | 适合场景 | 上手成本 |
|---|---|---|---|---|
| Mem0 | 记忆条目 + 向量检索 | 抽取、更新、删除自动化 | 个人助手、客服系统、轻量 Agent | 低 |
| Letta | 记忆块 + 虚拟上下文 | 模型自主管理上下文 | 研究型 Agent、复杂自主任务 | 高 |
| Zep | 事件 + 向量 + 知识图谱 | 时间感知、关系记忆 | 企业级客服、知识管理平台 | 中高 |
如果你只是想先跑通一个带记忆的 Agent,建议从 Mem0 开始;如果你已经在做复杂 Agent 系统,需要深入理解记忆如何融入模型行为,可以研究 Letta;如果你们有企业级数据和合规要求,Zep 值得评估。
4. 实操准备与核心配置
4.1 环境准备
长期记忆方案的上手实验不需要太重的环境。本文示例选用 Mem0,因为它的最小接入路径最短。
建议环境如下:
- 操作系统:Windows、macOS、Linux 均可
- Python:建议 3.10 及以上版本,示例用到新版类型标注
- LLM API:OpenAI 兼容接口的 Key,也可以按自身情况选择官方支持的模型
- 向量存储:本地测试可以不额外启动服务,生产环境推荐 Qdrant、pgvector 等
先创建虚拟环境并安装依赖:
python -m venv .venv source .venv/bin/activate # Windows 使用 .venv\Scripts\activate pip install "mem0ai"需要注意,Mem0 的包名在不同阶段可能有变化。如果安装失败,可以尝试:
pip install mem0具体包名和版本以官方文档为准。项目中不要追求最新版本而忽略接口变化,建议锁定版本号并在团队内部统一。
4.2 初始化记忆对象
最小环境下,只需要配置 LLM 和 Embedding 模型。Mem0 需要 LLM 来完成记忆抽取和更新判断,需要 Embedding 模型来完成记忆条目的向量化。
# 文件路径:examples/memory_init.py import os from mem0 import Memory os.environ["OPENAI_API_KEY"] = "your-openai-api-key" config = { "llm": { "provider": "openai", "config": { "model": "gpt-4o-mini", "temperature": 0.1, }, }, "embedder": { "provider": "openai", "config": { "model": "text-embedding-3-small", }, }, } m = Memory.from_config(config) print("memory init ok")这里有两个关键点。第一,temperature 设低一些,让记忆抽取更稳定;第二,embedding 模型要和向量检索的语义空间一致,实际项目中更换 embedding 模型后,旧向量索引通常需要重新生成。
如果你在本地测试,不一定需要配置 vector_store,默认会使用本地向量索引。生产环境建议显式配置分布式向量库,方便扩展和备份。
5. 完整示例:用 Mem0 给 Agent 增加长期记忆
5.1 基础记忆操作
下面是一个覆盖写入、检索、查看、更新、删除的最小示例。请注意,Mem0 的接口在版本升级后可能有变化,代码中的参数和返回字段以你安装的官方版本为准。
# 文件路径:examples/memory_basic.py from mem0 import Memory m = Memory() # 1) 写入记忆 m.add( "用户希望每天上午 9 点收到技术日报,并且喜欢简洁的中文摘要。", user_id="zhangsan", metadata={"source": "onboarding", "time": "2025-06-01"}, ) # 2) 检索记忆 result = m.search( "用户希望什么时间收到日报?", user_id="zhangsan", limit=3, ) print("search result:", result) # 3) 查看该用户全部记忆 all_memories = m.get_all(user_id="zhangsan") print("all memories:", all_memories) # 4) 更新记忆 # memory_id 来自 add / search / get_all 返回结果 m.update(memory_id="xxx", data="用户希望每天上午 8 点收到技术日报") # 5) 删除记忆 m.delete(memory_id="xxx")这段代码展示了记忆的基本生命周期。需要特别说明的是更新操作:实际项目中很少直接手写 memory_id,而是在写入新信息时,先 search 已有记忆,判断是否存在同一事实,再决定是新增还是更新。这一步可以自动完成,也是 Mem0 的核心价值之一。
5.2 把记忆封装成一个 Agent 工具
真实场景中,记忆操作不应该散落在业务代码里。更好的方式是把记忆封装成 Agent 的基础工具类,让模型和上层业务统一通过这个类存取记忆。
# 文件路径:examples/agent_with_memory.py from mem0 import Memory class MemoryAgent: def __init__(self, user_id: str): self.user_id = user_id self.memory = Memory() def remember(self, content: str, metadata: dict | None = None): """把一条值得长期保留的信息写入记忆库。""" return self.memory.add( content, user_id=self.user_id, metadata=metadata or {}, ) def recall(self, query: str, limit: int = 5) -> list[str]: """根据当前问题召回相关的历史记忆。""" result = self.memory.search( query, user_id=self.user_id, limit=limit, ) return [item["memory"] for item in result.get("results", [])] def clear(self): """清空当前用户的全部记忆,适合测试或用户主动重置。""" memories = self.memory.get_all(user_id=self.user_id) for item in memories.get("results", []): self.memory.delete(item["id"]) if __name__ == "__main__": agent = MemoryAgent(user_id="zhangsan") agent.remember("用户正在开发一个推荐系统,偏好 Python 和 PyTorch。") agent.remember("用户今天提到希望把推荐结果延迟控制在 200ms 以内。") retrieved = agent.recall("这个项目用什么语言和框架?") for text in retrieved: print("recall:", text)需要注意,不同版本get_all返回字段可能不同,有的版本返回results,有的版本返回memories,具体以官方文档为准。
5.3 在对话流程中接入记忆
一个常见的接入模式是:用户提问前,先根据问题召回相关记忆;模型回答后,再从用户消息中抽取并写入新的记忆。
伪代码流程如下:
# 文件路径:examples/chat_with_memory.py def handle_user_message(agent: MemoryAgent, user_message: str): # 1. 召回与当前问题相关的历史记忆 related_memories = agent.recall(user_message) # 2. 组装上下文,把记忆注入系统提示 system_prompt = "你是用户的长期助手。以下是与本次问题相关的历史记忆:\n" system_prompt += "\n".join(related_memories) # 3. 调用大模型生成回答(实际项目使用你自己的 LLM 客户端) # reply = llm.chat(system_prompt, user_message) # 4. 从用户消息中抽取值得长期保留的信息 agent.remember(user_message) return system_prompt这里真正容易踩坑的地方是:不要把召回的几十条记忆全部塞进 prompt。记忆注入数量要控制,只保留最相关的 3 到 5 条,否则不仅浪费 token,还可能让模型被无关记忆干扰。
6. 运行结果与效果验证
6.1 预期输出
运行第一个示例examples/memory_basic.py后,search 结果大致会返回一个 JSON,结构类似:
{ "results": [ { "id": "6f7b0b1e-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "memory": "用户希望每天上午 9 点收到技术日报,并且喜欢简洁的中文摘要。", "event": "ADD", "score": 0.87 } ], "meta": {} }score表示当前问题与记忆条目的语义相似度,通常越高越相关。event字段表示这条记忆最近一次发生的事件类型,可能包括新增、更新、删除、检索等。
6.2 验证方法
第一步,确认写入成功。运行python examples/memory_basic.py,观察add是否返回包含id的输出。没有返回 id,说明写入链路有问题,通常先检查 API Key 和模型权限。
第二步,验证语义检索。换一种措辞再查询一次,比如把“用户希望什么时间收到日报”改成“日报发送时间偏好是什么”。如果两种问法都能召回同一条记忆,说明 embedding 和向量检索工作正常。如果召回不到,优先调整查询表述,或检查 embedding 模型是否正确加载。
第三步,验证更新与删除。调用update修改数据,再search查看是否返回新内容;调用delete后再search,确认旧记忆不再出现。更新逻辑是长期记忆系统的核心,测试时要格外关注。
第四步,验证跨会话能力。关闭 Python 进程,重新启动一个新的进程,再构建同一个user_id的MemoryAgent,调用recall。如果还能召回之前的记忆,说明数据确实持久化到了外部存储,而不是只保存在内存里。
6.3 失败的后续排查顺序
如果运行失败,第一步看异常信息,第二步看 API Key 是否正确,第三步确认网络能访问模型服务地址,第四步确认向量库连接是否正常。需要强调的是,生产环境中这些配置都不应该在代码中硬编码,而是通过环境变量或密钥管理服务注入。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 添加记忆时报 401 或 403 | LLM 或 Embedding 的 API Key 无效、模型权限不足 | 检查环境变量、API 控制台用量和权限 | 更换有效 Key,确认账号有对应模型访问权限 |
| 检索不到相关记忆 | 查询表述和写入时的语义差异太大,或 limit 太小 | 打印召回结果,尝试换一种问法 | 调整 limit 和相似度阈值,保持查询语义与记忆内容一致 |
| 记忆内容出现明显错误 | 抽取阶段的 LLM 判断不准 | 查看 add 返回的新增记忆文本 | 拆分更小的事实片段,补充 metadata,对关键事实加入人工校验 |
| 同一事实被反复写入 | 写入前缺少去重逻辑,或更新判断不触发 | 查看 get_all 结果中的重复项 | 写入前先 search 同一主题,命中已有的高相似记忆时转为更新 |
| 上下文长度仍然爆炸 | 召回的长期记忆没有控制数量,和会话历史混在一起 | 检查系统提示和注入逻辑 | 只注入 top 3 到 5 条记忆,会话历史用摘要压缩,不要全文保留 |
| 敏感数据被写入记忆库 | 缺乏脱敏、权限控制和清理机制 | 审查记忆库内容和日志 | 对 PII 脱敏,按用户隔离数据,加密存储,提供用户主动删除入口 |
这些问题的共性在于:长期记忆系统如果只解决“存得下”和“查得回”,它只是数据管道的延伸;真正决定成败的是抽取质量、更新判据和边界控制。
8. 长期记忆工程的最佳实践
8.1 按用户、Agent、会话分层隔离
记忆数据必须支持多维度隔离。同一套系统可能服务多个用户,也可能一个用户使用多个 Agent。建议至少用user_id和agent_id区分,必要时加入session_id代表一次业务会话。查询和写入都要显式传递身份标识,防止数据串号。
8.2 元数据先行
每条记忆都要带上来源、时间、置信度等元数据。来源用于追溯,时间用于事件回看和衰减计算,置信度用于系统判断是否值得写入长期记忆。没有元数据的记忆库,最后会变成一团无法解释的黑盒。
8.3 控制检索阈值
召回结果不是越多越好。给相似度分数设置一个合理下限,过滤掉低相关条目;设置一个合理的 top_k 上限,控制注入 prompt 的 token 成本。对高价值任务,可以多轮查询后合并结果,而不是把整库结果都扔给模型。
8.4 更新优先于新增
长期记忆系统最怕重复。推荐流程是:写入前先搜索已有记忆中是否有同一主体、同一属性的高相似条目。如果有,走更新流程,保留原始来源并记录更新时间;如果没有,才走新增流程。这样记忆库会越来越精炼,而不是越来越臃肿。
8.5 给用户遗忘的权利
Regulation 与隐私要求越来越严格,产品上也要给用户提供“查看我的记忆”“修改我的记忆”“删除我的记忆”的入口。即使不涉及合规要求,这个能力对系统质量也有帮助:用户主动清除过时记忆后,Agent 下一次的表现通常会明显变好。
8.6 建立记忆评测集
很多团队上线了记忆功能,但没有定义“怎么样才算记住”。建议准备一组标准任务:用户偏好记忆、跨会话召回、重复信息去重、事实变更更新、主动遗忘。每一次功能迭代,用同一组任务回归测试,用召回准确率和任务成功率衡量系统是否在变好。
8.7 注意安全边界
记忆库里装的往往是用户隐私数据,安全等级高于普通日志。生产环境建议做到:传输加密、存储加密、最小权限访问、访问审计、定期清理。不要在代码和日志中打印完整记忆内容,不要在演示中暴露真实用户数据。
9. 总结与后续学习方向
从上面的方案和代码示例可以得出一个判断:Augmenting Long-Term Memory 的核心不是“把历史文本存进向量库”,而是围绕记忆生命周期建立起一套抽取、存储、检索、更新、遗忘的工程链路。记住哪些信息、何时更新、何时遗忘,比“有没有记忆系统”更重要。
如果你正在做 Agent 项目,建议先别急着上复杂框架。用今天这个最小链路跑通“写入-召回-更新”,你就能快速看清长期记忆的核心瓶颈在哪里。它通常出现在抽取质量、检索阈值、重复去重和用户遗忘触发条件上,而不是在向量数据库的选型上。
下一阶段可以研究两个方向:一是读 Letta 的 memory block 设计,理解模型自主管理上下文的思路;二是调研 Zep 的时间感知与图谱记忆,看能否满足企业级场景。无论走哪条路,最终都要回到一个问题:你的 Agent 知道自己该记住什么、该忘掉什么吗?这个问题的答案,决定了记忆系统是 Agent 的加分项,还是新的负担。