我先是自己在本地折腾过一版,后来又把社区里几个叫相同名字的开源实现翻了个遍,发现大伙儿的思路几乎是收敛的:采集对话、后台提炼、本地存储、按需召回、二次注入。这个套路不复杂,但细节非常多。这篇文章不打算写成软文或者说明书,我只把这套东西的原理、踩过的坑、实际能跑通的做法全部摊开讲一遍,希望能给想给Claude补记忆的朋友省点时间。
1. 整体架构拆解:一个记忆层该有哪些部件
1.1 四个核心环节:采集、提炼、存储、注入
如果只给一句话总结,claude-mem做的事情就是“把无状态的对话变成有状态的个人知识库”。但真正落地的时候,你会发现它其实是一条完整流水线,拆开看至少包含四个环节。
第一个环节是采集。来源很明确:你和Claude的每一轮对话。采集不是简单地把对话原文丢进数据库,因为原文里充满了废话、重复、临时性内容。真正值得留下的是那些“可复用的信息”:你承诺过的技术方案、用户的偏好、某个项目的关键路径、一个反复出现的报错及其解法。原始对话是原料,不是成品。
第二个环节是提炼。这一步通常交给大模型自己做。每隔一段时间(比如对话累计超过一定长度,或者每5分钟异步跑一次),把新增的对话片段发给模型,让它按照预设结构抽取“记忆条目”。抽取的格式可以很简单,就是一个包含role、topic、fact、timestamp的JSON。你也可以让模型判断这条记忆的持久性等级——有些是临时状态(比如“今天在调登录接口”),有些是长期偏好(比如“我习惯用TypeScript”),分级之后方便后续检索时加权。
第三个环节是存储。存储层不一定需要很重的东西。我的经验是SQLite在第一版里完全够用,一张表存对话摘要,一张表存事实/偏好,再加一张索引表。等数据量到了几十万条,或者你确实需要语义检索的时候,再考虑上向量数据库。没必要一上来就把架构搞复杂,记忆系统的瓶颈通常不在存储,而在召回策略。
第四个环节是注入。这是最容易被忽略但恰恰最关键的一步。很多人的第一版失败在:记忆存了一堆,但用的时候要么不相关,要么太多太杂,反而把上下文窗口挤爆了,模型表现变得更差。注入讲究的是“少而精准”,宁可一次只给三条强相关的记忆,也不要一股脑塞二十条让模型自己挑。
这四个环节串起来,才是一个完整的记忆闭环。缺了任何一个,你拿到的都不是记忆系统,只是另一个聊天记录备份工具。
1.2 存储选型:SQLite是首选,向量库什么时候上
我见过不少人一上来就用向量数据库,结果卡在安装依赖、Embedding模型选型、费用控制这些事上,最后项目搁浅。实际上,对于个人使用或中小规模项目,SQLite配合关键词索引能覆盖大多数场景。
为什么SQLite够用?因为记忆召回这件事,大部分时候靠的是“关键词 + 时间衰减 + 对话主题”的组合策略。Claude的对话虽然长,但每个人使用它的范围通常很窄,你反复问的东西往往高度重复。通过主题字段和关键词做过滤,再按recency排序,召回准确率就已经不低了。
什么时候才需要向量库?当你的记忆条目超过几万条,且用户的问题经常是“我之前好像提过一个想法,跟某个方案有关,但我不记得原话”这种模糊召回时,就得上向量检索了。我个人的阈值判断是:如果召回结果里Top 5经常出现完全无关的内容,说明纯关键词方式已经到瓶颈,此时再引入Embedding。
选向量库也有讲究。优先考虑轻量的LanceDB、Chroma或者sqlite-vss,不要一上来就部署Milvus或者Weaviate这类重型服务。个人项目追求的是“五分钟能爬起来”,而不是“支持十亿向量”。CLI工具跑在本地,存储文件直接落盘,配合定时备份就够了。
1.3 注入策略:系统提示注入 vs 上下文拼接
记忆注入有两种主流做法。第一种是拼到系统提示词里,告诉Claude“以下是你之前和用户一起沉淀下来的关键事实,请在回答时自然使用这些信息,不要直接复述”。这种方式适合持久性高、几乎每次对话都需要的记忆,比如用户的编程语言偏好、项目名、团队约定等。
第二种是拼接在用户消息前面,作为背景上下文。这种方式适合“临时召回”的记忆,比如“上次我们讨论过三个方案,结论是……”这类信息。因为它只对当前这轮对话有效,不需要让模型长期记住。
两种注入方式的触发时机也不一样。系统提示注入应该在每次对话开始时固定执行,凡是标记为global的记忆都往里塞;上下文拼接则应该按需触发,检索到相关记忆才拼接。如果你只做一个通用版,我建议先把前者做扎实,把全局记忆维护好,再考虑按需召回。很多usage场景其实对“按问题检索”没那么敏感,反而是“让Claude始终记得你是谁”的体验提升最明显。
2. 核心细节解析与实操要点
2.1 对话分段与记忆抽取的参数设计
对整个系统影响最大的不是存储,而是“怎么切分对话去做提炼”。切太碎,信息上下文不足,抽出来的记忆质量差;切太长,一次请求的token消耗高,而且摘要会丢失细节。
我自己实测下来,一个比较稳的分段策略是按“语义轮次”切,而不是按固定长度。一轮对话以用户发了新的提问为起点,以模型完整回答为终点。把连续5到10个轮次打包成一个block,如果有人在调试代码,则把同一主题下的所有轮次合并成一个更大的block。这个合并逻辑可以用最简单的规则:相邻轮次之间如果共享同一个top-level主题关键词,就继续合并,直到主题切换。
抽取时的模型参数也需要单独调。一定不要用和生产对话相同的temperature,建议低一点,0.2以下,因为记忆抽取是信息压缩任务,需要稳定和忠实,不需要创造性。在提示词里要明确三条约束:第一,只提取事实和偏好,不提取临时情绪;第二,如果对话里有明确的关键词或代码片段,原样保留,不要转述;第三,每条记忆必须自带“何时、何背景下产生”的元信息,否则后续无法做时间衰减。
抽取频率也值得控制。实时抽取太重,每轮对话都请求一次容易击穿费用预算。我采用的方案是:每累计3000字左右触发一次异步抽取,且只在本地队列空闲时执行。如果你用的是Claude API,异步能有效规避速率限制。
2.2 记忆去重与冲突解决
跑一段时间你一定会遇到两个问题:重复记忆和冲突记忆。重复的原因是同一件事在多个block里被反复抽取,比如你每次问Claude“帮我看看这个报错”,它每次都抽出了一条“用户遇到ModuleNotFoundError”。冲突更麻烦,比如前两天你说“项目的数据库用Postgres”,今天又说“准备换成SQLite”,如果不处理,旧记忆和新记忆同时存在,Claude会被自己搞晕。
去重我用的是比较朴素的三步法。第一步,对每条记忆算出“主题 + 关键实体”的指纹,用SHA256哈希一下;第二步,如果新记忆的指纹在已存储的记忆里出现过,就比较语义相似度,相似度超过0.85就把它当作重复项合并到旧条目里,只更新时间戳;第三步,如果指纹不同但语义相似度高,则将新记忆标记为“替代候选”,需要再攒两条相似度证据才决定是否替换旧记忆。
冲突解决我推荐“时间戳优先 + 可信度加权”。默认信任最近的那条记忆,但把旧的标记为“被替代”,不会立即删除,方便回溯。如果你希望系统能主动提示冲突,可以在提炼prompt里加一句“如果发现与已有记忆矛盾的新事实,请单独以conflict类型输出”。这样你就可以在界面上看到到底哪里产生了矛盾,而不是让模型默默覆盖。
2.3 隐私、成本、上下文窗口的平衡
这是实操里最容易被忽略的一块。Claude记忆系统的本质是把你的对话内容常驻在本地文件里,这跟云端的聊天记录完全不同。如果你在公司电脑上跑这个工具,务必注意别把客户敏感信息写进本地数据库。我的习惯是加一个敏感词过滤层:正则匹配邮箱、手机号、密钥格式,发现疑似敏感信息就直接不落库,或者用占位符替换。
成本控制主要卡在提炼请求上。每次提炼都要消耗一次LLM调用,如果对话量大,一天几十次API调用也是一笔开销。我的做法是只在对话“有明显新信息”时才触发提炼:通过一个轻量的本地TF-IDF对比,计算新增对话里是否出现了之前从未出现过的关键词,如果全都是旧词,就跳过提炼。这个前置过滤能把提炼请求减少一半以上。
上下文窗口的问题则出现在注入阶段。Claude的上下文窗口虽然大,但不是无限大,而且你塞进去的内容会挤占模型真正输出的空间。我给自己定的红线是:注入的记忆总量不超过当前上下文窗口的8%。如果窗口比较紧张,就只注入全局记忆,按需召回的部分截取成单条不超过200字的摘要再给。
3. 实操:从零把一个“claude-mem”跑起来
3.1 初始化项目与依赖
我的实现用Python,因为生态最省事。先建目录,初始化虚拟环境,然后装依赖:核心是anthropic(Claude API客户端)、sqlite3(标准库)、json,再加一个轻量的yaml做配置。如果后续要上向量检索,再加lancedb和sentence-transformers。
初始化时先建立数据库表结构。我推荐至少三张表:
CREATE TABLE IF NOT EXISTS conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, started_at TEXT NOT NULL, summary TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, conv_id INTEGER NOT NULL, memory_type TEXT NOT NULL CHECK (memory_type IN ('fact', 'preference', 'decision', 'conflict')), content TEXT NOT NULL, topic TEXT NOT NULL, entities TEXT NOT NULL DEFAULT '[]', confidence FLOAT NOT NULL DEFAULT 0.8, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, expiry_at TEXT ); CREATE TABLE IF NOT EXISTS memory_recalls ( id INTEGER PRIMARY KEY AUTOINCREMENT, memory_id INTEGER NOT NULL, query_text TEXT NOT NULL, recalled_at TEXT NOT NULL );记忆提取时用Claude模型,我常用claude-sonnet-4或claude-3-5-sonnet这类档位,追求速度的话上haiku也够。提取提示词我写了一个稳定版,核心逻辑是“只抽取、不总结、不添加不存在的信息”。
3.2 编写记忆提取模块
提取模块的核心函数不长,我把它简化一下贴出来,注意参数已经过我实测:
import anthropic import sqlite3 import json client = anthropic.Anthropic() SYSTEM_PROMPT = """ 你是一个记忆提取器。你的任务是从对话中提取可复用的记忆条目。 只提取三类信息: 1. fact:用户或项目相关的客观事实 2. preference:用户的偏好或选择 3. decision:已经做下的决策及原因 要求: - 每条记忆必须简洁,不超过30个字 - 必须包含topic字段,用于后续检索分类 - 必须原样保留代码片段、文件名、报错文本,不转述 - 不要输出对话中没有的信息 - 如果与已知事实冲突,单独以conflict类型输出 返回JSON数组,不要返回其他内容。 """ def extract_memories(conversation_block: str) -> list[dict]: response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1500, temperature=0.1, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": f"下面是一段对话,请提取记忆:\n\n{conversation_block}"}] ) raw = response.content[0].text try: return json.loads(raw) except json.JSONDecodeError: # 模型偶尔会多输出话,简单兜底 start = raw.find("[") end = raw.rfind("]") + 1 return json.loads(raw[start:end])这里的温度设0.1是我踩过坑之后定的。之前设0.7,结果模型会“自由发挥”,抽出了对话里根本没提过的内容,污染记忆库。另外一定要让模型输出纯JSON而不是带Markdown代码块,否则解析逻辑会变得很啰嗦。
提取后的写入逻辑也很简单,检查指纹去重后直接插入memories表。关键的指纹计算可以用topic + content的哈希,但注意content因为模型转述可能不完全一致,所以我实际上用的是topic + entities组合算指纹。
3.3 编写记忆检索与注入模块
检索模块我实现了两种模式。全局模式在每次对话开始时注入,拉取所有记忆类型为preference和decision、且updated_at在最近90天内的条目,按updated_at降序取前10条。按需模式则用关键词检索,把用户当前消息里的关键词、实体抽出来,拼成SQL模糊匹配,再按时间衰减排序。
时间衰减公式我用的是业界常见的指数衰退:score = 0.6 * keyword_score + 0.4 * recency_score,其中recency_score = exp(-days_since_update / 30)。也就是说30天后保留约37%的权重,90天后几乎不再影响排序。实际效果是近期记忆优先,远期但高度相关的关键词匹配还能保底。
注入组装成字符串后,放进系统提示词里。我最终采用的格式是:
Below are remembered facts from previous conversations with this user. Use them naturally if relevant, but do not mention them explicitly. <memories> - [fact] 项目使用Python 3.11 + FastAPI,数据库用SQLite - [decision] 登录模块采用JWT方案,不做session存储 </memories>这里有一句话必须加进去:“don't mention them explicitly”,否则Claude每次都会把记忆的来源说出来,“根据你之前的对话,我了解到……”观感非常糟糕。
3.4 与Claude CLI或API的对接方式
如果你用的是Claude官网或App,claude-mem只能以“外部辅助”的形式存在:定期把聊天记录导出、离线提取记忆,然后在下次输入时手动粘贴相关记忆摘要。这种方式很麻烦,但能跑通。
如果你用的是API或者Claude Code这类可编程接入方式,体验就好得多。我的推荐是把claude-mem做成一个轻量中间层,在调用messages.create之前先执行检索,再把结果拼进system参数。核心伪代码:
def chat(user_input: str, history: list, session_id: str) -> str: recalls = retrieve_memories(user_input) system = base_system + build_memory_injection(recalls) messages = history + [{"role": "user", "content": user_input}] response = client.messages.create( model="claude-sonnet-4-20250514", system=system, messages=messages, max_tokens=4096 ) # 对话结束后异步触发提炼 enqueue_extract(messages + [response_message]) return response.content[0].text如果你用的是Claude Code,还有更轻量的接法:把全局记忆写入项目根目录的CLAUDE.md文件,这样每个会话启动时Claude Code会自动加载。按需召回则单独做成一个子命令,比如claude-mem recall "那个登录报错",把结果复制进对话里。这样做的好处是不需要动Claude Code本身的配置,坏处是自动化和灵活性差一些。
4. 常见问题与排查技巧实录
4.1 记忆总是召回不到
这是频率最高的问题。我排查时通常会按下面的顺序过一遍:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 能搜到但排序靠后 | 关键词匹配度低 | 降低recency权重,提高keyword权重 |
| 完全搜不到 | 记忆压根没入库 | 检查提炼进程是否被限流/跳过 |
| 搜到的是无关内容 | 分词过于粗暴 | 增加实体识别,按topic过滤 |
| 全局记忆不生效 | 注入被系统提示前缀截断 | 检查拼接顺序 |
其中“记忆没入库”是最隐蔽的坑。我遇到过提炼任务明明执行了,但SQLite写入失败,原因是数据库被另一个进程锁住。解决方式是启用WAL模式,并且把所有写操作放进一个连接池里串行执行。
4.2 上下文被历史记忆撑爆
这个问题的根源是注入策略太激进。我早期会把所有相关记忆都压进上下文,结果一个长对话跑到后期,记忆占了小半窗口,模型的表现反而下降。后来定了一个硬规则:无论全局还是按需,单次最多加入10条记忆,每条不超过80个token。如果检索结果有20条,只保留分数最高的10条。
另外,我加了“记忆过期”机制。有些记忆明显是短期的,比如“今天在调试OAuth的回调地址”,这种应该在48小时后自动降权,7天后清理。做法是给expiry_at字段设定时间,检索SQL里默认过滤掉已过期的条目。
4.3 重复记忆和事实冲突
重复的问题靠指纹去重能解决大部分,但冲突始终是硬骨头。我遇到过一个典型场景:周一跟Claude说“项目部署用Docker Compose”,周五又说“改成了Kubernetes”,结果系统里同时存在两条decision,互相矛盾。Claude在回答时随机选择一条,逻辑完全是混乱的。
我现在强制要求提取模块把decision类型的记忆附带reason,显示“什么促使了这个决定”。当新决策的语义相似度超过阈值时,不是直接覆盖,而是把旧decision标记成superseded,并且在注入时只给最新的。这个逻辑用SQL就能实现,不需要额外引入复杂的图数据库。
4.4 运行成本失控
提炼请求是大头。每个对话block一次API调用,如果每天有几十个block,花费确实不少。除了前面提到的TF-IDF前置过滤,我还有两个省钱技巧。一是把多个小block合并成一个缓冲区,攒到5000字左右再一次提炼;二是用最便宜的模型跑提炼,用haiku而不是sonnet,因为提炼只是抽取信息,不需要复杂的推理。
做完这些优化后,我实测一个每天约两万token对话量的个人项目,提炼API调用从每天的40多次降到了8次左右,成本下降超过70%,同时记忆质量基本没下降。
5. 关于claude-mem的几条经验总结
这套系统我前后迭代了三周,最大的体会是:记忆系统的价值不在“存了多少”,而在“召回的对不对”。存进去再多的记忆,如果召回的时机、颗粒度、注入方式不对,对大模型来说就是噪音。与其费尽心思提升存储容量,不如把精力放在提炼质量、去重策略和注入控制上。
另外有一件事让我出乎意料:给Claude加记忆之后,使用习惯会发生改变。以前我会故意把问题写得很完整,把所有背景交代一遍,因为反正它下一轮就忘了。现在有了记忆层,我提问变得随意很多,很多上下文默认它会记得。这种体验上的变化,比单纯“答得更准”重要得多。
最后给一个非常实在的建议:如果你是新手,第一版不要贪大求全。先做一个只存全局偏好、只做系统提示注入的最小版本,跑通之后感受一下“它能记住我”这件事对工作流的真实影响,再决定要不要上按需召回和向量检索。很多人在做记忆工具时死在第二周,就是因为想一次把记忆系统做完整,结果卡在某个细节上丧失了动力。先把最小闭环跑起来,你会发现后续每个优化都能产生即时反馈,这个项目才能持续滚下去。