☰
给Claude Code加记忆:claude-mem架构解析与工程避坑实录
2026/10/28 16:33:17 网站建设 项目流程

前几天我和 Claude Code 聊一个改了好几十轮的接口方案,聊到第三轮它突然问我:"你之前说的那个分页字段,是叫什么来着?"我盯着终端愣了几秒——上一轮我们明明花了整整两分钟讨论这个字段,我还顺手给了它一段示例代码。这就是无状态模型最让人抓狂的地方:它真的记不住。为了不再每次都得把项目背景、技术偏好、历史决策重讲一遍,我开始折腾给 Claude 加"记忆"的方案,最终落地依赖的核心工具是 claude-mem。这篇博文就是我这三周从安装、接入到踩坑调优的完整实录,适合已经在用 Claude Code 写项目、并且厌烦了反复交代上下文的开发者参考。

1. 从"每次都要重新自我介绍"说起:无状态会话到底卡在哪

1.1 一次让我彻底崩溃的对话复盘

先还原那个让我决定改造的现场。我在做一个数据仓库的迁移项目,Claude Code 帮我把旧的重命名逻辑分批改到新方案。第一天它写得挺好,第二天我继续会话时,为了保险起见,我把项目的目录结构、命名规范、还有昨天它自己定的三步迁移策略全部重新贴了一遍。结果聊到第三轮,它又开始建议我把模块放到那个已经被废弃的旧目录里。

这不是它的错,而是对话架构的本质限制。GPT 这类大语言模型本身是无状态的:每次请求都只处理当前上下文窗口内的内容,窗口之外一概不知道。即使你把历史消息都发给它,只要超过 token 上限,最远的信息就会被无情截断。更关键的是,我每天开一个新会话,上一个会话里沉淀的结论根本不会自动带过来。像"支付回调字段改成了 order_no""这个项目必须用 Python 3.11"这种铁一般的事实,每次都得手工强调一遍。

1.2 现有"备忘"手段为什么只能算权宜之计

我知道大部分人会反驳:不是有 CLAUDE.md、项目文档、/compact 压缩对话吗?我全都试过,它们的局限性很明确。

在项目里维护 CLAUDE.md 静态文件,是最常用的方案。把命名规范、架构约定、常用命令写进去,Claude 每次启动都会读。问题在于它靠人手动维护,项目一周后变了,文件可能还是三天前的样子。我经常出现 CLAUDE.md 里写着旧的模块名,代码里已经重构完了的情况,反而误导模型。

每次开新会话时手工粘贴历史结论,倒是能保证上下文,但有两个痛点:一是你得自己从上周的对话里找出关键信息并整理,这活儿本身就费时间;二是对话越长,你粘贴进来的无关信息越多,白白浪费 token,还会干扰模型对当前任务的理解。

Claude Code 官方提供的 /compact 命令可以把当前对话压缩成一个摘要,继续接续聊天。但它同样是摘要,很多具体的文件路径、函数名在压缩过程中会被丢掉。而且它只对当前会话有效——你今天开一个新会话,昨天压完的那个摘要还是带不过来。

这些方案的共同点,是把"记住事情"的责任推给用户。可记忆本来应该是工具链自己该解决的事,就像浏览器会保存书签,编辑器会记住你上次打开的文件。为什么 AI 提示词工程里反而要让用户当人肉数据库?

1.3 我理想中的记忆层长什么样

动手之前,我先列了一份需求清单,也就是后文 claude-mem 实际解决的三件事:

  • 自动保存每次对话的原始消息,不需要我主动按保存键;
  • 自动从对话中抽取"值得记住的信息",不是存日志,而是提炼成条目,比如"生产库连接必须走内网地址""所有接口入参统一用 camelCase";
  • 新会话开始前,根据当前任务悄悄检索最相关的记忆并注入到上下文中,让我不用手动贴。

外加两个底线要求:数据必须存在本地,我能直接打开数据库查看、修改、删除;整套流程要有明确的操作入口,不能用黑盒方式偷偷往提示词里塞东西。

这个清单后来几乎原样对应了 claude-mem 的三层设计:存储层、记忆生成层、检索注入层。下面我分层拆开讲。

2. 一条记忆从产生到被想起:架构拆解

2.1 存储层:为什么是 SQLite 而不是 JSON 文件

claude-mem 的默认存储后端是一个 SQLite 数据库文件,一般位于~/.claude-mem/memories.sqlite3。我一开始以为是那种把所有对话原样堆进一个 JSON 文件的做法,打开数据库才看到真实的表结构,大致是这样:

CREATE TABLE sessions ( id TEXT PRIMARY KEY, created_at TEXT DEFAULT CURRENT_TIMESTAMP, summary TEXT ); CREATE TABLE messages ( id TEXT PRIMARY KEY, session_id TEXT, role TEXT CHECK(role IN ('user', 'assistant')), content TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE memories ( id TEXT PRIMARY KEY, content TEXT NOT NULL, strength REAL DEFAULT 1.0, last_accessed TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_memories_content ON memories(content);

选 SQLite 而不是 JSON 文件,原因很实际:单文件、零服务、备份就是一个文件拷贝。messages表存全量对话当原料,memories表存提炼后的记忆条目。两者分开,既能追溯一条记忆来自哪段对话,又能在注入时只读取精简的记忆条目而不需要重新解析原始日志。

这里有个关键设计:记忆条目不是重复的原文。比如用户在对话里说了一大段"我们团队约定所有 API 错误码统一返回 HTTP 400,body 里再放 code 字段表示业务错误",实际沉淀成的记忆条目可能是"错误码规范:业务错误统一返回 HTTP 400 + body.code"。长度砍掉一大半,保留的信息一点没少。这就是记忆层和简单日志保存的本质区别。

2.2 记忆生成:怎么从对话里抽出"事实"

记忆从哪来?我当时以为是每次对话随机存一句,实际看它的处理逻辑,是在一轮会话结束后异步触发生成任务的。触发条件通常是两类:一是文本里包含显而易见的事实断言,比如"以后不要用 MySQL,统一 PostgreSQL";二是包含明确的用户偏好或项目约束,比如"部署环境一律走 GitHub Actions"。

抽取策略是规则加模型的双层方式。先靠规则做粗筛,把包含"以后、记住、注意、统一、必须、不要"这类信号词的句子标记出来,滤掉寒暄和空话;再让语言模型对候选片段做摘要,压缩成一条条独立的记忆。这样能避免把"嗯嗯好的"这种无价值内容存进去。

这个设计让我意识到,记忆生成的目标不是"把对话录下来",而是"把对话里的事实挖出来"。对话是流水,事实才是金子。两者混在一起存会带来巨大的检索噪音,分开存才能保证注入时的高信噪比。

另外每条记忆都带一个strength字段,初始值是 1.0。每次被检索命中,strength 就增加一点;长时间没被命中,则随时间衰减。这为后文的遗忘机制打了地基。

2.3 检索与注入:新会话怎么想起旧事

记忆存了一堆,新会话怎么知道该用哪几条?这是整套系统最核心的环节,一共三步。

第一步是向量化。每条记忆会被 embedding 模型转换成一个高维向量,用户当前的问题或任务描述也会做同样的转换。然后计算余弦相似度,选出和当前任务最接近的若干条。这一步解决的是"语义相关"的问题,比如用户问"分页接口",记忆里的"列表查询参数含 page 和 size"能命中,即使它们没有完全相同的词。

第二步是关键词兜底。向量检索擅长语义但不擅长精确词,所以 claude-mem 会并行跑一个关键词检索,类似搜索引擎里的 BM25 算法,专门照顾那些包含特定函数名、文件路径、变量名的记忆。比如你问"在哪里改过parse_order这个函数",包含该函数的记忆必须被捞出来,靠向量反而可能失手。最后把两个检索结果做加权融合,取综合分最高的 top-k。

第三步才是注入。选中的记忆会被拼成一段结构化的提示词文本,追加到系统提示词末尾,格式大概是这样:

[Relevant memories] 1. (relevance: 0.87) 错误码规范:业务错误统一返回 HTTP 400 + body.code 2. (relevance: 0.81) 订单模块入口文件:app/order/service.py 3. (relevance: 0.72) Python 版本必须为 3.11,容器部署使用 poetry

这也就解释了为什么不能全量注入:每个会话窗口的 token 有限,塞进去的所有记忆都是要付钱的。如果 900 条记忆全进上下文,不仅成本爆炸,无关信息还会干扰模型判断,让回复变得模棱两可。检索的价值就是只把此时此刻最相关的三到五条捞进来。

3. 30 分钟跑通记忆层:安装与接入 Claude Code

3.1 安装 claude-mem 与依赖准备

我是在 macOS 的终端里操作的,前提是本机已经装好 Python 3.11 以上版本以及 uv 这个包管理器。安装命令比预想简单:

uv tool install claude-mem

安装完验证一下版本:

claude-mem --help

如果输出了子命令列表(通常有mcp、search、forget、stats这些),说明装好了。这里要提醒一句:claude-mem 目前还在快速迭代,不同分支或 fork 的子命令、参数名可能不一样,一切以你自己拉取版本的 README 为准。我第一次也遇到过命令对不上而卡住的情况,后来发现是自己 GitHub 上拉错了一个分支。

如果只想在普通终端里直接调 API 用,也可以把 claude-mem 当成一个命令行工具:跑完一段对话后手动执行claude-mem save来沉淀记忆。但最顺滑的方式还是走 MCP 接入 Claude Code,下面细说。

3.2 以 MCP Server 接入 Claude Code

MCP,全称 Model Context Protocol,你可以粗暴理解成 AI 助手的 USB 接口:只要支持这个协议,外部工具就能作为一个标准外设插进 Claude Code。claude-mem 的很大一部分价值就体现为提供一个 MCP server,让 Claude Code 在对话过程中自己调用记忆工具,不用我手动切来切去。

接入步骤是修改 Claude Code 的全局配置文件~/.claude/settings.json,把 claude-mem 注册进mcpServers:

{ "mcpServers": { "claude-mem": { "command": "claude-mem", "args": ["mcp", "--memory-backend", "sqlite"] } } }

保存之后重启 Claude Code(或者重载 MCP 服务器),在会话里输入/mcp命令,能看到claude-mem那个条目变成 connected,就说明握手成功了。这一步很容易出问题的是command路径:如果你用 uv 装的 claude-mem,可执行文件可能不在系统的默认 PATH 里。我在配置时遇到过 MCP 反复连不上,/mcp一直显示 failed,后来在终端里用which claude-mem查到实际的安装路径,在配置里填绝对路径才解决。

3.3 首次对话验证记忆是否真的生效

接入后别急着干大活,先做一个最基础的记忆写入与读取验证,确认链路是通的。

我在第一个会话里输入:

请你记住:本项目根目录是 /data/warehouse,所有脚本必须使用 Python 3.11。

Claude Code 会通过 MCP 调用 claude-mem 的保存工具,把这句话提炼成记忆条目。然后我直接退出这个会话,新开一个空会话,输入:

你还记得我这个项目的 Python 版本要求吗?

如果记忆层生效,它应该能回答出 Python 3.11。接着打开数据库看硬证据:

sqlite3 ~/.claude-mem/memories.sqlite3 SELECT content, strength, created_at FROM memories ORDER BY created_at DESC;

能看到一行记录,内容大概是"项目根目录 /data/warehouse,需求 Python 3.11"。这条链路通了,就说明存储、提取、检索、注入四个环节都正常工作。我第一次看到这个结果时还挺兴奋,因为这意味着以后再也不用把同样的项目背景粘贴第二遍了。

4. 让记忆好用,取决于检索策略而不是存储量

4.1 相关度:多相关才算相关

跑通之后我开始观察它的检索质量,发现一个很容易被忽略的点:记忆系统的上限不是存储量,而是检索准确率。存进去一千条质量参差的记忆,如果检索抓三条全是噪音,那它不如只存五十条但条条精准。

为此 claude-mem 的检索策略是可调的。首先 embedding 模型能配置,我在本地换用过几个模型,最终选了支持中文效果较好的版本。这个决定直接影响中文技术记忆的检索准确性,如果还用英文专用模型,很多"分页、限流、幂等"这类词就匹配得很差。其次,向量检索和关键词检索的融合权重也能调,默认是五五开,但我遇到精确匹配函数名需求更高的项目时会调高关键词权重。

调试方法是构造种子记忆,比如把"order 模块的入口在 app/order/service.py"存进去,然后在新会话里故意用不同说法问:"订单模块从哪进来的?"看它能不命中。如果经常落空,就检查是不是 embedding 模型不适合你的语言。检索效果差不要急着堆记忆量,先修检索。

4.2 注入口径:别让提示词被记忆挤爆

记忆需要注入提示词,就必然产生 token 开销。我最初设置的 top-k 是 10,也就是每次会话最多加载十条记忆。实测一条压缩后的记忆在 80 到 200 token 之间,十条最坏情况就是 2000 token。在现代 128k 上下文窗口里,这个占比不高,但要看具体场景。

如果任务本身很小,比如只是让 Claude 解释一段报错,那么加载两千 token 记忆反而会把回答带偏。我现在按项目复杂度分了档:日常小脚本任务 top-k 降到 3,大型重构任务提高到 8。配置方式通常在 claude-mem 启动时传参数或改配置文件,比如:

{ "mcpServers": { "claude-mem": { "command": "claude-mem", "args": ["mcp", "--top-k", "5", "--max-memory-chars", "600"] } } }

max-memory-chars限制单条记忆的最大长度,防止某条记忆把整个注入空间吃掉。

还有个细节值得注意:记忆注入发生在新会话开始时,对话进行中一般不会反复重新检索注入,原因一是避免上下文频繁变动引发模型混淆,二是节省 token。这意味着你在对话中问的问题越多,开始时注入的那几条记忆可能就不够用了,必要时可以手动触发一次二次检索,或者直接跟 Claude 说"查一下记忆里关于 XX 的内容"。

4.3 遗忘机制:让错误和有噪声的记忆自然消退

用着用着,我面对了比"记不住"更麻烦的问题:"记错了"。早期某次我把临时的调试结论也让它记住了,结果后面好几个新会话都受到污染。claude-mem 为此设计了记忆强度与时间衰减机制:每条记忆初始strength = 1.0,每次被命中访问加一点,但会按时间做阻尼衰减。长期没被使用的记忆会慢慢消失;被新记忆覆盖的旧事实会在冲突时降权。这相当于给记忆加了一个"自动复盘",避免越积越脏。

不过自动衰减处理不了"明显错误但反复命中"的记忆,这时候需要手动介入。它提供了两个命令:

claude-mem forget "Python 版本必须是 3.9" claude-mem clear

forget按关键词删除单条,clear清空全部记忆。我常用的是先search找到错误记忆的 id,再精准删除。勇于删记忆比囤积记忆更重要,尤其在项目方向发生变更时,旧记忆会持续误导新会话,体现为 Claude 固执地坚持已经废弃的结论——那不是它笨,是记忆层里没有执行"推翻"动作。这一点我在下一节详细说。

5. 三周实战:踩过最深的几个坑

5.1 记忆膨胀导致系统提示词动不动就超长

第一次让我警觉的项目,是高强度开发了一周之后。某天我新开会话,发现 Claude 的回答开始泛化,明明很直接的问题也要先铺垫一大段。我看了一下 API 日志里的请求体,系统提示词末尾挂了两千多 token 的记忆片段,而且里面还有三条明显重复的旧记忆在互相打架。

排查链路是这样的:先看一眼claude-mem stats,确认当前注入量;再在数据库里按order by strength desc, last_accessed desc查最常用记忆,发现有三条记录都在说同一个模块的入口路径,只是版本不同——第二条和第三条是后面对话里生成的,但它们的strength已经很高了,覆盖了当初那条新版的正确记忆。

最终处理是三条删掉两条,把正确的那个手动更新成最新路径,同时把top-k从 10 调到 5。教训很直白:记忆不是越多越好,要允许"少而准"。如果系统提示词里塞满了不确切的记忆,模型的表现会比没有记忆更差。

5.2 "陈年错误记忆"持续污染新会话

最让我头疼的一次,是早期某次会话里,对话中的某个中间假设被提炼成了记忆,内容是"项目使用 Django 2.x"。可事实是我们当时已经迁移到 Django 4.2,那条记忆是个临时状态,不是最终结论。结果后续至少三个会话里,Claude 都在按 Django 2.x 的 API 写代码,它反复提到老的中间件写法。

我花了不少时间才定位到真凶。排查过程是:先用claude-mem search "Django"查看所有含 Django 的记忆,发现一条strength = 4.2的老记忆稳居榜首;再去对应 session 里查原始对话,确认提取的源头就是那次中间讨论。原因很清楚,记忆生成阶段把"过程事实"和"结论事实"混为一谈,没有加置信度过滤。

修复就是删掉那条错误记忆,并重新开一段对话,明确告诉 Claude"项目已迁移到 Django 4.2,请记住这一点",让它生成一条新的高置信度记忆。这件事让我明白,记忆层的核心不是存得全,而是能纠正。之后我调低了记忆生成密度,在配置里增加了一个"仅提取结论性陈述"的选项,让临时猜测不再进入记忆库。

5.3 token 成本偷偷涨了

接入 claude-mem 一周后,我看了下 API 用量,发现平均每次会话的 token 消耗比之前涨了约 30%。虽然这其中有正常的工作量波动,但记忆注入确实贡献了一部分:默认 top-k 10,平均每天 20 个会话,每个会话多出 1500 token,一天就是 3 万 token 的固定开销。

核算一下投入产出比:如果每个会话真的省去了我粘贴一大段背景资料的时间,那这 1500 token 非常划算。但现实不是每个任务都需要历史记忆。我维护着两个项目,一个重上下文,一个轻任务,最终我是按 profile 区分的:重上下文项目用全量记忆,轻任务项目把 top-k 降到 2 甚至关闭注入。节约出的 token 比想象中多,而且回答质量没有下降。

5.4 静动态文件与动态记忆的协同方式

我最初以为 claude-mem 可以完全替代 CLAUDE.md,后来发现两者应该是配合关系。CLAUDE.md 放那些极少变更的约束,比如"所有提交信息必须遵循 Conventional Commits""禁止在源码里写绝对路径";claude-mem 承担动态部分,比如"本周新加的模块叫 settlement,入口在 app/order/settlement.py"。

这样分工的收益很明显:静态约束不会因为会话切换而丢失,动态变更不需要每次手改文档。我养成了一个小习惯,每周末花五分钟看一眼记忆库里的高频条目,把真正稳定的事实转移到 CLAUDE.md 里,再把已经固化进文档的记忆删掉。看似麻烦,但能让两套机制各自发挥优势,整体维持在一种"静的归文档、动的归记忆"的平衡状态。

下面把实战里最典型的几个坑汇总成一张排查表,方便遇到问题直接对号入座:

现象可能原因排查思路解决方案
回复泛化、答非所问注入的记忆过多或重复claude-mem stats看注入量,sqlite3查记忆表调低top-k,删除重复记忆
反复按旧方案回答存在高强度的过时记忆claude-mem search定位关键词forget删旧记忆,新会话重新生成正确记忆
每次会话 token 偏高全局注入的开销观察 API 日志,对比接入前后用量按 profile 分场景,轻任务关闭或调低注入
MCP 连接失败可执行文件不在 PATH终端执行which claude-mem配置里填绝对路径
中文检索不命中embedding 模型不适合中文构造种子记忆测相似度换支持中文的 embedding 模型

最后说一个我自己的体会。上线这套记忆方案两周后,我新开了一个会话,还没贴任何上下文,Claude 直接问:"这个模块还是按之前说的走事件驱动吗?"我愣了一下——它居然记得,而且记对了。那种"它终于记得我是谁"的感觉,远比多省了几次复制粘贴更值得。当然我也踩过不少坑,如果你也打算给工作流加记忆层,我的建议是先做薄、可审计,不要一开始就追求全量记忆;每条要它记住的内容,都问自己一句:这句一旦错了,会造成多大误导?想清楚这个问题,你就能在记忆与噪声之间找到自己那条线。

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

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

立即咨询