Claude Code 自带一套记忆系统,但它只是按项目目录划分的 markdown 文件。我想要的是一份持久的记忆:跨项目、跨会话跟随我,完全运行在自己的机器上,不依赖任何托管的记忆服务。下面是实现这一点的技术栈 —— 以及一路上踩过的坑。
整体结构
┌─────────────┐ SSE over HTTP ┌───────────────┐ SQL ┌──────────────┐
│ Claude Code │ ────────────────▶ │ mem0-open-mcp │ ──────▶ │ pgvector │
│ (MCP 客户端 │ :8765 + bearer │ (Docker) │ │ (Docker) │
│ + CLAUDE.md)│ │ mem0ai 库 │ │ mem0_claude │
└─────────────┘ └──────┬────────┘ └──────────────┘
│ OpenAI 兼容 API
▼ host.docker.internal:9800/v1
┌───────────────┐
│ oMLX (macOS) │
│ 对话 + 嵌入 │
└───────────────┘
三个活动部件:模型服务器、数据库、MCP 服务器。每一个单独看都很平淡。
1. oMLX 提供模型
mem0 需要两类模型调用:一个 LLM 负责判断哪些内容值得记住(从对话中提取事实),一个嵌入模型负责把记忆向量化以便检索。两者都通过 oMLX 在本地运行 —— 这是一个面向 Apple Silicon 的 OpenAI 兼容推理服务器:
- 对话模型:
gemma-4-e2b-it-4bit - 嵌入模型:
bge-m3-mlx-8bit—— 1024 维,记住这个数字
因为它提供标准的 /v1 API,mem0 的 openai provider 可以原样对接。Docker 容器通过 http://host.docker.internal:9800/v1 访问它 —— 这是 Docker Desktop 指向宿主机的别名。
2. pgvector 存储记忆
本地克隆的 mem0 仓库提供了一个 compose 项目(server/docker-compose.yaml),其中的 postgres 服务使用 pgvector/pgvector:pg17 镜像。compose 文件里完整的 mem0 API 服务和 dashboard 都用不上 —— MCP 服务器通过 mem0ai Python 库直接访问 Postgres —— 所以只需运行数据库容器。
一个挂载到 /docker-entrypoint-initdb.d/ 的初始化脚本会在首次启动时创建专用的数据库和用户,让 Claude 的记忆不与同一个 Postgres 上的其他数据混用 schema:
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'EOSQL'
CREATE USER mem0_claude_user WITH PASSWORD '<db-password>' SUPERUSER;
EOSQL
createdb -U "$POSTGRES_USER" mem0_claude -O mem0_claude_user || true
psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -d mem0_claude <<'EOSQL'
CREATE EXTENSION IF NOT EXISTS vector;
EOSQL
该目录下的脚本按字母序执行 —— zz- 前缀保证它在仓库自带的初始化脚本之后运行。
3. mem0-open-mcp 是桥梁
mem0-open-mcp 是开源的 MCP 服务器,把 mem0 暴露为一组工具:add_memories、search_memory、list_memories 等。它运行在一个小型自定义镜像里:
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir "mem0ai<2" "mem0-open-mcp" "mcp>=1.28,<2" "psycopg[binary,pool]"
# 修复 0.2.12 中的一个真实 bug:AsyncMemory.from_config 不是协程,
# 对它 await 会导致服务器在启动时崩溃。
RUN sed -i 's/await AsyncMemory\.from_config/AsyncMemory.from_config/g' \
/usr/local/lib/python3.11/site-packages/mem0_server/server.py \
/usr/local/lib/python3.11/site-packages/mem0_server/cli.py
EXPOSE 8765
CMD ["mem0-open-mcp", "serve"]
所有行为都由一个 bind-mount 的 YAML 决定,改配置不需要重新构建镜像:
server:
host: "0.0.0.0"
port: 8765
user_id: "claude_user_01"
auth:
api_key: "<客户端必须携带的-bearer-token>"
llm:
provider: "openai"
config:
base_url: "http://host.docker.internal:9800/v1"
model: "gemma-4-e2b-it-4bit"
api_key: "env:OPENAI_API_KEY"
embedder:
provider: "openai"
config:
base_url: "http://host.docker.internal:9800/v1"
model: "bge-m3-mlx-8bit"
api_key: "env:OPENAI_API_KEY"
embedding_dims: 1024
vector_store:
provider: "pgvector"
config:
host: "postgres" # compose 服务名,在 docker 网络内解析
port: 5432
collection_name: "claude_memories"
embedding_model_dims: 1024
extra:
dbname: "mem0_claude"
user: "mem0_claude_user"
password: "<db-password>"
在 mem0 克隆仓库中加一个 compose override,把该服务挂到与 Postgres 相同的网络上:
services:
mem0-open-mcp:
build:
context: ~/.config/mem0-claude
dockerfile: Dockerfile
image: mem0-open-mcp:local
restart: unless-stopped
volumes:
- ~/.config/mem0-claude/mem0-open-mcp.yaml:/app/mem0-open-mcp.yaml:ro
environment:
- OPENAI_API_KEY=${OPENAI_API_KEY}
networks:
- mem0_network
ports:
- "8765:8765"
command: mem0-open-mcp serve --port 8765 --user-id claude_user_01
depends_on:
- postgres
4. 教会 Claude Code 使用它
把服务器注册到 user 作用域,让所有项目都能用到:
claude mcp add --scope user --transport sse mem0 \
http://127.0.0.1:8765/mcp/claude/sse/claude_user_01 \
--header "Authorization: Bearer <bearer-token>"
URL 路径中编码了客户端名称和用户 id;bearer token 必须与 YAML 中的 auth.api_key 一致。
光有工具还不够 —— Claude 还需要知道什么时候调用它们。我的全局 ~/.claude/CLAUDE.md 里写了这样的规则:mem0 是主要的记忆存储 —— 持久的偏好和上下文保存在那里;当过去的会话可能有帮助时去搜索;只有在服务器不可用时才回退到基于文件的记忆。没有这段话,服务器就只会闲置在那里。
踩过的坑
awaitbug。 mem0-open-mcp 0.2.12 对AsyncMemory.from_config使用了 await,但它并不是协程 —— 服务器启动时直接崩溃循环。Dockerfile 里的sed补丁是正式版本修复前的临时方案。- 启动顺序很重要。 服务器在启动时构建异步连接池。如果此时 Postgres 还没准备好接受连接,初始化会永久失败,之后每次工具调用都会报错,直到重启容器。仅靠
depends_on并不会等待服务就绪。恢复方法是:先启动数据库,再docker restart mem0-open-mcp。(这篇文章的诞生,正是因为今天的会话恰好是从这个故障状态开始的。) embedding_dims必须与模型一致。 bge-m3 输出 1024 维向量,而 pgvector 的 collection 在首次写入时按配置声明的维度创建。不匹配会导致难以理解的错误,或者静默地建出错误的表。embedder和vector_store两处都要设置。- 按助手隔离命名空间。
user_id和collection_name(claude_user_01/claude_memories)让 Claude 的记忆与共用这套栈的其他智能体互不干扰。记忆只有在有边界时才有用。
验证它是否工作
claude mcp list # mem0 应显示 ✔ Connected
docker ps | grep -E 'mem0|postgres' # 两个容器都在运行
然后是真正的测试:告诉 Claude “记住我偏好 X”,开一个新会话,问它对你了解多少。如果答案回来了,这个闭环就完成了。