claude-mem 记忆系统设计:抽取、存储与召回三段式实战 1. 从零认识 claude-mem它到底解决什么问题第一次看到claude-mem这个名字很多人会以为它又是一个套壳的对话客户端。其实不是。claude-mem的核心定位是给 Claude 这类大语言模型补上一块“长期记忆”的拼图。用过 Claude 做长期项目的人都有体会每次开新会话它就像失忆一样昨天聊过的架构决策、上周定下的命名规范、上个月踩过的坑统统不记得。你得反复把背景贴进去token 烧得快人还累。claude-mem要干的事情很朴素——把对话里值得留存的信息抽出来存到一个可检索、可管理的地方下次需要的时候再按需喂回给模型。它解决的是“上下文窗口有限”和“跨会话记忆断裂”这两个老大难问题。适合谁来参考三类人最该关注一是拿 Claude 做长期开发辅助的工程师二是做 AI 应用、需要给自家产品加记忆能力的开发者三是喜欢折腾本地知识库、想让 AI 记住自己偏好的重度用户。我先把话说在前面claude-mem不是一个开箱即用的商业产品它更像一套思路加一组可复用的实现。你理解了它的设计逻辑完全可以照着搭一套属于自己的记忆层。下面我会从整体设计、核心细节、实操落地、问题排查四个维度把它拆开揉碎讲清楚。2. 整体设计与思路拆解为什么是“抽取 存储 召回”三段式2.1 记忆系统的本质是一套信息漏斗很多人一上来就想“把整段对话都存下来不就行了”。我试过这条路走不通。原因很简单全量存储等于没存储。你存了一万条对话下次召回的时候怎么挑全塞回上下文窗口直接爆掉随机挑几条大概率挑到废话。所以claude-mem的设计核心是一个漏斗原始对话进来先做抽取把有长期价值的信息决策、偏好、事实、约定拎出来再做存储用结构化方式落盘最后做召回根据当前问题动态挑选最相关的几条。这个三段式不是拍脑袋定的它对应了记忆系统的三个基本矛盾信息过载、检索效率、相关性匹配。抽取解决过载存储解决效率召回解决相关性。任何一环偷懒整个系统就会退化成“存了一堆用不上的东西”。2.2 为什么不做成纯向量数据库方案一提记忆很多人第一反应是上向量库把每句话 embedding 一下存进去查询时做相似度检索。这个方案能用但claude-mem的思路更克制。纯向量方案有两个坑一是 embedding 对“决策类”信息的语义捕捉并不精准比如“我们决定用 PostgreSQL 而不是 MySQL”向量检索可能给你召回一堆讨论数据库的闲聊二是向量库的维护成本不低小项目没必要。claude-mem更倾向于结构化字段 关键词/标签 可选向量的混合方案。每条记忆至少包含内容、类型决策/偏好/事实/待办、时间戳、来源会话、标签。召回时先用标签和类型做粗筛再用关键词或向量做精排。这样既保证了准确率又控制了复杂度。我实测下来对于个人和小团队场景混合方案比纯向量方案召回质量高出一截。2.3 记忆的“写入时机”比“存储格式”更关键这是最容易被忽略的一点。什么时候触发记忆写入如果每轮对话都写噪音太大如果只在会话结束时写可能漏掉中途的关键决策。claude-mem常见的做法是事件驱动 定期整理当检测到“决策性语句”“用户明确偏好”“重要事实陈述”时立即写入同时在会话结束或达到一定轮次时做一次批量整理把零散信息合并、去重、升级为长期记忆。提示写入时机的阈值需要根据你的使用频率调。用得太频繁记忆库会被琐事淹没调得太保守关键信息会漏。建议初期把阈值调低一点宁可多存后期再靠整理环节压缩。2.4 方案选型的取舍逻辑方案优点缺点适用场景全量对话存储实现简单不丢信息检索难窗口压力大几乎不推荐纯向量检索语义匹配强决策类召回不准维护重知识问答类结构化 关键词精准、轻量需要设计字段个人/小团队混合方案兼顾精准与语义实现稍复杂推荐默认选型的核心判断标准就一条你的记忆主要用来“回忆事实”还是“延续决策”。如果是后者结构化字段的权重必须拉高。3. 核心细节解析与实操要点记忆怎么抽、怎么存、怎么取3.1 抽取环节让模型自己判断“什么值得记”抽取是整套系统里最考验 prompt 设计的一环。我的做法是给 Claude 一个明确的抽取指令让它输出结构化 JSON。关键是要把“值得记”的标准写死否则模型会自由发挥。下面是我实际用的一套抽取 prompt 模板你是一个记忆抽取器。请从以下对话片段中提取具有长期价值的信息。 只提取以下四类 1. decision明确的决策或方案选择 2. preference用户的偏好、习惯、约定 3. fact关于项目、环境、人物的稳定事实 4. todo明确的待办或后续动作 输出 JSON 数组每项包含 type、content、tags 三个字段。 如果没有任何值得记录的内容返回空数组 []。 不要提取寒暄、临时性讨论、已被推翻的旧结论。 对话片段 {{conversation}}这套模板我调了大概七八版才稳定。早期版本没写“不要提取已被推翻的旧结论”结果模型把废弃方案也存进去了召回时经常给出过时建议。加上这句之后噪音明显下降。3.2 存储环节字段设计决定召回上限存储不是简单写个 JSON 文件就完事。字段设计直接决定了你后面能不能精准召回。我建议的最小字段集如下id唯一标识建议用时间戳加随机串typedecision / preference / fact / todocontent记忆正文一句话说清tags数组用于粗筛source来源会话 IDcreated_at创建时间updated_at更新时间statusactive / archived / superseded其中status字段特别重要。记忆是会过期的旧决策被新决策取代后不能直接删万一要追溯但也不能继续参与召回。用superseded标记召回时默认过滤掉需要时再手动查。存储介质上个人用 SQLite 完全够查询快、零运维、单文件好备份。团队用可以上 PostgreSQL加个 pgvector 扩展就能兼顾向量检索。别一上来就上重型数据库我见过太多人把精力耗在运维上记忆逻辑反而没打磨好。3.3 召回环节相关性排序的三个维度召回不是“查出来就完事”排序才是灵魂。我用的排序公式大致是三个维度加权类型匹配度当前问题如果是“我们之前怎么定的”decision 类型权重最高标签重合度当前对话的标签和记忆标签的重合数量时间新鲜度越新的记忆权重越高但衰减要平缓不能把三个月前的关键决策直接压没具体权重我一般设成 0.5 / 0.3 / 0.2然后根据实际效果微调。这个没有标准答案你得拿自己的真实对话去测。我建议先跑一周把召回结果和“你期望的结果”对比看哪类记忆老是被漏掉就调高对应维度的权重。注意召回条数不要贪多。我实测下来单次召回 3 到 5 条效果最好。超过 8 条模型反而会被无关信息干扰回答质量下降。这跟人一样你一次性给太多背景对方反而抓不住重点。3.4 记忆去重与合并的实操技巧去重是脏活但必须做。我的做法是新记忆写入前先用标签和类型做一次粗查把候选集拉出来再用一次轻量模型调用判断“是否与已有记忆重复或冲突”。重复的直接丢弃冲突的把旧的标记为superseded新的写入。这里有个坑不要用字符串相似度做去重。我试过效果很差。“用 PostgreSQL”和“选 Postgres 作为主库”字符串相似度不高但其实是同一条记忆。语义判断必须交给模型虽然多花一点 token但准确率天差地别。4. 实操过程与核心环节实现从零搭一套可用的记忆层4.1 环境准备与依赖选择先把地基打好。我推荐的起步技术栈是 Python SQLite 一个轻量 HTTP 框架。为什么用 Python因为和 Claude 的 API 交互、做文本处理、写脚本都顺手生态成熟。SQLite 前面说过了单文件、零配置、备份就是复制一个文件。依赖上核心就几个anthropic官方 SDK 用来调模型sqlite3标准库用来存储fastapi或flask用来暴露召回接口。别引入太多花哨的框架记忆系统的复杂度应该花在逻辑上不是花在依赖管理上。pip install anthropic fastapi uvicorn数据库初始化脚本我一般这么写import sqlite3 def init_db(pathmemory.db): conn sqlite3.connect(path) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, type TEXT NOT NULL, content TEXT NOT NULL, tags TEXT, source TEXT, status TEXT DEFAULT active, created_at TEXT, updated_at TEXT ) ) conn.execute(CREATE INDEX IF NOT EXISTS idx_type ON memories(type)) conn.execute(CREATE INDEX IF NOT EXISTS idx_status ON memories(status)) conn.commit() return conn索引只建在type和status上就够了标签查询用LIKE或 JSON 函数处理。别过度建索引写入性能会受影响。4.2 抽取函数的完整实现抽取函数是整个系统的入口我把它写成一个独立模块输入对话文本输出结构化记忆列表。核心逻辑是调 Claude 做抽取然后解析 JSON。这里有个细节模型偶尔会返回带 markdown 代码块的 JSON解析前要先清洗。import json import re from anthropic import Anthropic client Anthropic() EXTRACT_PROMPT 你是一个记忆抽取器...此处省略完整模板 def extract_memories(conversation: str) - list: resp client.messages.create( modelclaude-sonnet-4-20250514, max_tokens2000, messages[{role: user, content: EXTRACT_PROMPT.replace({{conversation}}, conversation)}] ) raw resp.content[0].text.strip() # 清洗可能的代码块包裹 raw re.sub(r^(json)?, , raw).strip() raw re.sub(r$, , raw).strip() try: return json.loads(raw) except json.JSONDecodeError: return []解析失败时返回空列表而不是抛异常这样单次抽取失败不会中断整个流程。我踩过的坑是早期版本直接抛异常结果一次格式错误导致整批对话的记忆全丢了。4.3 写入与去重的完整流程写入流程我拆成四步粗查候选、语义判重、写入或更新、更新索引。语义判重那一步是性能瓶颈所以候选集要尽量小。我的做法是先用type和标签做粗筛把候选集控制在 20 条以内再逐条做语义判断。def save_memory(conn, mem: dict): # 1. 粗查候选 candidates conn.execute( SELECT id, content FROM memories WHERE type? AND statusactive, (mem[type],) ).fetchall() # 2. 语义判重简化示意 for cid, ccontent in candidates: if is_duplicate(ccontent, mem[content]): return duplicate if is_conflict(ccontent, mem[content]): conn.execute(UPDATE memories SET statussuperseded WHERE id?, (cid,)) # 3. 写入 conn.execute( INSERT INTO memories (id, type, content, tags, source, created_at, updated_at) VALUES (?,?,?,?,?,?,?), (gen_id(), mem[type], mem[content], json.dumps(mem.get(tags, [])), mem.get(source), now(), now()) ) conn.commit() return savedis_duplicate和is_conflict内部各调一次模型判断逻辑要写清楚。判重看“是否表达同一事实”判冲突看“是否互相矛盾”。这两个判断分开做比合并成一个判断准确率高。4.4 召回接口的实现与参数调优召回接口对外暴露一个 HTTP 端点输入当前对话上下文输出排序后的记忆列表。核心是排序函数我前面说的三维度加权就在这里落地。def recall(conn, query: str, query_tags: list, top_k: int 5): rows conn.execute( SELECT * FROM memories WHERE statusactive ).fetchall() scored [] for row in rows: score 0.5 * type_match(row, query) \ 0.3 * tag_overlap(row, query_tags) \ 0.2 * freshness(row) scored.append((score, row)) scored.sort(keylambda x: x[0], reverseTrue) return [r for _, r in scored[:top_k]]freshness函数我用的是指数衰减半衰期设成 30 天。也就是说30 天前的记忆权重减半60 天前的减到四分之一。这个半衰期要根据你的项目周期调短期项目可以设短一点长期项目设长一点。提示召回接口一定要加缓存。同一段上下文短时间内可能被查多次缓存能省下大量模型调用。我用一个简单的内存字典做 LRU 缓存命中率能到 60% 以上。4.5 与 Claude 对话流程的集成最后一步是把记忆层接进实际对话流程。我的做法是在每次调用 Claude 之前先用当前用户输入去召回记忆把召回结果拼进 system prompt。这样模型在回答时就能“记得”之前的事情。集成时有个顺序问题先召回还是先抽取我的经验是召回用当前输入抽取用完整对话。也就是说用户发来消息后先召回相关记忆拼进 prompt等模型回复完再把这一轮完整对话送去抽取。这样既保证了回答时有记忆可用又保证了抽取时有完整上下文。5. 常见问题与排查技巧实录5.1 记忆库越用越乱怎么办这是最常见的问题。用了一两个月记忆库里几千条召回质量直线下降。根因通常是三个抽取阈值太低、去重没做好、缺少归档机制。我的排查顺序是先看type分布如果fact类占比超过 70%说明抽取太宽泛把闲聊也当事实存了再看重复率随机抽 50 条看有多少语义重复最后看status分布如果superseded占比极低说明冲突检测没生效。解决办法是加一个“整理任务”每周跑一次把低价值记忆批量归档把重复记忆合并。整理任务的 prompt 和抽取类似但判断标准更严只保留“三个月后仍然有用”的信息。5.2 召回结果总是答非所问召回不准八成是排序权重没调好。我建议做一个简单的评估集准备 20 个真实问题人工标注每个问题“应该召回哪几条记忆”然后跑召回看命中率。命中率低于 60% 就说明排序有问题。调权重时一次只调一个维度调完重跑评估集看命中率变化。别一次调多个否则你根本不知道是哪个改动起了作用。另一个常见原因是标签体系混乱。标签是粗筛的依据如果标签命名不统一一会儿“数据库”一会儿“db”一会儿“存储”粗筛就会漏掉大量候选。我的做法是维护一个标签白名单抽取时只允许从白名单里选标签新标签需要人工确认后才能加入。5.3 模型抽取结果格式不稳定JSON 解析失败是高频问题。除了前面说的清洗代码块还有两个技巧一是把max_tokens设得宽裕一点避免输出被截断导致 JSON 不完整二是在 prompt 里明确要求“只输出 JSON不要任何解释文字”。我实测下来加上这两条之后解析成功率能从 85% 提到 98% 以上。如果还是偶尔失败加一个重试机制解析失败时把原始输出和错误信息一起发回给模型让它修正格式。这个“自我修复”步骤能兜住绝大多数格式问题。5.4 常见问题速查表现象可能原因排查方向解决手段召回质量下降记忆库噪音大看 type 分布和重复率加整理任务收紧抽取阈值召回答非所问排序权重失衡跑评估集看命中率单维度调权重JSON 解析失败输出被截断或带解释看原始输出加清洗 重试机制记忆冲突未处理冲突检测未生效看 superseded 占比检查判冲突 prompt召回太慢候选集太大看粗筛条件加标签白名单缩小候选5.5 几个我踩过的坑第一个坑是过早引入向量检索。我一开始就上了向量库结果发现决策类记忆召回反而不如关键词准还多了一堆运维负担。后来退回混合方案简单又有效。第二个坑是忽略时间衰减。早期版本所有记忆权重一样结果三个月前的临时决定和昨天的关键决策被同等对待召回经常给出过时建议。第三个坑是抽取 prompt 写得太宽松。模型很“勤奋”什么鸡毛蒜皮都给你存下来记忆库很快就爆了。后来我把“值得记”的标准写死成四类情况才好转。注意记忆系统不是越复杂越好。我见过有人上来就搞多级记忆、记忆图谱、自动摘要结果每个环节都没打磨好整体效果还不如一个简单的结构化存储加关键词召回。先把三段式跑通再考虑加花活。6. 记忆系统的扩展方向与个人体会claude-mem这套思路跑通之后扩展空间其实很大。我目前在做的一个方向是记忆的自动摘要升级把同一主题下的多条零散记忆定期合并成一条高层摘要召回时优先返回摘要需要细节时再展开。这样能进一步压缩召回体积提升上下文利用率。另一个方向是跨项目记忆隔离不同项目的记忆分库存储避免互相干扰同时保留一个全局偏好库让模型记住你跨项目的通用习惯。我个人在实际操作中的体会是记忆系统的价值不在于技术多先进而在于“持续维护”。它更像养一盆植物你得定期浇水、修剪、换盆。指望搭好就一劳永逸最后只会得到一个越来越乱的记忆库。真正拉开差距的是那些愿意每周花半小时整理记忆、调优权重的人。最后再分享一个小技巧给每条记忆加一个“置信度”字段抽取时让模型自己评估这条记忆有多可靠召回时把置信度纳入排序。这个改动很小但对召回质量的提升出乎意料地明显。