1. 项目概述与核心定位第一次看到claude-mem这个名字我的直觉是这应该是一个给 Claude 系列模型做“记忆管理”的工具。事实也确实如此。简单来说claude-mem是一套面向 Claude 对话场景的持久化记忆层它解决的是一个非常具体、也非常痛的问题——模型本身没有跨会话记忆每次开新对话都像失忆一样从零开始。你肯定遇到过这种情况跟模型聊了半小时把项目背景、技术栈、代码规范、命名习惯都交代清楚了结果第二天开个新窗口又得从头讲一遍。上下文窗口再大也扛不住这种重复劳动而且一旦对话轮次多了早期信息被挤出窗口模型就开始“胡言乱语”忘记你之前定的规则。claude-mem要干的事就是把这些关键信息抽出来、存下来、在需要的时候自动塞回去让模型看起来像是“记得你”。这个项目适合谁三类人最值得关注。第一类是重度使用 Claude 做开发的人比如用 Claude Code 写项目、做重构、维护长期代码库的工程师记忆层能省掉大量重复交代背景的时间。第二类是做 AI 应用开发的工程师想给自己的产品加上长期记忆能力claude-mem的实现思路可以直接借鉴。第三类是对上下文工程感兴趣的技术爱好者想搞清楚“记忆”这件事在工程上到底怎么落地。需要先说明一点claude-mem不是一个官方大厂出品的成熟商业产品它更像是社区里针对真实痛点长出来的工具型项目。所以它的价值不在于功能有多全而在于它把“记忆”这个抽象概念拆成了可操作的工程模块——提取、存储、检索、注入每一步都有具体的设计取舍。我下面会围绕这四个环节把它的思路、实现要点、踩坑经验完整拆一遍。2. 记忆系统的整体设计与思路拆解2.1 为什么不能只靠“更大的上下文窗口”很多人第一反应是现在上下文窗口都到 20 万 token 了还需要单独做记忆吗我实测下来的结论是窗口大不等于记忆好。原因有三个。第一成本问题。每次对话都把全部历史塞进去token 消耗是线性增长的。聊到第 50 轮光历史就占了几万 token每次请求都在为重复内容付费。第二注意力稀释。窗口里塞的东西越多模型对关键信息的注意力越分散早期定的规则反而容易被忽略这就是所谓的“lost in the middle”现象。第三跨会话断裂。窗口再大也是单次会话内的关掉窗口就没了而真实项目是跨天、跨周甚至跨月的。所以claude-mem的核心思路不是“把历史全存下来”而是做有损压缩式的记忆管理——只保留对后续对话有价值的信息丢弃寒暄、试错过程、已经解决的临时问题。这个取舍非常关键它决定了整个系统的存储结构和检索策略。2.2 四层架构提取、存储、检索、注入我把claude-mem的工作流拆成四个阶段这也是我认为它设计上最清晰的地方。提取层负责从对话中识别“值得记住”的内容。不是每句话都值得存比如“好的”“明白了”这种就没有价值。真正要抓的是项目背景、技术决策、代码规范、用户偏好、未解决的问题、明确的约束条件。提取的时机通常在对话告一段落或者用户显式说“记住这个”的时候。存储层决定记忆以什么形式落地。常见方案有两种一是结构化存储比如 JSON、SQLite字段清晰、检索快二是向量存储把记忆转成 embedding靠语义相似度检索。claude-mem这类项目通常会两者结合——结构化字段做精确过滤向量做语义召回。检索层是记忆系统的灵魂。存了一堆东西怎么在需要的时候精准捞出来纯向量检索的问题是容易召回语义相近但实际无关的内容。所以实践中一般会加时间衰减、重要性评分、标签过滤等策略把“最近用到的”“标记为重要的”优先返回。注入层负责把检索到的记忆拼进当前 prompt。这里有个细节很容易被忽略注入的位置和格式会影响模型的使用效果。放在 system prompt 里通常比放在 user 消息里更稳定用清晰的分隔符比如 XML 标签包裹比纯文本拼接更不容易被模型忽略。2.3 方案选型背后的取舍为什么很多记忆项目选择“本地优先”而不是纯云端我的理解是三点隐私、延迟、可控性。对话里经常包含代码、内部文档、业务逻辑传到第三方服务总有顾虑。本地存储比如 SQLite 文件虽然牺牲了多设备同步的便利但换来了数据完全自主。延迟上本地读写是毫秒级云端检索动辄几百毫秒高频调用时体验差距明显。另一个取舍是自动提取 vs 手动标记。全自动提取省事但容易存进噪音全手动标记精准但用户懒得操作。claude-mem这类工具通常走中间路线自动提取 用户可干预默认自动跑但提供命令让用户手动增删改查。这个设计我觉得是对的纯自动的系统用久了记忆库会越来越脏。3. 核心细节解析与实操要点3.1 记忆的数据结构设计记忆存什么字段直接决定了后面能不能检索得准。我参考常见实践整理了一套比较通用的字段设计你可以直接抄字段名类型作用是否必填idstring唯一标识是contenttext记忆正文是typeenum类型事实/偏好/决策/待办是tagsarray标签用于过滤否importanceint重要性评分 1-10是created_attimestamp创建时间是last_used_attimestamp最近一次被检索的时间是use_countint被引用次数是embeddingvector语义向量否sourcestring来源会话标识否这里有几个字段的设计意图值得说清楚。importance不是拍脑袋定的通常由提取时的模型打分或者用户手动调整。last_used_at 和 use_count是给检索排序用的——一个记忆如果很久没被用到说明它可能过时了排序时应该降权。type字段让检索可以按类型过滤比如当前在写代码就优先召回“决策”和“偏好”类记忆而不是“待办”。注意embedding 字段不是必须的。如果你的记忆量在几百条以内纯关键词匹配 标签过滤就够用了上向量库反而增加复杂度。超过一千条再考虑加语义检索。3.2 提取环节的提示词设计提取质量决定了整个系统的上限。我试过几种提取策略最后发现分类型提取比“一把梭”效果好很多。具体做法是给模型一个结构化的提取指令让它按类别输出。一个可用的提取提示词骨架大概是这样你是一个记忆提取器。请从以下对话中提取值得长期记住的信息。 提取规则 1. 只提取对后续对话有价值的内容忽略寒暄和临时试错 2. 按以下类别分类输出 - fact: 客观事实项目背景、技术栈、环境信息 - preference: 用户偏好命名习惯、代码风格、沟通方式 - decision: 已做的技术决策及理由 - todo: 未完成的任务或待确认事项 3. 每条记忆给出 1-10 的重要性评分 4. 如果某条信息不确定是否长期有效评分不超过 5 对话内容 {conversation} 请以 JSON 数组输出每条包含 type、content、importance、tags 字段。这个提示词的关键在于强制分类和强制打分。分类让后续检索有维度可用打分让排序有依据。我踩过的坑是早期没要求打分结果所有记忆平权检索时只能按时间排老的重要信息被新噪音挤下去。3.3 检索排序的评分公式检索不是简单按相似度排就完事。我实际用下来一个综合评分公式比单一维度靠谱得多。参考实现大概是score w1 * semantic_similarity w2 * importance_normalized w3 * recency_score w4 * frequency_score其中各项的含义和取值建议semantic_similarity查询和记忆的向量余弦相似度范围 0-1权重建议 0.4importance_normalizedimportance 除以 10范围 0-1权重建议 0.3recency_score基于 last_used_at 的时间衰减比如exp(-days/30)权重建议 0.2frequency_scorelog(use_count1)/log(max_count1)权重建议 0.1这套权重的逻辑是语义相关性最重要但重要性和新鲜度能纠正纯语义检索的偏差。比如你问“项目用什么数据库”语义上可能召回一堆提到数据库的记忆但其中标记为“决策”且重要性高的那条才是真正答案importance 权重就起作用了。提示权重不是固定的根据你的使用场景调。如果是长期项目维护提高 importance 权重如果是快速问答提高 semantic 权重。3.4 注入格式与位置的选择记忆检索出来之后怎么塞给模型也有讲究。我对比过几种方式效果排序大概是结构化标签包裹 纯文本拼接 放在对话历史里。推荐用 XML 风格的标签因为 Claude 系列对 XML 结构比较敏感识别度高memory_context memory typedecision importance9 项目使用 PostgreSQL 作为主数据库理由是团队熟悉且需要事务支持。 /memory memory typepreference importance7 用户偏好用 4 空格缩进函数命名用 snake_case。 /memory /memory_context注入位置建议放在system prompt 的末尾紧挨着用户的实际问题。这样模型在生成回复时记忆内容还在注意力范围内。如果放在 system prompt 开头中间隔了一大段系统指令记忆容易被稀释。4. 实操过程与核心环节实现4.1 环境准备与依赖安装假设你要从零搭一个claude-mem风格的记忆系统我按最小可用版本给你走一遍。技术栈选 Python SQLite 一个向量库可选理由是轻量、无外部服务依赖、方便调试。先建项目结构和虚拟环境mkdir claude-mem cd claude-mem python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install anthropic sqlite-utils numpy如果你要加向量检索再装一个轻量向量库pip install chromadb选 SQLite 而不是 Postgres 的原因很简单单文件、零配置、方便备份。记忆数据量通常不大几千到几万条SQLite 完全扛得住。等真的到了百万级再迁移也不迟。4.2 数据库初始化建表语句我建议直接写成一个初始化脚本方便重复执行import sqlite3 def init_db(pathmemory.db): conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, content TEXT NOT NULL, type TEXT NOT NULL, tags TEXT, importance INTEGER DEFAULT 5, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_used_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, use_count INTEGER DEFAULT 0, embedding BLOB, source TEXT ) ) conn.execute(CREATE INDEX IF NOT EXISTS idx_type ON memories(type)) conn.execute(CREATE INDEX IF NOT EXISTS idx_importance ON memories(importance)) conn.commit() return conn两个索引是必须的type 索引用于按类型过滤importance 索引用于排序。别小看这两个索引记忆量上千之后没索引的查询会明显变慢。4.3 提取流程的代码实现提取环节调用模型把对话转成结构化记忆。核心函数大概长这样import json from anthropic import Anthropic client Anthropic() EXTRACT_PROMPT 你是一个记忆提取器...前面那段提示词 def extract_memories(conversation: str) - list[dict]: resp client.messages.create( modelclaude-sonnet-4-20250514, max_tokens2000, messages[{ role: user, content: EXTRACT_PROMPT.replace({conversation}, conversation) }] ) text resp.content[0].text # 容错处理模型可能返回带 markdown 代码块的 JSON text text.strip().removeprefix(json).removesuffix().strip() try: return json.loads(text) except json.JSONDecodeError: return []这里有个实战经验模型返回的 JSON 经常被包在 markdown 代码块里直接json.loads会报错。所以要先做清洗。另外建议加个 try-except解析失败就返回空列表不要让整个流程崩掉。4.4 存储与去重存之前一定要做去重否则同一件事聊三次就存三条记忆库很快就被污染。去重策略我推荐语义去重 精确去重双保险def save_memories(conn, memories, sourceNone): for m in memories: # 精确去重内容完全相同 cur conn.execute( SELECT id FROM memories WHERE content ?, (m[content],) ) if cur.fetchone(): continue # 语义去重相似度超过阈值则跳过需向量库支持 # ... 省略向量比对逻辑 conn.execute( INSERT INTO memories (id, content, type, tags, importance, source) VALUES (?, ?, ?, ?, ?, ?), (gen_id(), m[content], m[type], json.dumps(m.get(tags, [])), m.get(importance, 5), source) ) conn.commit()语义去重的阈值建议设在0.9 以上。太低会误杀真正不同的记忆太高又起不到去重作用。这个值我调过几次0.92 左右比较平衡。4.5 检索与注入的完整链路检索函数把查询转成向量结合评分公式排序返回 top-kdef retrieve(conn, query, top_k5): # 1. 粗筛按类型或时间过滤可选 rows conn.execute( SELECT * FROM memories ORDER BY importance DESC LIMIT 200 ).fetchall() # 2. 精排计算综合评分 scored [] for row in rows: sim compute_similarity(query, row) # 向量或关键词相似度 imp row[importance] / 10 recency compute_recency(row[last_used_at]) freq compute_frequency(row[use_count]) score 0.4*sim 0.3*imp 0.2*recency 0.1*freq scored.append((score, row)) scored.sort(reverseTrue, keylambda x: x[0]) results [r for _, r in scored[:top_k]] # 3. 更新使用统计 for r in results: conn.execute( UPDATE memories SET use_count use_count 1, last_used_at CURRENT_TIMESTAMP WHERE id ?, (r[id],) ) conn.commit() return results注意最后一步更新使用统计很重要它让记忆系统有了“自我进化”的能力——被频繁使用的记忆会越来越容易被召回形成正反馈。注入环节把结果拼成 XMLdef build_memory_context(memories): if not memories: return parts [memory_context] for m in memories: parts.append( f memory type{m[type]} importance{m[importance]} f{m[content]}/memory ) parts.append(/memory_context) return \n.join(parts)然后把这个字符串拼到 system prompt 末尾即可。5. 常见问题与排查技巧实录5.1 记忆库越来越脏怎么办这是最常见的问题。用了一段时间后记忆库里堆满了过时的、重复的、无价值的内容检索质量直线下降。我的处理办法是定期清理 自动衰减。定期清理可以写个脚本把满足以下条件的记忆标记为待删除importance 低于 3 且超过 60 天没被使用或者 use_count 为 0 且超过 90 天。自动衰减则是让 recency_score 随时间自然下降让老记忆在排序中自动沉底而不是直接删掉。实操心得我一般每月跑一次清理把待删除的记忆导出成文件人工过一遍确认没用的再真删。直接自动删有风险万一删掉的是关键决策就麻烦了。5.2 检索召回不准的排查思路召回不准通常有三个原因按排查顺序来现象可能原因排查方法解决方向该召回的没召回提取时就没存进去检查提取日志优化提取提示词召回了但排序靠后评分权重不合理打印各项得分调整权重召回了一堆无关的语义相似度阈值太低看相似度分布提高阈值或加过滤我遇到最多的是第一种——提取环节漏了。比如用户说“以后都用 TypeScript”提取器可能觉得这是句废话没存但这其实是重要的偏好。解决办法是在提取提示词里明确列出“偏好类信息必须提取”并给几个例子。5.3 注入后模型不遵守记忆怎么办有时候记忆明明注入了模型还是按自己的来。这通常是注入格式或位置的问题。排查清单记忆是否放在了 system prompt 末尾位置太靠前容易被忽略是否用了清晰的分隔标签纯文本拼接模型容易当成普通上下文记忆内容是否和当前问题直接相关不相关的记忆反而干扰是否注入了太多条超过 10 条时模型注意力会分散建议控制在 5 条以内我实测下来控制在 3-5 条、用 XML 标签、放在 system 末尾遵守率能到 90% 以上。5.4 性能与成本控制记忆系统本身也会消耗 token。提取要调模型检索如果走向量也要算 embedding。控制成本的关键是批处理 缓存。提取不要每轮对话都跑攒几轮或者等对话告一段落再跑一次。embedding 算一次就存下来不要每次检索都重算。检索时先做粗筛按类型、时间过滤只对候选集算相似度能把计算量降一个数量级。注意如果你的记忆量不大几百条其实可以完全跳过向量检索用 SQLite 的 LIKE 加标签过滤就够了。别为了技术而技术简单方案能解决问题就用简单的。6. 记忆系统的扩展方向6.1 从单用户到多用户隔离如果你要把这套东西做成产品多用户隔离是必须的。最简单的做法是在 memories 表加一个user_id字段所有查询都带上这个条件。再进一步可以给每个用户独立的数据库文件物理隔离更彻底但管理成本高。我倾向于单库加 user_id 字段配合索引性能和隔离性都够用。6.2 记忆的版本与冲突处理长期项目里同一个决策可能会变。比如“用 MySQL”后来改成“用 PostgreSQL”。这时候旧记忆不能直接删因为可能还需要追溯历史。我的做法是加一个superseded_by字段新记忆创建时把旧记忆标记为被取代检索时默认过滤掉被取代的但保留查询历史的能力。6.3 与工作流的深度集成记忆系统单独用价值有限真正发挥威力是嵌入到日常工作流里。比如在 Claude Code 里每次开新会话自动加载项目记忆在代码审查时自动召回团队的代码规范记忆在写文档时召回之前的决策记录。这些集成的核心都是同一件事在正确的时机把正确的记忆注入到正确的上下文里。我个人在实际操作中的体会是记忆系统的价值不在于技术多复杂而在于坚持用、持续清理、不断调优检索策略。我见过太多人搭了个记忆系统用两周就荒废了原因是记忆库脏了没人管检索越来越不准最后还不如不用。所以如果你打算上这套东西先想清楚谁来维护、多久清理一次这比技术选型重要得多。最后再分享一个小技巧给记忆加一个confidence字段记录这条记忆的置信度。用户明确说的记 1.0模型推断的记 0.7模糊的记 0.5。检索时把 confidence 乘进评分里能有效降低模型臆测内容的干扰。这个字段我加了之后召回准确率大概提升了 15% 左右成本几乎为零值得一试。
阅读完成 · 觉得有帮助?