你是不是也有这种感觉:跟AI助手聊了几个月,它却连你的名字都记不住。每次新开一个会话,它都像第一天上班那样客客气气,你被迫把项目背景、代码规范、个人偏好从头到尾再讲一遍。我这次做的这个小项目claude-mem,就是想解决这个真正的痛点——给Claude装上一层可持续累积的长期记忆,让它不再是"每次对话都失忆"的打工人。
claude-mem,名字已经说明了一切:它就是Claude的memory。它不是去改动Claude模型本身,而是作为一层外部记忆系统,在Claude和你的真实会话之间搭一座桥。它只做三件核心的事:把对话里值得长期保存的信息自动抽出来、存好;下次你提问时,把相关的历史记忆打捞回来塞进上下文;然后持续更新和整理自己的记忆库,让它越用越顺手、越用越懂你。
这个项目最适合三类人:一是重度使用Claude做编码和架构设计的开发者,告别"每次都要重新介绍项目结构"的尴尬;二是长期跟踪同一课题的研究型用户,需要让AI记住阅读进度和关键结论;三是所有被"AI需要重复交代背景"折磨过的普通使用者。这篇博客,我会从设计思路、技术选型、核心实现到踩坑实录,把整个项目从头到尾拆开讲清楚。
1. 项目起源与核心设计思路
1.1 AI对话的"失忆症"到底有多痛
先说痛点。Claude这类大模型本质上是无状态的——每次调用都是独立的,模型只看到你这次提交的上下文,除此之外它对你是谁、你做过什么、你上次讨论到哪,一概不知。这不怪模型,而是它的工作方式决定的:注意力机制的窗口就那么大,不可能把人类几个月的历史对话都装进去。
但工作场景下的真实需求恰恰相反。我维护一个中大型开源项目时,每天要问Claude的问题至少有几十个,涉及模块设计、接口规范、历史决策理由。如果每次对话前都要先把之前两三个小时的讨论结论复述一遍,这个时间成本几乎等于没用AI。更崩溃的是,有时候你讲完背景,它给出的答案和你上次已经确认过的结论刚好冲突——因为它根本不知道你已经排除了那个方案。
我尝试过一些替代方案,比如开一个超长会话不关闭,把历史全堆在上下文里。但很快发现token开销大得离谱,回答变慢,费用也直线上升。会话一旦达到长度上限,前面早讨论出的结论照样被截断,和失忆没区别。后来又试过手工维护一个"背景文档",每次提问前人工复制粘贴——效果还行,但太累,而且文档经常忘记更新,过几天就过期了。
claude-mem的灵感正是来自这种反复碰壁。与其靠人的自律去维护记忆,不如让系统在对话过程中自动沉淀记忆,再在合适的时机自动召回。这个方向做下来,效果比我预想的好得多。
1.2 设计目标:像一个贴身助理,而不是一个数据库
这个项目立项时,我给自己定了三条原则,后来每一步决策都在向这三条靠拢。
第一条,记忆必须自动沉淀。用户不应该有"保存记忆"这个额外动作。只要对话在正常进行,系统就应当悄无声息地把值得记录的信息抽出来。我后来实现时采用了"对话轮次触发 + 会话结束扫描"双保险机制,确保不遗漏关键信息。
第二条,记忆必须精准召回。塞进上下文的东西不是越多越好,而是越准越好。每条多余的记忆都是对模型注意力的浪费,甚至会造成混淆。所以召回质量是整个系统的生命线,我在相似度阈值、排序策略、数量上限上花了大量功夫。
第三条,记忆必须可管理。用户要能查看系统记住了什么、随时删掉不该记的东西。记忆是隐私的延伸,不能变成一个黑盒。我提供了CLI查询和可视化面板两个入口,后面会详细讲。
这三条原则其实对应了记忆系统的三个核心环节:写入(Memory Write)、存储(Memory Store)、召回(Memory Recall)。我在设计架构的时候,也坚持把这三个环节彻底解耦,每一层都可以单独替换。这样做的好处是:如果以后有更好的向量模型,我只需要换掉召回模块,完全不影响其他部分。
2. 整体架构与技术选型解析
2.1 记忆分层的思路
一开始我曾想做一个通用的"全局记忆库",所有信息都堆在一起,需要时统一检索。但这个方向很快就翻车了——不同类型的记忆,生命周期和重要性差异极大,混在一起既污染检索结果,也不便于管理。
举个例子,"用户叫张伟、偏好TypeScript、讨厌代码里出现any"这类信息是长期稳定的,属于用户画像层,几乎每次对话都该被考虑。"某某模块尚未完成、上轮讨论决定采用事件驱动架构"这类是项目状态层,会随项目推进而变化。"昨天你让我先跑一遍测试再提PR"这类是临时任务层,可能一天后就失效了。
我把记忆分成四层:用户画像层(Profile)、项目语义层(Semantic)、任务状态层(Task)和对话摘要层(Episode)。每层有不同的保留策略——用户画像永久保留,项目语义按有效性衰减,任务状态完成后标记归档,对话摘要则定期压缩合并成更上层的长期记忆。这个设计类似人的记忆机制:短期记忆经过睡眠巩固变成长期记忆,而不是把所有鸡毛蒜皮都原样保存。
这个分层后来被证明是项目成功的关键之一。没有分层时,检索"路由"是个大难题——你问"上次那个bug后来怎么修的",结果系统把"用户喜欢喝美式咖啡"也当相似内容返回了,因为它们的向量空间可能恰好接近。分层之后,每一层单独建索引、单独召回,语义干扰小了非常多。
2.2 存储选型:为什么是SQLite加向量索引
存储层的选型,我在三个方案之间权衡了很久:纯JSON文件、SQLite、专门的向量数据库(如Chroma、Qdrant)。
纯JSON文件最轻量,但检索完全靠暴力遍历,记忆一多就扛不住。向量数据库检索很强,但对本地单机场景来说太重了,还得单独跑一个服务进程,依赖太多。最后我选了SQLite作为主存储,配合一个可选的外部向量索引。
SQLite是单文件数据库,不需要额外服务进程,部署成本几乎为零,事务能力强悍,还支持全文搜索扩展FTS5。我的记忆条目由结构化字段和长文内容组成,用SQLite管理再合适不过。
向量索引这部分,我默认使用本地运行的sentence-transformers/all-MiniLM-L6-v2模型把记忆内容转成384维向量,直接序列化存在同一张表的embedding字段里。检索时用余弦相似度在内存里做粗排,再用关键词匹配做精排。这个组合方案既保证了一定的语义召回能力,又没有任何外部服务依赖,单机跑起来非常顺。
如果你有更大的语料规模或更高的精度要求,我也预留了适配层,可以对接Chroma或pgvector。接口我已经定义好了,切换只是改配置的事。
2.3 上下文注入:1%的上下文,80%的体验提升
Claude的上下文窗口不管多大,本质上是稀缺资源。把记忆无脑全塞进去,不但浪费token,还会稀释模型对当前问题的注意力。我在设计上下文注入策略时,给自己定的纪律是:注入的记忆总量控制在当前上下文token上限的1%到2%以内。
具体策略分三步:第一步,根据当前用户问题做向量召回,拿到Top-30候选记忆;第二步,用规则和轻量模型做重排,剔除与当前问题无关的、过期的、互相矛盾的记忆;第三步,按类型配额选择最终注入条目——通常用户画像层最多2条,项目语义层最多2条,任务状态层最多1条,对话摘要层最多1条。总条数上限默认5条,超出就压缩合并。
这个"少量精准"的策略,在实际体验中效果远好过"大量覆盖"。你给模型灌一百条背景,它反而不知道重点在哪。但只给五条最相关的记忆,它会非常明确地围绕你当前的问题行动。这也是整个项目里投入产出比最高的一项优化。
3. 核心功能拆解与实现细节
3.1 记忆写入:什么时候提取、怎么提取
记忆的写入是整个系统最容易出问题的环节。写得太勤,会存一堆噪声;写得太懒,又漏掉关键信息。我最后采用的是"双触发"策略:每满6轮对话自动做一次增量提取,会话结束时再补一次全面扫描。
提取过程分两段。第一段是规则筛选——先从句式特征上判断哪些对话值得进入记忆。比如出现"我喜欢""我习惯""我们决定""注意千万别""以后不要"这类带长期语义的信号词,就直接标记为高优先级候选。第二段是LLM抽取——把候选对话和已有记忆库一起交给模型,让它输出结构化记忆条目,每条包含内容、类型、重要性和有效性四个字段,并和已有记忆做去重。
提取提示词的模板大致长这样: 从下面的对话中提取值得长期记忆的信息。 只提取对将来对话有实际帮助的内容,忽略寒暄和临时情绪。 输出JSON数组,每个元素包含: - content: 记忆内容,保留具体细节 - memory_type: user_profile / project_semantic / task_state / episode_summary - importance: 0-10的整数,10为极重要 - valid_until: 过期时间,永久留空 对话内容: ... 已有记忆(供去重参考): ...这里有一个我踩过的坑:如果让模型自由提取,它倾向于把"用户今天心情不好"也写成长期记忆,因为情感信息在LLM看来很有"记下来"的价值。但实际上这类情绪碎片对长期工作毫无帮助,反而会干扰判断。我的对策是在提示词里加了反向说明,明确"忽略情绪性、临时性、闲聊性内容"。做了这个调整之后,记忆库的噪声水平直线下降。
3.2 记忆存储:表结构设计与降噪机制
存储层的表结构并不复杂,但也远比"一张表存所有"要精细。核心表memory_items包含几个关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | TEXT | 主键,UUID |
| user_id | TEXT | 用户标识,支持多用户隔离 |
| session_id | TEXT | 来源会话 |
| memory_type | TEXT | 记忆分层类型 |
| content | TEXT | 记忆正文 |
| metadata | TEXT | JSON,存标签、实体名等辅助信息 |
| embedding | BLOB | 384维向量序列化 |
| importance | INTEGER | 0-10 |
| created_at | INTEGER | 创建时间戳 |
| last_hit_at | INTEGER | 最近一次被召回的时间 |
| hit_count | INTEGER | 累计召回次数 |
| is_active | INTEGER | 0/1,软删除标记 |
last_hit_at和hit_count这两个字段一开始我并没打算加,后来发现它们是记忆健康度的晴雨表。我写了一个定期清理任务:超过90天没有被召回过的低重要度记忆,自动标记为不活跃;超过180天没有被召回过的,直接软删除。这保证了记忆库长期保持精炼,不会越积越满、检索越来越慢。
去重方面,除了写入时做语义相似度判断合并新内容外,我还会对同类型记忆做"版本化"处理。比如用户的偏好从"喜欢JavaScript"变成"优先TypeScript",旧条目不会直接删掉,而是标记deprecated并引用新条目,查询时只返回最新版本。这样即使记忆更新了,历史的变更有迹可循,不会因为错误合并而丢失信息。
3.3 记忆召回:从相关到有用的最后一公里
召回环节最微妙的一点是:模型判断"相关"和我们判断"有用"往往是两码事。向量相似度高不一定真的有用,尤其是当问题里带了很多通用词汇时,容易召回到一堆泛泛而谈的记忆。
我的召回链路是四段式:召回→重排→配额→格式化。
召回阶段,用当前用户消息的向量和记忆库里所有活跃条目做余弦相似度计算,取出Top-30。这里的相似度阈值默认0.35,低于这个的干脆不取。阈值太高容易漏,太低容易噪,这个0.35是我在本地测试集上调出来的,你的场景可能不一样,后面会讲怎么调。
重排阶段,按三要素打分:向量相似度占50%、记忆新鲜度(last_hit_at距离现在越近分越高)占30%、记忆重要性占20%。这样即使一条记忆向量得分稍低,但如果它昨天刚被用过且很重要,也会排在前面。
格式化阶段,把选中的记忆拼装成语义自然的前缀,追加到用户消息之前。这步很关键——直接用JSON格式灌给模型,它也能理解,但理解效率差不少。我用模板块拼装出下面这种形式:
[长期记忆参考] 以下是从历史对话中提取的与本问题相关的信息: - 用户偏好:后端使用Python/FastAPI,数据库使用PostgreSQL,不接受在核心链路引入Redis。 - 项目状态:订单服务模块尚未完成,已确定事件驱动方案,消息队列选用RabbitMQ。 - 近期任务:用户今天要求先完善接口文档,再开始写单元测试。用自然语言而不是原始JSON,模型可以更快把记忆内容融入到回答逻辑中。这个小改动让回答的"贴切感"提升了一个档次。
4. 实操过程:从零接入claude-mem
4.1 安装与初始化配置
claude-mem的安装很简单。如果你只想快速体验,直接通过包管理器装:
pip install claude-mem如果你打算改代码、用自己的记忆模型,就Clone源码:
git clone https://github.com/yourname/claude-mem.git cd claude-mem pip install -e .安装完后,第一次启动要跑初始化命令,它会自动创建数据目录和配置文件:
claude-mem init默认数据目录在~/.claude-mem/下,包含config.yaml和memory.db。第一次运行建议打开config.yaml确认几个关键参数,因为它们决定了系统的记忆行为:
memory: max_inject_count: 5 # 单次最大注入条数 similarity_threshold: 0.35 # 召回相似度阈值 extract_interval_rounds: 6 # 对话轮次触发间隔 importance_min: 4 # 写入最低重要度 enable_auto_extract: true # 是否开启自动提取 vector: model_name: "sentence-transformers/all-MiniLM-L6-v2" dimension: 384 storage: db_path: "~/.claude-mem/memory.db" enable_fts: true # 启用全文搜索 retention_days: 180 # 未命中保留天数importance_min这个参数很容易被忽略,但特别重要。它设定了记忆写入的重要度门槛,只有模型评估为4分以上的信息才允许入库。调高它可以让记忆库更精炼,调低则更完整、但噪声更多。我自己的配置是4,你可以根据自己的容忍度调整。
4.2 接入对话流程的两种方式
claude-mem接入实际对话流程,我支持两种方式。
第一种,作为MCP工具接入。如果你用的是Claude桌面端,可以在配置文件里添加MCP Server注册:
{ "mcpServers": { "claude-mem": { "command": "claude-mem", "args": ["mcp-server"], "env": { "CLAUDE_MEM_CONFIG": "~/.claude-mem/config.yaml" } } } }这样Claude在每次对话中会自动调用claude-mem提供的工具——search_memory、save_memory、update_memory、delete_memory。模型会在它认为合适的时候调用这些工具来查询或写入记忆。这是最省心的方式,用户完全无感知。
第二种,代理模式接入。如果你没有桌面端,或者想在命令行或者自己的脚本中接入,可以用代理模式:
claude-mem proxy --api-key $ANTHROPIC_API_KEY --port 8080启动后,你的API请求发给本地8080端口,代理会自动做记忆注入和提取,再转发给Claude。这个模式更灵活,适合放在自己的自动化流程里。我平时写代码用的就是这种模式,因为可以精确控制哪些消息流经记忆层。
4.3 验证记忆效果:一次真实的测试
接入完成后,我建议照着下面这个流程测试你的记忆层是否正常工作。
第一步,在第一个会话里说几条明显的偏好性信息:"我以后写Python函数都要加类型注解。""本项目数据库统一用PostgreSQL。""这个模块的接口变更必须要更新OpenAPI文档。"
第二步,等对话超过6轮,或者直接说"请记住这些",确保记忆被提取。然后查看记忆库:
claude-mem list --type user_profile此时应该能看到三条独立的记忆条目,并且内容基本准确。
第三步,新开一个会话,问一句和第一条偏好相关的问题:"给我一个用户登录的函数,注意结合我的编码习惯。"如果没有记忆层,Claude会给出一个普普通通的函数。有了记忆层,它应当主动生成带完整类型注解、用PostgreSQL风格SQL的示例。如果它做到了,说明记忆注入这条链路是通的。
第四步,检查召回日志:
claude-mem stats这个命令会显示每次召回命中了哪几条记忆、相似度得分多少、注入消耗了多少token。实测下来,我自己的项目注入成本大约占单次对话token总量的1.2%左右,完全可接受。
5. 常见问题与排查技巧实录
5.1 记忆完全不生效,排查链路长什么样
最常遇到的问题就是"我设置了记忆,但新会话里Claude完全没反应"。遇到这种情况,别慌,按照下面这条链路逐层排查。
第一层,确认记忆真的写入了。运行claude-mem list --all,看有没有条目。如果没有,问题在前面,检查提取触发条件——是不是对话轮次不够6轮?重要度是否低于4被过滤了?信号词规则是不是没触发?
第二层,确认召回真的执行了。开启调试日志:claude-mem proxy --debug,然后重新提问。看日志里有没有recall操作、命中了几条记忆、相似度得分是多少。如果相似度全部低于0.35阈值,说明你的提问方式和记忆内容差异太大,需要适当降低阈值。
第三层,确认记忆真的注入进了上下文。在调试日志里查看最终的请求体,看系统消息或者用户消息前面有没有"长期记忆参考"段落。如果没有,可能是注入模块被enable_inject: false关掉了,或者上下文配额已经满了。
这一套排查下来,90%的问题都能定位到。核心思路是:记忆写入、召回、注入是三个独立环节,哪个环节断了,就在哪个环节查日志。别一上来就改模型、换数据库。
5.2 上下文超限和token成本失控
有段时间我贪心,把max_inject_count调到了15,结果对话质量反而下降,token成本翻倍,甚至偶尔触发上下文超限。后来我意识到,记忆注入和对话本身是在抢占同一份上下文预算,必须给记忆设一个显式的预算上限。
我最终的策略是"固定配额 + 动态压缩":单个记忆条目内容超过200字符的,自动用LLM压缩成不超过80字符的关键信息摘要再注入;所有注入记忆的总字符数不超过800字符。这样既控制了token,又保证了信息的广度。实测中,800字符的记忆注入对对话质量提升最明显,再往上就是边际递减了。
如果确实有大量历史知识需要模型参考,别走记忆注入这条路,而是把详细内容写入一个单独的知识库文件,在需要时让模型用工具读取。这是"记忆层"和"知识库"的分工:记忆层管高频、轻量、跨会话的上下文;知识库管低频、重量、文档化的信息。
5.3 记忆串台和过时信息干扰
记忆系统用久了,一定会遇到"记忆串台"——几个项目的信息混在一起,或者旧决策和新决策打架。比如用户在不同项目里用过不同的技术栈,模型可能把A项目的经验搬到B项目上。
串台问题的根源是记忆库里不同实体的区分度不够。我的解决方案是引入项目命名空间(Project Namespace)。给每条记忆打上project标签,召回时优先检索当前激活项目的记忆,只有当前项目为空时才回退到全局记忆。这个改动极大降低了串台概率。
过时信息干扰则是另一个问题。项目状态变了,旧记忆还在库里,模型参考到旧信息就可能给出错误建议。我的对策是主动失效机制:如果新记忆和旧记忆在同一类型下冲突,写入新记忆时自动将旧记忆标记为conflicted,并设置deprecated_at。同时定期运行claude-mem prune,把已弃用的记忆归档到历史表,不再参与召回。
5.4 隐私与数据隔离
隐私问题我放在最后说,因为它最容易被忽略、但真的出事最麻烦。claude-mem默认把数据库文件放在用户目录下,权限设置为仅当前用户可读写。如果你处理的是敏感数据,建议开启SQLite加密扩展,或者直接把数据库放到加密卷里。
多用户共用一台机器时,务必给每个用户单独的数据库文件,并通过user_id字段隔离数据。我见过有人直接把全公司的记忆库混在一个库文件里,结果A同事问技术问题,被注入了一条属于B同事的项目决策记忆,场面相当尴尬。
另外,claude-mem的记忆写入和召回都是本地完成的,不会把记忆内容发给第三方。但如果你的Claude API请求本身要发到远端执行,那么注入进上下文的记忆会随请求一起发送——这个要在隐私要求极高的场景下特别注意。我给系统的设计里保留了--no-inject-sensitive开关,开启后涉及敏感标签的记忆条目会被拦截,不参与任何注入。
6. 进阶玩法与后续规划
6.1 多代理共享记忆和长期Agent
claude-mem目前的定位是单用户、跨会话的记忆层。但我在接口设计时预留了user_id维度,这为多代理共享记忆铺了路。想象这样一个场景:你有三个Agent,一个负责代码审查,一个负责文档维护,一个负责CI/CD配置。它们各自和Claude交互,但共享同一个项目记忆库。这样代码审查Agent作出的技术决策,文档Agent在下一次更新文档时能自然参考到。
实现上只需要给每个Agent分配不同的session_id和agent_id,记忆召回时按项目命名空间过滤即可。目前agent_id字段已经在表结构中预留,下一步我会把它做成正式的一等公民。
6.2 记忆可视化与管理面板
记忆系统跑久了,用户一定需要定期审视"AI到底记住了我什么"。命令行工具虽然强大,但对非技术用户不够友好。我目前正在开发一个本地Web面板,用gradio或streamlit实现,展示所有记忆条目、召回热度热力图、冲突记忆提示,并提供一键删除和导出功能。
这个面板加上后台自动清理任务,就能形成一个完整的记忆生命周期管理闭环:自动写入、精准召回、定期清理、用户可查。我的目标是可以完全做到"让用户忘了记忆层的存在,但它一直在正确工作"。
6.3 关于这个项目的一些真心话
做完claude-mem之后我有个很明显的感受:给AI加记忆这件事,技术挑战其实不在模型,而在工程——怎么判断哪些信息值得记,怎么平衡召回精度和成本,怎么避免旧信息干扰新决策。这个过程里,我试过把所有历史暴力塞进上下文(token爆炸)、试过只依赖向量召回(串台严重)、试过让模型每次对话都提炼记忆(噪声爆棚),最后才沉淀出现在这套分层、配额、双触发提取的方案。
如果你也打算做一个类似的记忆层,我想给你三个在我实际使用中被验证过的建议:第一,记忆一定要分层,没有分层的记忆库就是一锅粥;第二,注入宁可少而精,也不要多而杂,给窗口留白;第三,尽早考虑记忆的清理和隐私问题,这两个问题越晚处理越被动。
claude-mem目前已经能稳定支撑我的日常开发工作流,最近一次连续使用已经超过一个月,记忆库积累了两千多条条目,对话注入的token开销稳定控制在2%以内。后面我会继续完善多代理共享和可视化面板,让这个记忆层真正变成所有AI交互的基础设施。你如果也在被AI失忆症困扰,不妨照着这篇文章的思路搭一套自己的记忆层,用起来你就能体会到——一个真正记得住你的AI,和普通的AI,完全是两个物种。