claude-mem 记忆系统拆解:从写入到召回,给大模型外挂持久化记忆

发布时间:2026/10/28 6:50:52
claude-mem 记忆系统拆解:从写入到召回,给大模型外挂持久化记忆

1. 项目缘起与核心定位

第一次看到claude-mem这个名字,我的直觉是:这大概率是一个给 Claude 系列模型做“记忆层”的项目。事实也确实如此。它要解决的是一个所有长期跟大模型打交道的人都会遇到的痛点——模型本身没有跨会话记忆。你这次跟它聊完一个项目的架构设计,关掉窗口,下次再开,它对你、对你的项目、对你上次的决策一无所知,一切从零开始。

claude-mem的核心定位,就是给 Claude 这类对话式模型外挂一套持久化记忆系统。它让模型能够记住用户的偏好、历史对话中的关键事实、项目上下文,并在后续对话中自动召回这些信息。说白了,就是给“金鱼脑”装上一个可检索、可管理、可演进的长期记忆库。

这个项目适合谁?三类人最该关注。第一类是重度使用 Claude 做开发或写作的从业者,你每天要开很多会话,重复交代背景非常浪费时间。第二类是想自己搭建 AI 助手产品的开发者,你需要一套可复用的记忆中间件,而不是每次从零造轮子。第三类是对 RAG、向量检索、上下文工程感兴趣的技术人,claude-mem是一个非常好的学习样本,它把“记忆”这件事拆得足够清晰。

我先把结论放前面:claude-mem的价值不在于它用了多前沿的算法,而在于它把记忆的写入、存储、召回、注入这条链路工程化地串了起来,并且针对 Claude 的交互特点做了适配。这套思路你完全可以迁移到其他模型上。下面我会从设计思路、核心机制、实操落地、问题排查几个维度,把它彻底拆开讲。

2. 记忆系统的整体设计与思路拆解

2.1 为什么“记忆”不能只靠加长上下文

很多人第一反应是:现在模型上下文窗口都到 20 万 token 了,直接把历史对话全塞进去不就行了?这个想法在小规模场景下能跑,但一旦认真用就会崩。原因有三个。

第一是成本。上下文越长,每次请求的 token 消耗越大,而且是线性甚至超线性增长。你每天几十次对话,每次都带上几万 token 的历史,账单会非常难看。第二是注意力稀释。上下文里塞了大量无关历史,模型对当前问题的注意力会被分散,回答质量反而下降,这就是常说的“lost in the middle”现象。第三是信息组织问题。历史对话是流水账,而真正有价值的是提炼后的事实和偏好,原始对话里充斥着寒暄、试错、废弃方案,直接塞进去是噪音。

所以claude-mem的思路不是“记住所有对话”,而是“提炼并存储关键记忆,按需召回”。这是一个典型的检索增强生成(RAG)思路在记忆场景的应用,但比通用 RAG 更聚焦,因为它处理的是“关于用户和对话本身”的元信息。

2.2 记忆的分层模型

我在研究这个项目时,发现它把记忆做了分层处理,这个设计很关键。大致可以分成三层:

  • 短期记忆:当前会话内的上下文,就是普通的对话历史,会话结束即丢弃或压缩。
  • 长期记忆:跨会话持久化的事实、偏好、决策,存在数据库里,需要时召回。
  • 工作记忆:针对当前任务临时组装起来的一段上下文,是短期记忆和召回回来的长期记忆的混合体,最终注入给模型。

这个分层的好处是职责清晰。短期记忆负责连贯性,长期记忆负责持久性,工作记忆负责“这一次请求到底给模型看什么”。很多自建记忆系统失败,就是因为把这三层混在一起,导致要么什么都记、要么什么都记不住。

2.3 存储选型背后的考量

claude-mem这类项目通常会用向量数据库 + 关系型数据库的组合。为什么不是只用一种?

向量库(比如 Chroma、Qdrant、pgvector)负责语义检索。当用户说“上次我们讨论的那个缓存方案”,系统需要按语义相似度找到相关记忆,而不是按关键词精确匹配。这是向量检索的强项。

关系库(比如 SQLite、PostgreSQL)负责结构化元数据。每条记忆都有时间戳、来源会话 ID、记忆类型(事实/偏好/决策)、置信度等字段。你需要按时间过滤、按类型筛选、做去重和更新,这些是关系库的强项。

提示:如果你只是个人使用、数据量不大,直接用 SQLite 加一个向量扩展(如 sqlite-vec)就能跑起来,没必要一上来就上独立向量服务。运维复杂度是自建系统最大的隐性成本。

选型时我踩过的坑是:过早引入重型向量库,结果发现 90% 的查询其实用元数据过滤加简单相似度就够了。先用最简单能跑的方案,等数据量真的上来了再换,这是我一贯的建议。

3. 核心机制解析与实操要点

3.1 记忆的写入:什么时候记、记什么

写入是记忆系统的第一道关。记太多会污染,记太少会失忆。claude-mem的常见做法是在对话轮次结束后,用一个提炼步骤把原始对话转成结构化记忆。

提炼通常有两种触发方式。一种是每轮都提炼,实时性好但成本高。另一种是会话结束时批量提炼,成本低但可能丢失会话中途的关键信息。我实测下来,比较稳的方案是混合式:对话中如果检测到明确的“记住这个”“以后都这样”之类的指令,立即写入;其余内容在会话结束或达到一定轮次后批量提炼。

提炼出来的记忆一般长这样:

{ "type": "preference", "content": "用户偏好用 Python 而非 JavaScript 写脚本", "source_session": "sess_20240115_abc", "timestamp": "2024-01-15T10:32:00Z", "confidence": 0.85, "tags": ["language", "coding"] }

这里有几个实操要点。confidence 字段很重要,它让系统在召回时能排序,也能在记忆冲突时决定取舍。tags 用于快速过滤,避免每次都做全量向量检索。source_session 用于溯源,当记忆出错时你能追到是哪次对话产生的。

注意:提炼步骤本身也是一次模型调用,会消耗 token。如果你的对话量很大,建议对提炼用的模型做降级(用更小更便宜的模型),因为提炼任务对模型能力要求没那么高。

3.2 记忆的召回:怎么找得准

召回是记忆系统最容易翻车的地方。找不准,模型就会拿到无关信息,回答反而更差。claude-mem的召回一般走多路召回 + 重排的路子。

多路召回指的是同时用几种方式找候选记忆:向量相似度召回一批、按标签过滤召回一批、按时间近因召回一批。然后把这几批合并去重,再用一个重排模型或规则打分排序,取 top-k 注入上下文。

为什么不能只用向量召回?因为向量相似度对“时间”和“类型”是无感的。用户问“我上周说的那个方案”,向量检索可能找出一堆语义相似但时间不对的记忆。加上时间过滤和类型过滤,准确率会明显提升。

召回数量 k 的选择也有讲究。k 太小会漏,k 太大会引入噪音。我的经验是先召回 20 条候选,重排后取 5 到 8 条注入。这个数字不是拍脑袋,是因为注入的记忆会占用上下文预算,5 到 8 条通常能覆盖大部分场景,再多边际收益就递减了。

3.3 记忆的注入:怎么放进 prompt

召回回来的记忆不能直接一股脑塞给模型,需要格式化注入。常见做法是在 system prompt 里加一段“已知信息”区块:

以下是你需要记住的关于用户的信息: - [偏好] 用户偏好用 Python 而非 JavaScript 写脚本 - [事实] 用户的项目使用 PostgreSQL 作为主数据库 - [决策] 上次确定缓存层用 Redis,不用 Memcached

格式化的关键是标注类型和来源,让模型知道每条信息的性质。偏好类信息影响回答风格,事实类信息影响内容准确性,决策类信息影响方案一致性。混在一起不加区分,模型可能会把偏好当成硬约束,或者把过时决策当成当前事实。

提示:注入的记忆要带上时间戳或“截至某时间”的限定,尤其是决策类记忆。否则模型可能把三个月前的决策当成现在的决策,导致方案冲突。

4. 完整实操流程与关键环节实现

4.1 环境准备与依赖安装

假设你要从零搭一套claude-mem风格的记忆系统,我按最小可用版本给你梳理一遍。技术栈选 Python,因为生态最全。

python -m venv venv source venv/bin/activate pip install anthropic chromadb sqlite-vec sentence-transformers

这里anthropic是官方 SDK,chromadb做向量存储,sqlite-vec给 SQLite 加向量能力,sentence-transformers做本地 embedding。为什么用本地 embedding 而不是调 API?因为记忆写入是高频操作,每次调 embedding API 成本和延迟都受不了,本地模型跑一次几毫秒,划算得多。

4.2 数据库表结构设计

先建两张表,一张存记忆元数据,一张存向量。

CREATE TABLE memories ( id TEXT PRIMARY KEY, type TEXT NOT NULL, content TEXT NOT NULL, source_session TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, confidence REAL DEFAULT 0.8, tags TEXT ); CREATE VIRTUAL TABLE memory_vectors USING vec0( memory_id TEXT, embedding FLOAT[384] );

embedding 维度 384 对应的是all-MiniLM-L6-v2这个模型,它体积小、速度快,对记忆这种短文本场景够用。如果你追求更高精度,可以换 768 维的模型,但检索会慢一些。

4.3 记忆写入的实现

写入流程分三步:提炼、去重、落库。

def write_memory(raw_conversation, session_id): # 第一步:用模型提炼结构化记忆 prompt = f"""从以下对话中提炼需要长期记住的信息, 输出 JSON 数组,每条包含 type/content/confidence/tags。 只提炼事实、偏好、决策,忽略寒暄和试错过程。 对话内容:{raw_conversation}""" extracted = call_llm(prompt) memories = parse_json(extracted) # 第二步:去重,避免重复记忆 for mem in memories: similar = search_similar(mem["content"], threshold=0.92) if similar: update_memory(similar[0]["id"], mem) continue # 第三步:落库 mem_id = generate_id() insert_memory(mem_id, mem, session_id) insert_vector(mem_id, embed(mem["content"]))

去重这一步很多人会忽略,结果数据库里堆了一堆“用户喜欢 Python”的重复记忆,召回时全是冗余。相似度阈值设 0.92 左右比较合适,太高会漏掉该合并的,太低会把不同记忆错误合并。

4.4 记忆召回的实现

召回走多路合并。

def recall_memories(query, top_k=8): # 向量召回 query_vec = embed(query) vec_results = vector_search(query_vec, limit=20) # 标签召回 tags = extract_tags(query) tag_results = search_by_tags(tags, limit=10) # 时间召回(近因) recent_results = get_recent(limit=10) # 合并去重 candidates = merge_dedup(vec_results, tag_results, recent_results) # 重排:综合相似度、时间、置信度打分 scored = rerank(candidates, query_vec) return scored[:top_k]

重排的打分公式可以简单点:score = 0.6 * 相似度 + 0.2 * 时间衰减 + 0.2 * 置信度。时间衰减用指数衰减,比如exp(-days/30),让一个月前的记忆权重降到三分之一左右。这个权重不是固定的,你可以根据自己的场景调。

4.5 注入与对话主流程

最后把召回的记忆拼进 prompt,走正常对话流程。

def chat(user_input, session_id): memories = recall_memories(user_input) memory_block = format_memories(memories) system_prompt = f"""你是一个有长期记忆的助手。 以下是关于用户的已知信息: {memory_block} 请自然地运用这些信息,不要生硬复述。""" response = call_claude(system_prompt, user_input) # 会话结束后触发写入 schedule_memory_write(session_id) return response

注意:写入不要放在对话主流程里同步执行,否则每次回复都要等提炼完成,体验很差。用异步任务或后台队列处理,用户感知不到延迟。

5. 常见问题与排查技巧实录

5.1 记忆污染:模型记了不该记的

最常见的坑是提炼步骤把临时信息当成了长期记忆。比如用户随口说“今天先这样”,被记成“用户偏好简短回复”。解决办法是在提炼 prompt 里明确排除条件,并且给记忆加过期机制。事实类记忆可以长期保留,偏好类记忆设一个复核周期,决策类记忆在相关项目结束后标记为归档。

5.2 召回不准:找出来的记忆不相关

排查顺序是这样的:先看 embedding 模型是否适合中文,很多默认模型是英文语料训练的,中文语义区分度差;再看相似度阈值是否合理;最后看重排权重是否偏向错误的方向。我遇到过一次,召回全是时间最近的记忆,原因是时间衰减权重设太高,把相似度压过去了。调权重后立刻正常。

5.3 记忆冲突:新旧信息打架

用户上个月说用 MySQL,这个月说改用 PostgreSQL 了。如果两条记忆都在,模型会困惑。解决办法是写入时做冲突检测:新记忆和旧记忆语义相似但内容矛盾时,把旧记忆标记为 superseded,召回时只取最新的。判断矛盾可以用模型,也可以用简单的规则,比如同一 tag 下的同类记忆只保留最新一条。

5.4 性能问题:召回越来越慢

数据量到几万条后,全量向量检索会变慢。这时候要加索引,向量库一般支持 HNSW 或 IVF 索引,建索引后检索从秒级降到毫秒级。另外标签过滤要走在向量检索前面,先用标签把候选集缩小,再做向量相似度计算,能省很多算力。

问题现象可能原因排查方向解决手段
记忆重复堆积缺少去重检查写入流程加相似度去重,阈值 0.92
召回不相关embedding 不适配测试中文语义区分度换多语言模型
新旧记忆冲突无冲突检测检查同 tag 记忆标记 superseded
检索变慢无向量索引查看数据量建 HNSW 索引
注入后回答变差记忆过多或格式乱检查 top_k 和格式减到 5-8 条,加类型标注

5.5 几个我踩过的实操坑

第一个坑是embedding 和检索用了不同模型。写入时用 A 模型,查询时用 B 模型,向量空间不一致,检索结果全是乱的。这个错误很隐蔽,因为代码不报错,只是结果差。一定要保证写入和查询用同一个 embedding 模型。

第二个坑是记忆内容太长。一条记忆塞了几百字,embedding 被稀释,检索不准。记忆要短,一条一个点,长内容拆成多条。我一般控制在 50 字以内一条。

第三个坑是忽略会话边界。不同项目的记忆混在一起召回,导致回答串味。解决办法是给记忆加 project 或 workspace 字段,召回时按当前项目过滤。

6. 记忆系统的扩展方向

把基础版跑通之后,claude-mem这套思路还能往几个方向扩展。一个是记忆的自动衰减和归档,长期不被召回的记忆自动降权或归档,保持活跃记忆库的精简。另一个是记忆的可视化管理,做一个界面让用户能看到、编辑、删除自己的记忆,这对建立信任很重要,用户得知道系统到底记住了什么。还有一个是多用户隔离,如果你要做成产品,每个用户的记忆必须严格隔离,这涉及到权限和数据分区设计。

我自己在实际使用中最大的体会是:记忆系统的难点从来不是技术,而是产品判断——什么该记、什么该忘、什么时候该用。技术方案网上都能找到,但“记什么”这个决策直接决定了系统好不好用。我的建议是先从最保守的策略开始,只记用户明确要求记的和反复出现的事实,跑一段时间后再逐步放宽,这样比一上来就激进记录要稳得多。