1. 从零认识 claude-mem:它到底解决什么问题
第一次看到claude-mem这个名字,很多人会以为它又是一个套壳的对话客户端。实际上完全不是。claude-mem是一套围绕 Claude 对话过程做长期记忆管理的工具方案,核心目标只有一个:让 AI 在跨会话、跨项目、跨时间的协作中,记住该记住的东西,忘掉该忘掉的东西。
我接触它的起因很朴素。那段时间我同时推进三个项目,每个项目都要反复跟 Claude 解释同样的背景:技术栈是什么、命名规范是什么、上次那个 bug 修到哪一步了、为什么某个方案被否决了。每次开新会话,我都要把几百字的上下文重新粘贴一遍,粘到后来自己都烦。更麻烦的是,有些关键决策散落在几十个历史会话里,想找回来得靠翻聊天记录,效率极低。
claude-mem要解决的就是这个痛点。它把"记忆"从单次会话里抽出来,变成一份可以持久化、可以检索、可以按项目隔离的外部资产。你可以把它理解成给 AI 配了一个随身笔记本:每次对话结束,重要的结论、偏好、待办被记下来;下次对话开始,相关的记忆被自动调取出来,塞进上下文。
这套东西适合谁?我梳理了三类人。第一类是长期用 Claude 做开发或写作的重度用户,会话数量多、上下文重复率高,收益最明显。第二类是团队协作场景,需要把某个项目的共识沉淀下来,避免每个人都要重新对齐。第三类是对隐私和本地化有要求的人,因为claude-mem的记忆存储通常落在本地文件或自建存储里,数据不出自己的机器。
需要先说明一点:claude-mem并不是 Anthropic 官方发布的产品,它更像是一个社区里逐渐成型的实践模式,围绕 Claude 的上下文机制、文件读写能力和外部存储做组合。所以不同人手里的claude-mem实现细节会有差异,但底层思路是相通的。下面我讲的这套方案,是我自己实际跑通并稳定用了几个月的版本,涉及具体参数和步骤的地方,我会明确标注哪些是通用原理、哪些是我基于常见实践补全的选型。
2. 记忆系统的整体设计与思路拆解
2.1 为什么不能只靠"把历史全塞进上下文"
很多人第一反应是:既然 Claude 支持长上下文,那我干脆把所有历史对话都拼进去不就行了?我试过,结论是行不通,原因有三个。
第一是成本。上下文越长,每次请求消耗的 token 越多,费用是线性甚至超线性增长的。你不可能为了记住一句"这个项目用 pnpm 不用 npm",每次都带上十万 token 的历史。
第二是信噪比。历史对话里大量内容是寒暄、试错、被否决的方案。这些信息混在上下文里,会稀释真正重要的指令,模型反而更容易跑偏。我实测过一个场景:把 50 轮历史全塞进去,模型对最新指令的遵循度明显下降,因为它被中间那些废弃方案干扰了。
第三是冲突。历史里可能同时存在"用方案 A"和"后来改成方案 B"两条记录。如果不做时间排序和优先级处理,模型不知道该听谁的。
所以claude-mem的核心设计哲学是:记忆不是存储,而是检索。存的时候要压缩、要结构化;用的时候要按相关性召回,而不是全量加载。
2.2 三层记忆结构的设计考量
我最终采用的是三层结构,这个划分参考了认知科学里"工作记忆 / 短期记忆 / 长期记忆"的经典模型,落地到工程上就是三个不同的存储层。
| 层级 | 名称 | 存储内容 | 生命周期 | 存储位置 |
|---|---|---|---|---|
| L1 | 工作记忆 | 当前会话的即时上下文 | 单次会话 | 内存 / 会话变量 |
| L2 | 短期记忆 | 最近几次会话的摘要 | 数天到数周 | 本地 JSON 文件 |
| L3 | 长期记忆 | 项目级共识、用户偏好、关键决策 | 长期 | 结构化数据库或 Markdown 库 |
L1 不用我们操心,那是 Claude 会话本身自带的。真正要设计的是 L2 和 L3。
L2 我选择用会话摘要而不是原始记录。每次会话结束,让 Claude 自己生成一段 200 字以内的摘要,包含:本次解决了什么、产生了什么结论、有什么未完成事项。这段摘要存成 JSON,字段包括时间戳、项目标签、摘要正文、关键词数组。为什么用摘要?因为原始对话动辄几千字,检索和加载都太重,而摘要保留了 90% 的有用信息,体积只有 5%。
L3 是重头戏,我把它拆成三类内容分开存:
- 偏好类:用户或团队的固定习惯,比如"代码注释用中文""提交信息遵循 Conventional Commits"。这类内容变化少,但每次都要用。
- 决策类:项目里做过的关键技术选型,带时间戳和理由。比如"2024-03 决定用 SQLite 而非 Postgres,因为部署环境不支持独立数据库服务"。
- 事实类:项目的客观信息,比如目录结构、接口约定、环境变量清单。
分开存的好处是召回策略可以差异化。偏好类几乎每次都全量加载,因为它短且通用;决策类按关键词检索;事实类按需加载。
2.3 为什么选文件系统而不是向量数据库
网上很多记忆方案一上来就上向量数据库,做 embedding 检索。我一开始也跟风搭了一套,后来放弃了,改用纯文件系统加关键词检索。原因很实际。
向量检索的优势是语义相似度,能召回"意思相近但用词不同"的内容。但它的劣势在我的场景里被放大了:一是不可解释,召回了什么、为什么召回,很难调试;二是维护成本,embedding 模型要更新、索引要重建,对一个个人项目来说太重;三是精度问题,记忆条目通常很短,短文本的 embedding 质量不稳定,经常召回一堆似是而非的东西。
文件系统方案就朴素多了:每条记忆是一个 Markdown 或 JSON 条目,带标签和关键词。检索时用关键词匹配加时间衰减。我实测下来,在记忆条目数量低于几千条时,这种朴素方案的召回准确率反而更高,因为记忆内容本身就是高度结构化的,关键词命中率很高。
提示:如果你预计记忆条目会超过一万条,或者需要跨语言检索,那向量方案值得重新考虑。但对绝大多数个人和小团队场景,文件系统足够用,而且调试起来舒服得多。
3. 核心细节解析与实操要点
3.1 记忆条目的数据结构设计
数据结构设计得好不好,直接决定了后面检索顺不顺。我踩过的第一个坑就是一开始用自由文本存记忆,结果检索时只能全文模糊匹配,噪音极大。后来改成结构化字段,问题迎刃而解。
我最终用的条目结构是这样的(以 JSON 为例):
{ "id": "mem_20240315_001", "type": "decision", "project": "blog-engine", "created_at": "2024-03-15T10:23:00Z", "updated_at": "2024-03-15T10:23:00Z", "keywords": ["数据库", "SQLite", "部署"], "content": "决定使用 SQLite 作为主存储,原因是目标部署环境不提供独立数据库服务,且数据量预估在 10 万条以内。", "reason": "部署环境限制 + 数据量评估", "status": "active", "supersedes": null }几个字段值得单独说。
type字段是检索的第一道过滤。偏好、决策、事实三类的召回策略不同,先按 type 过滤能大幅缩小范围。
keywords是我手动或半自动打的标签。这里有个经验:关键词不要打太多,3 到 5 个最合适。打多了等于没打,因为每个词都会命中,反而失去区分度。我一般让 Claude 在生成记忆时顺便提取关键词,然后我人工过一遍,删掉太泛的词。
status和supersedes是处理记忆冲突的关键。当一条新决策推翻了旧决策,不是删掉旧的,而是把旧的status改成superseded,新的条目supersedes指向旧条目 ID。这样既保留了历史,又能在召回时排除失效记忆。这个设计我是从数据库的软删除思路借鉴来的,非常实用。
reason字段单独拎出来,是因为我发现决策的理由比决策本身更重要。半年后你回头看"为什么当时不用 Postgres",如果只存了结论,你可能会重新踩一遍坑。存了理由,就能避免重复决策。
3.2 记忆的写入时机与触发条件
记忆不是越多越好。我早期犯的错是每轮对话都写记忆,结果库里塞满了"用户问了 X,我答了 Y"这种无价值条目,检索时全是噪音。
后来我定了三条写入触发规则,只有满足其一才写:
- 产生了明确结论:比如"确定用方案 A""这个 bug 的根因是 X"。判断标准是这句话能不能独立成一条可复用的知识。
- 用户表达了偏好:比如"以后都用中文回复""这个项目不要用某个库"。偏好类记忆优先级最高,因为复用频率最高。
- 出现了未完成事项:比如"下次要验证 Y 方案"。这类记忆带一个
todo标记,下次会话开始时主动提醒。
写入动作我做成半自动的:会话结束时,我让 Claude 按上面的规则生成候选记忆条目,输出成 JSON,我扫一眼确认或修改,然后追加到记忆库文件里。为什么不完全自动?因为自动写入容易把临时性的、错误的结论也存进去,污染长期记忆。人工确认这一步花不了 30 秒,但能保证记忆库的干净。
注意:千万不要把"用户说错了然后纠正"这个过程里的错误结论存进去。我踩过这个坑,结果模型后来反复引用一个已经被推翻的错误认知,排查了半天才发现是记忆库污染。
3.3 记忆的召回策略与上下文注入
召回是整套系统里最考验设计的一环。我的召回逻辑分三步走。
第一步是全量加载偏好类记忆。这类记忆通常只有几十条,总量可控,而且几乎每次都用得上,所以直接全量塞进上下文。加载时按updated_at倒序,最新的在前。
第二步是按当前会话主题检索决策类和事实类。检索用关键词匹配,具体做法是把当前会话的前几轮内容提取关键词,然后跟记忆条目的keywords字段做交集。命中数越多的条目排越前。这里加一个时间衰减因子:同样命中数的情况下,越新的记忆权重越高。公式大概是score = 命中关键词数 * 1.0 + 时间衰减系数,时间衰减系数我用的简单线性衰减,超过 180 天的记忆权重减半。
第三步是冲突消解。召回结果里如果同时出现status为active和superseded的条目,只保留 active 的。如果两条 active 记忆内容矛盾(比如都涉及同一个技术选型但结论不同),按时间取最新的,并在注入上下文时明确标注"以下为最新决策,早期决策已废弃"。
注入上下文时,我会给记忆加一个明确的边界标记,比如用[MEMORY]和[/MEMORY]包起来,并在前面加一句说明:"以下是历史记忆,供参考,如与当前指令冲突以当前指令为准。" 这句话很重要,它防止模型把过时记忆当成硬性约束。
3.4 记忆的压缩与归档机制
记忆库用久了会膨胀,需要定期压缩。我的做法是每月做一次归档整理,具体三步。
第一步,把status为superseded且超过 90 天的条目移到归档文件,主库不再加载。归档文件保留着,需要考古时还能翻。
第二步,把同一主题下的多条零散记忆合并成一条。比如关于"日志规范"可能有五条分散记忆,合并成一条完整的规范说明。合并时保留所有原始时间戳作为附注。
第三步,检查有没有长期未被召回的条目。如果一条记忆半年内一次都没被命中过,要么是它不重要,要么是关键词打得不好。前者删掉,后者修关键词。
这套压缩机制让我的记忆库在用了几个月后依然保持在 300 条以内的活跃规模,检索速度和准确率都没退化。
4. 实操过程与核心环节实现
4.1 环境准备与目录结构搭建
先说环境。claude-mem本身不需要什么特殊依赖,核心就是文件读写。我用的是最朴素的方案:一个本地目录,里面放几个 JSON 和 Markdown 文件。如果你用 Claude 的桌面端或 API,都能通过文件读写能力对接。
目录结构我这样组织:
claude-mem/ ├── memory/ │ ├── preferences.json # 偏好类记忆 │ ├── decisions.json # 决策类记忆 │ ├── facts.json # 事实类记忆 │ └── archive/ # 归档目录 │ └── 2024-Q1.json ├── sessions/ │ └── 2024-03-15-summary.json # 会话摘要 ├── scripts/ │ ├── recall.py # 召回脚本 │ └── write.py # 写入脚本 └── config.json # 全局配置为什么按类型分文件而不是全放一个文件?因为加载策略不同。偏好类每次全量加载,单独一个文件读起来快;决策类和事实类按需检索,分开存方便做不同的索引。如果全塞一个文件,每次都要读全量再过滤,效率低。
config.json里放几个关键参数:
{ "recall_limit": 20, "time_decay_days": 180, "preference_full_load": true, "archive_after_days": 90, "max_keywords_per_memory": 5 }recall_limit是单次召回的最大条目数,我设 20。设太大上下文会被记忆占满,设太小又可能漏掉关键信息。20 是我实测下来比较平衡的值。
4.2 会话摘要的自动生成流程
会话摘要是 L2 记忆的来源,我把它做成了半自动流程。每次会话结束前,我会发一条固定指令给 Claude:
请为本次会话生成摘要,输出 JSON 格式,包含以下字段: - summary: 200 字以内的会话摘要 - conclusions: 本次产生的结论列表 - todos: 未完成事项列表 - keywords: 3-5 个关键词 - project: 所属项目标签Claude 返回 JSON 后,我把它存到sessions/目录,文件名用日期加序号。然后跑一个脚本,把摘要里的conclusions按规则转成记忆条目,追加到对应的记忆文件。
这里有个细节:摘要生成要用独立的会话,不要跟主会话混在一起。因为主会话上下文很长,让模型在长上下文里做摘要,质量反而不如开个干净会话、把关键内容贴进去让它总结。我试过两种方式,独立会话的摘要质量明显更高,关键词也更准。
4.3 召回脚本的核心逻辑实现
召回脚本是整个系统的心脏,我用 Python 写,核心逻辑大概 80 行。下面贴关键部分并解释。
import json import re from datetime import datetime, timedelta def load_memories(path): with open(path, 'r', encoding='utf-8') as f: return json.load(f) def extract_keywords(text, top_n=5): # 简化版关键词提取,实际可用 jieba 等分词库 words = re.findall(r'[\u4e00-\u9fa5]{2,}|[a-zA-Z]{3,}', text) freq = {} for w in words: freq[w] = freq.get(w, 0) + 1 return [w for w, _ in sorted(freq.items(), key=lambda x: -x[1])[:top_n]] def score_memory(memory, query_keywords, now): hits = len(set(memory['keywords']) & set(query_keywords)) if hits == 0: return 0 created = datetime.fromisoformat(memory['created_at'].replace('Z', '+00:00')) days_old = (now - created).days decay = max(0.5, 1.0 - days_old / 360) return hits * decay def recall(query_text, config): now = datetime.now() query_kw = extract_keywords(query_text) results = [] for fname in ['decisions.json', 'facts.json']: memories = load_memories(f'memory/{fname}') for m in memories: if m['status'] != 'active': continue s = score_memory(m, query_kw, now) if s > 0: results.append((s, m)) results.sort(key=lambda x: -x[0]) return [m for _, m in results[:config['recall_limit']]]这段代码里,score_memory是核心。命中关键词数决定基础分,时间衰减决定权重。衰减公式我用的是max(0.5, 1.0 - days_old / 360),意思是记忆在一年内线性衰减到 0.5 倍权重,之后不再继续衰减。为什么设下限 0.5?因为有些老记忆(比如项目的基础架构决策)虽然旧,但依然重要,不能让它衰减到零。
extract_keywords我用的是简化版正则,实际生产里建议用分词库,中文分词质量会好很多。但即便用这个简化版,实测召回效果也能接受,因为记忆条目的关键词是我人工确认过的,匹配精度本来就高。
4.4 上下文注入的格式与边界处理
召回出记忆后,怎么塞进上下文也有讲究。我用的格式是这样的:
[MEMORY] 以下是与当前任务相关的历史记忆,供参考: [偏好] - 代码注释使用中文 - 提交信息遵循 Conventional Commits [决策] - (2024-03-15) 使用 SQLite 作为主存储,原因:部署环境限制 - (2024-02-20) 前端框架选定 Vue 3,原因:团队熟悉度高 [事实] - 项目根目录为 /workspace/blog-engine - 环境变量配置文件为 .env.local [/MEMORY] 如以上记忆与当前指令冲突,以当前指令为准。几个设计点解释一下。按类型分组是为了让模型快速定位;每条记忆带时间戳是为了让模型判断新旧;最后那句"以当前指令为准"是防止模型被过时记忆绑架。我实测过,加不加这句话,模型对冲突指令的处理差异很明显,加了之后模型更倾向于遵循最新指令。
提示:记忆注入的位置也有讲究。我一般放在系统提示之后、用户当前问题之前。放在最前面容易被忽略,放在最后又可能干扰当前问题。中间位置是实测效果最好的。
5. 常见问题与排查技巧实录
5.1 记忆污染:模型引用了错误的历史结论
这是最常见也最头疼的问题。表现是模型在回答里引用了一条明显错误或过时的记忆,导致整个回答跑偏。
排查思路分三步。第一步,先确认这条记忆是不是真的存在。去记忆库里搜关键词,看有没有对应条目。第二步,如果存在,看它的status是不是active。很多时候是旧记忆没被正确标记为superseded,导致它还在被召回。第三步,如果 status 正常,看它的关键词是不是打得太泛,导致在不该命中的场景被召回了。
解决方法:给旧记忆补上superseded标记,并让新记忆的supersedes指向它。同时收紧关键词,把太泛的词(比如"配置""方案")删掉,换成更具体的词。
我踩过最典型的一次坑:早期存了一条"考虑用 Redis 做缓存",后来决定不用了,但忘了标记旧记忆。结果模型在讨论缓存方案时反复提 Redis,我还纳闷它怎么这么执着,查了半天才发现是记忆库的问题。
5.2 召回为空:明明存了记忆却检索不到
这个问题的原因通常是关键词不匹配。你存记忆时打的关键词,和当前会话提取出的关键词对不上,交集为空,自然召回不到。
排查方法:手动跑一次召回脚本,打印出当前会话提取的关键词,再打印出记忆库里的所有关键词,对比看差在哪。常见情况是:记忆里存的是"数据库",当前会话说的是"DB";记忆里存的是"部署",当前会话说的是"上线"。
解决方法是建一个同义词映射表,把常见的同义表达归一化。比如:
| 标准词 | 同义词 |
|---|---|
| 数据库 | DB, database, 存储 |
| 部署 | 上线, deploy, 发布 |
| 配置 | config, 设置, 参数 |
召回时先把查询关键词和记忆关键词都映射到标准词,再做匹配。这个表不用一开始就建全,遇到一次补一次,慢慢就完善了。
5.3 上下文超限:记忆太多把上下文撑爆了
当召回条目太多,或者单条记忆太长时,注入的上下文会挤占正常对话的空间,导致模型"记不住"当前问题。
排查方法:统计每次注入的记忆总字符数。我的经验阈值是不超过 2000 字。超过这个数,就要考虑精简。
解决方法有三个。一是降低recall_limit,从 20 降到 10。二是对长记忆做摘要,把超过 200 字的记忆压缩到 100 字以内。三是分级加载,偏好类全量加载,决策类和事实类只加载 top 5。我一般三个方法组合用,效果最好。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查动作 | 解决方法 |
|---|---|---|---|
| 模型引用错误结论 | 旧记忆未标记失效 | 检查 status 字段 | 补 superseded 标记 |
| 召回为空 | 关键词不匹配 | 对比查询与记忆关键词 | 建同义词映射表 |
| 上下文超限 | 召回条目过多 | 统计注入字符数 | 降 limit / 压缩记忆 |
| 记忆库膨胀 | 未定期归档 | 统计活跃条目数 | 每月归档 + 合并 |
| 摘要质量差 | 在主会话里做摘要 | 检查摘要生成方式 | 改用独立会话生成 |
| 偏好不生效 | 偏好未全量加载 | 检查加载策略 | 偏好类强制全量加载 |
5.5 几条独家避坑心得
第一,记忆库要版本控制。我用 Git 管理记忆文件,每次修改都提交。这样万一改错了,能回滚。而且提交历史本身就是一份记忆变更日志,排查问题时特别有用。
第二,不要存"过程",只存"结果"。我早期存了很多"讨论了 A 方案和 B 方案"这种过程性记忆,后来发现完全没用。真正有用的是"最终选了 A,因为 X"。过程可以丢,结论必须留。
第三,定期做记忆库的"体检"。我每月花 20 分钟,随机抽 10 条记忆,问自己:这条还有用吗?关键词准吗?内容还准确吗?这个习惯帮我清掉了不少僵尸记忆。
第四,给记忆加"置信度"字段。有些结论是确定的,有些是"暂时这么定,可能还会改"。我在content里用"确定:"和"暂定:"前缀区分。召回时,暂定类记忆会带上"此结论可能变更"的提示,避免模型把它当铁律。
6. 记忆系统的扩展方向与个人体会
6.1 从个人记忆到团队记忆的演进
个人用顺了之后,我试着把它扩展到小团队。核心变化是记忆库从本地文件变成共享存储,加了一层简单的权限和冲突处理。
团队场景下最大的挑战是记忆的写入冲突。两个人同时往记忆库写,可能产生矛盾条目。我的处理方式是引入一个简单的审核队列:所有新记忆先进入pending状态,由一个人定期审核合并,通过后才变成active。这个流程听起来重,但实际每天也就几条新记忆,审核花不了几分钟。
另一个变化是记忆的归属标记。团队记忆里要区分"全局共识"和"个人偏好"。全局共识所有人都加载,个人偏好只对本人加载。这个区分很重要,否则你的个人习惯会污染别人的上下文。
6.2 记忆与提示词工程的结合
用久了之后我发现,claude-mem其实可以跟提示词工程深度结合。具体做法是把高频使用的提示词模板也存进记忆库,作为"偏好类"记忆的一种。
比如我有一套固定的代码审查提示词模板,以前每次都要手动粘贴。现在把它存成一条记忆,类型标记为template,召回时自动加载。这样每次做代码审查,模板自动就位,省了不少事。
这个思路可以进一步扩展:把常用的工作流、检查清单、输出格式要求都做成模板记忆。本质上,claude-mem从"记住事实"进化成了"记住工作方式"。
6.3 我个人的使用体会
用了几个月下来,最大的感受是:记忆系统的价值不在于记住多少,而在于忘掉多少。一开始我贪多,什么都想存,结果记忆库成了垃圾场,检索质量直线下降。后来学会做减法,只存真正会复用的东西,系统反而越来越好用。
另一个体会是,人工确认这一步不能省。全自动写入看起来很美好,但记忆库的干净程度直接决定系统上限。花 30 秒确认一条记忆,比事后花半小时排查污染划算得多。
最后分享一个小技巧:我会在记忆库里单独维护一个meta.json,记录记忆库自身的统计信息,比如总条目数、各类型占比、最近一次归档时间、召回命中率。这个文件不参与召回,纯粹是给我自己看的仪表盘。每次打开看到命中率在 70% 以上,就知道系统运转正常;如果掉到 50% 以下,就该做一次体检了。
这套东西没有什么高深技术,核心就是"结构化存储 + 关键词检索 + 人工把关"三件事。但就是这三件事做扎实了,跨会话协作的体验会有质的提升。如果你也在被重复解释上下文的问题困扰,不妨从最简单的版本开始搭,先跑起来,再慢慢优化。