1. 项目缘起与核心定位第一次看到claude-mem这个名字我脑子里蹦出来的第一反应是终于有人把「记忆」这件事单独拎出来做成一个独立组件了。如果你最近半年一直在用 Claude 系列模型做开发或者内容工作大概率会遇到一个非常尴尬的场景——每次开新对话模型就像失忆一样之前聊过的项目背景、代码规范、命名习惯、甚至你反复强调过的偏好全部归零。你得重新贴一遍上下文重新解释一遍需求运气不好还会因为上下文窗口塞太满导致响应变慢、成本飙升。claude-mem要解决的就是这个痛点。它不是一个大而全的框架也不是某个云平台的功能而是一个围绕「给 Claude 增加持久化记忆能力」这个目标构建的轻量方案。核心思路很朴素把对话中值得留存的信息抽取出来结构化存储到本地在需要的时候再按相关性检索回来拼进新的上下文里。听起来简单但真正落地时会牵扯到抽取策略、存储格式、检索算法、上下文预算控制、隐私边界等一系列细节这也是为什么值得单独写一篇来拆。这篇文章适合三类人看。第一类是日常用 Claude 做长周期项目的开发者比如维护一个持续迭代的代码库希望模型记住项目约定第二类是做 AI 应用的产品或独立开发者想给自己的产品加上「记忆」这个卖点第三类是对上下文工程感兴趣、想搞清楚 RAG 之外还有哪些记忆管理思路的技术爱好者。不管你是哪一类下面的内容都会从设计思路一路讲到可复现的实操步骤尽量让你看完就能动手搭一套自己的版本。需要先说明一点claude-mem这类项目在社区里存在多种实现形态有的偏向命令行工具有的偏向库有的直接做成 MCP 服务。我下面讲的是一套经过验证的通用架构和落地方法具体到你手上的版本接口命名可能略有差异但核心逻辑是相通的。我会在关键处标注哪些是「常见实践补充」方便你对照自己的场景做取舍。2. 记忆系统的整体设计与思路拆解2.1 为什么不能只靠「把历史全塞进去」很多人第一反应是上下文窗口不是越来越大了吗直接把所有历史对话都带上不就行了我实测下来这条路在真实项目里走不通原因有三个。第一是成本。上下文越长每次请求消耗的 token 越多长周期项目动辄几十上百轮对话全量携带的费用会迅速失控。第二是信噪比。历史里大量内容是寒暄、试错、被否决的方案这些信息混在上下文里会干扰模型判断反而让它在关键决策上跑偏。第三是注意力稀释。模型对超长上下文的中间部分注意力会下降这是业界公认的现象你真正想让它记住的那条关键约束很可能被淹没在几千行无关内容里。所以记忆系统的本质不是「存更多」而是「存得更聪明」。claude-mem的设计哲学就建立在这个认知上抽取 存储 检索 注入四个环节环环相扣任何一个环节偷懒整体效果都会崩。2.2 四层架构的拆解我把一套完整的记忆系统拆成四层这也是我建议你在自己实现时遵循的分层方式。抽取层负责从对话流里识别「值得记住的东西」。这里的关键判断是什么算值得记住我的经验是分三类——事实类项目名、技术栈、文件路径、API 约定、偏好类代码风格、命名习惯、输出格式要求、决策类为什么选 A 不选 B、哪些方案被否决了。寒暄和纯试错过程不存。存储层负责把这些信息持久化。常见选择是本地 SQLite 或者 JSON 文件轻量、可移植、方便调试。也有人用向量数据库但对于个人项目规模SQLite 加全文检索往往就够了没必要上重型武器。检索层负责在新对话开始时根据当前问题把相关记忆捞出来。这里有两种主流策略基于关键词的全文检索和基于语义的向量相似度检索。前者实现简单、可解释性强后者召回更准但需要嵌入模型。claude-mem类项目通常两者结合先用关键词粗筛再用语义精排。注入层负责把检索到的记忆以合适的格式拼进上下文。这一步最容易被忽视但格式设计直接影响模型能不能用好这些记忆。我的做法是给记忆加上明确的分区和标签比如「项目约定」「历史决策」「用户偏好」让模型一眼知道每段信息的性质。2.3 方案选型背后的取舍为什么我倾向于本地优先而不是云端同步因为记忆里往往包含项目敏感信息、内部约定、甚至一些未公开的思路。本地存储让你对数据有完全掌控调试时也能直接打开文件看内容排查问题效率高得多。云端方案虽然多设备同步方便但引入的信任成本和网络依赖在个人开发场景里并不划算。为什么用 SQLite 而不是纯文本因为你需要检索。纯文本文件在记忆条目超过几百条后检索会变得非常痛苦。SQLite 自带全文检索能力配合简单的索引就能实现毫秒级查询而且单文件、零配置迁移时拷贝一个文件就行。为什么抽取环节要用模型而不是正则因为自然语言里的「值得记住」太灵活了正则只能覆盖固定模式。用一个小模型或者让主模型在对话结束时做一次总结抽取效果会好很多。当然这会增加一点成本但相比全量携带历史这点开销完全值得。3. 核心细节解析与实操要点3.1 记忆条目的数据结构设计数据结构设计得好不好直接决定了后面检索和注入顺不顺畅。我踩过的坑是一开始只存了一段纯文本结果检索时没法按类型过滤注入时也没法控制优先级。后来改成结构化条目问题迎刃而解。一条记忆我建议至少包含这些字段字段类型说明id整数主键自增type字符串记忆类型fact / preference / decisioncontent文本记忆正文一句话说清tags字符串逗号分隔的标签便于过滤source文本来源对话的标识或时间戳created_at时间创建时间weight浮点权重用于排序默认 1.0type字段是我最看重的。事实类记忆通常长期有效偏好类会随项目变化决策类需要保留上下文。检索时按类型给不同权重能让注入的内容更贴合当前需求。weight字段则给了你一个手动调节的抓手比如某条约定特别重要就把它调高检索时自然排前面。注意content一定要控制长度单条记忆建议不超过 200 字。太长的记忆检索时匹配度会下降注入时也占预算。如果一条信息确实复杂拆成多条关联记忆比塞成一条更好。3.2 抽取时机的选择抽取什么时候做是个容易被忽略但很关键的问题。常见有三种时机各有优劣。实时抽取每轮对话结束就判断有没有新记忆。优点是及时缺点是频繁调用模型成本和延迟都上去了而且单轮对话往往信息量不足容易抽出碎片。会话结束抽取一次完整对话结束后统一抽取。这是我最推荐的方式。此时上下文完整模型能看清整段对话的脉络抽出的记忆质量最高。实现上可以在用户主动结束会话时触发或者设置一个空闲超时。定时批量抽取积累一批对话后统一处理。适合对话频繁但单次很短的场景缺点是记忆有延迟新对话开始时可能还没抽出来。我自己的项目里用的是会话结束抽取为主、实时抽取为辅的混合策略。对于明确的事实类信息比如用户说「我们项目用 TypeScript 严格模式」实时抓一下对于需要综合判断的决策类信息留到会话结束再处理。3.3 检索策略的细节检索环节我试过好几种方案最后稳定下来的是一套「两阶段检索」。第一阶段用关键词做粗筛。把当前问题分词提取关键词在content和tags字段里做全文匹配召回一批候选。这一步快但召回可能不全因为用户提问的措辞和记忆里的措辞不一定一致。第二阶段用语义相似度精排。对候选记忆和当前问题分别算嵌入向量按余弦相似度排序取 top-k。如果候选集本身就不大这一步甚至可以省略直接用关键词得分排序。这里有个实操技巧给不同类型的记忆设置不同的召回数量。事实类记忆通常需要精确匹配召回 3 到 5 条就够偏好类记忆可以多召回一些因为偏好往往是软约束多给几条让模型自己判断。决策类记忆召回 2 到 3 条即可太多会干扰当前决策。提示检索时一定要加时间衰减。三个月前的项目约定可能已经过时了给旧记忆乘一个衰减系数能让系统更倾向于使用近期信息。衰减系数我一般设成按天 0.99具体数值可以根据项目节奏调整。3.4 注入格式的设计注入格式这块我见过太多人随便拼一段文本就塞进去结果模型根本用不好。好的注入格式应该做到三点分区清晰、标签明确、指令到位。我常用的模板长这样[记忆上下文] 以下是本项目的历史记忆供你参考不要直接复述 ## 项目约定 - (fact) 项目使用 TypeScript 严格模式 - (fact) 所有 API 返回统一包装为 { code, data, message } ## 用户偏好 - (preference) 代码注释使用中文 - (preference) 优先使用函数式写法 ## 历史决策 - (decision) 选择 SQLite 而非 Postgres因为部署简单注意最后那句「不要直接复述」这是防止模型把记忆内容当成用户输入来回应。分区标题让模型知道每类信息的性质前缀标签则方便它在需要时引用。这套格式我用了大半年稳定性很好。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把基础环境搭起来。我用的是 Python 3.10 以上SQLite 是标准库自带的不需要额外装。嵌入模型这块如果你不想依赖外部服务可以用本地的小型嵌入模型比如 sentence-transformers 系列装起来也就一条命令。pip install sentence-transformers pip install sqlite-utilssqlite-utils是个很好用的库能让你用 Python 代码优雅地操作 SQLite省去手写 SQL 的麻烦。如果你更喜欢原生 sqlite3也完全没问题只是代码会啰嗦一些。目录结构我建议这样组织claude-mem/ ├── memory.db # SQLite 数据库 ├── extractor.py # 抽取逻辑 ├── retriever.py # 检索逻辑 ├── injector.py # 注入逻辑 └── config.py # 配置项分文件的好处是每层职责清晰调试时能单独测试某一层不用每次都跑完整流程。4.2 数据库初始化建表语句我建议加上全文检索的虚拟表这样关键词检索会快很多。import sqlite3 def init_db(pathmemory.db): conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT , source TEXT DEFAULT , created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, weight REAL DEFAULT 1.0 ) ) conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(content, tags, contentmemories, content_rowidid) ) conn.commit() return conn全文检索表用fts5这是 SQLite 内置的支持中文需要额外配置分词器如果嫌麻烦可以先用LIKE查询顶着等数据量大了再优化。我实测在几千条记忆的规模下LIKE查询也就几十毫秒完全够用。4.3 抽取逻辑的实现抽取的核心是给模型一个清晰的指令让它输出结构化的记忆条目。我用的提示词大致是这样EXTRACT_PROMPT 你是一个记忆抽取器。请从下面的对话中提取值得长期记住的信息。 只提取以下三类 1. fact客观事实如项目名、技术栈、文件路径、API 约定 2. preference用户偏好如代码风格、输出格式、命名习惯 3. decision历史决策如为什么选某个方案、哪些方案被否决 输出 JSON 数组每条包含 type、content、tags 三个字段。 content 控制在 200 字以内tags 用逗号分隔。 如果没有值得记住的信息返回空数组。 对话内容 {dialogue} 拿到模型返回的 JSON 后解析入库。这里要加一层校验防止模型输出格式跑偏。我的做法是用json.loads包一层 try-except解析失败就记录日志并跳过不要让一条坏数据污染整个流程。import json def save_memories(conn, raw_json): try: items json.loads(raw_json) except json.JSONDecodeError: return 0 count 0 for item in items: if not all(k in item for k in (type, content)): continue conn.execute( INSERT INTO memories (type, content, tags) VALUES (?, ?, ?), (item[type], item[content], item.get(tags, )) ) count 1 conn.commit() return count4.4 检索与注入的串联检索时我先把当前问题分词用关键词在content和tags里做匹配拿到候选集再按weight和时间衰减排序。def retrieve(conn, query, top_k8): keywords query.split() conditions OR .join([content LIKE ? OR tags LIKE ?] * len(keywords)) params [] for kw in keywords: params.extend([f%{kw}%, f%{kw}%]) rows conn.execute( fSELECT id, type, content, tags, weight, created_at FROM memories WHERE {conditions}, params ).fetchall() # 按权重和时间衰减排序 scored [] for row in rows: days_old (time.time() - parse_time(row[5])) / 86400 score row[4] * (0.99 ** days_old) scored.append((score, row)) scored.sort(reverseTrue, keylambda x: x[0]) return [r for _, r in scored[:top_k]]注入时按类型分组套用前面说的模板。这里有个细节如果检索结果为空就不要注入任何记忆上下文避免给模型一个空的「记忆区」造成困惑。4.5 参数选择的计算过程几个关键参数我来说说怎么定。top_k 取多少取决于你的上下文预算。假设每条记忆平均 50 tokentop_k8 就是 400 token加上模板开销约 500 token。这个量级对大多数模型来说可以忽略不计。如果你发现注入后响应明显变慢就往下调。时间衰减系数我设 0.99 是按天算的意味着 70 天后权重衰减到约 0.5。这个节奏适合迭代周期以月为单位的项目。如果你的项目变化很快可以调到 0.95让旧记忆更快退场。权重默认值统一设 1.0然后对特别重要的记忆手动调到 1.5 或 2.0。不要一上来就给所有记忆设高权重那样排序就失去意义了。5. 常见问题与排查技巧实录5.1 记忆越存越多检索越来越慢这是最常见的问题。我一开始没做清理半年后数据库里堆了几千条记忆检索明显变慢而且召回质量下降因为很多过时记忆还在参与排序。解决办法是加一套记忆生命周期管理。我的做法是超过 90 天且权重低于 1.0 的记忆自动归档到一个单独的归档表不参与常规检索。如果某条记忆被反复检索到可以加一个 hit_count 字段统计说明它仍然有价值就保留。这套机制跑下来活跃记忆稳定在几百条检索一直很快。5.2 抽取出的记忆重复或矛盾模型抽取时经常会把同一件事记两遍或者新记忆和旧记忆冲突。比如先记了「用 Postgres」后来改成「用 SQLite」两条都在库里检索时一起注入模型就懵了。我的处理方式是入库前做去重和冲突检测。去重比较简单算一下新记忆和已有记忆的文本相似度超过阈值就跳过。冲突检测麻烦一些我的土办法是如果新记忆和旧记忆属于同一 type 且 tags 有重叠就把旧记忆的权重降下来让新的占主导。更严谨的做法是让模型判断是否冲突但那样成本高个人项目没必要。5.3 模型不按记忆内容执行有时候记忆明明注入了模型还是按自己的习惯来。排查下来通常是两个原因一是注入格式不清晰模型没意识到那是约束二是记忆内容和当前问题关联度低模型忽略了。针对第一个原因我在注入模板里加了明确的指令比如「以下约定具有高优先级请严格遵守」。针对第二个原因我提高了检索的精度确保注入的都是真正相关的记忆而不是凑数的。5.4 常见问题速查表问题现象可能原因排查方向解决建议检索结果为空关键词不匹配检查分词和 LIKE 条件引入语义检索兜底注入后响应变慢top_k 过大统计注入 token 数下调 top_k 或压缩记忆记忆内容过时缺少生命周期管理查看 created_at 分布加时间衰减和归档抽取格式错误模型输出不稳定检查原始返回加 JSON 校验和重试记忆互相矛盾缺少冲突检测按 tags 分组查看降权旧记忆或人工清理5.5 几个我踩过的坑第一个坑是把记忆和 RAG 混为一谈。RAG 检索的是外部知识库记忆检索的是对话历史两者在数据特征和检索策略上差别很大。我一开始用同一套逻辑处理效果很差后来分开才理顺。第二个坑是过度依赖自动抽取。有些关键约定模型就是抽不出来比如用户随口提的一句「这个字段名别改」模型可能觉得不重要。我的补救办法是提供一个手动添加记忆的接口重要的东西自己记一条比什么都靠谱。第三个坑是忽视隐私边界。记忆里可能混入敏感信息比如密钥、内部地址。我在抽取后加了一层过滤用正则匹配常见的敏感模式命中就丢弃并告警。这一步千万别省否则记忆库会变成安全隐患。6. 记忆系统的扩展方向把基础版本跑通之后我陆续加了一些扩展效果不错分享给你参考。记忆的层级化。把记忆分成「项目级」「会话级」「临时级」三层。项目级长期保留会话级在会话结束后合并到项目级临时级用完即弃。这样能更精细地控制记忆的生命周期。记忆的可视化。写了个简单的命令行工具能列出所有记忆、按类型过滤、手动编辑权重。调试时特别有用能直观看到系统到底记住了什么。多项目隔离。给每条记忆加一个 project_id 字段检索时按项目过滤。这样同一套系统可以服务多个项目互不干扰。记忆的主动遗忘。除了自动衰减还支持手动删除。有些记忆是错的或者过时的留着只会添乱该删就删。这套系统我用了大半年最大的体会是记忆管理的核心不在于技术多复杂而在于对「什么值得记」的判断。抽取策略、检索算法、注入格式这些都是手段真正决定效果的是你对项目上下文的理解。你得清楚哪些信息是长期有效的哪些是临时的哪些是约束哪些是参考。想清楚这些再简单的实现也能跑出好效果。最后分享一个小技巧定期导出记忆库人工过一遍。你会发现模型抽出来的东西里总有一些让你意外的条目——要么是它理解偏了要么是它注意到了你没意识到的细节。这个过程本身就是对项目的一次复盘挺有意思的。