项目代号叫 claude-mem,先简单说结论:这是一个跑在我本地终端里的 AI 对话记忆管理工具。它做的事可以概括成一句话——把散落在各次会话里的有效信息,自动抽取成结构化的“记忆卡片”,保存到本地,支持搜索、归档、回顾,也可以在新会话开始时按需把这些记忆重新带回来。
如果你也重度依赖 AI 助手来理思路、写方案、做调研,你一定遇到过这种情况:上周明明聊清楚的一个技术决策,这周开新会话,对面就像失忆了一样;明明记得某次对话里提过一个关键参数,回头翻历史记录却怎么都想不起关键词。我受够了这种“聊过等于没聊”的感觉,所以动手写了这个工具。
这篇文章适合三类人。第一类是天天用 AI 助手的个人用户,想低成本做到“聊过的事不丢”;第二类是在做自动化信息整理或者个人知识库相关工具的开发者,里面的抽取流程、去重策略、排序逻辑可以借鉴;第三类是刚接触本地存储和数据处理的初学者,跟着后面的设计思路,也能搭一套完全离线、可备份的个人记忆库。
说明:项目代号里的 claude-mem 只是我随手起的工程名,它并不绑定某一家 AI 厂商。这里整个方案只依赖“能输出结构化文本的语言模型”和本地存储,换个助手同样适用。
1. 项目背景与需求拆解
1.1 数据浪费是最大的痛点
先说说我为什么要做这个工具。过去一年我几乎每天都会和 AI 助手讨论技术方案、写文档、拆解需求,很多思路是在对话中逐步成型的。按理说,这些对话记录是最有价值的思维资产,但现实是它们基本躺在一个导出的 JSON 文件里,再也不被打开。
问题出在两个层面。第一,会话之间是隔离的,新会话不知道旧会话聊过什么。每次换话题后,想让 AI 理解上下文,就得把之前的内容重新描述一遍。第二,历史记录本身不合理,按时间顺序排下来,一天能有一百多条消息,中间混合着客套话、跑题内容和真信息。想从中找出“当时我们决定用哪个方案”,靠 Ctrl+F 基本无能为力。
我统计过本地导出的会话记录,一条十轮左右的对话大概能产出三到五条值得长期保留的信息。一条不提取,这些信息就只会躺在文件里占空间。信息本身不产生价值,能够被重新找到的信息才有价值。
1.2 需求拆解:不做什么比做什么更重要
动手之前,我先把需求写成了清单,避免把项目做成一个没有边界的大杂烩。
核心需求有五条:
- 自动抽取。我不接受手动把人说的话填到表格里,这样的习惯很难坚持。
- 结构化存储。每条记忆得有确定字段,至少包含:类型、内容、来源、时间、置信度。
- 按需检索。能以关键词找到相关的旧记忆,也能按时间、类型筛选。
- 本地优先。数据只放在我自己的硬盘上,不依赖云端同步,一个文件夹就能备份。
- 可解释。当一条记忆被检索出来,我能知道它来自哪次会话、哪一轮消息,方便回去查原始上下文。
同时我明确划掉了三件“肯定不做的事”。第一,不做实时同步,我不需要每次对话都实时写库,阶段性导入就够。第二,不做复杂的关系图谱,什么知识图谱、实体链接,初期统统不要,那是锦上添花不是雪中送炭。第三,不做对话机器人,工具只负责记录和读取,不和用户闲聊。
这条边界非常关键。很多个人工具做着做着就失控,原因就是想把产品化功能都塞进去。我只保留一个目标:解决“记不住”和“找不到”两个问题。
2. 整体架构与方案选型
2.1 三种可行方案的对比
在决定架构之前,我把可能的方案列了一遍,分别为三种思路做过真实验证。
方案 A 最简单粗暴:每次新会话开始时,把全部历史记录拼进提示词里。我试过,大约在两百条消息以内还能忍受,超过之后响应质量明显下降。
方案 B 是维护一份人工笔记,每次聊完自己整理要点。这个方案执行了两周就放弃了,因为整理行为本身太反人性,忙的时候根本想不起来。
方案 C 是自动抽取:对话记录经过一条处理流水线,变成结构化的记忆卡片,只在新会话需要时,把最相关的一小部分注入。我最后选择的就是方案 C。它最麻烦,但上限最高,数据是结构化的,意味着可以做检索、排序和后续扩展。
做技术选型时先划分边界。我的边界很明确:本地、单用户、数据量在几万条级别、没有复杂的实时性要求。在这个边界下,不需要引入多个微服务,更不需要维护一套分布式系统。
| 方案 | 实现成本 | 信息找回能力 | 上下文控制 | 长期维护性 |
|---|---|---|---|---|
| A:全量拼进提示词 | 低 | 弱 | 差 | 差 |
| B:人工整理笔记 | 中 | 中 | 中 | 差 |
| C:自动抽取记忆卡片 | 高 | 强 | 强 | 好 |
2.2 设计核心处理流水线
方案确定之后,我把流水线拆成六步:导入、清洗、分段、抽取、合并、索引。
导入和清洗负责把原始会话文件读进来,去掉系统消息、时间戳,统一格式。分段环节比较重要,长对话不能整段丢给模型抽取,一方面超出上下文窗口,另一方面指令响应更差。我按“单一主题”的粒度切分,用固定窗口加重叠带,保证跨段的信息不会在边界处断掉。
抽取是整个流水线的核心,把一段对话转成 JSON 格式的记忆卡片。合并是为了解决重复问题,同一信息在多次对话里反复出现,不能每出现一次存一条。最后是索引,存入数据库并建立全文检索,方便后面搜索。
这套流水线有一个隐含设计原则:每一层只做一件事,每两层之间用明文格式衔接。所以中间产物全部是 JSON,调试时可以随时停下来,检查某一步的输出是否符合预期。
2.3 存储和检索:本地优先的具体选型
存储层我考虑过几个选项。
外部向量数据库虽然检索能力强,但要额外跑服务,数据要离本地,备份也麻烦。对于几万条记录的个人工具,用一个单文件数据库就够了。我最后选择的方案是:原始记录用 JSON Lines 保存方便调试和迁移,处理后的记忆卡片放进嵌入式数据库,全文索引用嵌入式数据库自带的全文检索模块。
这套方案有几个实在的好处。数据全部落在本地文件夹里,备份就是复制文件,不会出现“数据库连不上”导致工具瘫痪的情况。单文件数据库在几万条数据规模下性能足够,实测本地一万两千条消息、两千多张记忆卡的规模,全文检索平均耗时不到五十毫秒,完全感知不到延迟。
还有一点常被忽略:单文件存储意味着数据可以被任何支持该格式的工具打开。就算有一天工具挂了,数据本身仍然能导出、能迁移,不会被锁死在某一个工具里。
3. 核心模块的实现细节
3.1 记忆抽取:提示词模板与 JSON 输出
抽取模块要做的事情,是让语言模型把一段对话里的有效信息提取成固定结构的 JSON。
一开始我随性设计了字段,结果输出格式经常漂移。后来改成严格的 JSON Schema,并为每类记忆设计了固定字段。下面是我用的一段简化模板:
你是一个会话记忆抽取器。请从下面的对话中抽取值得长期保留的信息。 只输出 JSON 数组,每个元素包含: - type: decision|task|fact|preference|quote - summary: 一句话描述,不超过30个字 - detail: 详细内容,尽量保留原始表达中的关键信息 - timestamp: 对话发生的日期(保持原有格式) - confidence: 0到1之间的小数,表示你对抽取内容的把握程度 规则: 1. 不抽取寒暄和无关内容 2. 同一条信息只保留一次,不要重复输出 3. 如果对话中有明确决策,type 必须为 decision 4. 不要修改原始表达中的数字、名词、方案名称 对话内容: {segment} 只输出 JSON,不要输出其他解释。这套固定结构帮了大忙。confidence 字段用于过滤模型“自己都拿不准”的记忆,宁缺毋滥。timestamp 保留原始对话日期,避免用抽取时间代替事实时间。
抽取的执行频率也有讲究。我一开始对每条消息都调用一次模型,成本高,而且相邻消息之间会重复生成几乎一样的记忆。后来改成按分段批量抽取,每段一次调用,输出整个 JSON 数组,效率提高了很多。
3.2 去重、合并与冲突消解
这是整个项目里最繁琐的部分。抽取完的记忆卡片会大量重复,同一件事被不同的话换着说,如果不处理,检索结果全是同一信息的不同版本。
我设计了四层去重规则。
完全一致的表述,直接丢弃。摘要和详情文本完全相同的情况很少见,但处理起来最容易。
高度相似但措辞不同的,只保留时间更新的一条。相似度用文本归一化加关键词重合度计算,超过阈值就认为是同一信息,简称“合并”。
表达相近但存在增量信息的,比如某条记忆只有结论,另一条多了适用条件和限制范围,两条合并成一条更完整的记忆。这个规则是价值最高的部分。
最后还有一种情况:两条记忆相互矛盾但各自由不同会话来源支撑,不强行合并,同时保留并把矛盾标记出来。我在记忆字段里加了一个pending_review标志,等待后续对话给出足够证据后再消解。
合并规则里要特别注意,别把两条“看起来像”但实际不同的记忆合并掉。我踩过的判断方法是:如果两条段落的摘要完全一致,但详情里的数字、范围、限定条件任意一个不同,默认不合并,改为保留两条并在检索时优先显示新增的那条。
3.3 时效与热度:记忆不能一视同仁
记忆也是有生命周期的。三个月前的一条临时想法,和昨天刚确定的方案,对当前决策的价值完全不同。
我给每条记忆计算一个动态分数,检索时按分数排序。公式是:
score = 0.4 * recency_score + 0.3 * mention_score + 0.2 * type_weight + 0.1 * confidencerecency_score 根据时间衰减,最近七天内为最高值,七天到三十天开始线性下降,超过九十天降到很低。mention_score 是记忆被检索或匹配到的次数,被反复用到的内容热度高,自动获得更高排序。type_weight 是我按使用价值打的权重,比如decision类型权重最高,因为它代表着已经确定的东西;preference类权重次之;quote类权重较低,只作为背景参考。
权重比例来自我一段时间的实际使用统计。最开始时 recency_score 权重给了 0.6,结果发现近期的一般性记录反复顶掉重要的旧决策,所以降低了时间权重,把一部分权重分给 type_weight,旧的重要决策才有机会浮上来。
3.4 检索与上下文注入:控制信息浓度
检索要做的事情,是根据当前会话的主题,找出一批最相关的记忆。注入到新会话时,并不是越多越好,模型一次能关注的上下文有限,塞得多了反而干扰。
我控制在每次最多注入六到八条记忆,且总量不超过一千五百个词。这个量级基本能覆盖住“用户过去是怎么处理这类问题的”,又不至于淹没当前的新信息。
注入的格式是这样的:
以下是用户在过往会话中留下的相关记忆: - [决策] 缓存方案优先本地内存缓存,异地容器间用消息队列失效通知(来自 2025-06-01 会话) - [任务] 需要验证消息队列在极端情况下的积压恢复时间(来自 2025-06-01 会话) 用户接下来要处理的问题: ...注意,不要把记忆原封不动地塞入,而是以“第三人称摘要”的方式注入,这样可以让模型把记忆当作参考资料,而不是用户当前说出来的话。如果直接复制原文,模型容易误解为新的用户指令,导致对话走向偏掉。
4. 实操演示:从对话记录到记忆库
4.1 先用起来:四个常用命令
我把它做成了命令行工具,四个核心命令覆盖日常使用频率最高的环节。
# 初始化记忆库 mem init --storage ./memory-store # 导入导出的会话记录 mem import ./exported/chat-2025-06-01.jsonl --format jsonl # 按关键词检索记忆 mem search "告警方案" # 查看记忆库统计 mem statsinit 做的就是创建目录和数据库表结构,重点在本地路径的规划。我把记忆库放在独立分区的一个文件夹下,不和项目代码混在一起,避免误删或覆盖。import 负责把导出的会话文件喂进流水线,支持 JSON 和 JSON Lines 两种格式。search 走的是全文索引,支持多关键词和类型筛选。stats 则能显示记忆卡片总量、类型分布、最近七天的增长量,用来评估自己的使用习惯。
这四条命令已经覆盖了核心流程:先初始化,再导入,然后检索,最后看统计。
4.2 三个实际使用场景
场景一:技术方案决策的回顾。
我在一次会话里和 AI 讨论过“多环境配置管理是用集中配置中心还是拉平配置文件”。对话结束后导入记忆库,检索“配置中心”时,返回了这条记忆:
[决策] 多环境配置优先使用集中配置中心,保留环境变量覆盖入口 来源:chat-2025-06-03 补充:团队内统一用配置中心,本地开发通过环境变量覆盖 状态:已确认有了这条记忆,后续新会话里再讨论部署问题时,就不需要重新争论一次方案,直接引用结论就行。这就是记忆库最有价值的地方,不是记录,而是不再重复做已经做过的决定。
场景二:任务追踪。
用 AI 梳理任务清单时产生的待办类记忆,我会单独建立筛选。我习惯在每周结束时用mem list --type=task把所有待办拉出来,相当于自动生成一份跨会话的周报底稿。任务完成之后,我再通过交互式修改把它标记为 done,它就不会出现在下一次待办筛选中。
场景三:阅读笔记。
AI 助手作为阅读辅助工具时,会产生大量值得留存的要点。我的做法是把每次阅读对话导入记忆库,然后用preference和quote类型保存原文重要语句和我的偏好记录。比起在文档里划高亮,这种方式的优势在于:未来我搜索任意关键词时,这些阅读笔记会和其他记忆一起被召回,更容易形成跨领域的知识关联。
4.3 顺手导入的小习惯
工具再方便,不用也是白搭。我需要把导入这个动作变成习惯的一部分。
我的做法是固定一个导出目录,会话结束后,随手把导出文件丢进去,晚上统一做一次导入。现实中没有固定行为模式的人,可以用另一个办法:在每段重要会话收尾时,主动要求 AI 输出一段“本次会话摘要”,然后直接把这段文本交给工具导入。这个习惯不需要任何额外的环境配置,操作成本低,也更容易坚持。
导入频率不用太高。每天一次足够,频率过高反而会产生大量重复记忆,给合并模块增加无谓的压力。我试过每两小时导入一次,结果一天下来的重复记忆占了全部卡片的三分之一,后来就改成按周批量导入了。
5. 常见问题与排查实录
5.1 高频问题速查表
| 问题 | 触发原因 | 解决办法 |
|---|---|---|
| 检索到大量重复记忆 | 合并阈值设置过低 | 提高相似度阈值,优先杀掉摘要完全相同的结果 |
| 重要旧记忆搜不出来 | 时效权重过高,旧决策被新内容挤掉 | 调高 type_weight,决策类记忆单独排序 |
| 中文检索不准 | 嵌入式数据库全文检索对中文分词不友好 | 导入时额外生成一份以字符为单位的索引字段 |
| 抽取内容含大量寒暄 | 提示词里“不抽取寒暄”约束不够 | 增加示例对,明确给出哪些句子该被忽略 |
| 导入太慢 | 每段都调用模型,没有批量处理 | 改为按分段批量调用,单次生成整个 JSON 数组 |
| 记忆库文件损坏 | 频繁强杀进程导致写入中断 | 增加临时文件写入,写入完成后原子替换 |
这个表是我实际使用中最常翻的部分。尤其是中文检索,嵌入式数据库内置分析器在英文分词上很顺手,中文却常常被整句切到一起。我的解决方法是额外存一份“字符倒排索引”,虽然占空间,但在个人数据量级完全无所谓,换来的是中文搜索稳定了不少。
5.2 我踩过的三个坑
第一个坑是抽取粒度。最初我把一整段长对话直接丢给模型,让它找出所有值得记录的信息。结果模型经常“头重脚轻”,前面聊的内容记得多,后半段容易被忽略。后来我改成固定窗口加重叠带的分段方案,每段五百到八百字,相邻段重叠五十字左右,抽取覆盖率明显提升。
第二个坑是过度依赖向量相似度。我一开始觉得语义搜索很酷,每条记忆都生成向量,检索时全做向量匹配。实际用了几天发现问题很大,用户搜索的词往往非常具体,比如某个函数名、某个版本号,向量匹配对精确词的召回效果反而不如简单的关键词倒排索引。后来改成双通道方案:先用全文检索做精确召回,再用向量相似度做重排。两个通道的产出合并,效果明显好过单个方案。
第三个坑是没有遗忘机制。一开始所有记忆一视同仁地永久保存,三个月后库里堆了大量“当初觉得有用后来再也没看过”的信息。它们占据了检索结果的大量空间,真正重要的记忆反而被淹没。后来我增加了冷归档策略:连续九十天没有被检索到的记忆,自动移出活跃索引,只保留在归档表里。这样既没有真正删数据,又保证了日常检索结果的干净度。
5.3 后续可以怎么扩展
这个工具做到现在,已经满足我最初的需求了。但我在使用过程中看到几个可以继续往下走的点。
第一个是自动注入。现在每次新会话还要手动去搜记忆再粘贴,后续可以做成一个插件,在新会话启动时自动拉取前几条相关记忆,形成真正无感的记忆回填。
第二个是待办提醒。记忆库里已经有大量的任务类记忆,可以加一个定时任务,把到期的待办事项推送出来。这就等于把 AI 对话里聊出来的任务,变成了一个本地轻量级待办系统。
第三个是跨知识领域的关联。现在的检索仍然是“点对点”式,输入关键词返回相关记忆。如果按来源会话把记忆卡片连起来,可以看到不同领域之间通过同一段对话产生的隐含联系。这不需要做很复杂的知识图谱,只需要给每条记忆增加一个conversation_id字段,检索时按来源聚合,就能看到一批记忆的上下文全貌。
先说一个前提:这类个人工具的扩展一定要克制,每加一个功能都要重新审视一遍“它是否仍然服务于记不住和找不到这两个核心问题”,否则很容易又滑向一个维护负担极重的半成品。
最后分享一条在使用中沉淀下来的体会:不要追求一次把工具做完整,也不要用一个宏大架构来开头。先用一个文本文件,把对话里最有价值的一条信息记下来,坚持一周;当你发现手动记录已经跟不上需求时,再考虑加抽取;当检索慢到不可接受时,再考虑加索引。工具永远是在和真实使用习惯的磨合中长出来的,而不是一开始设计出来的。