拆解 claude-mem:为 Claude Code 构建 AI 长期记忆层
你有没有遇到过这种场景:昨天刚和 AI 助手讨论完数据库选型,今天开个新会话想继续,结果它一脸茫然,连你用的是 PostgreSQL 都不记得,还要从头解释一遍。我在过去一周里被这个问题折磨得够呛,所以才认真把一个叫 claude-mem 的项目拆了个底朝天。它不是一个花哨的聊天插件,而是一个典型的"AI 长期记忆层":在后台把会话沉淀成结构化记忆,等下次对话时按需检索回来,让 Claude Code 这类工具真正"记住"你。这篇文章我尽量不写官腔,直接从架构设计、数据模型、接入方式和踩坑经验讲清楚,适合正在做 Agent 记忆系统的人,也适合被上下文窗口逼疯的重度用户。
1. 先认清问题的本质:对话窗口不负责记住你
先说一个很容易混的概念:上下文窗口不是记忆,它只是工作台。LLM 能看到的只有你当前塞进 prompt 窗口里的内容,一旦窗口滚动、会话关闭,它对刚才说过的话就毫无印象。这就像一个人手里能同时拿的牌是有限的,但牌桌旁边的抽屉才是真正的记忆库——而大多数 AI 应用目前只有牌,没有抽屉。
我在实际使用时被这个问题坑得特别明显。Claude Code 这类工具效率很高,但我往往同一个任务要分好几天完成,比如先调研技术方案,第二天写代码,第三天调 bug。每天新开会话后,它不认识我之前的结论,我得重新复述项目背景、技术约束、甚至自己的口味偏好。这个过程中浪费的 token 其实是小头,真正难受的是决策断裂:头天晚上确定用方案 A 的理由,第二天它忘了,又给我推荐方案 B。
claude-mem 解决问题的思路非常朴素:把记忆外置。它监听会话过程,在合适的时间对对话做提炼,把值得长期保存的信息抽取成结构化记忆存进本地数据库;下一次对话时,又通过检索把相关记忆注入回 prompt。这个外置记忆跟会话日志有本质区别,我列个表:
| 维度 | 原始会话日志 | claude-mem 式长期记忆 |
|---|---|---|
| 存储内容 | 逐字保存全部消息 | 提炼后的事实、偏好、实体关系 |
| 检索方式 | 按时间翻页 | 按语义相似度召回 |
| 注入策略 | 需要手动复制 | 自动挑选 Top K 事实注入 |
| 隐私控制 | 难以细粒度管理 | 可标记实体私密、过滤敏感信息 |
| 适用场景 | 审计、回溯 | 让 AI 在后续对话中像老朋友一样工作 |
换句话说,它关心的不是"上次完整说了什么",而是"哪些事值得一直记住"。这是一条截然不同的技术路径,也是我觉得这个项目值得拆解的原因。
顺着这个思路往下走,真正要回答的问题是:会话进来之后,怎么变成可检索的记忆?
2. 拆解 claude-mem 的核心数据流:从原始会话到结构化记忆
整个系统可以看成一条流水线:原始会话 → 实体识别 → 事实/偏好抽取 → 向量嵌入 → 语义检索 → 上下文注入。每个环节都不复杂,但串起来之后效果非常稳。
2.1 四个核心对象:实体、事实、偏好、会话
我拆解这个项目的数据模型时,发现它划分得非常干净。先有实体(entity),再挂事实(fact)和偏好(preference),会话(conversation)则是辅助溯源的东西。
实体指的是记忆的主人公,可以是一个用户、一个 GitHub 账号、一个 Slack 成员,也可以是一个团队或一个项目。事实是关于这个实体的客观陈述,比如"用户的生产环境使用 PostgreSQL 15";偏好是用户明确表达的做事倾向,比如"希望所有 SQL 变更先给 EXPLAIN 分析"。会话则承担了"出处"的角色,每条记忆都尽量保留 source_message_id,一旦记忆内容有争议,可以直接回到原始对话里去核对。
这个拆分很有讲究。如果只存整段聊天文本,虽然数据完整,但后面检索时常常把一堆噪音捞出来;如果只存一句话总结,又丢失了"这句话是谁说的、什么时候说的"这些关键信息。实体+事实+偏好的结构让记忆具备图式化的特征,后续做去重、过期、隐私控制都有抓手。
我按这个思路仿写过一张最简表结构,供参考:
CREATE TABLE entities ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, entity_type TEXT NOT NULL, -- user / github / slack / project is_private INTEGER NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE facts ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_id INTEGER REFERENCES entities(id) ON DELETE CASCADE, content TEXT NOT NULL, source_message_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE preferences ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity_id INTEGER REFERENCES entities(id) ON DELETE CASCADE, content TEXT NOT NULL, source_message_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );实际项目里字段会比这复杂,比如会加 metadata 存来源渠道、有效时间、更新次数等,但骨架就是这个。有了这套结构,记忆才能像数据一样被查询、修改、删除,而不是一段躺在日志里等着发霉的文本。
2.2 抽取记忆不是记流水账,而是有损压缩
如果直接把整段对话丢进 SQLite,那不叫记忆库,叫日志库。claude-mem 的做法是在后台调用一次 LLM,让模型担任"提炼者"的角色。
这个提炼过程本质上是一种有损压缩,但损失的是废话,保留的是语义。我在实现同类功能时用的 prompt 长这样:
你的目标是从对话中提取值得长期记住的信息。 只输出三类内容: 1. FACT:关于用户/团队的客观事实; 2. PREF:用户明确表达的偏好、规则或习惯; 3. NONE:无需长期记忆的内容。 输出格式: - 实体:用户John - [FACT] 使用PostgreSQL 15作为生产数据库 - [PREF] 希望所有SQL变更先给EXPLAIN分析我刻意在 prompt 里加了两个约束:一是每条内容必须有来源依据,禁止模型脑补;二是只输出对"未来对话有用"的信息,而不是复述本次任务。这两个约束直接决定了记忆库的含金量,如果模型把"今天改了三个 bug"这种一次性信息也存进去,很快整个库都是噪音。
这个抽取步骤通常是异步做的。用户在 Claude Code 里聊完一轮,系统先把消息落盘,然后后台任务慢慢读队列、调用小模型做抽取。因为不是每轮都要立刻出结果,所以对交互延迟几乎没影响。我自己的经验是,抽取模型用便宜的小参数模型就够,没必要每次都调用旗舰模型,省下的成本很可观。
2.3 语义召回:不靠关键词,靠向量相似度
记忆存好之后,真正难的是"怎么找回来"。如果你是做后端开发的,第一反应可能是 LIKE 查询或者干脆上一套全文搜索,但事实类记忆没那么听话。用户今天说"我用 Postgres",明天问"数据库连接池该怎么配",这两个句子之间没有共享关键词,靠 text search 根本匹配不上。
claude-mem 在这里切向量路线。每条事实写入时,会被嵌入模型转成一个向量,存进专门的向量存储;检索时把用户的当前问题同样转成向量,按余弦相似度或者内积排序,取出最相关的若干条。这个思路已经被知识库 RAG 验证过无数遍了,关键在于工程实现上的取舍。
向量存哪?社区惯例是直接用向量数据库,但 claude-mem 的定位是轻量级、本地优先,所以它把向量存进了 SQLite 体系,配合嵌入扩展做近邻查询。你可以把 SQLite 理解成一个自带文件柜的小仓库,不需要额外起服务,不容易丢,备份就是复制一个文件。对于个人记忆这种量级,SQLite 完全够用;要是真到了百万条记忆的规模,再把底下的存储替换成真正的向量数据库也不迟,上层检索逻辑不需要大改。
链路合起来就变成这样:写路径是"消息落盘 → 抽取记忆 → 向量入库";读路径是"用户提问 → 向量化 → 相似度 Top K → 拼进上下文"。到这里,记忆系统已经能转起来了,接下来就是产品层面的接入方式问题。
3. 三种打开方式:CLI 安装、Docker 托管与 MCP 协议对接
claude-mem 的部署形态比较灵活,我用过的有三种:CLI 直接装、Docker 跑独立服务、以 MCP 协议的方式嵌进各种支持 MCP 的客户端。三种方式本质上共享同一套记忆数据库,区别只是接入路径不同。
3.1 本地安装:一条命令把插件注册进 Claude Code
我当前用的版本是以 npx 方式分发的,安装入口很轻,大致是执行npx claude-mem@latest install。注意,这条命令做的事情不是下载一个二进制完事,而是把 Claude Code 的生命周期钩子注册进去。
简单说,它会在 Claude Code 合适的位置写入配置,让每次对话结束或特定节点触发时,自动把会话内容交给后台处理。这有点像给编辑器装了个 on-save 钩子,你正常写代码,系统在后台帮你做格式化。装完之后重启一下 Claude Code,就能在会话里感知到额外的记忆工具。
如果你不想长期挂服务,只是偶尔想看看记忆状态,也可以直接跑带子命令的版本,比如npx claude-mem@latest run把 MCP 服务跑起来,或者自己敲--help看当前版本支持哪些子命令。不同版本之间子命令名字略有变化,这个倒不重要,帮助文档永远是准的。
3.2 Docker 部署:把记忆服务隔离成一个独立进程
我比较推荐在长时间使用时用 Docker 方式。官方镜像一直有维护,拉下来的体积也不夸张,关键是可以挂一个持久化数据目录,把记忆库和开发环境彻底隔离开。
docker run -d \ --name claude-mem \ -v claude-mem-data:/data \ -e CLAUDE_MEM_EMBEDDING_BACKEND=local \ -p 3939:3939 \ ghcr.io/devxoul/claude-mem:latest这种方式的优势是干净。你不用在 Node 环境和虚拟环境里折腾依赖,也不怕某个实验把记忆库搞崩;记忆数据落在 volume 里,想备份就复制 volume,想迁移就换台机器重新挂载。我在公司内部给同事推广时,也优先用 Docker,因为每个人本机环境差异太大,容器把变量都挡在外面了。
唯一要注意的是,Docker 容器如果要在本机外被其他机器访问,需要自己控制好网络暴露范围。毕竟记忆数据是隐私敏感度比较高的东西,不要让监听的端口随便暴露到公网。
3.3 MCP 连接:为什么协议化接入才是关键
MCP(Model Context Protocol)值得单独拎出来说。因为 claude-mem 并没有把自己绑死在 Claude Code 一棵树上,它对外提供的是一个 MCP 服务,任何支持 MCP 的客户端都可以对接。
你可以把 MCP 理解成 AI 应用里的 USB-C 接口。以前每个工具都要给客户端单独做适配,现在大家统一用同样的一套协议,服务端暴露一组工具,客户端自动发现并调用。claude-mem 暴露出的工具大致包括记忆检索、记忆写入、实体列表、隐私标记等操作。客户端不需要理解背后的 SQLite 结构,只需要按协议调用。
这也意味着,你在 Cursor、其他 MCP 客户端或者自己写的 Agent 框架里,只要支持 MCP 客户端协议,就能复用同一套记忆数据。这个设计思路特别值得学:记忆能力应该是一种基础服务,而不是某个产品独享的私货。
4. 接入 Claude Code 后的真实体验:Hook 归档、记忆检索与上下文预算控制
工具讲完,说说我把它接进 Claude Code 之后最真实的使用感受。很多项目演示起来很惊艳,但实际用三天就会发现各种问题。claude-mem 属于那种第一天没感觉、第三天开始真香的类型。
4.1 自动归档:装了之后基本不用管
我一开始最担心的是"要我手动开开关关"。结果它安装时注册的钩子把这块做完了:对话进行过程中,消息会被持久化;一轮对话结束后,后台异步做抽取和入库。整个过程对前台交互完全透明,你继续正常聊天,它默默在后台记。
有一次我特意做了个实验:开新会话,不提任何背景,直接问"你还记得我那个项目的数据库选型吗?"大约半秒后,模型通过工具检索返回了一段记忆内容,然后基于那段记忆继续回答。那一刻确实有"它终于长记性了"的感觉。
不过也要提醒一下,异步归档意味着记忆入库存在延迟。我刚聊完一句话后立刻让新会话去检索,偶像是空的,但等个十几秒再看基本就齐了。对真实使用节奏来说完全可以接受。
4.2 记忆注入不是把历史全塞进 prompt
这是整个设计里最关键的一个点:它没有把所有记忆都塞进上下文。很多初做记忆系统的人容易犯的毛病是"反正存了,干脆全注入给模型",结果上下文窗口瞬间爆掉,钱也烧得飞快。
claude-mem 的做法是先由检索工具根据当前对话内容做语义匹配,选出最相关的若干条事实,再把它们作为工具返回结果注入。这个"You ask, I recall"的逻辑像极了一个文档管理员:你要的是一份资料,他绝不会把整个档案室给你搬过来。具体 Top K 是多少,可以通过配置调,实际使用中 K=5 到 K=10 都比较合理,既够模型理解背景,又不会喧宾夺主。
4.3 手工维护一些信息,才知道工具在哪
真实使用中我也会定期手工检查记忆库。比如某条事实已经过期了,项目已经从 PostgreSQL 换成 MySQL,如果不删掉旧记忆,模型反而会被误导。这时需要手动删除那条 fact 或者直接把对应实体标记成私密。
我开始觉得这个"遗忘"功能才是记忆系统的魂魄。能记、能查、能改、能删,才配叫记忆;只能不断累加的话,那只是回流的数据垃圾。玩了两周之后我的习惯是:每周末花五分钟过一遍本周新增的实体和事实,把过期或错误的一键清掉,这个习惯让记忆库始终保持在比较清爽的状态。
5. 自建一个浓缩版 claude-mem:最小骨架也能跑通记忆闭环
如果不想直接依赖现成项目,自己写一个"穷人版"的 claude-mem 也只需要一个下午。核心就是三条代码路径:抽取、存储、召回。这里我给出一个可复现的最小骨架,原理上跟 claude-mem 同构。
5.1 存储层:SQLite 加一张向量表
除了前面那四张基础表,我还额外维护了一张 embeddings 表,用来存事实对应的向量。向量在 SQLite 里的存法有两种:一种是直接存成 JSON 文本,另一种是用 sqlite-vec 这类插件建虚拟表。起点阶段我推荐直接存 BLOB 或者 JSON,检索时在内存里算余弦相似度,等数据量真的过万条了再考虑 sqlite-vec 的加速。
5.2 三条核心代码路径
路径 A:抽取。拿一段对话,调一次 LLM,得到候选记忆列表。
import json def extract_memories(messages: list[dict]) -> list[dict]: prompt = build_extract_prompt(messages) # 就是前面那段模板 response = llm_complete(prompt, model="便宜的小模型") return parse_memory_list(response)路径 B:嵌入与入库。对每条记忆生成向量,再写进 SQLite。
def store_fact(entity_name: str, content: str, source_message_id: str): entity_id = get_or_create_entity(entity_name) vector = embed_text(content) # 调用嵌入接口 with db() as conn: conn.execute( "INSERT INTO facts (entity_id, content, source_message_id) VALUES (?,?,?)", (entity_id, content, source_message_id), ) fact_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] conn.execute( "INSERT INTO embeddings (fact_id, vector) VALUES (?,?)", (fact_id, json.dumps(vector)), )路径 C:检索。把当前问题嵌入,和库里的向量做相似度排序,挑出 Top K 事实。
def recall(entity_name: str, question: str, top_k: int = 5): query_vector = embed_text(question) rows = load_all_facts(entity_name) # 取该实体下所有 fact + vector scored = [ (cosine_similarity(query_vector, r["vector"]), r["content"]) for r in rows ] scored.sort(key=lambda x: -x[0]) return [content for _, content in scored[:top_k]]第三步把它拼回 prompt 就行:
memories = recall("John", "数据库连接池到底怎么配?") augmented_prompt = f""" 已知关于用户的长期记忆: {chr(10).join('- ' + m for m in memories)} 请基于以上背景回答用户的当前问题。 """这一段代码看起来简单,但它就是一个记忆系统的全部骨架。真把它接进 Agent 后你会发现,模型回答的稳定性明显提升,很多背景不需要用户重复交代了。
5.3 验证记忆闭环是否真的跑通
联调的时候我用了一个很简单的验收标准:第一天告诉模型"我的生产库是 PostgreSQL",第二天开新会话直接问"我生产库用什么",看它能不能答对。如果答对,说明抽取、入库、检索、注入四大环节全部通畅。再用一个干扰问题验证相似度排序,比如问"我平时喝什么咖啡",模型不应该把数据库选型那条事实捞出来。这个小验证你能跑通,就说明你已经写出一个 mini 版 claude-mem 了。
6. 实际使用中我的真实翻车记录与优化手段
光看着架构完美是不够的,真用一阵你就知道哪儿容易出问题。我觉得最典型的坑有三个:记忆膨胀、隐私边界、成本失控。
6.1 重复事实导致记忆膨胀
第一个坑是同样的信息被反复抽取。比如用户连续三天都在调数据库连接池,每次对话都会抽出"用户在排查连接池问题"这条事实,三天下来库里存了三份近义词。这种重复不会让模型更聪明,只会让检索结果里全是同质内容,挤掉真正有用的背景。
应对方法我在自己的骨架里加了去重逻辑:每条 fact 写入前,对实体+内容做规范化,可以用哈希比对,也可以用一个小向量相似度阈值判断。如果新事实跟已有事实的相似度大于 0.95,就选择更新原条目的时间戳而不是新建。本质上这向"记忆合并"服务——比无脑去重要高级,也更符合真实记忆的"沉淀"过程。
6.2 隐私边界:默认本地存储,但也要做好擦除
既然这套系统会读取所有对话,隐私就是一个绕不开的工程问题。claude-mem 的做法是默认本地存储,把所有数据留在用户自己的机器上,不从外部拿、不偷偷上传。这一点我在用之前就确认过,否则不敢让它长期挂在工作环境里。
但"本地存储"不等于"绝对安全"。抽取过程中,如果对话里出现了 API Key、密码这类内容,依然可能变成事实存进 SQLite,被后续检索捞出来。我的习惯是,在配置里开启敏感信息过滤,把这类模式直接替换成占位符,同时对特定实体标记为私密,这样检索结果里也不会轻易带上它。
另外提醒一点:如果你在公司电脑上用,或者打算把 Docker 服务暴露到局域网,数据备份和访问控制一定要跟上。记忆库就是你的数字分身拼图,丢了或泄了都不是小事。
6.3 成本控制:抽取模型选小的,嵌入走批量
第三个坑是成本。每次抽取都要调用一次 LLM,初看单价不高,但对话一多,积少成多还是让人肉疼。
我的优化方法是分三层:第一层,抽取用廉价小模型,不让旗舰模型干这种粗活;第二层,做增量处理,只有新增消息才进抽取队列,历史消息不重复跑;第三层,嵌入调用尽量走批量接口,把几十条文本一次发给嵌入服务,减少请求次数。这样下来,即使每天有两三百轮对话,记忆层的成本也就占整体 API 消耗的一个零头。别让记忆系统变成吞金兽,你好我好模型好。
7. 想把它当"第二大脑"之前,建议先想清楚的三件事
最后聊几句我自己的思考。claude-mem 这类项目看起来很实用,但"AI 记忆"这个方向有没有看起来那么美好,其实取决于你对三件事的预期。
第一件,记忆不是状态,是辅助上下文。哪怕记忆系统再完美,也不该直接把检索结果当权威知识源。记忆只是给模型递小抄,最终做判断的还是模型当前的推理能力。如果你遇到一个原本回答不好问题的模型,不要指望加了记忆它就忽然变专家。
第二件,遗忘能力比记住能力更重要。一个只进不出的记忆库,半年之后就是一团乱麻。我见过不少人在 AI 伴侣类应用里把所有对话全塞进去,结果模型越聊越散,因为太多旧记忆在互相干扰。给记忆设计过期、合并、删除机制,甚至比设计存储和检索更考验工程素养。
第三件,数据归属权要想清楚。当记忆库变成你的外置大脑,里面存的不只是事实,还有偏好、习惯、人际关系。这些数据应该可以被你随时导出、完全删除,而不是锁在某个服务商的数据库里。这也是我偏爱 SQLite 本地存储这类设计的原因:它把控制权还给了用户,让记忆工具回归工具本位。
需要补充一句的是,claude-mem 目前对 embedding 模型、检索阈值和上下文注入量的可配置性还比较开放,你完全可以按照自己的使用习惯调参。我不建议一上来就贪多,先把"记住关键事实、忘记噪音"这一个大目标做好。等这套机制成为习惯之后,再逐步扩展实体维度、跨设备同步甚至多 Agent 共享记忆,你会发现 Agent 真正开始像一个"有连续经验的工作伙伴",而不是每次见面都需要重新自我介绍。