自托管 mem0:给所有 AI Agent 装上共享的长期记忆
2026年8月2日 · 2882 字
前言
用 AI 助手久了,最烦的一件事就是:它永远不记得你。
每次开新会话,它都得重新认识你,你是做什么的、你喜欢什么、你之前决定过什么。明明上个月刚跟它聊过的话题,今天它一脸茫然。所有对话都是「一锤子买卖」。
现在流行的解法是给 AI 接一个「记忆层」:把用户的事实、偏好、决策提取出来存起来,下次对话时自动检索喂给它。这个领域最出名的开源项目就是 mem0,社区俗称「给 LLM 装记忆」。
官方有云平台,但按我折腾 Omnivore 时的老习惯,家里有台群晖 NAS 不用白不用,数据捏在自己手里总归最稳妥。于是就有了这篇文章:在群晖上自托管 mem0,并把记忆能力接入我日常用的所有 AI Agent。
为什么选 mem0
选型时考虑过几个方向:
- 对话历史方案(如 LangChain 的 message history):只记对话原文,不做提取,查询效率低、上下文还容易超限;
- 向量库 + Embedding 自己写:灵活但要自己管理向量化、去重、检索调度,工作量大;
- mem0:开箱即用,自带「对话 → 事实提取 → 向量化存储 → 语义检索」的完整链路,还支持 user / agent / run 三层记忆归属模型,正好契合「多个 Agent 共享一套记忆」的需求。
mem0 平台(platform)是官方维护的自托管版本,一个 docker-compose 就能拉起 API 服务和前端 Dashboard,数据库用 PostgreSQL + pgvector 向量扩展。
架构总览
最终跑起来的架构:
| 组件 | 说明 |
|---|---|
| mem0 API | FastAPI + uvicorn,端口 59888 |
| mem0 Dashboard | Next.js 前端,端口 59500,用于可视化管理和配置 |
| PostgreSQL + pgvector | 部署在另一台机器(192.168.3.24),存业务数据和向量 |
| LLM(事实提取) | DeepSeek 官方 API,模型 deepseek-v4-flash |
| Embedder(向量化) | SiliconFlow 的 BAAI/bge-m3,1024 维 |
记忆的写入链路是:Agent 说了一句话 → LLM 从中提取「值得记住的事实」→ Embedder 把事实向量化 → 存入 pgvector;读取时把查询向量化,做相似度检索返回最相关的记忆。
部署:直接用官方镜像
部署本身没太多波折,官方提供现成镜像(mem0/mem0-api-server、mem0/openmemory-ui),一个 docker-compose 就能拉起全部服务,不需要自己 clone 仓库构建。我当时已经把仓库拉下来了,实际用上的是 compose 拉镜像的方式。中途有一次 docker compose up 下载到一半想取消,用 docker image prune 清理掉已下载的镜像层即可。
我把端口自定义成:前端 59500、API 59888。
整体思路很优雅,但从部署到跑通,我踩了整整四个连环坑。下面按顺序记录,每个坑都标了根因,希望后人少走弯路。
坑一:登录一直卡在登录页
装完启动,打开 Dashboard 登录,输入账号密码后登录成功,然后瞬间被弹回登录页。无限循环,看着就血压高。
排查过程:浏览器控制台没有任何报错,Network 面板能看到登录接口返回 200 和 token,但刷新后 token 就没了。用 curl 抓包确认 POST /auth/login 确实返回 200,token 也在响应体里。
期间还一度怀疑「自部署是不是必须要 HTTPS」,很多应用的 Secure cookie 只在 HTTPS 下工作,这也是当时的排查方向之一,差点就去配反向代理了。
翻源码,发现 mem0 Dashboard 有个 shouldUseSecureCookie() 函数:
NODE_ENV === "production" && 没配置 DASHBOARD_URL 时 → cookie 带 Secure 标志
问题就在这:Secure 标志的 cookie 只能在 HTTPS 下发送,而我是纯 HTTP 内网访问,浏览器直接把 cookie 丢了。
解法:在 docker-compose 里给 dashboard 容器加环境变量:
environment:
- DASHBOARD_URL=http://192.168.3.9:59500
重启容器,登录恢复正常。
经验:凡是 Next.js 类的自托管应用出现「登录成功后弹回登录页」,先查 cookie 的 Secure 标志,再看有没有
DASHBOARD_URL/NEXTAUTH_URL/APP_URL这类配置项。
坑二:自定义 LLM / Embedder 配不上
平台配置页面要求填 provider,但下拉框只有 openai、anthropic、gemini 三个,连 BaseURL 的输入框都没有,我用的 DeepSeek 和 SiliconFlow 都不在白名单里,一度以为没法自定义。
看源码发现 provider 白名单写死在 server/main.py:
BUNDLED_LLM_PROVIDERS = ("openai", "anthropic", "gemini")
改白名单意味着改源码 + 重新构建镜像,成本高。
其实不必:DeepSeek 和 SiliconFlow 都提供 OpenAI 兼容的 API。解法是选 provider: openai,然后通过 openai_base_url 指向任意兼容端点:
{
"llm": {
"provider": "openai",
"model": "deepseek-v4-flash",
"api_key": "sk-xxx",
"openai_base_url": "https://api.deepseek.com"
},
"embedder": {
"provider": "openai",
"model": "BAAI/bge-m3",
"api_key": "sk-xxx",
"openai_base_url": "https://api.siliconflow.cn/v1"
}
}
顺带说一句:DeepSeek 官方 API 现在只提供
deepseek-v4-flash和deepseek-v4-pro两个模型,网上大量教程还在教deepseek-chat,那个已经下线了。
坑三:持续 502,一连串根因
配置完点保存,接口开始 POST /memories 一直 502。这是今天最折磨人的一环,前后排了四次根因。
平台前端只显示一句 {"detail":"Upstream provider error"},上游错误被包装了,必须看容器日志:
docker logs mem0 --tail 50
3.1 key 和 base_url 张冠李戴
第一次配置时,LLM 的 base_url 填了 DeepSeek 官方地址,但 key 用的是 SiliconFlow 的。两个服务商各用各的 key,混在一起 401。
解法:LLM 和 Embedder 两套配置的 key、base_url、provider 必须一一对应,各归各。
3.2 SiliconFlow 欠费:402 code 30001
日志里出现 402 和 code 30001。查了下 SiliconFlow 是充值制,账户余额不足直接拒绝调用。充值之后问题消失。
教训:看到 402 先别怀疑代码,先去服务商控制台看余额。
3.3 pgvector 扩展没装:extension "vector" is not available
接着冒出来一个更隐蔽的:FeatureNotSupported: extension "vector" is not available。
原因是我给 mem0 配的 PostgreSQL 是自己搭的实例,不是 mem0 容器自带的,所以没初始化 pgvector 扩展。到数据库里执行:
CREATE EXTENSION IF NOT EXISTS vector;
结果又报权限不足:InsufficientPrivilege: permission denied; Must be superuser,普通业务账号没权限建扩展。最后用 postgres 超级用户执行才成功。
3.4 向量维度对不上:1024 vs 1536
pgvector 建表时会把向量维度写死在 DDL 里:
CREATE TABLE ... (vector vector(1536))
而 bge-m3 输出的是 1024 维。1536 维的表插 1024 维的向量,直接报错。
解法:把平台的 vector_store.config.embedding_model_dims 改成 1024,对齐 Embedder 的维度。
注意:pgvector 建表后维度不可修改,只能 drop 表重建。换 Embedder 前先确认新旧模型维度是否一致,否则要清空重建向量表。
验证:端到端跑通
四个坑全部填平之后,用 API 做了一次完整的端到端验证:
# 写入一条记忆
curl -X POST http://192.168.3.9:59888/memories \
-H "X-API-Key: m0sk_xxx" \
-d '{"messages":[{"role":"user","content":"我喜欢 Vim 编辑器"}],"user_id":"HeTony"}'
# 语义搜索
curl -X POST http://192.168.3.9:59888/search \
-H "X-API-Key: m0sk_xxx" \
-d '{"query":"我用什么编辑器","filters":{"user_id":"HeTony"},"top_k":5}'
搜索返回了正确结果,相似度 0.83,「用 Vim」能搜到「用什么编辑器」,说明语义检索真正工作了。
接入所有 Agent
平台跑通只是第一步,关键是让 AI 用它。我这里有好几个 Agent:
- pi(Windows 电脑 + Mac 电脑各一个)
- hermes(NAS 上跑的)
- opencode(偶尔用)
MCP 的弯路
在动手接入之前,我认真调研过 MCP 路线,结论是不适合自托管场景:
- 官方 MCP 服务(
https://mcp.mem0.ai/mcp)只连 mem0 云端,需要 MEM0_API_KEY,连不上自己的 NAS; - 官方早期的
mem0-mcp仓库已归档停维护,同样偏向云端; - 社区有
elvismdev/mem0-mcp-selfhosted,但它是一整套独立栈(自带 Qdrant + Ollama + Neo4j),并不是「连接我已部署平台」的薄适配器,重复部署成本高; - 而且 pi 明确不支持 MCP。
结论:与其维护一个 MCP 适配层,不如按 Agent 各自的能力做两层接入:pi 用扩展,其他 Agent 用通用 Skill。
pi 扩展(深度集成)
pi 支持 TypeScript 扩展,可以注册原生工具。我写了一个 mem0.ts:
- 注册 4 个工具:写入 / 搜索 / 列出 / 删除记忆;
- 注册
before_agent_start钩子:每次对话前自动检索相关记忆注入上下文,Agent 无需任何操作就能想起你; - 配置存
~/.config/.env,API key 不进源码。
通用 Skill(所有 Agent 可用)
把记忆能力抽象成一个 Skill:SKILL.md 负责告诉 Agent 何时用、怎么用,外加一个纯 Python 标准库写的 CLI,零第三方依赖,任何环境能跑。
python mem0.py add "用户偏好 X" --user-id HeTony
python mem0.py add "pi 的配置位置" --agent-id pi-windows
python mem0.py search "关键词"
这样不管哪个 Agent,读一遍 SKILL.md 就知道怎么存取记忆。
记忆按实体归属
mem0 的记忆模型分 user / agent 两个维度,我的划分是:
| 实体 | 归属 | 例子 |
|---|---|---|
user_id=HeTony | 用户本人 | 偏好、环境、项目、决策 |
agent_id=pi-windows / pi-mac / hermes | 各自的 Agent | 该 Agent 的配置、行为、状态 |
于是「你在 Codex 里让 Agent 记住的事,pi 下次也能搜到」,反之亦然,所有 Agent 共享同一份记忆,但各 Agent 自己的状态互不混淆。Windows 和 Mac 的 pi 用不同的 agent_id 区分,一眼看出记忆来源。
怎么让 Agent 主动写记忆?
这是接入过程中讨论最多的问题:怎么让 Agent 不靠人指挥,自己判断「这句话值不值得记」?
社区的主流答案是 Tool Calling + System Prompt:把 add_memory / search_memory 暴露成工具,在系统提示里写清楚什么时候该调用,用户透露长期偏好、做出项目决策就写,一次性闲聊就不写,剩下的判断交给模型自己决定。
我的扩展在此基础上更进一步,用了「Lifecycle Hook」思路:在 before_agent_start 钩子里对话开始前自动检索,把相关记忆注入上下文,Agent 根本不用记得调用 search_memory,记忆会自己找上门;写入则仍由模型通过工具调用自主决定:
User: 我以后都用 uv 不用 pip
pi: (Thought: 这是长期偏好) → 调用 mem0_add("用户所有 Python 项目使用 uv")
而「今天吃了火锅」这种一次性的事情,不该进记忆。判断标准就一句话:存进去的信息,下次对话还会用得上吗?
安全与日常维护
- API key 集中管理:所有配置放
~/.config/.env,源码里不写明文 key; - 删除保护:CLI 的 delete 必须带
--confirm才能执行,防止 Agent 误删记忆; - 自动检索克制:相关度阈值 0.55 以下不注入、冷却期 5 分钟、默认不在界面弹出,记忆是「安静地在后台帮你」,而不是每条对话都刷屏;
写在最后
折腾一整天,从「登录都进不去」到「所有 Agent 共享一套长期记忆」,学到的东西:
平台把上游错误包装成一句笼统的话时,第一反应应该是看容器日志,而不是对着平台代码猜;OpenAI 兼容协议是事实标准,白名单之外的任何模型都能用 openai provider + base_url 接进来;给工具做一层通用封装比绑死某个 Agent 的扩展机制走得更远,换 Agent 零成本迁移。
现在每个 Agent 都真正「记得我」了。下次新开会话时,我不用再自我介绍,它已经知道我是谁。