1. 项目概述与核心定位
1.1 这个项目到底在解决什么问题
第一次看到claude-mem这个名字,我的直觉是:这应该是一个给 Claude 系列模型做“记忆管理”的工具。事实也确实如此。它要解决的核心痛点非常明确——大语言模型在长对话、多轮任务、跨会话协作中,上下文窗口有限、历史信息容易丢失、重复交代背景的成本极高。
你可以把它理解成给 Claude 配了一个“外置大脑”。平时你跟 Claude 聊天,聊完就散了,下次再聊它完全不记得你是谁、之前做过什么。claude-mem做的事情,就是把这些对话中的关键信息抽取出来,存到一个结构化的记忆库里,下次需要的时候再按需召回,塞回上下文里。
这个项目适合谁?我梳理了三类人:
- 重度 Claude 用户:每天要跟 Claude 来回几十轮,做代码审查、文档撰写、方案设计的人。
- AI 应用开发者:想在自己的产品里给 Claude 加上长期记忆能力,但不想从零造轮子。
- AI Agent 研究者:关注记忆机制、上下文压缩、检索增强生成(RAG)在对话场景落地的技术人。
1.2 为什么“记忆”这件事值得单独做一个项目
很多人会问:Claude 本身不是支持 200K 甚至更长的上下文吗?直接全塞进去不就行了?
这个问题我踩过坑。上下文窗口大,不代表你应该把所有东西都塞进去。原因有三个:
第一,成本。上下文越长,token 消耗越大,API 调用费用是线性甚至超线性增长的。你每次对话都带 10 万 token 的历史,钱包扛不住。
第二,注意力稀释。模型在超长上下文里,对关键信息的注意力会被大量无关内容稀释。我实测过,同样一个问题,带 5 万 token 冗余历史 vs 带 2000 token 精准记忆,后者的回答质量明显更稳。
第三,跨会话持久化。上下文窗口再大,也是单次会话内的。你关掉窗口,一切归零。而真实的工作场景是跨天、跨周、跨项目的,需要的是持久化记忆。
claude-mem的价值就在于:它不追求“记住所有”,而是追求“记住该记的,忘掉该忘的,需要时能精准找回来”。
1.3 核心能力全景
我把它的核心能力拆成四个层次,方便你建立整体认知:
| 能力层次 | 具体功能 | 解决的核心问题 |
|---|---|---|
| 记忆抽取 | 从对话中识别关键信息 | 什么值得记 |
| 记忆存储 | 结构化持久化到本地或数据库 | 存在哪、怎么存 |
| 记忆召回 | 按相关性检索并注入上下文 | 怎么找回来 |
| 记忆管理 | 去重、更新、过期、压缩 | 怎么保持记忆库健康 |
这四层是递进关系。抽取做不好,存的就是垃圾;召回做不好,存了也白存;管理做不好,记忆库会越来越臃肿,最后拖垮整个系统。
提示:很多人一上来就关注“怎么存”,其实最难的、最影响效果的是“抽取”和“召回”。存储层用 SQLite 还是向量库,反而是最容易替换的部分。
2. 记忆机制的设计思路与方案选型
2.1 为什么不用纯向量检索
一提到“记忆”,很多人的第一反应是上向量数据库,把所有对话切片、embedding、存进去,需要时做相似度检索。这个方案能用,但claude-mem这类项目通常不会只用向量检索,原因值得说清楚。
纯向量检索有三个硬伤:
- 语义漂移:用户说“上次那个方案”,向量检索可能召回一堆“方案”相关的片段,但未必是“上次”那个。时间、指代、上下文关系,向量表达不好。
- 无法精确匹配:用户问“我上周三提到的那个 API key 叫什么”,向量检索对精确的实体、时间、数字不敏感。
- 缺乏结构:记忆之间是有关系的,比如“项目 A 用了技术 B,技术 B 有个坑 C”。向量检索是扁平的,表达不了这种图结构。
所以成熟的做法是混合检索:向量检索负责语义相似,关键词检索(BM25 之类)负责精确匹配,再加一层结构化过滤(时间、类型、来源)。三者加权融合,效果比单一方案稳得多。
2.2 记忆的分层设计
我在实际项目里总结出一个好用的分层模型,claude-mem的设计思路也基本吻合:
第一层:工作记忆(Working Memory)就是当前会话的上下文,容量小、更新快、随会话结束而清空。这一层不需要持久化,交给模型的原生上下文就行。
第二层:短期记忆(Short-term Memory)最近几次会话的关键信息,比如最近三天的对话摘要、当前正在进行的任务状态。这一层需要持久化,但可以设置过期时间。
第三层:长期记忆(Long-term Memory)稳定的用户偏好、项目背景、领域知识、重要决策记录。这一层长期保留,需要定期整理和压缩。
分层的意义在于:不同层用不同的存储策略、不同的召回优先级、不同的过期规则。如果所有记忆一锅炖,要么召回不准,要么存储爆炸。
2.3 抽取策略:什么该记,什么该忘
这是整个系统里最考验设计功力的地方。我的经验是,抽取要基于“信息熵”和“复用价值”两个维度判断。
高复用价值 + 高信息熵的内容,比如:
- 用户的明确偏好(“我习惯用 TypeScript,不要给我 JavaScript 示例”)
- 项目的关键约束(“这个系统不能停机,所有改动要支持热更新”)
- 重要的决策和原因(“选 PostgreSQL 是因为需要 JSONB 和全文检索”)
- 具体的实体和参数(API 地址、字段名、配置项)
低价值、该过滤掉的内容:
- 寒暄和确认(“好的”“明白了”“谢谢”)
- 重复信息(同一件事说了三遍)
- 临时性、一次性的内容(“帮我把这句话翻译一下”)
实操中,抽取通常用一个轻量 prompt 让模型做判断,输出结构化的 JSON。这里有个技巧:不要只让模型判断“是否重要”,而是让它输出记忆的类型、内容、置信度和建议的过期时间。这样后续管理才有依据。
2.4 存储选型:SQLite 还是向量库
这是被问得最多的问题。我的建议是:起步阶段用 SQLite + 向量扩展,规模上来后再考虑专用向量库。
理由如下:
- SQLite 零运维、单文件、事务可靠,配合
sqlite-vec或sqlite-vss扩展就能做向量检索。 - 记忆数据量在个人使用场景下,通常几千到几万条,SQLite 完全扛得住。
- 结构化字段(时间、类型、来源)和向量字段放一起,查询方便,不用跨系统 join。
- 迁移成本低。真到了需要专用向量库的时候,数据导出再导入就行。
如果你一上来就上分布式向量库,运维复杂度会吃掉你大量精力,而收益在早期几乎为零。这是我踩过的坑,分享给你避雷。
3. 核心实现细节与实操要点
3.1 记忆抽取的 Prompt 设计
抽取质量直接决定整个系统的上限。我用的 prompt 结构大致是这样的(这是基于常见实践的合理补全):
EXTRACT_PROMPT = """ 你是一个记忆抽取器。从下面的对话中,识别出值得长期记住的信息。 判断标准: 1. 用户偏好、习惯、明确要求 2. 项目背景、技术约束、关键决策 3. 具体实体:人名、项目名、API、配置项、参数 4. 未完成的任务和待办事项 不要抽取:寒暄、确认、重复内容、临时性请求。 对每条记忆,输出: - type: preference | fact | decision | task - content: 简洁的陈述句 - confidence: 0-1 - ttl_days: 建议保留天数,0 表示永久 对话内容: {dialogue} 以 JSON 数组输出。 """这里有几个关键点:
第一,类型划分要克制。类型太多,模型判断容易乱;类型太少,后续召回没法做结构化过滤。我试过 4 到 6 个类型是比较舒服的区间。
第二,让模型输出置信度。后续召回时可以按置信度加权,低置信度的记忆不参与召回,或者只在没有高置信度结果时才用。
第三,TTL 让模型建议,但最终由系统决定。模型对时间的判断不一定准,但它的建议可以作为参考。系统层面再根据类型设置默认 TTL,比如 preference 永久,task 默认 30 天。
3.2 记忆去重与合并
不做去重,记忆库会迅速膨胀。用户每次说“我用 TypeScript”,都存一条,一个月后就有几十条重复。
去重的策略分两步:
第一步:精确去重。对记忆内容做归一化(去空格、统一大小写、同义词替换),然后做哈希,哈希相同直接丢弃。
第二步:语义去重。对归一化后的内容做 embedding,计算余弦相似度。相似度超过阈值(我一般设 0.92)的,判定为重复或高度相似。
对于高度相似但不完全相同的记忆,做合并而不是简单丢弃。合并规则:
- 同类型、同主体的记忆,保留信息量更大的那条。
- 如果新记忆包含旧记忆没有的细节,用新记忆替换旧记忆,但保留旧记忆的创建时间。
- 冲突的记忆(比如用户先说“用 MySQL”,后说“改用 PostgreSQL”),保留新的,旧的标记为 superseded,不删除但不再召回。
注意:冲突记忆不要直接删。用户可能改主意改回来,保留历史能避免“记忆丢失”的尴尬。标记状态比物理删除更稳妥。
3.3 召回策略:怎么找得准
召回是整个系统里最影响体验的环节。我的做法是三路召回 + 重排序:
第一路:向量召回。把查询和记忆都做 embedding,算余弦相似度,取 Top-K。
第二路:关键词召回。用 BM25 或简单的关键词匹配,处理实体、数字、专有名词。
第三路:结构化召回。按类型、时间、来源过滤。比如用户问“我上次说的那个偏好”,就优先召回 type=preference 且时间较近的记忆。
三路结果合并后,用一个重排序模型(或者简单的加权公式)打分。我的加权公式大致是:
final_score = 0.5 * vector_score + 0.3 * keyword_score + 0.2 * recency_score权重不是固定的,要根据场景调。技术问答场景,关键词权重可以高一点;闲聊场景,向量权重高一点。
召回数量控制也很关键。我一般召回 5 到 10 条,总 token 控制在 1000 以内。召回太多,反而稀释注意力;召回太少,可能漏掉关键信息。
3.4 上下文注入的格式
召回的记忆怎么塞回 prompt,也有讲究。我试过几种格式,最后固定用这种:
[相关记忆] - (preference, 2024-01-15) 用户偏好使用 TypeScript,不喜欢 JavaScript 示例。 - (decision, 2024-01-10) 项目选用 PostgreSQL,原因是需要 JSONB 和全文检索。 - (task, 2024-01-20) 待办:完成用户认证模块的单元测试。 [记忆结束]要点:
- 带类型和时间,让模型知道这条记忆的性质和新鲜度。
- 用列表不用段落,模型对结构化列表的解析更稳。
- 明确边界标记,避免模型把记忆和当前对话混淆。
- 按相关性排序,最相关的放最前面。
我实测下来,这种格式比纯文本拼接的召回准确率高不少,模型也更少出现“张冠李戴”的情况。
4. 完整实操流程与关键环节
4.1 环境准备与依赖安装
假设你用 Python 做实现,基础环境这样搭:
python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install anthropic sqlite-vec numpy如果你要用本地 embedding 模型,再加:
pip install sentence-transformers选sqlite-vec而不是chromadb或faiss,理由是:单文件、零服务、和 SQLite 原生集成,个人项目和小团队用起来最省心。等数据量真的上来了再换也不迟。
4.2 数据库表结构设计
我用的表结构大致如下,兼顾结构化和向量检索:
CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, content TEXT NOT NULL, content_hash TEXT UNIQUE, confidence REAL DEFAULT 1.0, source_session TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMP, status TEXT DEFAULT 'active' ); CREATE VIRTUAL TABLE memory_vectors USING vec0( memory_id INTEGER PRIMARY KEY, embedding FLOAT[768] );几个设计决策的解释:
content_hash做精确去重,加 UNIQUE 约束,插入时冲突直接忽略。status字段管理记忆生命周期,active / superseded / expired 三态。expires_at为空表示永久,有值则到期后由清理任务处理。- 向量单独一张虚拟表,通过 memory_id 关联,查询时 join。
4.3 记忆写入流程
完整的写入流程分五步:
- 对话结束触发:会话结束时,把整段对话传给抽取器。
- 抽取:调用抽取 prompt,拿到结构化记忆列表。
- 归一化与去重:对每条记忆做归一化,算哈希,查库判断是否已存在。
- 语义去重:对通过精确去重的记忆做 embedding,和库中同类型记忆比对相似度。
- 写入:新记忆插入,相似记忆按合并规则处理,冲突记忆标记旧记录。
这里有个实操技巧:写入不要同步做,用队列异步处理。抽取和 embedding 都要调模型,同步做会阻塞主流程。我一般用一个简单的后台任务队列,会话结束后丢进去,用户无感知。
4.4 记忆召回流程
召回流程分四步:
- 查询改写:把用户当前输入改写成适合检索的形式。比如“上次那个方案怎么样了”改写成“方案 进度 状态”。
- 三路召回:向量、关键词、结构化并行执行。
- 融合重排:按加权公式打分,取 Top-K。
- 注入上下文:按固定格式拼接到 system prompt 或用户消息前。
查询改写这一步很多人会忽略,但它对召回质量影响很大。用户的自然语言输入往往包含大量指代和省略,直接拿去检索效果很差。用一个轻量 prompt 做改写,成本很低,收益很高。
4.5 记忆清理与压缩
记忆库需要定期维护,否则会越来越臃肿。我设置三个清理任务:
过期清理:每天跑一次,把expires_at已过期的记忆标记为 expired。
低频清理:统计每条记忆的召回次数,超过 90 天没被召回过的,降权或标记为 archived。
压缩合并:对同类型、同主题的多条记忆,用模型做一次摘要合并,把多条压成一条。比如用户分五次说了五个偏好,可以合并成一条“用户偏好:1... 2... 3...”。
压缩这一步要谨慎,合并后信息密度高了,但可能丢失细节。我的做法是:合并后的记忆保留原始记忆的 ID 列表,需要时可以回溯。
5. 常见问题与排查技巧实录
5.1 召回不准的排查思路
召回不准是最常见的问题,排查要按顺序来:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全召回不到 | 记忆没写进去 / 索引没建 | 直接查库看数据 |
| 召回了无关内容 | embedding 质量差 / 阈值太低 | 看相似度分数分布 |
| 该召回的没召回 | 查询改写有问题 / 权重不合理 | 打印三路召回结果 |
| 召回内容过时 | TTL 没生效 / 冲突没处理 | 检查 status 字段 |
我的经验是,80% 的召回问题出在查询改写和 embedding 质量上,而不是检索算法本身。先把这两块调好,再考虑换检索方案。
5.2 记忆冲突的处理
用户改主意是常态。处理冲突的原则是:新记忆优先,旧记忆保留但降权。
具体做法:写入新记忆时,检索同类型、同主体的旧记忆,如果语义冲突(比如一个说用 A,一个说用 B),把旧记忆 status 改为 superseded,新记忆正常写入。召回时只召回 active 状态的记忆。
但有个例外:如果旧记忆的置信度明显高于新记忆(比如旧的是用户明确强调的,新的是随口一提),可以保留旧记忆,把新记忆标记为 pending,等下次确认。
5.3 性能优化的几个关键点
记忆系统用久了会变慢,优化点主要在三个地方:
第一,embedding 缓存。同一条记忆不要重复算 embedding,算一次存起来。查询的 embedding 也可以做短期缓存。
第二,向量索引。SQLite 的向量检索在数据量超过几万条后会明显变慢。这时候要么加 IVF 索引,要么迁移到专用向量库。
第三,召回数量控制。不要为了“保险”召回几十条,token 成本和延迟都会上去。5 到 10 条是甜点区。
5.4 我踩过的几个坑
坑一:抽取太激进。一开始我把阈值设得很低,什么都往库里存,结果记忆库一周就上万条,召回全是噪音。后来把抽取标准收紧,只存高价值信息,效果反而好了。
坑二:忽略时间维度。早期召回只看语义相似度,不看时间。结果用户问“我最近在做什么”,召回的全是半年前的旧事。加上 recency 权重后,体验明显改善。
坑三:没有做记忆隔离。不同项目、不同场景的记忆混在一起,互相干扰。后来加了 source 字段,召回时按来源过滤,干净多了。
坑四:同步写入阻塞主流程。一开始写入是同步的,每次会话结束要等好几秒。改成异步队列后,用户完全无感知。
提示:记忆系统的调优是个持续过程,不要指望一次调好。建议加个日志,记录每次召回的结果和用户反馈,用数据驱动优化。
6. 扩展方向与个人体会
6.1 可以继续深挖的几个方向
claude-mem这类项目,基础版本跑通后,有几个方向值得继续投入:
记忆图谱化。把扁平的记忆变成图结构,记忆之间建立关联。比如“项目 A”关联“技术 B”,“技术 B”关联“坑 C”。召回时可以做图遍历,找到间接相关的记忆。
多模态记忆。现在主要处理文本,未来可以扩展到图片、文件、代码片段。比如用户上传的设计稿,也可以作为记忆存储和召回。
记忆共享与协作。团队场景下,多个人的记忆可以共享。比如 A 记录的决策,B 也能召回。这需要解决权限和冲突问题,但价值很大。
主动记忆。现在的记忆是被动召回,未来可以让模型主动判断“这个问题我需要查一下记忆”,然后自己去检索。这更接近人类的记忆机制。
6.2 我个人的使用体会
用了一段时间下来,我最大的感受是:记忆系统的价值不在于“记住多少”,而在于“忘得对不对”。
一个什么都记的系统,和一个什么都不记的系统,体验一样差。真正好用的记忆系统,是知道什么该记、什么该忘、什么时候该想起来。这背后是对场景的深刻理解,而不是堆技术。
另外,不要过度设计。我见过太多人一上来就搞分布式向量库、图数据库、多级缓存,结果基础功能都没跑通。先用 SQLite 把闭环跑通,验证价值,再逐步优化。这是我踩过坑之后最想分享的一条经验。
最后分享一个小技巧:给记忆加一个“手动标记”入口。用户觉得某条信息重要,可以手动标记为“永久记忆”,系统优先保留。这个功能实现简单,但用户感知很强,能显著提升信任感。