Claude长期记忆外挂:claude-mem构建会话记忆系统实践
1. 这个项目到底是什么解决什么问题先说结论claude-mem 是一个针对 Claude 对话场景的记忆外挂核心目标是把 Claude 在交互过程中产生的会话数据、上下文信息、关键决策点给自动沉淀下来形成可持续复用的长期记忆。简单说它做的是一件很多人手动做过、但没做彻底的事——把对话里值得留的东西从临时窗口里捞出来存进持久化的结构里下次问同样的问题、或者换一个会话继续往下做的时候不需要从头再讲一遍背景。这玩意儿适合谁两类人。第一类是重度 Claude 用户日常拿它写代码、做研究、梳理文档一天几十轮对话起步经常发现我上周明明让 Claude 帮我设计过那个模块的接口今天打开新会话它完全不记得了。第二类是团队协作场景几个人共用一套 Claude 账号或者通过 API 做自动化流程聊过的结论、定过的方案散落在各个会话里谁也说不清当时到底怎么定的。claude-mem 就是在这两者之间加了一层会话外置记忆把临时对话变成可查询、可检索、可复用的知识资产。我最初看到这个项目名的第一反应是这不就是给 Claude 装了个脑机接口吗Claude 的原生上下文窗口再大也是有上限的会话一结束数据就基本失去了被主动检索的能力。 claude-mem 的思路则是绕开窗口限制把记忆搬到会话之外用一套结构化的方式管起来。这个定位听起来简单真正动手做的时候会发现坑不少下面我把整个项目的核心设计、实现路径、踩坑实录一次说透。注意本文不涉及任何第三方代理、网络接入类内容只聚焦于 Claude 会话数据的本地处理与记忆管理技术方案。所有实操均基于本机命令行环境与公开接口能力展开。2. 整体设计思路拆解为什么记忆不能直接塞进上下文2.1 上下文窗口的物理限制与逻辑限制Claude 的上下文窗口决定了单次会话能同时看到多少信息这个数字再大也有限而人的工作流程是连续的今天聊了一半的需求明天继续这个项目里的方案下个项目也许还要复用。如果每次开新会话都把历史内容重新粘进去一方面浪费 token另一方面 prompt 会变得越来越臃肿反而干扰模型的判断。 claude-mem 走的是外部化路线对话结束后把该记的内容抽取出来、分门别类存好需要的时候再按需召回。打个比方Claude 原生工作方式像是用一块白板做题写满了就擦掉下一题重新来。claude-mem 相当于在旁边放了个笔记本白板上写不下的、或者今天写完明天可能还要用的顺手誊到笔记本里。笔记本不会塞进白板但任何时刻需要都可以翻到对应页码抄回来。这个抄回来的动作对应到技术实现上就是记忆检索与上下文注入。2.2 为什么不用全部历史拼接方案有人会问那我直接把所有历史对话都拼进 prompt 不就行了短期行长期不行。第一是成本问题对话越积越多每轮请求都携带全量历史token 消耗指数级上升第二是精度问题大段历史里大量内容跟当前问题无关模型注意力被稀释关键信息反而容易被忽略。 claude-mem 的做法是先结构化再按需检索——把记忆切成可寻址的片段query 时只找回相关度高的那部分。这也是我在实际使用中最看重的一点。它不是一个什么都存的垃圾桶而是一个知道什么该存、什么该扔、什么时候该拿出来用的整理师。要做到这一点核心在两点记忆抽取策略哪些内容值得进入长期记忆记忆召回策略用户提问时怎样把最相关的记忆片段捞出来两件事都跟会话数据的结构解析和语义相关性计算有关下面逐条展开。2.3 设计目标拆解从能存到好用我梳理了一下 claude-mem 隐含的几个设计层次从底层到上层分别是层次要解决的问题实现难度关键产出数据采集怎么拿到完整的会话内容低会话日志、API 请求记录结构化存储用什么样的格式组织记忆中分类、标签、时间线语义索引怎么让机器理解记忆内容高embedding、倒排索引智能召回怎么把记忆变成 prompt 的一部分中高上下文注入模块交互优化怎么让用户无感使用中命令别名、自动钩子大多数人在自己的脚本方案里只做到了第一层和第二层也就是把聊天记录存成文本文件。但存下来和用起来之间差着十万八千里。claude-mem 的完整之处在于把上面五层都打通了尤其第三层和第四层是拉开差距的地方。3. 核心细节解析与实操要点3.1 会话数据的来源与获取方式要做记忆管理第一步自然是拿到原始数据。在 Claude 的使用场景里主要有两个来源交互式对话通过 Claude 官方客户端或终端 REPL 完成的对话。API 调用通过 Anthropic API 发送的请求与响应。claude-mem 围绕交互式场景设计时最常见的手段是监听本地会话日志目录。Claude Code 之类的工具会在本地写入会话记录每个会话对应一个 JSON 或文本文件里面包含用户消息、助手回复、时间戳、会话ID等元信息。从工程角度这一步相当于把白板上的字抄到草稿纸上——还没整理但不至于丢。实操要点在接入 claude-mem 之前建议先确认你用的客户端是否会持久化会话文件、存放在哪个目录、格式是什么样的。不同客户端差异很大。有些只存控制台输出有些连工具调用的中间过程都记录在内。越完整的原始数据对后续的记忆抽取越有利。3.2 记忆抽取什么该记什么该扔拿到原始会话文本之后下一个问题是怎么从一大段对话里筛选出值得长期保存的内容我个人的标准有三个决策类用户和 Claude 最终定了什么方案选了哪个技术栈为什么这么选。上下文类项目背景、术语定义、用户偏好、已知约束。问题解决类某个 bug 是怎么定位的用了什么命令修复的验证结果如何。与之相对寒暄、重复提问、中间态的错误尝试、与主题无关的闲聊都属于低价值内容。claude-mem 在实现上通常采用分层策略先按消息角色拆分再按话题段落聚类然后对每个片段做重要性打分。打分可以基于规则比如出现决定、选择、原因是这类词权重更高也可以基于模型二次判断——让 Claude 自己总结哪些值得记。实操心得这里有个不少新手容易忽视的细节——不要只存结论要把结论成立的条件一起存。比如项目使用 PostgreSQL 作为主数据库这条记忆如果不同时记下因为业务强一致要求、且团队已有 DBA 经验那三个月后看到这条记忆的人可能还是你自己就很难判断这个选择在今天是否仍然成立。 claude-mem 的记忆条目如果能形成结论理由时效性的三元结构价值会成倍提升。3.3 存储方案对比SQLite、JSON 文件、向量库存储方案优点缺点适用场景JSON Lines 文件简单直观、可读性好、无依赖查询能力弱、并发写入麻烦原型验证、单人轻量使用SQLite结构化查询强、事务可靠、单文件易备份语义搜索能力弱中型项目、本地优先向量数据库语义相似度检索强、支持海量记忆部署复杂、资源占用高大规模记忆、团队共享场景claude-mem 这类项目通常会采用组合方案SQLite 存结构化元信息时间、会话ID、标签、类型向量索引存语义向量用于相似度召回。两条腿走路的好处是精确检索走 SQL模糊语义走向量互不干扰。我实际测试中遇到的坑是SQLite 的全文搜索FTS5对中文支持不够友好分词效果直接决定了检索质量。如果你的记忆内容以中文为主一定要在存储层多留一个心眼可能需要引入独立的分词组件或者干脆让语义召回覆盖更多场景弱化对关键词精确匹配的依赖。3.4 向量化与语义召回的关键参数向量化是整个记忆系统里技术含量最高的一环。简单说就是把一段记忆文本映射成一个高维向量让语义相近的文本在向量空间里距离更近。用户提问时把问题同样转成向量然后在记忆库中找最近的邻居这些邻居对应的原文就是应该注入上下文的候选记忆。实操中的两个关键参数Embedding 模型的选择不同模型的向量维度、语义表达能力和速度差异很大。常见的方案有本地小模型和云端 API 两类本地模型隐私性好但效果相对弱API 模型效果强但有网络依赖和成本。Top-K 召回数量召回太少可能漏掉关键记忆召回太多则会让上下文变得冗长。经验值建议从 5 到 10 起步视具体任务复杂度调整。我在实际使用时倾向取中间值代码任务取 6~8纯文档类任务取 4~5。另一个容易忽略的点是时间衰减。三个月前的记忆和三天前的记忆对当前问题的参考价值完全不同。好的记忆系统应该给记忆条目加上时间权重近期记忆在召回时适当加权远期记忆除非相关性极高否则不应频繁闯入 prompt。很多人在自建方案里没有做这一步导致系统什么都在提体验反而变差。4. 实操过程与核心环节实现从零搭一套可用的记忆管道4.1 环境准备与基础依赖下面是一套我实测可用的最小实现方案。技术栈刻意选得比较轻Python 脚本 SQLite 本地 embedding 模型不依赖重型服务单机就能跑。# 项目目录结构 claude-mem-demo/ ├── ingest.py # 会话日志解析与记忆抽取 ├── store.py # SQLite 存储层 ├── embed.py # 向量化模块 ├── recall.py # 召回与 prompt 组装 ├── config.yaml # 参数配置 └── data/ ├── sessions/ # 原始会话日志 └── memory.db # 记忆数据库需要安装的 Python 包很少sqlite3 是标准库embedding 部分我用的是 sentence-transformers 框架加载本地小模型。整个环境十分钟内能搭完。pip install sentence-transformers第一次运行会下载模型权重建议提前确认网络连通性。如果是在离线环境可以预先将模型文件拷到本地缓存目录避免每次初始化都重复下载。4.2 会话日志解析与记忆抽取实现先写一个最基础的日志解析函数把原始会话数据转成统一的消息列表import json from pathlib import Path def parse_session(file_path: Path) - list[dict]: 从会话文件解析出消息列表。 raw json.loads(file_path.read_text(encodingutf-8)) messages [] for item in raw: role item.get(role, unknown) content item.get(content, ) if isinstance(content, list): # 兼容多段内容格式 content .join( seg.get(text, ) for seg in content if isinstance(seg, dict) and text in seg ) messages.append({ role: role, content: content.strip(), ts: item.get(timestamp, ), }) return messages这一步看似简单实际最容易出问题的是内容格式兼容——不同客户端写入的会话文件字段名可能不一样有的是text有的是content嵌套对象。建议在解析前先打印一条原始记录看看结构再写解析逻辑。接着做记忆抽取。最简单有效的方式是按轮次拆分 规则过滤不依赖大模型二次总结速度快且可控def extract_memories(conversation: list[dict]) - list[dict]: 从对话中抽取候选记忆片段。 memories [] keywords [决定, 选择, 原因, 注意, 结论, 改用, 弃用, 必须] for i, msg in enumerate(conversation): if msg[role] ! assistant: continue for kw in keywords: if kw in msg[content]: # 抽取出包含关键词的句子简化处理 sentences msg[content].split(。) for sent in sentences: if kw in sent and len(sent) 20: memories.append({ text: sent.strip(), source_msg_idx: i, created_at: msg[ts], }) break return memories这个规则版的抽取器虽然笨但胜在可解释、无幻觉、不消耗额外 API 额度。对大多数场景来说它已经能抓到七八成真正值得记的内容。如果后续想要更高的抽取质量再把每个候选片段发给 Claude 做一次是否值得长期记忆的二元判断也不迟。4.3 向量化与存储入库抽取出的记忆片段需要两样东西才能入库一个自增 ID、一个语义向量。存储层用 SQLite 建两张表一张存原文一张存向量当然也可以直接存在同一张表里看个人偏好。import sqlite3 from sentence_transformers import SentenceTransformer # 加载本地 embedding 模型 model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) def init_db(db_path: str data/memory.db): conn sqlite3.connect(db_path) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, text TEXT NOT NULL, created_at TEXT, tags TEXT ) ) return conn def add_memory(conn, text: str, created_at: str ): 将记忆原文与向量同时入库。 vector model.encode(text).tolist() cur conn.execute( INSERT INTO memories (text, created_at) VALUES (?, ?), (text, created_at), ) mid cur.lastrowid # 向量单独存 JSON 文本列 conn.execute( UPDATE memories SET tags ? WHERE id ?, (json.dumps(vector), mid), ) conn.commit() return mid我故意把向量以 JSON 字符串形式存进同一张表是为了演示方便。生产级方案应当单独建一张向量表或用专门的向量库但核心流程是一样的先编码再入库召回时编码查询语句计算余弦相似度。4.4 召回与 prompt 注入召回模块是整个管道里最体现设计功力的地方。好的召回不能只做相似度排序还要做去重和上下文裁剪。我实现了一个带时间权重的召回函数import numpy as np from datetime import datetime def recall(conn, query: str, top_k: int 6, time_decay: float 0.1): 按语义相似度召回记忆并叠加时间衰减权重。 q_vec np.array(model.encode(query)) rows conn.execute(SELECT id, text, created_at, tags FROM memories).fetchall() scored [] for mid, text, created_at, tags in rows: vec np.array(json.loads(tags)) sim np.dot(q_vec, vec) / (np.linalg.norm(q_vec) * np.linalg.norm(vec) 1e-8) # 时间衰减距离越近权重越高 if created_at: days (datetime.now() - datetime.fromisoformat(created_at)).days else: days 0 score sim * np.exp(-time_decay * days) scored.append((score, text, mid)) scored.sort(reverseTrue) return scored[:top_k]召回结果拼装成 prompt 时我习惯加上一层记忆引用标记让模型知道哪些内容是外部记忆、不是当前对话的一部分以下是来自历史记忆的相关片段仅供参考可能有噪音 [记忆1] 2026-01-12: 项目确定使用 PostgreSQL原因是事务一致性要求高 [记忆2] 2026-01-15: 后台任务改用 Celery Redis弃用 Django Q 请结合以上记忆片段回答用户当前问题。这样做的好处是模型能区分当前事实和历史信息不会把过期内容当成最新的唯一依据。我在实测中发现加了这层说明之后模型引用历史记忆的准确率明显提升也很少出现自己编一个历史的情况。4.5 自动钩子让记忆系统无感运转最后一步同样关键——不要把 claude-mem 用成手动保存笔记的工具那样没人坚持得了。理想状态是会话一结束记忆自动沉淀新会话一启动相关知识自动注入。实现上可以借助对应客户端的 hook 机制在会话结束命令触发时运行ingest.py在会话启动时运行recall.py。我自己的使用习惯是写一个 shell 别名每次对话结束前敲一下把所有新产生的会话文件一次性处理入库alias claude-mem-ingestpython ingest.py --dir ~/.claude/sessions alias claude-mem-recallpython recall.py --query $(cat /tmp/current_task.md)第二个别名依赖一个临时任务描述文件。我会在开始一个项目前先把背景写进这个文件每次新会话启动时自动带着召回的记忆开始。这套流程跑顺之后效果很接近Claude 真的有记忆了——它知道你上次说到哪、定了什么方案、踩过什么坑不用你再复述一遍。5. 常见问题与排查技巧实录5.1 召回的记忆不相关怎么办这是所有记忆系统最容易翻车的地方。我调试时发现问题往往不在向量模型而在抽取阶段——入库的记忆本身太碎或者太偏召回时自然匹配不到有价值的内容。解决办法是提高抽取门槛短于 20 个字的片段不要没有结论或者约束含义的不要。宁缺毋滥。另外一个技巧给记忆条目打标签召回时先按标签过滤再按向量排序。比如代码类记忆打code决策类记忆打decision用户问代码问题的时候只从code和decision里召回不需要让模型在全部记忆里大海捞针。5.2 向量模型选型与中英文混合问题我最初用了一款纯英文优化的 embedding 模型中文记忆的召回效果堪称灾难——问一句中文召回的全是不相干英文片段。后来换成了多语言模型才好转。如果你处理的内容中英文混杂务必在选型时确认模型对中文支持到底如何不能用号称支持中文的英文模型。离线环境下的做法是先从网上下载好模型权重放到~/.cache/huggingface下。我用的是 paraphrase-multilingual-MiniLM-L12-v2体积适中CPU 推理速度也能接受百条级别的记忆库召回延迟在几十毫秒以内体感完全无感。5.3 记忆污染与过期记忆处理记忆系统用得越久垃圾记忆越多。比如早期试用阶段存了一堆今天尝试了某方案但失败的片段后面召回来只会干扰判断。解决思路有两个一是定期清理低分记忆二是给记忆加使用频率字段长时间未被召回的条目自动降权或者归档。我在实际使用中还发现某些记忆本身没错但时效性极强。比如当前分支是 fix/login-bug这条三天后就是干扰信息了。对这种记忆最好的处理是短期记忆区和长期记忆区分离短期记忆自动过期长期记忆手动确认。 claude-mem 的架构里如果预留了这个分层长期使用的体验会好很多。5.4 多设备同步与并发写入冲突如果只有一台电脑SQLite 文件方案完全够用。但如果你跟我一样在笔记本和台式机之间切换就需要考虑记忆库同步问题。我现在的做法是用同步盘把data/memory.db同步到各设备同时把写入操作做成追加而非覆盖——每轮 ingest 都只插入新记录不修改旧记录。这样即使同步有延迟也不会出现写冲突导致的数据损坏。# 启用 WAL 模式提升并发读写体验 conn.execute(PRAGMA journal_modeWAL)这个一行配置强烈建议加上。SQLite 的 WAL 模式在并发场景下表现好很多读操作不会被写操作阻塞对于一边会话结束在写库、另一边新会话在查询的场景非常有用。5.5 关于 token 成本的核算很多人担心引入记忆系统会显著增加 token 消耗。我用一个数据说话假设每条记忆片段平均 100 token每次召回 6 条每轮请求额外携带的记忆开销是 600 token 左右。对比动辄上千 token 的历史拼接方案这个开销是值得的。而且真正查询时很多记忆片段跟问题无关增加的成本远低于预期。如果还想进一步压缩可以对召回片段做摘要再压缩——让模型把命中记忆合并成一段更短的概述再注入。这个操作会多一次模型调用但能显著减少 prompt 长度适合记忆库规模很大、召回内容很多的场景。6. 工具选型解析与扩展方向6.1 Bolt 与 claude-mem 的边界感知我注意到 claude-mem 同名项目生态里有个叫 Bolt 的组件经常被一起提及。这里要区分清楚Bolt 通常承担的是把对话记录同步到外部知识库的职责而 claude-mem 更聚焦在本地记忆的抽取-存储-召回闭环。如果只用 Bolt 做同步你得到的是一个历史存档库但缺少语义召回和 prompt 注入能力只有把两者串联起来才能形成真正的记忆系统。从这个角度看claude-mem 项目最值得借鉴的并不是某一段具体代码而是记忆闭环的产品思维采集不是目的检索才是存储不是目的复用才是。6.2 从本地单机到团队共享的演进路径如果你的使用场景从个人扩展到团队架构上要做三件事将 SQLite 换成 PostgreSQL支持多人并发写入。将本地 embedding 模型换成服务化的向量索引支持共享检索。新增权限与命名空间隔离避免 A 项目记忆污染 B 项目。这个演进路径并不复杂但每一步都会引入新的运维成本。我个人建议先单机跑通流程确认记忆系统真的能提升自己的效率再考虑团队化。不要一上来就部署一套分布式记忆系统最后发现根本没那么多值得记的东西。6.3 后续可扩展的三个方向一是记忆可视化给记忆库加一个 Web 界面按时间线、标签、会话来源浏览记忆内容方便人工校对和清理。二是记忆生命周期管理自动识别过期记忆、冲突记忆两次结论矛盾、低价值记忆并给出处理建议。三是跨模型记忆迁移把同一套记忆结构适配给其他大模型使用让记忆资产不绑定在某一家模型上。7. 写在最后的实操体会我自己把 claude-mem 的思路跑通之后最大的变化不是省了多少 token而是敢开新会话了。以前一个项目聊到一半因为上下文太长不得不开新会话时总要把关键背景重新粘贴一遍粘贴的过程还会遗漏细节。现在有了记忆系统新会话一启动该有的背景自动出现在 prompt 里我只需要补充上次说完之后发生了什么新情况对话的连续性一下就接上了。最后分享一个小技巧不要一开始就追求全自动。把 ingest 和 recall 先做成手动命令跑一周看看哪些记忆真的有用、哪些是多余的再逐步把自动化程度提上去。记忆系统的价值在于准而非多——存了一万条垃圾记忆不如存一百条关键时刻能想起的记忆。想让 Claude 长期陪你干活先把它的笔记本整理好。