1. 先想清楚AI 应用的长期记忆到底在解决什么问题当你在做一个 AI 应用聊到第三轮它就把你上一轮说过的话忘得干干净净时你就知道纯粹靠上下文窗口撑不住长期记忆这个事了。Mem0 是专门来解决这个问题的开源记忆层给 LLM、AI Agent 和各类对话应用补上长期记忆能力。这篇文章我会从 Hello World 的代码讲起一路聊到生产环境的多用户隔离、记忆更新和排错技巧适合正在做 AI 应用开发、AI 智能体或者被上下文丢失问题折腾过的人。先用大白话解释一下我理解的“长期记忆”。在大多数聊天应用里你和模型之间的上下文窗口相当于一块白板。它承载了当前对话的历史但一旦超出 token 限制就会截断会话结束之后基本就归零了。你做会员系统用户上个月说“我对健身和减脂餐很感兴趣”这个月你重新开一个会话模型完全不知道这件事。这种体验对一次性问答没事但对 AI 助手、AI Agent、私人助理这类需要持续服务的产品就是致命伤。1.1 对话上下文不是长期记忆很多人一开始会想那我多塞一点对话历史不就行了听起来简单实际踩坑很多。第一上下文窗口有成本你塞得越多每次请求花的钱越多响应还变慢。第二你不能把所有历史都无限拼接进去总有截断策略而截断策略本身就是个难题——哪些信息该留哪些该丢规则写起来非常痛苦。第三就算你硬塞了历史模型面对一大段杂乱对话时仍然可能在关键信息上“迷路”。它不会自动把“用户是后端工程师”“用户最近在做一个 AI Agent 项目”“用户不喜欢冗长回答”这些点单独提炼出来。我见过很多团队第一版都是自己写 session 存储用 Redis 存最近 N 轮消息然后拼进 prompt。这个方案解决的是“当前会话内的短期上下文”不是长期记忆。长期记忆的核心在于跨会话、跨用户、跨场景的一致性用户三个星期前表达过的偏好系统能不能在今天的请求里主动想起来并正确使用。1.2 生产环境真正需要的记忆能力长期记忆在真实产品里不是“多一张表存聊天记录”那么简单。你仔细拆一下至少要满足下面这些能力一是记忆的抽取。不是每条用户输入都值得记。用户说“你好”当然不用存但说“我下个月开始休假所有会议都重新安排”这句话可能影响后续所有日程类交互。系统要从对话里自动判断哪些是值得沉淀的信息并且以结构化、便于检索的方式保存。二是记忆的更新。人的偏好是变化的。用户上周说“我喜欢邮件沟通”这周说“还是直接微信吧”。如果只存一条新增记忆系统会出现两个矛盾的事实。长期记忆系统必须能识别出旧记忆需要更新或删除而不是简单追加。三是记忆的隔离。如果是单机 Demo一个全局记忆池没问题。但生产环境通常有大量用户、多个 Agent、不同业务线。张三的记忆绝对不能出现在李四的回答里客服机器人的记忆也不能污染用户画像系统。这要求记忆在写入和检索时都带上清晰的 ID 维度。四是记忆的可控性。实际业务总会有敏感信息手机号、身份证、企业内部机密。长期记忆层必须支持按业务规则做过滤、脱敏、删除。否则你等于在生产环境埋了一颗数据合规的雷。这些需求堆在一起自己从头写一套会非常费劲。你需要一个抽 LLM 的模块一个管理向量库的模块一个做关系图的模块还要处理各种边角冲突。所以我觉得把这一步交给成型的开源方案是更现实的选择。Mem0 就是这个定位它不是一个简单的“向量搜索工具箱”而是一个把记忆抽取、存储、更新、撤销都打包好的记忆层。2. Mem0 是什么架构思路与核心机制Mem0 的定位可以理解成 AI 应用外面的一个记忆服务或者叫 Memory Layer。它不关心你的主模型是什么也不关心你用的是 LangChain、LlamaIndex、Spring AI 还是自研编排层它只负责一件事把长期记忆管好。它最特别的一点是记忆的增删改不是靠普通规则判断而是靠 LLM 本身来驱动。我第一次用 Mem0 时心里是有怀疑的所有记忆操作都让 LLM 来决策这稳定吗后来我理解到这恰恰是它的核心设计思路。向量相似度只能判断“这段话和之前哪段话像”但判断不了“用户说这句话是在补充新事实还是在纠正之前的说法”。要让记忆跟上用户真实的变化必须有一个能理解语义的环节。Mem0 的做法是把记忆当作 Agent 任务来管理用 LLM 扮演记忆管理员。2.1 一条用户消息进去发生了什么以最新版本的常见行为来说当你调用memory.add(用户喜欢 Python, user_idzhangxiaobei)或传入一段对话消息列表时Mem0 后台会经历这样几步第一步LLM 先判断这段输入里有没有值得长期记忆的信息。它会分析输入和系统里已有的记忆输出一个操作意图ADD、UPDATE、DELETE 或 NONE。这个判断过程不是简单关键词匹配而是语义级别判断。举个例子用户说“我最近不怎么喝咖啡了”如果系统里已经有“用户喜欢喝咖啡”这条记忆LLM 会倾向于选择 UPDATE 或 DELETE而不是新增一条冲突事实。第二步对于需要写入的内容Mem0 会把它进一步提炼成一条独立的记忆。这里注意它存的不是聊天原文而是一条条简洁、明确的事实描述。比如输入是一大段“我叫王小明我做前端最近在学 Rust”它可能会拆成几条记忆王小明、前端工程师、正在学 Rust分别存进向量库。第三步在写入向量库的同时Mem0 还会把事实之间的关系维护进图数据库。为什么需要图因为很多问题不是“用户喜欢什么”这种单点查询而是需要跨事实推理比如“这个用户有没有可能认识做 AI 基础设施的人”向量检索很难回答这类关系问题但图数据库可以。第四步当你要召回记忆时调用memory.searchMem0 会同时走向量检索和图关系遍历再把结果做一轮排序和去重返回当前最相关的几条记忆。你在业务代码里拿到这些记忆后就可以拼进 prompt让主模型“带着记忆”回答问题。2.2 向量存储之外的图关系很多人把 Mem0 简单理解成“带 LLM 抽取的向量数据库”这个理解还不完整。向量数据库负责你的“似曾相识”类召回它擅长找相似文本但长期记忆里经常有“实体关系”比如“某某和某某是同事”“某公司买了某产品”。这类关系如果只靠向量表达和查询都很别扭。引入图存储之后Mem0 才能回答更复杂的跨记忆问题。我个人的体会是图数据库在生产环境里不一定马上用到如果你的产品场景只是个人偏好记忆向量库基本够了。但如果你做的是 CRM 助手、企业知识 Agent、多参与方协作工具这类需要实体关系的场景图存储会很香。Mem0 在配置里允许你决定是否启用 graph_store不需要可以不配需要的时候再加灵活性还是可以的。3. Hello World5 分钟跑通一个带记忆的 AI 应用讲了这么多原理还是先上手跑一遍最实在。这一节我从安装开始带你写一个最小可运行的 Mem0 例子。你不需要有一整套生产环境本地装好 Python 3.10 以上版本就行。3.1 安装与最小配置安装只装一个包pip install -U mem0ai需要注意包名是mem0ai但代码里导入的是from mem0 import Memory。我第一次就踩了这个坑按直觉import mem0也能导入但后面用Memory会容易弄混还是按官方习惯用from mem0 import Memory保险。然后准备一个 API Key。Mem0 需要调用 LLM 来抽取记忆也需要 Embedding 模型来生成向量。默认情况下如果你直接Memory()它会读取环境变量里的OPENAI_API_KEY。export OPENAI_API_KEYsk-你的key如果你不想污染全局环境变量可以像我一样在项目里建一个.env文件OPENAI_API_KEYsk-你的key然后在代码里显式加载import os from dotenv import load_dotenv load_dotenv()3.2 用 add 和 search 完成第一次记忆写入与召回最经典的 Hello World 就是写入一条用户信息然后搜索它。import os from mem0 import Memory os.environ[OPENAI_API_KEY] sk-你的key m Memory() m.add( 用户张小北是后端工程师喜欢 Python 和 Rust正在做 AI Agent 开源项目。, user_idzhangxiaobei ) results m.search( 这个用户的技术背景是什么, user_idzhangxiaobei ) for item in results: print(item[memory], item.get(score))如果你一切顺利search应该会返回类似这样的一条记录用户张小北是后端工程师喜欢 Python 和 Rust正在做 AI Agent 开源项目同时带一个相关度分数。这里有一个和纯向量数据库不一样的地方add不是简单把这句话切块塞进向量库。它会先让 LLM 看一遍输入再决定怎么存。所以你后续搜索时哪怕 query 和原文措辞完全不一样只要语义相关也能召回。比如你搜“他的技术栈有哪些偏好”也能命中。这就是抽取式记忆和零散文本存储的区别。还有一个更贴近真实对话的写法直接传一段消息列表。messages [ {role: user, content: 你好我叫林一平时做 AI 应用开发。}, {role: assistant, content: 很高兴认识你林一。}, {role: user, content: 最近想在项目里引入长期记忆你有推荐吗}, ] m.add(messages, user_idlinyi) result m.search(用户叫什么名字做什么方向, user_idlinyi) print(result)这种方式在生产里很常用。你不需要自己从一堆对话里挑重点直接把 user/assistant 交替消息扔给 Mem0它会判断哪些信息值得沉淀。这也意味着接入成本很低你本来就有对话历史数组多调一次add而已。3.3 把记忆注入到模型上下文写入和搜到记忆只是第一步真正要让 AI 应用“记住”用户需要把search的结果拼进 prompt。这是很多初学者容易忽略的地方。Mem0 不是中间层自动改主模型请求它把记忆查出来给你拼装还是你自己的事。一个最小拼接思路是这样的recalled m.search(body.content, user_idbody.user_id) memory_block \n.join(f- {item[memory]} for item in recalled) prompt f 下面是关于用户的长期记忆请把它当作背景信息来回答。 {memory_block} 用户现在说{body.content} # 然后再用你的主模型 API 调用这个 prompt你可能会说这也太简单了对核心就这一步。长期记忆的价值不在于“魔法般自动生效”而在于它能稳定地、按需地提供高质量背景信息。我建议你在做集成时把“查记忆”和“写记忆”分别挂在请求管线的两端请求进来先查记忆拼进 prompt请求结束后再把这一轮对话交给add沉淀记忆。4. 生产用法多用户隔离、长连接与可观测性Hello World 能跑通离生产可用还有一段距离。这一节我会重点讲几个我实践后认为最重要的点ID 隔离、生产配置、异步写入和记忆的更新删除。这些点不处理好上线第一天就可能出脏数据或性能问题。4.1 用 agent_id 和 user_id 隔离每一层记忆Mem0 的检索和写入接口都支持user_id和agent_id两个维度。你可以把它们理解成记忆分区user_id区分不同的最终用户。同一个用户在不同业务下可能产生不同的记忆。agent_id区分不同的 AI 智能体或助手。同一个用户和“客服助手”聊天和与“个人秘书助手”聊天沉淀的记忆不应该混在一起。一个比较典型的用法是# 用户在小助手里的记忆 m.add(用户偏好用邮件接收日报, user_idu_1001, agent_iddaily_assistant) # 用户在客服机器人里的记忆 m.add(用户上次反馈订单发货太慢, user_idu_1001, agent_idsupport_bot)这样即使两个 Agent 面对同一个用户记忆也是互相隔离的。在实际落地时我建议把user_id和agent_id作为必传参数并且在封装函数里做校验防止漏传导致数据串场。漏传或者传空轻则查不到重则把所有用户记忆混进一个池子这种事故在生产的危害性非常大。还有一点user_id不要直接用明文手机号、邮箱这类敏感信息作为存储 ID。最稳妥的做法是在你自己的业务系统里维护一个匿名 ID或者对用户标识做哈希后再传给 Mem0。这样就算记忆库被拖走外部也无法直接关联到具体个人。4.2 生产配置切换向量库与模型默认配置适合本地实验但真要跑生产我建议显式配置三块东西LLM、Embedding 模型、向量库。LLM 决定记忆抽取质量Embedding 决定召回效果向量库决定读写性能和稳定性。下面是一份我在生产里用过的配置模板from mem0 import Memory config { llm: { provider: openai, config: { model: gpt-4o-mini, temperature: 0.1, }, }, embedder: { provider: openai, config: { model: text-embedding-3-small, }, }, vector_store: { provider: qdrant, config: { collection_name: mem0_prod, host: 127.0.0.1, port: 6333, embedding_model_dims: 1536, }, }, graph_store: { provider: neo4j, config: { url: bolt://127.0.0.1:7687, username: neo4j, password: yourpassword, }, }, } m Memory.from_config(config)为什么 LLM 要用gpt-4o-mini或者更便宜的模型而不是最贵的大模型因为记忆抽取任务相对固定不太需要极强的创造力温度调低一点保证输出稳定更重要。如果你想更省成本也可以用 OpenAI 兼容接口接入本地私有化模型比如通过一个本地推理服务暴露/v1地址然后在llm.config里指定openai_base_url。这样抽取和向量化都能在内网完成延迟更低数据也更可控。向量库这里我选 Qdrant 是因为它部署轻、API 干净支持过滤条件配合 Mem0 的 ID 隔离比较顺手。如果你已经在用其他库Mem0 也支持不少常见向量库核心切换成本不大。唯一要注意的是embedding_model_dims必须和你的 Embedding 模型输出维度一致比如text-embedding-3-small是 1536 维如果你换成了 3072 维的大模型这里也要改。维度写错最典型的现象是写入时报维度冲突或者创建集合时直接失败。4.3 与 FastAPI 集成回调式写入记忆生产环境最常见的形态是提供一个 HTTP 服务给前端或内部业务调用。我用一个简化版 FastAPI 示例说明接入点应该放在哪里。from fastapi import FastAPI, BackgroundTasks from pydantic import BaseModel from mem0 import Memory app FastAPI() memory Memory() class ChatIn(BaseModel): user_id: str content: str class ChatOut(BaseModel): reply: str app.post(/chat) async def chat(body: ChatIn, background_tasks: BackgroundTasks): # 1. 先查长期记忆 recalled memory.search(body.content, user_idbody.user_id) memory_context \n.join(f- {item[memory]} for item in recalled) # 2. 拼进 prompt 再调主模型 prompt f用户长期记忆\n{memory_context}\n\n用户{body.content} reply await call_llm(prompt) # 这里替换为你的真实模型调用 # 3. 把这一轮对话交给后台任务去沉淀记忆不阻塞用户请求 background_tasks.add_task( memory.add, [ {role: user, content: body.content}, {role: assistant, content: reply}, ], user_idbody.user_id, ) return ChatOut(replyreply)这里最值得说的点是“读同步、写异步”。读记忆直接在主流程里同步执行因为用户请求在等待结果必须快。写记忆则丢到 BackgroundTasks 里让接口先返回避免每轮对话都额外增加一次 LLM 抽取的时间。如果请求量再大一点我更建议把memory.add放进消息队列或者定时批量任务里。另外如果 Mem0 版本支持异步接口比如asearch和aadd优先用异步版本如果版本还不支持就把同步调用丢进线程池跑。别在异步接口里直接同步阻塞等 LLM 返回高并发下很容易拖垮进程。4.4 记忆的更新与删除长期记忆系统最怕的是什么是“记错了还改不了”。Mem0 提供了按记忆 ID 更新和删除的接口生产里一定要把这几个操作暴露给运维或后台管理。# 先查出一条记忆的 ID results m.search(用户目前的工作方向, user_idzhangxiaobei) memory_id results[0][id] # 更新这条记忆 m.update(memory_id, 用户已经不做后端了现在转向 AI 产品方向。) # 删除这条记忆 m.delete(memory_id) # 查看某个用户当前的全部记忆 all_memories m.get_all(user_idzhangxiaobei) for item in all_memories: print(item)我建议在后台管理页面加一个“记忆管理”入口至少让运营或产品同学能看到某个用户记住了什么、改了什么、删了什么。因为 LLM 抽取记忆不是 100% 准确一旦出现错误记忆只要用户没有明确纠正系统就可能一直带着这条错误信息跑。给运营一个可操作的手动修正入口比反复改代码要高效得多。5. 常见问题与排查技巧实录不管框架多好用落地总会遇到各种怪问题。这里把我自己踩过或帮别人排查过的几个典型问题整理出来。5.1 记忆不出来或查不到最常见的现象是明明调用了add但search结果为空或者主模型回答里完全没体现记忆。这类问题我把排查路径整理成了下面的速查表。现象可能原因排查建议搜索结果完全为空写入时没传user_id或者传入的user_id与查询时不一致先调用get_all(user_idxxx)看该 ID 下有没有数据搜索结果为空add内部调用 LLM 时失败了但异常被吞掉或没注意打印add的返回值观察里面是否有报错搜索能查到但排序不好查询语句和记忆描述差异太大向量召回效果差考虑换更强的 Embedding 模型或调整召回数量只有一个用户有问题该用户的数据写进了默认分区没有按业务 ID 隔离检查代码里是否所有入口都统一传 ID偶尔查不到异步写入还没完成主流程就去查了确认写入是同步完成还是异步完成后再判断查询时机如果只是本地快速验证最笨也最有效的办法是把get_all先打出来确认这个用户下到底有没有数据。没有就说明写入链路已经出了问题问题就不在检索而在抽取或数据库写入。有数据却搜不到再往 Embedding 和向量库配置方向查。5.2 记忆重复、互相矛盾由于抽取模型的不稳定性同一件事用户说了两遍Mem0 有时候会生成两条相近记忆有时候用户已经改变主意但旧记忆没有及时更新导致系统同时存在“用户喜欢 A”和“用户不喜欢 A”两条。我的处理经验是三管齐下。第一在调用add时给足够的消息上下文不要只给一句用户输入把最近几轮历史一起带上LLM 对“这是新增还是更新”的判断会更准。第二建立定期的记忆清洗任务每天或每周对用户的记忆列表做一次去重和矛盾检测把明显冲突的旧记忆清理掉。第三在业务上允许用户显式纠正比如用户说“我之前说的不算”这时立刻去更新或删除对应记忆。还有一个控制重复的有效手段不要把所有对话都丢进add。你可以在自己的业务层先做一次判断只有当对话中出现“偏好、事实、计划、身份、目标”等信息时才把这一轮交给 Mem0。这样能大幅降低 LLM 抽取次数也减少脏记忆。5.3 成本与延迟控制Mem0 的每次add和search背后都涉及 LLM 调用。这意味着如果不做控制记忆模块本身可能比主模型调用还贵。这是个容易被低估的成本点。我从项目上线后的账单里总结出三条控制策略。第一抽取模型用小模型gpt-4o-mini级别就够用不要拿最强模型来做重复性的格式化任务。第二降低写入频率按“会话结束”或“关键信息出现”来触发add而不是每条消息都写。第三向量库和 Embedding 服务尽量和主应用部署在同一个网络环境避免每次查询都跨地域走公网。延迟方面除了前面说的“读同步、写异步”还可以考虑对记忆查询结果做短时间缓存。比如在同一个会话内用户偏好不会频繁变化你可以在 Redis 里给某些热门的记忆结果设置 5 到 10 分钟的过期时间大幅减少向量检索压力。5.4 测试与回归要点既然抽取逻辑依赖 LLM记忆模块的测试就不能只靠“跑一次看看”。我在团队里推的是静态确定性测试加少量端到端验证结合的方式。静态测试比如检查add方法被调用时是否传了正确的user_id搜索方法返回后是否把记忆拼进了 prompt。这种测试不依赖真实 LLM适合在 CI 里跑。端到端测试则用固定 ID 和固定测试数据在一个独立测试库中执行。示例def test_add_and_search(): m Memory.from_config(test_config) m.add(User likes to ride bicycles, user_idtest-user) results m.search(What does the user like?, user_idtest-user) assert any(bicycle in item[memory].lower() for item in results)测试用例里尽量用不变的字符串不要用随机文本否则 LLM 抽取结果稍微波动测试就会挂。另外测试结束后要清理数据否则测试库会越攒越多。如果有预算也可以在 CI 里把 LLM 调用替换成 mock只验证代码逻辑和配置是否正确不验证抽取质量。6. 我的一点实战体会在做 AI 应用时很多人会把“长期记忆”理解成一个技术组件装上去就完事。我用了 Mem0 一段时间后最大的体会是它更像一条需要持续维护的数据管道。它帮你省去了自己写 LLM 抽取逻辑、自己管理向量库更新、自己处理记忆冲突的麻烦但它不会替你回答“什么信息值得记”“记多久该忘”这些产品问题。6.1 在真实项目里怎么评估“加记忆”的收益我比较推荐的做法是先选一个对用户粘性影响最大的场景做改造比如个人助理的偏好记忆或者客服系统的用户画像。改造前记录一组指标用户二次会话唤醒率、会话内信息重复率、用户主动纠正次数。改造后跑两到四周再看变化。不要一开始就追求所有 AI 功能都带记忆那样排查问题会非常难。6.2 最后提醒记忆不是万能的别让记忆层变成一个新的不可控因素。定期 review 记忆库里的真实数据理解它记住了什么、遗忘什么、在哪些场景下会答非所问。这样才能在用户说“你怎么又忘了”之前先把问题修掉。如果你刚开始尝试建议先跑通 Hello World再把add和search接到一个真实业务场景里最后才做多用户隔离和成本优化。这个顺序走下来你对 Mem0 的理解会比直接抄一段生产配置扎实得多。