
1. 项目缘起与核心定位第一次看到claude-mem这个名字我的直觉是这大概率是给 Claude 这类大模型做“记忆层”的东西。事实也确实如此。它要解决的是一个所有长期用大模型干活的人都绕不开的痛点——模型本身没有跨会话的持久记忆。你这次跟它聊完一个项目的架构决策关掉窗口下次再开它对你、对你的项目、对你们之前达成的共识一无所知一切从头再来。claude-mem就是冲着这个来的。它本质上是一套面向 Claude 的持久化记忆系统把对话中产生的关键信息抽取、存储、组织起来在后续会话里按需召回重新注入上下文。你可以把它理解成给模型外挂了一个“笔记本 检索器”模型负责思考和生成笔记本负责记住“你是谁、你在做什么、上次聊到哪”。这套东西适合谁我梳理了三类人。第一类是重度依赖 Claude 做长期项目的开发者比如连续几周迭代同一个代码库每次都要重新解释项目背景效率损耗极大。第二类是把 Claude 当知识助手用的研究者或写作者需要它记住自己的偏好、术语体系、已确认的事实。第三类是想自己动手搭一套记忆系统的技术爱好者claude-mem的架构思路本身就很有参考价值哪怕你不用 Claude换成别的模型这套“抽取—存储—召回—注入”的范式照样能迁移。需要先说明一点claude-mem这类项目在社区里有多种实现形态有的偏命令行工具有的偏 MCP 服务有的直接做成 SDK。下面我讲的架构和实操是基于这类记忆系统最常见的工程实践来展开的具体到你手上的版本接口名和配置项可能有差异但核心逻辑是通的。这也是我一贯的建议——先吃透设计思想再对着具体实现调参数不然换个版本你就懵了。2. 记忆系统的整体架构拆解2.1 为什么不能简单粗暴地“把历史全塞进去”很多人第一反应是记忆嘛把之前所有对话拼起来一起发给模型不就行了我一开始也这么想过实测下来根本行不通原因有三个而且一个比一个致命。第一是上下文窗口的硬约束。哪怕现在窗口开到几十万 token你连续用一个月历史对话轻松突破这个量级。全量拼接迟早撑爆而且撑爆之前成本已经先爆了——token 是按量计费的每次请求都带上几万 token 的历史账单会让你怀疑人生。第二是信噪比崩塌。历史里大量内容是“好的”“明白了”“那我们来试试”这种废话真正有价值的决策、结论、事实可能只占百分之几。全塞进去模型注意力被稀释反而更容易抓错重点回答质量下降。第三是冲突与过期。你上周说“数据库用 MySQL”这周改成了“PostgreSQL”全量历史里两条都在模型到底听谁的没有一套机制去标记“哪条是最新的、哪条已作废”记忆就会自相矛盾。所以claude-mem这类系统的核心价值不在于“存”而在于有选择地存、有策略地取。存的时候做压缩和结构化取的时候做相关性排序这才是关键。2.2 四层架构抽取、存储、召回、注入我把这类系统的骨架归纳成四层claude-mem基本也是这个路子。抽取层负责从原始对话里“提炼记忆”。它不是无脑存原文而是让模型自己判断这段对话里哪些是值得长期记住的通常抽取的是几类东西——用户的事实性信息身份、偏好、技术栈、项目状态当前进展、待办、已决策事项、以及明确的结论和约定。抽取的时机一般是会话结束或者每积累 N 轮触发一次。存储层负责把抽取出来的记忆落盘并组织。最简单的是一堆文本文件进阶一点用向量数据库做语义索引再复杂点会做知识图谱把实体和关系连起来。存储层要解决的核心问题是怎么让召回又快又准。纯文本靠关键词匹配向量库靠语义相似度图谱靠关系推理各有取舍。召回层是每次新会话开始时触发的。它拿当前对话的意图或者用户的第一句话去存储层里查找出最相关的若干条记忆。这里的关键是排序策略不能只按相似度还要考虑时间新近度、重要性权重、以及去重。召回太多会污染上下文召回太少又漏信息通常控制在几条到十几条之间。注入层负责把召回的记忆“喂”回给模型。它不是简单粘贴而是要包装成一段结构化的上下文比如“以下是关于该用户和项目的已知信息……”放在系统提示或对话开头。注入的措辞很讲究措辞不好模型会把它当成用户说的话产生混乱。2.3 方案选型文件、向量库还是图谱这三种存储方案我都在不同项目里试过给你一个直接的对比。方案实现难度召回精度适合场景主要坑点纯文本文件极低低靠关键词个人小项目、记忆量少语义匹配差同义不同词就找不到向量数据库中高语义相似记忆量大、需要模糊召回需要嵌入模型有额外成本知识图谱高高关系推理实体关系复杂的领域构建和维护成本高容易过度设计我的建议很明确从纯文本起步痛了再上向量库图谱除非你真有强关系推理需求否则别碰。很多人一上来就搭向量库、接嵌入模型结果发现记忆总共就几十条关键词搜一下就够用纯属给自己找麻烦。工程上最忌讳的就是过度设计claude-mem这类工具的价值是解决问题不是炫技。3. 核心细节与实操要点3.1 记忆抽取让模型自己判断“什么值得记”抽取层是整个系统的源头源头脏了后面全白搭。我的做法是给模型一段明确的抽取指令让它输出结构化的 JSON。指令大概长这样请从以下对话中提取值得长期记忆的信息按类别输出 JSON - facts: 关于用户或项目的客观事实技术栈、偏好、约束 - decisions: 已做出的决策及理由 - todos: 待办事项和当前进展 - entities: 涉及的关键实体人名、项目名、工具名 只提取有长期价值的信息忽略寒暄、临时讨论和已被推翻的内容。 如果某类没有内容返回空数组。这里有几个实操心得。第一一定要让它输出结构化格式自由文本后面没法检索。第二明确告诉它忽略什么不然模型会把“你好”“谢谢”也当成记忆存进去。第三让它标注置信度和时间后面召回排序用得上。注意抽取指令里的类别不要设太多。我见过有人设了十几个类别结果模型每类都只抽出一两条还经常分错类。四到六个类别是比较舒服的区间覆盖事实、决策、待办、实体就够了。3.2 存储格式给每条记忆加上“元数据”存的时候千万别只存一句话。一条合格的记忆记录至少包含这几个字段{ id: mem_20240115_001, content: 项目数据库从 MySQL 迁移到 PostgreSQL原因是需要更好的 JSON 支持和并发性能, type: decision, timestamp: 2024-01-15T10:30:00Z, importance: 0.8, source_session: sess_abc123, tags: [database, architecture], status: active }importance是重要性权重0 到 1抽取时让模型打分。status用来标记记忆是否还有效被推翻的决策标成deprecated召回时直接过滤掉这就解决了前面说的“冲突与过期”问题。tags是给关键词检索用的兜底向量检索失灵时还能靠标签捞回来。我特别想强调status这个字段。很多人搭记忆系统只想着“怎么存进去”不想“怎么让它失效”。结果用了一个月记忆库里全是过期信息模型召回一堆作废的决策回答越来越离谱。记忆系统的一半功夫在“遗忘”上这个认知我是踩了坑才建立起来的。3.3 召回策略相似度不是唯一标准召回的时候如果只用向量相似度排序你会遇到一个典型问题越新的记忆往往越相关但相似度不一定最高。比如你刚改完数据库新记忆是“改用 PostgreSQL”旧记忆是“用 MySQL”两句话语义高度相似纯按相似度排旧的可能还排在前面。我的排序公式是这样的你可以参考final_score 0.5 * similarity 0.3 * recency 0.2 * importancesimilarity是语义相似度recency是时间新近度越新越高可以用指数衰减importance是存储时打的权重。三个权重可以根据你的场景调比如做长期知识管理importance权重可以调高做实时对话辅助recency权重调高。召回数量上我的经验是控制在 5 到 10 条。少于 5 条容易漏关键信息多于 10 条上下文开始变脏模型会抓不住重点。如果召回结果里有明显重复或高度相似的做个去重只留分数最高的那条。3.4 注入措辞别让模型把记忆当成用户的话注入层最容易被忽视但坑最多。我踩过的一个典型坑是把记忆直接拼在用户消息前面结果模型以为这些是用户说的回复时对着记忆内容回应完全跑偏。正确的做法是把记忆放在系统提示里并用明确的分隔和说明。比如[系统记忆] 以下是关于当前用户和项目的已知信息供你参考不要在回复中直接复述 - 用户是后端工程师主力语言 Go 和 Python - 项目 X 使用 PostgreSQL已从 MySQL 迁移 - 上次会话确认API 采用 RESTful 风格暂不考虑 GraphQL [记忆结束][系统记忆]这种标记很重要它给模型一个清晰的边界。另外“不要在回复中直接复述”这句也很有用不然模型会机械地把记忆念一遍体验很差。4. 完整实操流程与关键环节4.1 环境准备与依赖安装假设我们用 Python 来实现一套最小可用的claude-mem。先明确依赖调用 Claude 需要官方 SDK存储用 SQLite 起步够用且零配置向量检索如果后面要上再引入嵌入模型。pip install anthropic # SQLite 是 Python 内置的不用装 # 如果后续要向量检索再装 # pip install sentence-transformers numpy目录结构我建议这样组织清晰且好扩展claude-mem/ ├── memory/ │ ├── store.py # 存储层读写记忆 │ ├── extract.py # 抽取层从对话提炼记忆 │ ├── recall.py # 召回层检索相关记忆 │ └── inject.py # 注入层组装上下文 ├── data/ │ └── memory.db # SQLite 数据库 └── main.py # 主流程为什么用 SQLite 而不是直接上向量库因为起步阶段记忆量小SQLite 的全文检索FTS5配合标签过滤已经能覆盖大部分需求。等你记忆上千条、关键词搜不准了再迁移到向量库也不迟。迁移成本不高但一开始就上向量库的复杂度成本很高。4.2 数据库表结构设计存储层的表结构直接决定了后面召回能玩出什么花样。我的设计是这样的CREATE TABLE memories ( id TEXT PRIMARY KEY, content TEXT NOT NULL, type TEXT NOT NULL, timestamp TEXT NOT NULL, importance REAL DEFAULT 0.5, source_session TEXT, tags TEXT, status TEXT DEFAULT active ); CREATE VIRTUAL TABLE memories_fts USING fts5( content, tags, contentmemories, content_rowidrowid );memories是主表memories_fts是全文检索的虚拟表用 FTS5 做关键词匹配。这样即使不上向量库也能靠关键词搜到记忆。status字段默认active作废时改成deprecated召回时加个WHERE status active就过滤掉了。提示tags字段存的是逗号分隔的字符串简单够用。如果你后面要做复杂的标签查询可以拆成单独的表但起步阶段没必要。4.3 抽取环节的完整实现抽取是调模型的地方也是最花 token 的地方。我的实现思路是把一段对话通常是最近 N 轮喂给模型让它输出结构化 JSON。import anthropic import json client anthropic.Anthropic() EXTRACT_PROMPT 请从以下对话中提取值得长期记忆的信息按类别输出 JSON。 类别包括 - facts: 关于用户或项目的客观事实 - decisions: 已做出的决策及理由 - todos: 待办事项和当前进展 - entities: 涉及的关键实体 每条记忆包含 content内容、importance0-1 重要性、tags标签数组。 只提取有长期价值的信息忽略寒暄和临时讨论。 输出格式{memories: [{type: ..., content: ..., importance: 0.8, tags: [...]}]} def extract_memories(conversation: str) - list: response client.messages.create( modelclaude-sonnet-4-20250514, max_tokens2000, messages[{ role: user, content: f{EXTRACT_PROMPT}\n\n对话内容\n{conversation} }] ) text response.content[0].text # 容错处理模型有时会包一层 markdown 代码块 text text.strip().removeprefix(json).removesuffix().strip() data json.loads(text) return data.get(memories, [])这里有个容错细节必须提模型输出 JSON 时经常手滑包一层json代码块直接json.loads会报错。我加了removeprefix和removesuffix来兜底。更稳的做法是用正则提取第一个{到最后一个}之间的内容但简单场景下前缀后缀处理就够了。4.4 召回与注入的串联召回环节我先把当前对话的意图转成一个查询然后走“关键词 标签 时间”的综合排序。def recall_memories(query: str, top_k: int 8) - list: # 1. 关键词检索FTS5 keyword_results search_by_keyword(query) # 2. 标签过滤如果 query 里能提取出标签 # 3. 综合排序 scored [] for mem in keyword_results: similarity compute_similarity(query, mem[content]) recency compute_recency(mem[timestamp]) importance mem[importance] final 0.5 * similarity 0.3 * recency 0.2 * importance scored.append((final, mem)) scored.sort(reverseTrue, keylambda x: x[0]) return [mem for _, mem in scored[:top_k]]compute_similarity在纯文本阶段可以用简单的词重叠率上了向量库就换成余弦相似度。compute_recency用指数衰减比如exp(-days / 30)30 天前的记忆权重衰减到约 0.37这个衰减速度我觉得比较合适你可以根据项目节奏调。注入环节就是把召回的记忆拼成前面说的[系统记忆]块放在系统提示里。整个主流程串起来就是会话开始 → 召回 → 注入 → 正常对话 → 会话结束 → 抽取 → 存储。一个闭环。5. 常见问题与排查技巧实录5.1 记忆越存越多召回越来越慢这是最常见的抱怨。我一开始也遇到记忆存到几百条后每次召回要遍历全部慢得明显。排查下来有两个原因一是没建索引二是没做归档。解决思路给timestamp和status建索引召回时先按status active过滤再按时间范围缩小。另外超过一定时间比如半年且重要性低的记忆可以归档到单独的冷表不参与日常召回。记忆系统不是只进不出的仓库定期归档和清理是必须的运维动作。5.2 模型把记忆内容当成用户指令前面提过这个坑这里给完整的排查路径。症状是模型回复时对着记忆内容说话比如你记忆里存了“用户喜欢简洁回复”模型回复开头就是“好的我会保持简洁”——它把记忆当成了刚收到的指令。根因是注入位置和措辞不对。修复方法确保记忆放在系统提示system prompt里而不是用户消息里用[系统记忆]明确标记加一句“这些是背景信息不要直接回应”。改完这三处基本就解决了。5.3 抽取出来的记忆质量参差不齐有时候模型抽出来的记忆是“用户问了关于 Python 的问题”这种废话没有长期价值。根因是抽取指令不够具体模型不知道什么算“有价值”。修复方法在指令里给正反例。比如明确说“不要提取‘用户提问了 X’这类描述要提取 X 本身的结论”。另外让模型给每条记忆打importance分低于 0.3 的直接丢弃这个阈值过滤能挡掉大部分垃圾记忆。5.4 常见问题速查表问题现象可能原因排查方向解决动作召回结果不相关相似度算法太弱检查是否只用了关键词引入语义相似度或调权重记忆自相矛盾旧记忆未失效检查 status 字段抽取时标记冲突作废旧条抽取 JSON 解析失败模型输出格式漂移看原始输出加正则提取和容错上下文被记忆撑爆召回数量过多统计注入 token 数降到 5-10 条做去重模型无视记忆注入位置错误检查在 system 还是 user移到 system prompt5.5 几条踩坑换来的经验第一别追求“记住一切”。记忆系统的价值在于精准不在于全。存得越多召回越难噪声越大。宁可少存精存。第二给记忆加“过期时间”。不是所有记忆都永久有效待办类记忆完成后就该失效决策类记忆被推翻后要作废。没有失效机制的记忆库用一个月就废了。第三抽取和召回要用不同的模型或参数。抽取需要模型有判断力召回需要的是快速匹配。如果都用同一个大模型成本高且没必要。召回阶段能用轻量方法就用轻量方法。第四一定要有日志。每次召回命中了哪些记忆、注入了多少 token、模型回复质量如何都记下来。出问题时这些日志是唯一的排查依据。我吃过没日志的亏一个召回 bug 查了一下午。6. 后续可扩展的方向这套最小实现跑通之后有几个方向可以按需扩展。向量检索是最自然的下一步接一个嵌入模型把compute_similarity换成余弦相似度召回精度会有明显提升代价是多一个模型依赖和一点计算成本。记忆图谱适合实体关系复杂的场景把entities抽出来连成图支持“和项目 X 相关的所有决策”这类关系查询但构建成本高想清楚再上。还有一个我觉得很有价值的方向是记忆的自动衰减与强化。被频繁召回的记忆自动提升重要性长期没被召回的记忆逐渐降权甚至归档。这模拟了人类记忆的“用进废退”能让记忆库自己保持精简。实现上就是在每次召回后更新importance字段逻辑不复杂但效果很实在。我自己用下来最大的体会是记忆系统的难点从来不在技术而在“判断什么值得记”。这个判断标准会随着你的使用不断调整抽取指令要反复打磨。别指望一次调好把它当成一个需要持续喂养和修剪的东西它才会越用越顺手。