☰
为Claude搭建持久记忆层:从对话提取、存储到自动召回的关键设计
2026/10/10 17:19:50 网站建设 项目流程

1. claude-mem 是什么:为什么需要一个记忆层

如果你经常跟 Claude 这类大模型打交道,应该有个很强烈的体验:单次对话很强,但聊完就忘。换一个会话窗口,它连你刚说的偏好的编程语言都能忘得一干二净。我一开始以为是自己 Prompt 写得不对,后来才反应过来,这是 API 本身的状态机制决定的。claude-mem 就是针对这个痛点做的一个轻量记忆工具,它位于 Claude 的上游,把每次对话里值得留下的信息拆出来,存到本地文件,等下一次需要时再自动放回上下文里。它解决的不是“让 Claude 更聪明”,而是“让 Claude 记得住”。适合写自动化脚本、跑批量任务、做个人知识助手的开发者,也适合任何不想每次重讲一遍背景信息的人。

1.1 模型会话为什么会“失忆”

很多人第一次用 Claude 的时候,都会怀疑是不是自己的 Prompt 写得不到位。明明上一轮说了“后面所有命令都用同一套测试框架”,下一轮它照样给你推荐另一套。问题的根源在于,大部分模型 API 都是无状态请求,服务端不会因为你多聊了几句就自动保留一份聊天档案。每一次调用送上去的就是一段独立文本,模型看到什么,就只根据什么来回答。这不是它笨,而是在架构上本来就允许这样。

这就引出了另一个问题:既然无状态,能不能把历史对话全部塞进上下文?理论上可以,但实际上不划算。上下文窗口再大,也是有限预算。塞满之后,延迟、成本、有效注意力都会明显下降。我试过用纯拼接方式把半天的工作对话全部丢回给模型,结果模型确实记得前面说了什么,但反而被大量“中间过程”干扰,连核心任务都开始答得含糊。所以关键不是“能塞多少”,而是“该塞什么”。

claude-mem 的出发点很直接:无状态是底层接口能力决定的,我们改变不了它,但可以在客户端自己补一层持久化。每次对话结束后,从原始 session 里提炼出值得长期保留的信息;新对话开始前,把相关的记忆摘要重新注入。这样从模型的角度看,它仍然是无状态调用,但实际上是带着一份“工作简历”进来的。

1.2 这个工具要解决哪些实际场景

我自己的需求大概可以分成三类。第一类是个人知识助手,比如每天和 Claude 聊技术笔记、整理阅读材料,第二天继续聊时,我希望它记得我昨天看到哪一节、已经排除过哪些方案。第二类是长周期执行,例如一个跨几天的迁移脚本,第一天定了目录结构,第二天继续写时不能再问一遍。第三类是始终在线的小客服,不少人在做一个固定知识库的问答机器人,用户反复咨询类似问题,会话记忆能让它少问很多“你之前报错是什么”。

这三类场景有一个共同点:高频复用少量稳定信息。不是每个字都要记住,真正需要记住的其实是路径、偏好、结论、当前状态。只要把这些信息结构化地保存下来,新会话就能非常自然得衔接上。一开始我直接用 JSON 文件手工维护,后来发现手动提取太慢,于是开始让 Claude 自己从对话里做摘要,再由一个命令行工具统一管理。这个方案后来整理成了 claude-mem 项目。

1.3 设计取舍:为什么不做成又一个向量数据库

最开始我也认真考虑过用向量数据库存记忆。毕竟现在检索增强方案很成熟,对文本做 embedding,召回时算相似度,听起来很优雅。但实际用了一段时间后,我在 claude-mem 里主动放弃了这个方向。原因很简单:会话记忆的数据量通常很小,一个重度使用者跑一年可能也就几 MB 文本。在这个量级上,用 SQLite 自带全文检索加几个排序规则,效果和向量检索差不了太多,但部署复杂度低得多。

这有点像为了买瓶酱油专门去趟机场免税店。向量数据库本身是好东西,适合百万级文档的语义检索场景,但对一个只想记住“项目根目录在哪”的小工具来说,杀鸡用了牛刀。claude-mem 的核心诉求是轻、能离线跑、开发者看得懂存储结构。所以我选了 JSONL 作为原始记录格式,SQLite 做索引和检索,所有数据都留在本地目录里。整个工具不需要额外启动服务,也不需要维护一个独立数据库进程,放在可执行脚本里就能工作。

2. 核心机制拆解:记忆从哪来、存到哪里、怎么回去

要让记忆真正可用,至少要解决三件事:从对话里提取什么,用什么结构保存,以及在新会话时如何选择并注入。claude-mem 把这三件事拆成了三个清晰步骤,分别是 digest、storage 和 recall。下面逐个拆开看。

2.1 记忆提取:请 Claude 自己当“摘要器”

第一步不是存储,而是判断什么值得存。原始对话里百分之九十的内容都是过程性的话,比如“这个函数跑不通”“我再试一次”“报错信息贴给你”,这些对下一次会话没有太多复用价值。我的做法是设置一个 digest 步骤,把一段对话交给 Claude,让它按照固定的 JSON Schema 输出结构化记忆。这个步骤相当于让模型自己给自己做笔记。

我常用的 Schema 长这样:

{ "entities": ["某项目", "模块列表"], "facts": ["根目录为 /data/myapp", "使用某开源语言"], "tasks": ["当前正在优化导入性能"], "preferences": ["错误日志用 JSON 格式输出"] }

实体、事实、任务、偏好,四类信息基本覆盖了日常会话里需要跨会话保留的内容。为什么不让 Claude 直接输出一段自然语言摘要?因为自然语言摘要很难被程序后续处理。如果用 JSON 结构化输出,过滤、排序、去重、按标签召回都会方便很多。当然,这需要消费一次模型调用,如果不想在每次对话后都多花这个成本,也可以设定低峰时间统一处理,或者只对超过指定轮数的 session 做 digest。

2.2 本地存储结构:像记事本一样可以直接打开

claude-mem 的数据目录放在工作区里的.claude-mem/下,目录结构大致如下:

.claude-mem/ config.json session_0042.jsonl session_0043.jsonl memories.json index.db

原始对话按 session 保存在 JSONL 文件里,每条消息一行,带序号、角色和时间戳。digest 之后的结构化记忆写入memories.json,查询索引用 SQLite。这样设计有个明显好处:如果你怀疑哪里出了问题,直接用文本编辑器打开 JSON 文件就能看到模型到底记住了什么,不需要查数据库表。

一条记忆在memories.json里长这样:

{ "id": "mem_24a7c9", "content": "项目根目录为 /data/myapp", "kind": "fact", "tags": ["path", "某项目"], "created_at": "2025-01-20T10:12:00+08:00", "updated_at": "2025-01-20T10:12:00+08:00", "source_session": "session_0042", "hits": 3 }

hits字段表示这条记忆被召回过多少次。排序的时候,高频命中通常意味着用户确实需要反复使用,可以给更高权重。另外还留了updated_at,时间越近的记忆越可能代表当前状态,比如“正在迁移”这种临时任务,过两周可能就不重要了。保存原始 session 的价值在于,用户随时可以手动浏览或重新摘要,不用重新翻日志。

2.3 召回与注入:把记忆变成“前情提要”

召回的核心是控制注入量。如果一次把所有记忆都塞给模型,那就是用一个低效的上下文窗口硬扛所有历史,跟开始时的问题没什么区别。claude-mem 的召回分三步:先按关键词和标签过滤出一批候选记录,再按热度、时效、相关度做一个简单打分排序,最后把排在前面的几条交给 Claude 压缩成一段 150 字左右的前情提要。之后,这段摘要会被放进系统提示词里。

我用的注入模板大致是这样:

下面是用户之前的偏好、事实和任务状态,请结合后文问题使用,但不要直接复述记忆原文: <memory> (此处是由 claude-mem 生成的前情提要) </memory>

这里有一个容易被忽略的细节:不要要求模型“背诵”记忆内容,而是让它“结合当前问题使用”。我在早期版本里踩过坑,模型收到记忆后喜欢把旧事实原封不动地复述一遍,像个复读机,反而忽略了用户当前的问题。改成“参考使用”之后,回答质量明显提升了。

2.4 命令行设计:不接管主流程,只做插件

一开始我想做一个常驻服务,监听终端里的输入,自动保存和注入。后来发现这样太侵入,用户很难判断哪句话被记录、哪句话被忽略。所以 claude-mem 改成了命令式设计,本身不直接接管 Claude 的调用,而是提供一组命令给开发者组合使用。

命令作用典型场景
claude-mem init初始化记忆工作区在新项目里启用记忆
claude-mem record追加一条原始对话记录每个会话结束后调用
claude-mem digest对指定 session 抽取结构化记忆定时任务或会话结束时运行
claude-mem recall根据问题召回相关记忆新会话开始前调用
claude-mem forget删除指定记忆发现记忆过时或错误时
claude-mem status查看记忆库概览调试时确认写入生效

命令式的好处在于足够透明。开发者可以决定在哪个环节触发 digest,在哪个环节把 recall 结果拼进 Prompt,甚至可以在自己的脚本里完全绕过内置逻辑,只把 claude-mem 当成一个存储后端。对我自己来说,这种灵活性比大而全的“全自动记忆模块”更实用,因为每个项目的调用链路都不一样,统一收口反而难适配。

3. 实战部署:十分钟接入一个可用的记忆层

下面按我实际使用的一整套流程来走一遍。假设你已经有一个可以调用 Claude API 的 Python 脚本,现在只缺一个记忆层。整个过程不需要写太多代码,也不需要改现有主程序逻辑,只需要在两个位置上插入 claude-mem 的调用。

3.1 安装与初始化

环境要求是 Python 3.10 以上,需要能访问 Claude 的 API。安装命令很简单:

pip install claude-mem

然后进入一个项目目录,初始化记忆工作区:

cd ~/notes/chat-archive claude-mem init --workspace .

init 会在当前目录生成.claude-mem/目录和一份 config.json。初始化完成后,可以先运行claude-mem status确认目录已经正确创建。如果之前没有设置 API Key,这里不会有任何报错,因为 claude-mem 本身不直接用 API Key 做一些全局操作,只有当你选择让 Claude 执行 digest 或 summarize 时才会用到。

config.json 初始内容大致如下:

{ "memory_dir": ".claude-mem", "default_session_id": "session_0001", "recall_budget": 800, "auto_summarize": true, "model": "your-llm-model-id" }

model是占位值,你要换成自己能调用的模型标识。选择模型时不用追求最强参数,关键是能稳定输出 JSON。recall_budget表示最多允许多少 token 的记忆内容进入最终上下文,800 是我测试下来比较舒服的默认值,比这个低容易丢细节,比这个高又会挤压主任务空间。

3.2 在现有 Python 脚本里插入记忆调用

接下来是核心改造。我习惯写一个非常薄的封装,把 claude-mem 命令包成几个函数,这样业务代码不用到处调 subprocess。下面这段代码是实际可运行的:

import os import subprocess def claude_mem(*args): result = subprocess.run( ["claude-mem", *args], capture_output=True, text=True, check=True, ) return result.stdout.strip() def build_memory_context(query): memory = claude_mem("recall", "--query", query, "--limit", "5", "--budget", "800") if not memory: return "目前没有历史记忆。" return memory def send_message(user_query, history_response=None): memory = build_memory_context(user_query) system_prompt = ( "你是一个有连续记忆的助手。" "请结合下面的记忆片段理解当前对话背景,但不要机械复述。\n" f"<memory>\n{memory}\n</memory>" ) # 在这里调用你正在使用的 Claude API,传入 system_prompt 和 user_query # response = your_claude_api_call(system_prompt, user_query) # 演示用,实际需要替换为真实返回 response = "这里替换为模型返回内容" claude_mem( "record", "--session-id", "session_0042", "--input", user_query, "--response", response, ) return response

这段代码的用意很明确:每次发消息前,先从记忆库里根据当前问题召回相关内容,拼到 system prompt 里;拿到模型响应后,再把这一轮问答原样写入 session 文件。这样 claude-mem 既负责“读记忆”,也负责“累积记忆”。

我在实际使用时还会把--session-id设计成稳定的任务编号,而不是每次随机生成。比如某个迁移任务叫migration_202501,那么整个周期里的会话都写进这个 session,后面抽查历史时会非常方便。

3.3 手动验证记忆是否生效

接入完成后,先别急着跑复杂任务,做一次最小验证。开一个空目录,初始化后手动写入一条记忆:

claude-mem note "我最近计划把数据库迁移到新的存储层"

然后模拟一个新会话,查询相关关键词:

claude-mem recall "当前数据库迁移计划"

如果配置正确,recall 会把刚写入的记忆找回来,类似这样的输出:

[claude-mem] 命中的记忆: 1. [fact] 我最近计划把数据库迁移到新的存储层 (hits=1)

我建议把这条测试记忆删掉,然后正式跑第一轮真实对话。验证完成后,可以打开.claude-mem/session_0042.jsonl看看原始记录是否完整写入,点了 record 之后文件末尾应该多出了两行,一行是用户输入,一行是模型输出。如果这一步没问题,说明数据链路已经通了。

3.4 用定时摘要控制记忆膨胀

如果只是把原始对话全部存下来,永远不会自动变成结构化记忆,那 claude-mem 就只是个日志工具。所以 digest 这一步很关键。我一般会在每天结束前跑一次当天会话的摘要:

claude-mem digest --session-id session_0042

digest 会读取session_0042.jsonl,调用模型生成结构化记忆,然后合并到memories.json。注意第二次跑同一个 session,不会重复生成一堆重复记忆,claude-mem 会先对比已存在的记忆条目,新内容才追加,旧内容只是更新时间戳。这个幂等设计非常重要,否则每天跑一次定时摘要,记忆库会被重复内容塞满。

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

项目看起来不复杂,真正跑起来后问题不少。下面这些坑是我自己和几个早期使用者真实踩过的,按频率排序写出来。

4.1 召回结果总是驴唇不对马嘴

第一个经常出现的问题是,写进去的明明是“项目根目录为 /data/myapp”,新会话问“项目文件在哪”,结果召回不到。问题往往出在写入阶段:原始对话里这句话可能没有上下文,摘要模型不知道该不该把它记为 fact,或者只打了path标签,没有打project标签,导致检索时没有命中。

我的排查路径通常是,先打开memories.json看这条记忆有没有被正确写入。如果已写入但召回不到,就调高主题标签的权重,或者在 recall 命令里加上--tag参数手动指定范围。另外,写入摘要用的模型输出质量会直接影响召回,遇到这种情况不要急着调排序,先检查是不是摘要阶段漏掉了关键实体。

4.2 记忆重复累积导致上下文越用越堵

用了一两周之后,memories.json里可能会有好几条内容相似但措辞不同的记忆。比如“当前数据库是新的存储层”“已经把数据迁到新的存储层”“存储层替换完成”,这三句话其实是同一个事实的演进过程。如果不做合并,recall 可能同时返回三条,上下文里全是重复信息。

claude-mem 在 digest 阶段增加了语义相近合并机制。如果新记忆和已有记忆的相似度达到阈值,就用新内容更新旧条目,而不是新增一条。同时,recall 返回前还会做一次压缩摘要,把多条候选压成一段,避免 Token 浪费。我自己的经验是,记忆库超过 300 条之后,这类合并会直接影响系统稳定性,不是可选项,而是必选项。

4.3 敏感信息被带到模型请求里

这一点值得反复强调。claude-mem 只是把记忆保存在本地,但当 recall 结果被注入 system prompt 后,这些内容会作为请求的一部分发送给模型服务端。换句话说,本地存储不等于彻底私密,只要在对话里使用了,就会被模型服务商看到。所以在使用 claude-mem 时,不要记录密码、密钥、身份证、银行卡这类明文敏感信息。

如果确实需要记录一些看起来像凭证的内容,可以在配置里加脱敏规则,比如:

redact: - "(?i)(sk-[A-Za-z0-9]{16,})" - "password[=: ]+\\S+"

同时,最好把.claude-mem/目录权限设置为本用户可读写:

chmod 700 .claude-mem

这个习惯一开始麻烦一点,但能避免数据文件被同机其他用户不小心读走。

4.4 多个脚本同时写导致记忆文件损坏

claude-mem 采用了 JSONL 追加写入,正常情况下单次 append 是原子的。但如果你同时跑两个脚本,一边写入 session 文件,另一边又执行 digest 读取同一个文件,就可能出现读了一半还没写完的情况,运气好一点是少记一条,运气差一点是摘要内容错乱。

我的解决办法是在需要写文件的命令上加了文件锁。如果你是在自己的脚本里调用 claude-mem,建议保证同一时间只有一个 digest 任务运行。简单做法是用一个定时器把摘要任务串起来,比如每天凌晨统一跑,不跟业务请求抢资源。更稳妥的方式是接入文件锁,虽然会增加一点代码量,但并发环境下不会产生脏数据。

4.5 多项目共用一个记忆空间

claude-mem 设计上允许一个 workspace 只放一套记忆,但你如果图省事,把所有项目聊天都往同一个目录里塞,很快就会发现召回结果互相污染。比如工作记忆里有一条“根目录是 /data/project-a”,私人笔记里问另一个项目时,这条记忆也会被召回,模型还以为你在同一个项目上下文里。

我的建议是一个项目一个.claude-mem目录,或者至少在 config.json 里把memory_dir指向不同路径。虽然看起来占地方,但隔离带来的确定性远胜那点磁盘空间。如果你希望全局共享某些偏好,可以单独建一个 global 目录,把通用偏好放进去,再用两段分别注入。

5. 进阶玩法:从一个记忆工具变成知识沉淀系统

如果核心的记忆读写已经稳定,你可以在这个基础上做一些更有意思的扩展,让 claude-mem 不只是给模型续命,而是帮你自己整理信息。

5.1 对话回放与自动周报

session 文件里保留了完整的原始对话链路,只要定期跑 digest,你就能从大量碎片里提炼出一个时间段内的高频主题。我每周会让 claude-mem 读取最近一周的 session,把每个任务的进展、结论、卡点按主题汇总成一段叙事。这些内容我会直接放到周报里,不用再翻聊天记录。

实现起来并不难,本质上就是把所有 session 文件传给模型,让它按时间线和主题组织成要点。claude-mem 不限制外部脚本读取这些文件,因为它就是普通的 JSONL,我可以用任何脚本把它变成 Markdown 或表格。这个玩法特别适合跟客户确认进展的场景。

5.2 把任务状态变成可追踪的清单

记忆里的tasks字段如果只是保存一句“正在优化导入性能”,价值不大。更好的做法是每次会话结束时,要求模型更新任务状态,包括未开始、进行中、阻塞、完成。这样下次打开工作区,直接问一句“现在卡在哪里”,就能根据最近更新的任务状态快速恢复上下文。

我经常用的方式是让 claude-mem 在 digest 时额外输出一个status.json,单独保存任务列表。这个文件比 memories.json 更薄,更适合作机器人快速读取。如果你想做日程提醒,甚至可以根据任务的更新时间戳做延迟判断,实现一个简单的任务提醒器。

5.3 与个人知识库联动

claude-mem 的存储格式足够开放,稍加处理就能和其他笔记系统打通。因为memories.json是纯文本 JSON,任何脚本都可以读取,然后按标签分发到对应的笔记文件、看板或知识库。比如我可以写一个同步脚本,把所有带architecture标签的事实追加到某个架构设计文档里。

对我来说,这比直接在笔记工具里维护知识库更自然,因为记忆是随着对话自然产生的,不需要额外整理。对话本身就是工作流,记忆只是副产品。如果你也有一个长期使用的知识管理方式,可以尝试把 claude-mem 作为它的前级过滤器,先把对话里的稳定信息抽出来,再决定后续怎么展示和加工。

最后分享一个我个人测试后的体会:记忆工具最大的坑不是技术,而是克制。我之前总想让系统记住所有对话,结果召回上下文里的噪音很大,模型反而出现前后矛盾。后来我把每条记忆当成一条 issue 来管理:没有明确复用价值的不记,长期不命中的就淘汰。现在的流程很简单,对话结束提取事实,下次开始注入摘要,隔段时间清理旧记录。看起来没那么智能,但用起来很稳。如果你也在给 Claude 写脚本,建议先在一个小工作区里试,先记录路径和偏好这类稳定信息,等流程跑顺了再往上加任务状态和实体关系,这样既能控制成本,也更容易调出可靠的效果。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询