每次新开一个会话,AI 就从"记得所有事的同事"退化成了"考场里刚拿到卷子的学霸"。昨天刚确认过的项目目录结构、已经调通的参数组合、反复讨论后定下的命名规则,今天必须从头解释一遍。这种"失忆循环"用一阵子真的会把手感磨没。所以我把给 AI 助手加持久记忆这件事认真做了一次,工具代号就叫 claude-mem。
它解决的核心问题很直白:在一个长期使用 AI 的工作流里,把那些不该被反复交代的背景信息沉淀下来,在需要的时候自动塞回上下文。这不是什么重型的"通用人工智能记忆"方案,而是一套轻量的、自己可以掌控的记忆层。适合两类人看:一类是重度使用 AI 辅助开发、写作、研究的同学,另一类是想给自己的小工具加记忆能力、又不想一上来就上重型向量库的开发者。
下文我会把设计思路、存储选型、召回策略、代码实现、踩坑经历一次性讲清楚。全程不依赖任何云服务,核心存储就是一台普通电脑上的本地数据库。
1. claude-mem 要解决的核心问题:AI 会话的"失忆症"
1.1 为什么我们总觉得 AI 忘了事情
现在主流的大语言模型本身没有跨会话记忆。每次对话,模型看到的上下文往往只包含当前会话窗口里能塞得进去的内容。窗口再大,也有清空和压缩的时候。表面上你觉得"它记得我们上次聊了什么",其实那只是你把上一次的关键结论又一次贴进了提示词。
我在实际使用中最明显的感受是:我经常花十几分钟做"背景同步"——跟 AI 说明我维护的项目结构是三层还是两层、数据库里有哪些关键表、我习惯用什么风格命名分支。这类信息一旦换会话就归零。如果一天开五六个会话,那么一大半的 token 预算都浪费在重复介绍上,真正该解决的问题反而没空间展开了。
claude-mem 的做法很朴素:把这类"跨会话稳定信息"单独存起来,存进本地数据库。在下一次会话开始前,或者对话中途的某个合适节点,把相关条目查出来拼进上下文。让 AI 看起来"记得"你之前交代过的东西,但本质是我们的工具在做记忆管理。
1.2 记忆系统的设计边界:该记住什么,不该记住什么
给 AI 加记忆,最怕的不是记得少,而是记得太杂。我见过一些方案把所有历史对话全部灌给模型,结果 AI 输出里到处都是"上次我们说过""根据之前的讨论"这类废话,而且彼此冲突的观点打架,可用度反而下降。
所以我一开始就给 claude-mem 划了三条边界:
- 只存事实,不存情绪。比如"用户喜欢用 pytest 而不是 unittest"可以存,"用户上次很生气"坚决不存。
- 只存低变化率信息,不存高频噪音。项目技术栈、代码规范、部署流程这类几个月不变的信息最值得存;"今天下午三点开会"这种就别进记忆库了。
- 只存可被复述的信息,不存大段原文。原始代码、长文摘录直接留在历史文件里就行,记忆库应该存摘要、结论、链接、指向性描述。
这个边界想清楚之后,工具就变得很克制。它不会尝试记住所有东西,只负责把真正有价值的那 10% 提炼出来。
2. 记忆引擎的设计关键:存储、召回、注入、遗忘
2.1 存储选型:为什么本地 SQLite 是合理起点
一开始我也差点上了向量数据库。后来冷静算了一笔账:个人工作流的记忆量级一天撑死几百条,检索场景是低并发的"人在回路里提问",根本不需要分布式、不需要毫秒级高并发。这种情况下 SQLite 是最扎实的起点。
SQLite 的好处业内人士都清楚,但落到记忆场景里我有几个具体体会:
- 单文件部署:整个记忆库就是一个
.db文件,备份、迁移、删除都非常简单。我甚至直接把它丢进同步盘里,实现两台电脑共享同一份记忆。 - 事务可靠:写入记忆这种操作虽然简单,但发生频率不低。SQLite 的 ACID 保证让多线程下写入也不会出现稀奇古怪的损坏。
- 检索能力够用:字符串匹配、时间范围过滤、基础倒排索引,这三样已经覆盖了 80% 的记忆召回场景。
当然,如果你真的积累到了几十万条结构化记忆,或者想对记忆内容做深层的语义关联,那再引入向量库不迟。但作为个人工具,我认为顺序应该是"先用 SQLite 跑通,再用数据说话"。
2.2 召回策略:关键词匹配为主,嵌入计算为辅
claude-mem 的核心召回逻辑我设计成了双通道:关键词通道+于语义相关的主题通道。
关键词通道很好理解:当用户问"上次说的部署脚本放哪了",我会先把"部署脚本"拆成几个 token,然后用 SQL 的LIKE或者基础分词去匹配title、tags、content字段。这个通道速度快,逻辑透明,出了问题也好排查。
语义通道是可选项。如果机器上已经装了本地 embedding 模型,我会把记忆条目的摘要向量化,查询时同样向量化,算个余弦相似度做召回排序。但这里有个关键设计:向量结果只做"初筛",不会直接作为最终输出。最终交给 AI 的记忆块,一定还是人类可读的文本形式。
为什么这样做?因为 AI 提示词需要的是"能直接被模型读懂的上下文",不是向量碎片。向量召回只是用来找出"哪几条记忆最可能和当前问题相关",找出来之后,仍然是整条文本进入上下文。这种"二分"设计让系统既拿到了语义相似度的表达能力,又保住了提示词的可解释性。
2.3 注入时机:三种触发方式
记忆注入的时机决定这个系统会不会惹人烦。我的经验是,只在三个节点注入:
- 会话开始:启动新会话时,把最近 7 天的活跃记忆条目摘要自动带出。这样 AI 会"天然知道"你的工作背景,不用等你开口。
- 任务边界:当你准备开始一项新任务,比如"现在写个爬虫抓某网站数据",工具会拿这个任务描述去做一次召回,把和爬虫、数据抓取、反爬经验相关的记忆精确注入。这个时机比较容易被人忽略,但实际效果最好。
- 用户显式触发:提供一个手动指令,比如在对话框中输入
/记忆 关键词,可以强制拉取相关记忆。我习惯在做重大决策前手动过一遍,防止漏掉早期讨论过的约束。
注入的内容也会做数量控制。默认上限是 8 条,单条长度超过 300 字的自动截短成 150 字摘要。宁可少给,也不要把提示词塞爆。这个规律我在后面测试时会用数据说明。
2.4 记忆卫生:衰减、去重与合并
长期使用的记忆库必然产生脏数据。我设计了一个轻量级的"记忆卫生机制":
- 时间衰减:每条记忆有
last_access_at字段,超过 30 天没被命中过的条目,权重自动下降。超过 90 天没命中的,进入待归档状态,不再参与默认召回。但不物理删除,防止你想翻旧账时找不到。 - 去重合并:如果新写入的记忆和现有条目在标题上高度相似(比如相似度超过 0.85),工具不会硬插入,而是更新原条目的
content,追加最新结论,并把version加一。 - 人工复核:每隔一周我会手动打开一次记忆库列表,把明显过时的条目标记为
archived。这个简单动作能保证记忆库反映的是"当前真实状态",而不是一个月前的过期观点。
记忆系统最忌讳"只进不出"。没有衰减和合并机制,库会越来越大,召回的准确率反而会下降。所以我把"忘掉什么"和"记住什么"放在同等重要的位置来设计。
3. 从零开始搭一套 claude-mem 的轻量实现
3.1 数据表结构和初始化脚本
实际代码我尽量保持精简。整个记忆库只用一张主表,结构如下:
CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT '', source TEXT DEFAULT 'manual', importance INTEGER DEFAULT 5, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, last_access_at TEXT NOT NULL, access_count INTEGER DEFAULT 0, status TEXT DEFAULT 'active' );字段含义一句话就能说清:title是记忆标题,content是正文,tags用逗号分隔方便检索,importance是 1-10 的重要度,status区分 active / archived。没有设计外键,也没有复杂关联,因为个人工具不需要。
初始化时我顺手建了两个索引:
CREATE INDEX idx_memories_status ON memories(status); CREATE INDEX idx_memories_tags ON memories(tags);status索引用于快速过滤有效记忆,tags索引用于打标签查询。对几千条数据来说,这两个索引已经让查询速度在毫秒级。
3.2 写入路径:三种触发方式
写入逻辑封装成了一个remember()函数,支持三种调用场景。
第一种是手动写入。直接在命令行调:
claude-mem add "项目部署脚本位置" \ --content "正式环境的部署脚本统一放在 ops/deploy.sh,测试环境是 ops/deploy-test.sh" \ --tags "部署,运维" \ --importance 8这个命令适用于自己捕捉到的关键信息。我会在完成一次技术决策、确认一个路径、定下一个规范后,立刻手动录入,几秒钟的事。
第二种是自动提取。把 AI 的一轮对话内容丢给一个小的"摘要模型",让模型生成"如果下个会话还要继续这个话题,最需要记住什么",然后写入记忆库。提取模板大概长这样(以 Python 为例,省略了模型调用细节):
def auto_extract_and_save(dialogue_text): summary = summarize_for_memory(dialogue_text) if not summary: return remember( title=summary["title"], content=summary["content"], tags=summary.get("tags", ""), source="auto_extract" )这种方式的坑在于会写入大量低价值信息,所以自动提取的条目默认importance=3,比手动写入低一等。
第三种是定时快照。每天的固定时间,把当天处理过的事务性笔记做一次批量提炼,生成"今日要记住的事"清单。这个快照不以单条入库,而是合并成一个"每日摘要"条目,避免一天十几条碎记忆把库刷屏。
3.3 读取路径:召回排序与上下文格式化
读取是记忆系统的高频操作,我单独写了一个recall()函数。核心流程分四步:
- 解析查询语句里的关键词,去掉停用词。
- 先跑 SQL 关键词匹配,拿到候选集。
- 如果配置了 embedding,再用向量相似度给候选集重新排序。
- 按
(importance * 0.4 + 最近访问权重 * 0.3 + 匹配度 * 0.3)的加权分取前 N 条。
加权分公式是我多次试出来的。你会发现没让importance权重超过 0.5,因为过分依赖重要度会把冷门但相关的记忆挤掉。最近访问权重能让近期常用记忆保持活跃,匹配度则保证相关性优先。
最终输出给 AI 的上下文模板如下:
【记忆参考 #1】 标题:项目部署脚本位置 标签:部署,运维 内容:正式环境的部署脚本统一放在 ops/deploy.sh,测试环境是 ops/deploy-test.sh 最近访问:2 天前这种格式化文本进来后,AI 能很明确地区分"这是记忆库里的背景信息"和"这是用户当前的问题",不会混在一起。我测试过,加入分隔标记后,回答跑偏的概率低了很多。
4. 实际应用中的踩坑记录与排查链路
4.1 记忆碎片太多,召回结果反而不可用
第一次跑满一个月后,我明显发现召回质量下降。明明查"数据库备份策略",返回的候选里却混着"备份文件路径"“备份脚本报错”"备份依赖库安装"好几条,内容互相覆盖又互相没什么补充关系。问题的根子在于:我把一次性问题和长期结论混在同一个库里。
排查链路是这样的:先按标题统计重复度,发现大量条目都以相同关键词开头,比如"备份"。再看内容,里面既有"备份命令是什么"这样的一次性操作记录,也有"备份策略应该保留七天"这样的长期约定。一次性记录当然会被频繁命中,因为它们和关键词相关,但真正该被 AI 记住的长期约定被埋没了。
修复方案分两步。第一步,把所有一次性操作记录从主记忆表里移到一张event_log表,只作历史查询用,不参与召回。第二步,引入"主题归并"机制,凡是标题里出现相同领域词的条目,查询时按领域分组,每组最多取一条优先级最高的。这样既保留了信息的多样性,又避免了自说自话。
4.2 时间衰减设太激进,关键背景被洗没了
首次配置时我天真地把衰减周期设成了 7 天。结果一周后,之前记录的很重要的项目背景被自动降权,第二次会话里 AI 就"忘了"我之前确过的技术选型。排查时我看了last_access_at,发现这些记忆不是不重要,而是那段时间没有触发相关话题,所以一直没被访问过。衰减逻辑把"低频"和"过期"画了等号,但这在个人工作流里是不成立的。
修复方式:不再单纯用时间衰减降权,而是加入"首次创建后 45 天保护期"。在保护期内,即使没有被访问,也不会因为时间而降低权重。同时,把importance >= 8的高优记忆默认豁免衰减。只有常规级别的记忆才参与时间衰减。这样既保住了长期事实,也保留了自动淘汰噪音的能力。
4.3 多进程并发写入导致 SQLite 锁冲突
早期我给 claude-mem 接了一个定时任务,会在每天固定时间批量写入快照。结果那段时间偶尔会出现database is locked错误。一开始我以为是磁盘问题,后来看日志才发现:定时任务和用户手动写入同时发生,SQLite 默认的写锁机制不允许两个写事务同时进行。
排查链路并不复杂:先复现,再定位,再改配置。复现很简单,写一个脚本同时发起 10 并发写入即可稳定触发。定位后改了三件事:
- 把数据库连接模式设为
WAL(Write-Ahead Logging),读和写可以并发。 - 给写操作加了
busy_timeout=5000,冲突时等待而不是立刻报错。 - 对批量写入使用单事务包裹,几百条一次提交,而不是一条一条 commit。
改完之后,持续压测一整天,没再出现锁冲突。这里建议所有把 SQLite 作为在线存储的人尽早把 WAL 模式打开,体验提升非常明显。
4.4 没有脱敏,记忆库变成了风险点
这个坑说实话是后知后觉。某天我在整理记忆库备份时,发现里面竟然躺着一条完整的 API 密钥记录。那是之前排查问题时 AI 助手帮我贴进去的,当时没在意,它就这么进了长期记忆。如果这个库文件被同步到网盘、被其他工具读取,风险一下就上来了。
修复方案分三层:第一层,在写入路径上做敏感信息过滤。用正则匹配常见的密钥特征(长随机字符串、位密钥前缀等),匹配到就不允许入库,直接扔到隔离区。第二层,对记忆库文件本体加密。我用了本机的加密卷存放.db文件,至少在文件层面多一层保护。第三层,备份时做脱敏导出。导出之前跑一遍扫描脚本,把所有疑似密钥的字段替换成[REDACTED]。
这三层作完之后,记忆库才敢放开用。这里也给所有做记忆工具的人一个提醒:记忆库收集的往往是高度上下文化的信息,它比普通聊天记录更容易暴露你的工作习惯、技术栈、内部系统结构。脱敏不做,工具就是个定时炸弹。
5. 效果测试与实际使用建议
5.1 我的评测样例:三组对比
为了验证这套记忆系统有没有实际价值,我做了三个测试场景。每个场景都开两次会话,中间清空上下文,看第二次会话 AI 能不能给出"知道我之前交代过"的回答。
第一个场景是技术偏好记忆。第一次会话我说明"本项目的测试框架用 pytest,所有新增模块必须配套测试"。第二次会话我问"帮我在新模块里加个测试文件",不提任何框架信息。没有 claude-mem 的时候,AI 问了一堆"你想用什么测试框架";有记忆库之后,它直接生成了 pytest 风格的测试文件。
第二个场景是路径记忆。第一次会话我确认了一个文件的存放路径,第二次会话我问"部署那个脚本在哪",有记忆时它能直接给出准确路径。这个场景虽然简单,但省掉了大量来回确认的时间。
第三个场景是反向测试:我故意在记忆库里存了一条过期的技术选型,比如"项目使用 Python 3.8"。然后在新的会话里问"当前项目的 Python 版本要求是什么"。结果发现如果记忆库没有及时更新,AI 会忠实复述过期信息。这个测试暴露出的问题不是记忆系统本身,而是"记忆没有版本控制"导致的。所以我后来给content更新时强制保留变更摘要字段,每次更新都记录"为什么改"。
5.2 哪些内容真正值得写入记忆
根据三个月的实际使用,我给可写入内容分了个优先级。下面这张表可以直接当参考:
| 内容类型 | 是否写入 | 重要度 | 典型例子 |
|---|---|---|---|
| 项目技术栈与版本约束 | 强烈建议 | 8-10 | "后端用 FastAPI + SQLAlchemy 2.0" |
| 目录结构、部署路径 | 强烈建议 | 7-9 | "脚本统一放 ops/ 目录" |
| 团队/个人的代码规范 | 强烈建议 | 7-9 | "提交信息必须带模块前缀" |
| 已排除的错误方案 | 建议 | 5-7 | "不要再用 XX 方案,因为会遇到 XX 问题" |
| 进行中任务的中间结论 | 建议 | 4-6 | "当前正在做的模块是订单导出" |
| 一次性问题解答 | 不建议 | — | "某函数的参数怎么传" |
| 情绪性/临时性内容 | 禁止 | — | "今天心情很差" |
| 敏感密钥、隐私信息 | 禁止 | — | API Key、密码、身份证号 |
我个人之所以特别看重"已排除的错误方案",是因为人类很容易重复踩同一个坑,AI 也一样。如果把"为什么这个方案不行"沉淀下来,下次它会默认绕开,而不是重新探索一遍。这是记忆库对效率提升最明显的一类内容。
5.3 后续扩展方向:从关键词检索到更聪明的召回
我现在用的检索主体仍是关键词匹配,对大多数场景够用。但如果记忆条目涨到几万条,或者记忆之间的关联关系变得复杂,我会考虑三条扩展路径:
- 引入基于词向量的本地召回,但注意控制 embedding 模型的体积和响应时间,本地推理比云 API 延迟可控。
- 引入图结构记忆:把条目之间的关系做成有向边,比如"A 依赖 B""A 解决了 B 的问题"。这样在召回时可以沿着图做一跳扩散,找到间接相关的记忆。
- 引入定时复核流程:每周自动生成一份"记忆健康报告",列出长时间未访问、重复度高的条目清单,辅助人工清理。
扩展的前提还是那句话:先把基础功底打牢。存储结构清晰、召回逻辑可解释、写入和检索链路都是可控的,后面接任何算法都能接得住。反过来,如果一味的堆新技术,可能只是把记忆库变成一个需要更高运维成本的黑盒。
我个人在实际使用中最深的体会是:记忆系统的价值不在于"记得多",而在于"记得准"和"忘得掉"。给 AI 配记忆层,其实是在给自己的工作流做知识管理,它倒逼你去思考哪些信息真正值得沉淀,哪些信息应该任由它消失。这个思考过程本身,价值甚至比工具本身还大。后面我会继续沿着"更准的召回、更聪明的遗忘"这个方向打磨,届时再拿新数据来复盘。