1. 项目概述与核心定位
1.1 这个项目到底在解决什么问题
claude-mem 这个标题,从字面拆解来看,核心落在两个词上:claude和mem。mem 是 memory 的缩写,也就是记忆。合在一起,它指向的是一个非常具体且长期困扰大模型应用开发者的痛点——如何让 Claude 这类对话式 AI 拥有跨会话的持久记忆能力。
我先把这个问题的背景说清楚。用过 Claude 或者其他对话式 AI 的人都有一个共同体验:每次开启一个新对话,模型就像失忆了一样,完全不记得你上次跟它聊过什么。你上次告诉它你的项目架构、你的代码风格偏好、你正在排查的那个 bug 的上下文,新开一个窗口,全部归零。对于一次性问答这没什么,但如果你把 Claude 当成一个长期的编程助手、写作搭档或者知识管理工具,这种"每次都要重新自我介绍"的体验就非常割裂。
claude-mem 要做的,就是给 Claude 装上一个"外挂大脑"。它通过一套机制,把对话中产生的关键信息持久化存储下来,在后续对话中按需召回,让 Claude 表现得像真的"记得你"。这不是简单的把聊天记录塞回上下文——那样 token 消耗爆炸且噪音极大——而是要有选择地提取、压缩、存储、检索。
1.2 适合谁来研究和使用
这个项目不是给纯聊天用户准备的。它的目标人群很明确:
- 重度 Claude 使用者:每天用 Claude 写代码、做技术方案、管理项目的人,跨会话记忆能省下大量重复沟通成本。
- AI 应用开发者:想在自己的产品里集成"记忆"能力的工程师,claude-mem 提供了一套可参考的架构思路。
- 知识工作者:用 Claude 做研究、写作、整理资料的人,希望 AI 能记住自己的知识体系和偏好。
- 对 RAG 和向量检索感兴趣的实践者:claude-mem 本质上是 RAG 思路在"个人记忆"场景下的一个具体落地。
如果你只是偶尔问问 Claude 天气或者让它翻译一段话,那这个项目对你价值不大。但只要你有"持续和同一个 AI 协作"的需求,它就值得你花时间研究。
1.3 核心能力全景
在深入细节之前,我先把 claude-mem 这类项目通常具备的核心能力列出来,让你有个整体认知。需要说明的是,以下能力是基于"一个成熟的对话记忆系统"的常见实践总结出来的,具体实现可能因版本而异:
| 能力模块 | 作用 | 关键技术点 |
|---|---|---|
| 记忆提取 | 从对话中识别值得记住的信息 | 摘要生成、实体识别、重要性打分 |
| 记忆存储 | 把提取的信息持久化 | 向量数据库、结构化存储、文件系统 |
| 记忆检索 | 在新对话中召回相关记忆 | 语义相似度、关键词匹配、时间衰减 |
| 记忆注入 | 把召回的记忆喂给模型 | 上下文拼接、prompt 工程、token 预算控制 |
| 记忆管理 | 增删改查和生命周期管理 | 去重、合并、过期策略、隐私清理 |
这五个模块构成了一个完整的记忆闭环。理解了这张表,后面所有的技术细节你都能找到对应的位置。
2. 记忆系统的整体架构设计思路
2.1 为什么不能简单粗暴地存聊天记录
很多人第一反应是:记忆嘛,把聊天记录存下来,下次全塞回去不就行了?这个方案在理论上可行,实践中会立刻撞墙。
第一个问题是token 成本。Claude 的上下文窗口虽然大,但你把过去几十次对话全塞进去,token 消耗是线性增长的,费用会迅速失控。而且大部分历史信息对新对话是无关的,属于纯噪音。
第二个问题是注意力稀释。就算你不在乎钱,把海量无关信息塞进上下文,会显著降低模型对真正相关信息的注意力。这在业界被称为"lost in the middle"现象——上下文中间部分的信息容易被模型忽略。
第三个问题是信息质量。原始聊天记录里充斥着"好的""嗯""让我想想"这类无价值内容,真正有价值的信息(比如"我们的数据库用的是 PostgreSQL 15")淹没在噪音里。
所以 claude-mem 这类系统的核心设计哲学是:记忆不是存储,而是提炼。它要做的不是"记住所有话",而是"记住关键事实和偏好",并且在需要的时候精准召回。
2.2 分层记忆架构
一个设计良好的记忆系统通常采用分层结构,我把它拆成三层来讲:
第一层:工作记忆(Working Memory)
这就是当前对话的上下文窗口。它容量有限,但访问速度最快,模型可以直接"看到"。claude-mem 需要管理的是:哪些长期记忆应该被提升到这一层。
第二层:短期记忆(Short-term Memory)
存储最近几次会话的摘要。比如你昨天和 Claude 讨论了一个 bug,今天继续,系统会把昨天的对话摘要作为短期记忆注入。这一层的特点是时效性强、细节适中。
第三层:长期记忆(Long-term Memory)
存储经过提炼的、跨会话稳定的信息。比如你的技术栈偏好、项目背景、常用代码风格。这一层是持久化的,通过向量检索按需召回。
这种分层的好处是:不同时效性、不同重要性的信息用不同的策略管理,避免"一刀切"带来的成本和效果问题。
2.3 存储选型:向量数据库还是文件系统
这是实操中必须做的第一个关键决策。我对比一下常见方案:
| 方案 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 纯文件(JSON/Markdown) | 简单、可读、易调试、零依赖 | 检索能力弱、规模大了性能差 | 个人使用、记忆量小 |
| 向量数据库(如 Chroma、Qdrant) | 语义检索强、可扩展 | 部署复杂、需要 embedding 模型 | 记忆量大、需要精准召回 |
| 混合方案 | 兼顾可读性和检索能力 | 实现复杂度高 | 生产级应用 |
我的经验是:个人使用从纯文件起步,遇到检索瓶颈再上向量库。很多人一上来就搭向量数据库,结果发现自己总共就几百条记忆,用 grep 都能搜出来,纯属过度工程。claude-mem 如果面向个人用户,大概率会提供轻量方案;如果面向团队,向量检索就是刚需。
2.4 记忆的写入时机
什么时候该把信息写入记忆?这个决策点很关键。常见策略有三种:
- 实时写入:每轮对话结束就提取一次。优点是及时,缺点是频繁调用模型做摘要,成本高。
- 会话结束写入:一次对话结束后统一提取。平衡了成本和及时性,是主流做法。
- 手动触发写入:用户显式说"记住这个"。最省成本,但依赖用户习惯。
实际系统往往是组合策略:会话结束自动提取,同时支持用户手动标记重要信息。这样既保证了基础体验,又给了用户精确控制的手段。
3. 核心细节解析与实操要点
3.1 记忆提取:怎么判断什么值得记
记忆提取是整个系统的入口,也是最难做好的环节。提取质量直接决定了后续所有环节的上限。
我常用的提取维度有这么几个:
事实性信息:明确陈述的客观事实。比如"我的项目用 TypeScript 5.3"、"部署环境是 Ubuntu 22.04"。这类信息优先级最高,因为它们是稳定的、可复用的。
偏好性信息:用户的习惯和喜好。比如"我喜欢函数式写法"、"回复尽量简洁"、"代码注释用中文"。这类信息决定了 AI 的交互风格,价值极高。
任务上下文:正在进行的工作。比如"正在重构用户认证模块"、"这个 bug 和缓存失效有关"。这类信息时效性强,需要标注时间。
关系性信息:实体之间的关系。比如"项目 A 依赖库 B"、"同事张三负责后端"。这类信息构建了知识图谱。
提取的实现方式,通常是让模型对对话做一次结构化摘要。prompt 大致长这样:
请从以下对话中提取值得长期记忆的信息,按以下 JSON 格式输出: { "facts": ["客观事实列表"], "preferences": ["用户偏好列表"], "context": ["任务上下文列表"], "importance": 1-10 的重要性评分 } 只提取跨会话仍然有价值的信息,忽略一次性的、临时的内容。 对话内容: {conversation}注意:提取 prompt 里一定要强调"跨会话仍然有价值",否则模型会把大量临时信息也提取出来,导致记忆库迅速膨胀且充满噪音。
3.2 记忆去重与合并
记忆库用久了必然出现重复和冲突。比如你三次对话都提到"我用 PostgreSQL",就会产生三条几乎一样的记忆。不去重的话,检索时会召回一堆冗余信息,浪费 token。
去重的常见做法是:写入前先做一次相似度检索,如果发现已有高度相似的记忆,就选择更新而不是新增。更新的策略可以是:
- 覆盖:新信息直接替换旧的(适合偏好类,用户改主意了)
- 合并:把新信息补充进旧记忆(适合事实类,信息更完整了)
- 并存:都保留,标注时间(适合有冲突且无法判断哪个更新的情况)
冲突处理是个难点。比如你之前说"用 MySQL",后来说"迁移到 PostgreSQL 了"。系统需要识别这是"状态变更"而不是"矛盾",应该以最新的为准。实践中可以给记忆加时间戳,检索时优先返回最新的。
3.3 检索策略:语义还是关键词
检索环节决定了"能不能在对的时候想起对的事"。主流方案是语义检索,也就是把记忆和当前对话都转成向量,算余弦相似度,取 top-k。
但纯语义检索有短板。比如你搜"数据库配置",语义检索可能召回"我用 PostgreSQL"这条记忆,但如果记忆里写的是"数据存储层选型",语义相似度可能不够高。这时候关键词检索(BM25 之类)就能补上。
所以成熟方案通常是混合检索:语义检索和关键词检索各取一批结果,然后用 RRF(Reciprocal Rank Fusion)之类的算法融合排序。这样既能抓住语义相关,又不漏掉字面匹配。
检索数量也要控制。top-k 取太大,噪音多;取太小,可能漏掉关键信息。我的经验值是 k=5 到 10 之间,具体看记忆库规模。如果记忆库很大,可以先粗排取 50 条,再精排取 5 条。
3.4 记忆注入的 token 预算
召回的记忆最终要拼进 prompt 喂给模型。这里有个硬约束:token 预算。你不能把所有召回的记忆都塞进去,得给当前对话留足空间。
我的做法是给记忆分配一个固定的 token 预算,比如总上下文的 20%。然后按相关性排序,从高到低填充,填满为止。如果单条记忆太长,就先做一次压缩再注入。
注入的位置也有讲究。通常放在 system prompt 之后、用户消息之前,用明确的分隔符标出来:
[相关记忆] - 用户项目使用 TypeScript 5.3 和 PostgreSQL 15 - 用户偏好函数式编程风格,代码注释用中文 - 当前正在重构用户认证模块 [记忆结束] [用户消息] 帮我看看这段认证代码...提示:记忆注入时一定要加清晰的分隔标记,否则模型可能把记忆内容和当前指令混淆,产生奇怪的行为。
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
假设我们用 Python 来实现一个 claude-mem 的最小可用版本。先明确技术栈:
- Python 3.10+
- 一个向量检索库(轻量起步用
sentence-transformers+faiss,或者直接用chromadb) - 一个本地存储(SQLite 或 JSON 文件)
- Claude API 的 SDK
安装依赖:
pip install anthropic chromadb sentence-transformers如果你只是想快速验证,chromadb是最省事的选择,它自带持久化和检索,不用自己搭向量索引。
4.2 记忆存储的数据结构设计
先定义记忆的数据结构。我用一个 dataclass 来表示:
from dataclasses import dataclass, field from datetime import datetime from typing import Literal @dataclass class Memory: id: str content: str # 记忆正文 category: Literal["fact", "preference", "context"] importance: int # 1-10 created_at: datetime updated_at: datetime source_session: str # 来源会话 ID embedding: list = field(default=None) # 向量,延迟计算这个结构里,category决定了检索时的权重策略,importance决定了记忆的保留优先级,时间戳用于处理冲突和过期。
4.3 写入流程的完整实现
写入流程分四步:提取、去重、存储、索引。
第一步,调用 Claude 做提取。这里要注意,提取用的模型不需要太强,用 Haiku 这类小模型就够了,省钱又快:
import anthropic import json client = anthropic.Anthropic() EXTRACT_PROMPT = """从以下对话中提取值得长期记忆的信息。 只提取跨会话仍有价值的内容,忽略临时性、一次性的信息。 按 JSON 输出: { "memories": [ {"content": "...", "category": "fact|preference|context", "importance": 1-10} ] } 对话: {conversation} """ def extract_memories(conversation: str) -> list[dict]: resp = client.messages.create( model="claude-3-haiku-20240307", max_tokens=1024, messages=[{ "role": "user", "content": EXTRACT_PROMPT.format(conversation=conversation) }] ) text = resp.content[0].text # 容错处理:模型可能带 markdown 代码块 text = text.strip().removeprefix("```json").removesuffix("```").strip() return json.loads(text)["memories"]第二步,去重。对每条新记忆,先检索已有记忆,如果相似度超过阈值(比如 0.9),就判定为重复,走更新逻辑:
def dedupe_and_store(new_mem: dict, collection): results = collection.query( query_texts=[new_mem["content"]], n_results=3 ) # 检查是否有高度相似的 if results["distances"] and results["distances"][0]: top_dist = results["distances"][0][0] if top_dist < 0.1: # 距离越小越相似,阈值需按实际调整 # 更新已有记忆 existing_id = results["ids"][0][0] collection.update( ids=[existing_id], documents=[new_mem["content"]], metadatas=[{"category": new_mem["category"], "importance": new_mem["importance"]}] ) return "updated" # 新增 collection.add( ids=[generate_id()], documents=[new_mem["content"]], metadatas=[{"category": new_mem["category"], "importance": new_mem["importance"]}] ) return "added"注意:相似度阈值这个参数非常依赖你的 embedding 模型和实际数据分布,不要照搬别人的值。建议先跑一批真实数据,观察相似记忆的距离分布,再定阈值。
第三步和第四步在 ChromaDB 里是合并的,add和update会自动处理索引。
4.4 检索与注入的实现
检索环节,我建议用混合策略。先做语义检索,再做一次关键词过滤:
def retrieve_memories(query: str, collection, top_k: int = 5): results = collection.query( query_texts=[query], n_results=top_k * 2 # 多取一些,后面重排 ) memories = [] for i, doc in enumerate(results["documents"][0]): meta = results["metadatas"][0][i] dist = results["distances"][0][i] # 综合打分:相似度 + 重要性权重 score = (1 - dist) * 0.7 + (meta["importance"] / 10) * 0.3 memories.append({"content": doc, "score": score, **meta}) # 按综合分排序,取 top_k memories.sort(key=lambda x: x["score"], reverse=True) return memories[:top_k]这里我加了一个重要性权重。原因是纯相似度检索会忽略"这条记忆本身重不重要"。一条 importance=10 的偏好信息,即使和当前 query 相似度中等,也应该优先于一条 importance=3 的临时上下文。这个权重比例(0.7/0.3)是我实测下来比较平衡的值,你可以根据自己场景调整。
注入环节,把检索结果拼成文本:
def build_prompt_with_memory(user_message: str, memories: list) -> str: if not memories: return user_message mem_text = "\n".join(f"- {m['content']}" for m in memories) return f"""[相关记忆] {mem_text} [记忆结束] [用户消息] {user_message}"""4.5 会话结束的触发逻辑
最后一步是把整个流程串起来。会话结束时,把完整对话传给提取函数,然后逐条去重存储:
def on_session_end(conversation: str, collection): memories = extract_memories(conversation) stats = {"added": 0, "updated": 0} for mem in memories: result = dedupe_and_store(mem, collection) stats[result] += 1 print(f"本次会话:新增 {stats['added']} 条,更新 {stats['updated']} 条") return stats这套流程跑通之后,你就有了一个能用的记忆系统。实测下来,几百条记忆的规模下,检索延迟在几十毫秒级别,完全不影响交互体验。
5. 常见问题与排查技巧实录
5.1 记忆库膨胀失控怎么办
这是最常见的问题。跑了一两周,发现记忆库里有几千条,检索出来的全是噪音。原因通常是提取环节太宽松,把临时信息也存了。
排查思路:先抽样看最近 50 条记忆,统计有多少是"一周后就没价值"的。如果超过 30%,说明提取 prompt 需要收紧。我的做法是在提取 prompt 里加一条硬规则:"如果这条信息只对当前这次对话有意义,不要提取。"
另一个手段是加过期策略。给 context 类记忆设置 TTL(比如 7 天),到期自动归档。fact 和 preference 类不设过期,因为它们长期有效。
5.2 检索召回不相关怎么调
如果发现召回的记忆和当前问题八竿子打不着,按这个顺序排查:
| 排查项 | 检查方法 | 常见原因 |
|---|---|---|
| embedding 模型 | 手动算两条相关文本的相似度 | 模型不适合中文或领域不匹配 |
| 相似度阈值 | 看召回结果的距离分布 | 阈值太松,召回了低相关项 |
| 记忆质量 | 抽样看记忆正文 | 记忆本身表述模糊,语义不清 |
| query 构造 | 看实际用于检索的文本 | 直接把整段对话当 query,噪音太大 |
最后一条特别容易被忽略。很多人拿整个用户消息去检索,但用户消息可能很长很杂。更好的做法是先用模型把用户消息压缩成一个"检索意图",再拿这个意图去检索。
5.3 记忆冲突导致模型行为异常
有时候模型会表现出矛盾行为,比如一会儿用你旧的代码风格,一会儿用新的。这通常是记忆库里同时存在新旧两条冲突记忆,检索时都召回了。
解决办法是引入冲突检测。写入时如果发现新记忆和旧记忆在同一主题上冲突(比如都是关于"数据库选型"),就标记旧记忆为"已废弃",检索时过滤掉。实现上可以给记忆加一个status字段,检索时只查status="active"的。
5.4 隐私与敏感信息处理
记忆系统会长期保存用户信息,隐私问题必须重视。我的建议是:
- 提取阶段就过滤掉明显的敏感信息(密码、密钥、个人身份信息)
- 提供一键清空记忆的功能
- 记忆存储加密,尤其是云端存储时
- 明确告知用户哪些信息会被记住
提示:在提取 prompt 里加一句"不要提取任何密码、密钥、token 等敏感凭证信息",能挡掉大部分意外泄露。
5.5 性能优化经验
记忆量大了之后,检索会变慢。几个实测有效的优化:
- 分层检索:先按 category 粗筛,再在子集里做向量检索
- 缓存 embedding:同一条记忆的向量只算一次,存起来复用
- 批量写入:会话结束时批量处理,而不是每条都单独调 API
- 异步提取:提取不阻塞用户交互,后台慢慢跑
我在一个记忆量约 5000 条的项目里,用分层检索把平均检索延迟从 200ms 降到了 40ms 左右,效果很明显。
6. 记忆系统的扩展方向
6.1 从被动记忆到主动记忆
现在的系统是"用户问什么,检索什么",属于被动召回。更进一步的方向是主动记忆:系统根据当前对话上下文,主动判断"这里可能需要某条记忆",提前注入。比如用户提到"上次那个 bug",系统应该主动去检索相关的历史 bug 记录,而不是等用户说清楚。
这需要在对话过程中做意图识别,判断哪些时刻需要触发记忆检索。实现复杂度更高,但体验提升明显。
6.2 记忆的可视化与管理界面
纯命令行操作记忆库对普通用户不友好。做一个简单的 Web 界面,能查看、搜索、编辑、删除记忆,会大大提升可用性。技术上用 FastAPI + 一个前端框架就能搞定,核心是把记忆的增删改查暴露成 API。
6.3 多用户记忆隔离
如果要做成多人使用的工具,记忆隔离是必须的。每个用户有独立的记忆空间,检索时只查自己的。实现上就是在存储层加一个user_id维度,所有查询都带上这个过滤条件。ChromaDB 支持 metadata 过滤,做起来不难。
6.4 记忆的导入导出
用户可能想把自己的记忆迁移到别的工具,或者备份。提供标准的导入导出格式(比如 JSON)是个好习惯。导出时注意脱敏,导入时注意去重。
我在实际使用中最大的体会是:记忆系统的价值不在于技术多复杂,而在于提取质量。同样一套检索和存储架构,提取 prompt 写得好不好,效果能差出好几倍。花时间打磨提取逻辑,比折腾向量数据库选型划算得多。另外,别一上来就追求大而全,先用最小可用版本跑起来,积累真实数据,再根据实际问题迭代,这样每一步优化都有据可依,不会陷入过度设计的坑里。