claude-mem 实战:为 Claude 构建跨会话长期记忆系统

发布时间:2026/10/7 3:58:48
claude-mem 实战:为 Claude 构建跨会话长期记忆系统 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, encodingutf-8) as f: return json.load(f) def extract_keywords(text, top_n5): # 简化版关键词提取实际可用 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(), keylambda 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(fmemory/{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(keylambda 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% 以下就该做一次体检了。这套东西没有什么高深技术核心就是结构化存储 关键词检索 人工把关三件事。但就是这三件事做扎实了跨会话协作的体验会有质的提升。如果你也在被重复解释上下文的问题困扰不妨从最简单的版本开始搭先跑起来再慢慢优化。