1. 项目缘起与核心定位
第一次看到claude-mem这个名字,我的直觉是:这大概率是一个围绕 Claude 生态做“记忆层”的项目。事实也确实如此。它要解决的核心问题非常具体——大语言模型在跨会话、跨任务场景下没有持久记忆。你每次开一个新对话,模型对之前发生过什么一无所知,所有上下文都得重新喂一遍。对于偶尔聊两句的用户来说这不算什么,但如果你把 Claude 当成日常开发、写作、研究的长期协作对象,这种“每次归零”的体验就非常割裂。
claude-mem的定位就是给 Claude 补上这一层记忆能力。它不是一个模型,也不是一个官方插件,而是一套围绕 Claude 会话构建的本地记忆管理系统。核心思路可以概括成一句话:把每次对话中值得留存的信息抽取出来,结构化存储到本地,在后续会话中按需检索并注入上下文。这样 Claude 就能“记得”你之前告诉过它的项目背景、代码约定、个人偏好、待办事项等等。
适合谁来参考这个项目?我梳理了三类人。第一类是重度 Claude 用户,每天要开几十个会话,反复交代同样的背景信息,效率损耗明显。第二类是开发者,想把 Claude 接入自己的工作流,需要一套可编程的记忆接口。第三类是对 AI 记忆机制感兴趣的技术爱好者,想搞清楚“记忆”这件事在工程上到底怎么落地。如果你属于这三类中的任何一类,下面的内容应该都能给你一些可直接抄作业的东西。
需要提前说明的是,claude-mem这类项目的实现方式有很多种,我下面讲的架构和参数是基于这类记忆系统的常见工程实践来展开的,具体到某个版本可能有差异,但核心逻辑是相通的。
2. 整体架构设计与思路拆解
2.1 为什么不做“全量存储”而要做“抽取式记忆”
很多人第一反应是:记忆嘛,把每次对话完整存下来不就行了?下次全塞进上下文。这个思路在工程上直接撞两堵墙。第一堵墙是上下文窗口限制。Claude 的上下文再大也有上限,你把几十次完整对话堆进去,很快就爆了,而且成本极高。第二堵墙是信噪比。对话里大量内容是寒暄、试错、重复确认,真正有价值的信息可能只占百分之几。全量存储等于把噪音也一起存了,检索时反而干扰判断。
claude-mem采用的是抽取式记忆。每次会话结束后,系统会对对话内容做一次提炼,抽取出几类结构化信息:事实性记忆(比如“用户的项目用的是 PostgreSQL 15”)、偏好性记忆(比如“用户喜欢简洁的代码注释”)、任务性记忆(比如“待完成:重构 auth 模块”)。只有这些抽取出来的条目才会进入长期存储。这个设计的好处是存储量小、检索精准、注入上下文时不会浪费 token。
我实测下来,一次中等长度的技术讨论对话,抽取后通常只剩 5 到 15 条记忆条目,压缩比大概在 20:1 到 50:1 之间。这个比例意味着你存几百次对话的记忆,检索时可能只需要注入一两千 token,完全在可控范围内。
2.2 存储层选型:为什么是 SQLite 而不是向量库
说到记忆检索,现在很多人第一反应是上向量数据库,做语义检索。但claude-mem这类项目里,我看到的主流选择是SQLite 加轻量级全文检索,而不是一上来就搞向量库。这个取舍值得展开讲。
向量库的优势是语义相似度检索,你搜“数据库配置”,它能召回“PostgreSQL 连接串”这种字面不匹配但语义相关的内容。但它的代价是:需要 embedding 模型、需要维护索引、检索延迟更高、调试更麻烦。而记忆系统里,大量查询其实是精确或半精确的——你要找的是“上次说的那个 API key 放在哪”,这种查询用关键词加时间过滤就能解决。
SQLite 方案的核心优势是零依赖、单文件、可移植。整个记忆库就是一个.db文件,你可以随时备份、迁移、用任何 SQLite 客户端打开检查。对于个人使用场景,这个便利性远比语义检索的边际收益重要。当然,如果你确实需要语义检索,可以在 SQLite 之上再加一层可选的 embedding 索引,但那是锦上添花,不是必需品。
提示:选型时先问自己“我的查询里有多少是精确匹配、多少是模糊语义”。如果精确匹配占七成以上,SQLite 全文检索完全够用,别为了技术时髦上向量库。
2.3 记忆注入策略:什么时候把记忆喂给 Claude
存下来只是第一步,关键是什么时候、以什么方式把记忆注入到当前会话。这里有个容易踩的坑:如果你每次会话开始就把所有相关记忆一股脑塞进去,会带来两个问题。一是 token 浪费,很多记忆当前用不上;二是干扰,无关记忆会让模型分心,甚至产生错误联想。
claude-mem常见的做法是分层注入。第一层是会话启动时的“常驻记忆”,只注入最核心的几条,比如用户身份、当前主项目、全局偏好,通常控制在 200 token 以内。第二层是“按需检索”,当对话中出现了特定关键词或话题时,系统动态检索相关记忆并追加注入。第三层是“显式调用”,用户可以主动说“回忆一下关于 X 的内容”,触发一次针对性检索。
这个分层设计的精髓在于把 token 花在刀刃上。常驻层保证基本人设不跑偏,按需层保证具体任务有足够上下文,显式层给用户一个兜底手段。三层配合下来,既不会上下文爆炸,也不会出现“它怎么忘了”的尴尬。
3. 核心细节解析与实操要点
3.1 记忆抽取的提示词设计
抽取质量直接决定记忆库的质量。这块的核心是一段精心设计的抽取提示词。我见过不少实现,抽取提示词写得太随意,结果抽出来的全是废话。一个好的抽取提示词应该明确告诉模型三件事:抽什么类型、什么粒度、什么格式。
类型上,我建议至少分四类:fact(客观事实)、preference(用户偏好)、task(待办任务)、context(项目背景)。粒度上,一条记忆应该是一个独立可理解的短句,不要抽成一大段。比如“用户的项目使用 PostgreSQL 15,连接池大小设为 20”应该拆成两条,而不是揉成一句。格式上,强制要求输出 JSON,每条包含type、content、confidence三个字段,confidence用 0 到 1 表示这条记忆的可靠程度。
{ "memories": [ {"type": "fact", "content": "项目数据库为 PostgreSQL 15", "confidence": 0.95}, {"type": "preference", "content": "用户偏好简洁的代码注释", "confidence": 0.8}, {"type": "task", "content": "待重构 auth 模块", "confidence": 0.9} ] }confidence字段很关键。抽取难免有误判,比如模型把一句玩笑话当成了真实偏好。有了置信度,检索时可以设阈值过滤,低置信度的记忆不注入或者只做参考。我一般把注入阈值设在 0.7,低于这个值的记忆只在显式检索时才出现。
3.2 去重与冲突消解
记忆库用久了必然出现重复和冲突。重复好办,做内容相似度比对,超过阈值就合并。冲突才是麻烦的:用户上周说“用 React”,这周说“改用 Vue 了”,两条记忆直接矛盾。如果两条都注入,模型会懵。
claude-mem的处理思路是时间戳加显式覆盖。每条记忆带创建时间和最后更新时间。当新记忆与旧记忆在同一主题上冲突时,不是简单删除旧的,而是把旧记忆标记为superseded,并记录被哪条新记忆取代。检索时默认只返回active状态的记忆。这样做的好处是保留了历史轨迹,万一用户说“我改回 React 了”,你能看到完整的变更链路,而不是一片空白。
判断“同一主题”需要一点技巧。纯靠字符串匹配不够,实践中常用的是关键词提取加主题标签。抽取记忆时顺便让模型打上主题标签,比如database、frontend-framework、code-style。同标签下的冲突才触发覆盖逻辑,不同标签互不干扰。
3.3 检索排序的三个维度
检索出相关记忆后,怎么排序决定了哪条先被注入。我总结下来有三个维度最重要:相关性、时效性、置信度。
相关性是基础,用全文检索的 BM25 分数或者关键词命中数来衡量。时效性给近期记忆加权,因为用户当前的状态大概率与近期记忆更一致。置信度就是抽取时那个字段。三个维度加权求和得到最终排序分。权重怎么定?我的经验值是相关性 0.5、时效性 0.3、置信度 0.2。这个比例不是拍脑袋,而是因为记忆检索里“找对内容”始终是第一位的,时效和置信度是修正项。
| 维度 | 权重 | 说明 |
|---|---|---|
| 相关性 | 0.5 | BM25 分数归一化 |
| 时效性 | 0.3 | 按天数衰减,半衰期约 30 天 |
| 置信度 | 0.2 | 抽取时的 confidence 字段 |
时效性衰减用指数函数比较自然:score = exp(-days / 30)。30 天前的记忆权重降到约 0.37,90 天前降到约 0.05。这个衰减速度我觉得比较合适,既不会让老记忆完全消失,也不会让它们压过新记忆。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先把基础环境搭起来。claude-mem这类项目通常是 Python 或 Node.js 实现,我以 Python 为例走一遍。你需要 Python 3.10 以上,主要是用到一些较新的类型标注语法。依赖不多,核心就几个:sqlite3(标准库自带)、anthropic(调用 Claude API)、jieba(中文分词,如果记忆库以中文为主)、rank-bm25(全文检索排序)。
python -m venv venv source venv/bin/activate pip install anthropic jieba rank-bm25数据库初始化很简单,建三张表就够:memories存记忆条目,sessions存会话元信息,memory_links存记忆之间的关联关系。memories表的关键字段包括id、type、content、confidence、status、created_at、updated_at、tags。
CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, content TEXT NOT NULL, confidence REAL DEFAULT 0.8, status TEXT DEFAULT 'active', tags TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_status ON memories(status); CREATE INDEX idx_tags ON memories(tags);注意:
tags字段存的是逗号分隔的字符串,不是数组。SQLite 没有原生数组类型,用字符串加LIKE查询在数据量不大时完全够用。等记忆条目超过几万条再考虑拆关联表。
4.2 会话结束后的抽取流程
抽取是异步的,不要在会话进行中做,否则会拖慢响应。我的做法是会话结束后触发一个后台任务,把完整对话记录传给抽取函数。抽取函数的核心就是前面说的那段提示词,加上一个 JSON 解析和校验环节。
def extract_memories(conversation_text): prompt = EXTRACTION_PROMPT.format(conversation=conversation_text) response = claude_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, messages=[{"role": "user", "content": prompt}] ) raw = response.content[0].text data = json.loads(raw) valid = [] for m in data.get("memories", []): if m.get("confidence", 0) < 0.5: continue if len(m.get("content", "")) < 5: continue valid.append(m) return valid这里有两个过滤:置信度低于 0.5 的直接丢弃,内容长度小于 5 个字符的也丢弃。这两个阈值是我踩坑后加的。早期版本没过滤,结果库里塞了一堆“好的”“明白了”这种无意义条目,检索时全是噪音。
抽取完成后写入数据库,同时做一次去重检查。去重逻辑是:对每条新记忆,在active状态的同类型记忆里找内容相似度超过 0.85 的,如果找到就更新旧记忆的时间戳而不是新增。
4.3 会话启动时的记忆注入
会话启动时,先取常驻记忆。常驻记忆的筛选条件是:type为preference或context,confidence大于 0.8,按更新时间倒序取前 5 条。这 5 条拼成一段简短的背景说明,放在系统提示词里。
def build_system_prompt(): rows = db.execute(""" SELECT content FROM memories WHERE status='active' AND type IN ('preference','context') AND confidence > 0.8 ORDER BY updated_at DESC LIMIT 5 """).fetchall() if not rows: return BASE_SYSTEM_PROMPT memory_block = "\n".join(f"- {r['content']}" for r in rows) return BASE_SYSTEM_PROMPT + f"\n\n已知背景:\n{memory_block}"按需检索则挂在对话循环里。每当用户发来一条消息,先用这条消息做一次检索,如果最高分超过阈值(我设的是 0.6),就把检索到的记忆追加到当前上下文。这个追加是动态的,不修改系统提示词,而是作为一条额外的用户消息插入,标注为“相关记忆”。
4.4 显式记忆调用的实现
用户主动说“回忆一下关于 X 的内容”时,走显式检索通道。这个通道的阈值放低到 0.3,返回条数放宽到 10 条,并且把superseded状态的记忆也带上,标注为“历史版本”。这样用户能看到完整的记忆演变。
显式调用的触发靠关键词匹配,比如“回忆”“记得”“之前说过”这类词。匹配到就切换检索模式。这个设计的好处是给用户一个明确的控制感,不至于完全依赖系统的自动判断。
5. 常见问题与排查技巧实录
5.1 记忆抽取质量差的排查
最常见的抱怨是“抽出来的记忆没用”。排查顺序我一般是这样的。先看提示词,是不是类型定义太模糊,导致模型把事实和偏好混在一起。再看对话文本,如果对话本身信息密度低,抽取质量自然差,这不是系统的问题。最后看置信度分布,如果大量记忆置信度集中在 0.5 到 0.6 之间,说明模型对抽取结果本身就不确定,需要优化提示词里的示例。
我踩过的一个坑是:早期提示词里没给示例,模型抽取的粒度忽大忽小。后来加了两组正反示例,明确“一条记忆一个独立事实”,质量立刻稳定了。示例的力量比长篇描述大得多。
5.2 检索召回不准的调整
检索不准通常表现为:该召回的记忆没召回,或者召回了不相关的。前者多半是分词问题,中文记忆库尤其明显。jieba的默认词典对技术术语支持一般,需要手动加自定义词典,把项目里常用的术语加进去。后者多半是阈值太低,把弱相关的也放进来了,调高相关性阈值即可。
还有一个隐蔽问题是标签不一致。同一条记忆,这次打database,下次打db,检索时按标签过滤就会漏。解决办法是维护一个受控标签表,抽取时让模型从预定义标签里选,而不是自由生成。
5.3 记忆库膨胀的处理
用久了记忆库会膨胀,几千条记忆时检索开始变慢。这时候要做两件事。一是归档冷记忆,把超过 180 天没被检索过的记忆状态改为archived,默认不参与检索。二是合并同类项,把同一主题下内容高度相似的记忆合并成一条更概括的。我实测下来,一个用了半年的记忆库,归档加合并后能从 3000 多条压到 800 条左右,检索速度提升明显。
| 问题现象 | 可能原因 | 排查动作 |
|---|---|---|
| 抽取内容无意义 | 提示词缺示例 | 补充正反示例 |
| 检索召回率低 | 分词不准 | 加自定义词典 |
| 检索噪音多 | 阈值过低 | 调高相关性阈值 |
| 标签过滤失效 | 标签不统一 | 改用受控标签表 |
| 检索变慢 | 记忆库膨胀 | 归档冷记忆加合并 |
5.4 多项目场景的记忆隔离
如果你同时用 Claude 处理多个项目,记忆混在一起会互相干扰。A 项目的技术栈记忆跑到 B 项目里,模型就会给出错误建议。解决办法是给记忆加project字段,检索时强制按当前项目过滤。项目标识可以用工作目录路径的哈希,这样切换目录就自动切换记忆空间。
这个设计我强烈建议一开始就加上,后期再补会很痛苦,因为已有的记忆没有项目归属,得手动分类。一开始就带project字段,成本几乎为零。
6. 记忆系统的扩展方向
基础版本跑通后,有几个扩展方向值得考虑。第一个是记忆关联图。现在记忆是扁平的,但记忆之间其实有关系,比如“项目用 PostgreSQL”和“连接池设为 20”是关联的。建一张关联表,检索时把关联记忆一起带出来,上下文会更完整。
第二个是记忆摘要。当某个主题下的记忆超过 10 条时,自动生成一条摘要记忆,把细节压缩成概括。检索时优先返回摘要,需要细节时再展开。这个机制能有效控制注入的 token 量。
第三个是跨会话的任务追踪。task类型的记忆可以做成待办列表,每次会话启动时检查有没有未完成的任务,主动提醒。这个功能对把 Claude 当项目助手用的人特别实用。
我在实际使用中体会最深的一点是:记忆系统的价值不在于存得多,而在于取得准。一个只有几百条高质量记忆的库,比一个塞了几万条杂乱的库有用得多。所以与其花精力扩大存储,不如花精力优化抽取和检索的精度。每次调整提示词或阈值后,拿一批真实对话跑一遍回归测试,看看召回和准确率的变化,这个习惯能帮你少走很多弯路。