给Claude装上外置记忆:claude-mem的LLM记忆增强实践

发布时间:2026/10/28 4:11:43
给Claude装上外置记忆:claude-mem的LLM记忆增强实践

如果你用 Claude 做过长期项目,一定被同一个问题折磨过:它记不住昨天的事。今天聊的这个项目 claude-mem,就是专门给 Claude 加一块“外置记忆”的 CLI 工具。它把每一次会话中值得保留的信息抽出来,存进本地 SQLite,下次对话启动时把相关记忆自动注入到语境里,让模型真的能“想起来”你上个礼拜交代过的偏好和约束。

我用了大概一周时间,把 claude-mem 接进了日常开发流,也顺手翻了它的源码。这个项目解决的痛点非常明确:Claude 本身是无状态的,每开一次新对话,之前的上下文全部归零。你当然可以手动把旧内容贴回去,但对话一长就复制到崩溃。claude-mem 的思路是“记忆外置”——不依赖模型自身长上下文,而是用工具帮你把记忆沉淀下来,按需取用。这篇文章我会从设计思路、原理拆解、实操接入一直讲到踩坑记录,适合正在用 Claude API 或 Claude Code 写项目的人参考,也适合所有对“LLM 记忆增强”感兴趣的人。不管你懂不懂代码,看完都能明白它是怎么转的。

1. 项目核心思路:为什么要给 Claude 做一块外置记忆

1.1 模型无状态是痛点的根源

大模型本身就像一个大脑清零的天才:知识丰富,推理快,但聊完就忘。每次会话结束,刚才的上下文就被丢掉了。这在闲聊场景没事,但在真实项目里非常致命。比如我在维护一个开源文档项目,里面有几十个文件的命名约定、术语表、目录结构。每次我开新对话跟 Claude 说“接着改一下第三章节的措辞”,它根本不知道第三章节在哪,也不知道“术语表”里那几个词是什么。

我试过几种土办法:把整个项目背景复制粘贴进每条新消息;把常用约束写进系统提示词文件;甚至有人把历史对话导出成 Markdown,再手动喂给模型。这些方案都能跑,但维护成本极高——背景一长,token 就爆;文件一多,模型就分不清哪些是当前任务、哪些是历史包袱。这时候就需要一个能自动沉淀、自动检索、自动注入记忆的工具。claude-mem 做的就是这个。

1.2 记忆放本地,不折腾云服务

市面上很多“记忆增强”方案会把记忆存到云端,用向量数据库、Embedding API、云函数,听着高大上,实际用起来配置一大堆。claude-mem 的思路是本地优先:一个 SQLite 文件搞定存储,FTS5 全文索引搞定检索。你不需要起任何服务,不需要注册任何云账号,装好直接用。

SQLite 这个选择我觉得很聪明。Python 自带的 sqlite3 就能操作,Go、Rust 也都有官方库,部署时零依赖。FTS5 的全文检索配合 BM25 排序,在几万条记忆的规模下响应速度毫秒级,完全够用。很多项目一上来就上向量检索,本质上是对记忆检索的复杂度误判了:个人项目的记忆量级,根本到不了需要向量数据库的地步;关键词匹配加上时间衰减排序,效果已经不错。等真到了几十万条,再迁移也不是难事。

1.3 整体工作流程:先写库,再读库

claude-mem 的运转逻辑可以概括成两条链路。写入链路:一次对话结束后,你(或一个钩子脚本)把对话内容喂给 claude-mem,它会对原始记录做拆解:提取问题、回答、结论、约束、待办,压成一条结构化的记忆记录,然后写进 SQLite。读取链路:新对话开始前,claude-mem 会拿当前对话的开场信息去库里检索,把命中条目按相关度和时间排序,拼成一个“记忆上下文块”注入到系统提示词或首条用户消息里。

这两条链路一个向东、一个向西,互不干涉,但组合起来就形成了一个完整闭环。你可能会问:为什么不直接把整个历史对话塞进上下文?答案是 token 预算不允许,而且噪音太大。一个项目聊了 200 轮,里面八成是寒暄和过程性内容,真正有价值的信息可能只占两成。claude-mem 做的事情本质上是“蒸馏”:把两成的价值提出来,把八成的噪音扔掉。这样既保留了记忆,又不会挤占当前任务的上下文空间。

2. 核心机制拆解:记忆是怎么被写入、检索和注入的

2.1 写入链路:从流水账到结构化记忆

claude-mem 的写入不是简单把聊天记录整段存进去,而是做了结构化处理。一条记忆记录,我打开数据库看,大概长这样:

CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, project TEXT NOT NULL, -- 所属项目 session_id TEXT, -- 来源会话 created_at TEXT NOT NULL, -- 时间戳 updated_at TEXT, -- 更新时间 question TEXT, -- 触发场景 answer TEXT, -- 关键回答 summary TEXT NOT NULL, -- 蒸馏后的摘要 importance INTEGER DEFAULT 3, -- 重要度 1-5 tags TEXT DEFAULT '[]' -- 标签 JSON );

写入时它会做三件事。第一,给原始对话分段,拆成“问题-回答”对;第二,对每一对做摘要,把长度压到 200 字以内;第三,按规则打上重要度和标签。你可以手动调用claude-mem add添加记忆,也可以在脚本里让它自动抽取。我建议在对话结束后自己快速过一遍抽取结果——AI 自动摘要偶尔会把关键约束漏掉,手动纠偏值得的。

2.2 检索链路:SQLite FTS5 帮了大忙

读取时最怕的就是找不到。claude-mem 的检索默认走 FTS5 全文索引。关键词先被分词器切碎,然后走 BM25 算法打分,返回一条条候选记录。我翻源码时看到查询大概长这样:

SELECT id, project, summary, importance, created_at, bm25(memories_fts) AS score FROM memories_fts JOIN memories ON memories_fts.rowid = memories.id WHERE memories_fts MATCH ? ORDER BY score DESC LIMIT 20;

这个MATCH ?支持布尔语法,比如含义 AND 目录或章节 OR 目录。BM25 打分是全文检索的事实标准,它会把“词频高但常见词”降权,把“稀疏但精准的词”提高权重。claude-mem 在基础上还做了二次排序:把importance和时间衰减算进去,新近且重要的记忆排前面,这样即便关键词匹配一般,也能靠时间权重抬上来。

2.3 记忆注入的模板设计

光检索出来还不够,怎么塞回模型上下文也有讲究。claude-mem 默认的注入模板长这样:

以下是过去关闭的项目记忆中,可能与本次对话相关的部分。 请在回答时优先参考以下事实;如果它们与本次对话冲突,请明确指出冲突点。 <memories> <memory time="2025-01-12" project="docs-site" importance="5"> 用户坚持目录文件命名统一使用 kebab-case,不要驼峰。 </memory> <memory time="2025-01-10" project="docs-site" importance="4"> 术语表中“回调”统一翻译为 callback,不要写“回到调用”。 </memory> </memories>

用<memories>这种边界标签包起来,模型能明确区分“这是历史记忆”和“这是当前对话”。我实测下来,加了这个边界之后,模型对历史内容的引用准确率明显提升,不会把记忆当成新的用户指令执行。这个设计代价很小,收益却很实在。

2.4 上下文窗口的预算管理

Claude 的上下文窗口虽然长,但也不是无限的。claude-mem 在注入时会对结果做一步截断:先设置一个总预算,比如 1024 token,再按排序结果逐条塞,塞到预算用完为止。为什么不是一次塞 20 条?因为每条记忆平均占 150~200 token,20 条就是 4000 token,直接吃掉整个窗口的 2%~4%。对于大批量任务这无所谓,但你要是有很长的工具定义和示例要放,记忆部分就该压缩。

我自己的经验:预算设成 512~1024 token 比较平衡。太低的话模型容易忽略记忆;太高的话主任务上下文被挤压。检索结果宁可少而精,不要多而杂。claude-mem 的配置里可以直接改inject_token_budget,按项目需求调整。

3. 实操接入:从零开始跑通 claude-mem

3.1 安装与初始化

claude-mem 是命令行工具,安装方式看项目 README 为主。一般就两种:预编译二进制,或者源码编译。我用的是从源码编译的方式:

git clone https://github.com/your-local-clone/claude-mem.git cd claude-mem make build sudo install -m 0755 build/claude-mem /usr/local/bin/

装好之后先初始化配置。初始化会生成一个默认配置文件,同时创建一个空的 SQLite 库。第一次跑:

claude-mem init claude-mem status

如果初始化成功,status会显示数据库路径、记忆条数、当前项目名。我建议把数据库单独放一个目录,比如~/.local/state/claude-mem/memories.db,这样备份和迁移都方便,不会跟项目代码混在一起。

3.2 配置文件修改

初始化生成的配置文件是 TOML 格式,我在~/.claude-mem/config.toml里改了几个关键项:

[storage] db_path = "/home/you/.local/state/claude-mem/memories.db" [inject] enabled = true token_budget = 768 max_results = 10 min_importance = 2 [retrieval] match_limit = 100 min_score = 12.0 [project] name = "default" auto_tag = true

min_importance = 2意味着重要度低于 2 的记忆不参与注入,过滤掉大部分过程性记录;min_score = 12.0是检索分数阈值,低于这个分数说明匹配度太低,不值得占用预算。这两个阈值是暴力调参调出来的,不同项目建议自己跑几次claude-mem search "关键词"看看分数分布再定。

3.3 把 claude-mem 接进 Claude Code

我日常主力是用 Claude Code 写代码,所以最关心怎么把 claude-mem 嵌进去。Claude Code 支持 hook 机制,可以在会话启动和会话结束时执行外部命令。我在项目根目录配了一个.claude/settings.json:

{ "hooks": { "PreToolUse": [ { "matcher": "UserPrompt", "command": "claude-mem inject --format compact" } ] } }

这样每轮用户提问之前,claude-mem inject会先把相关记忆查出来,以紧凑格式追加到对话开头。格式如果选compact,注入内容会短很多,适合高频轮次;如果选full,会把完整时间戳和标签也带进去,信息量更足但 token 消耗也更高。

3.4 用 Python 接入 Claude API

如果你不是用 Claude Code,而是直接调 API,那接 claude-mem 更简单。核心逻辑就是:调 API 之前先claude-mem search,把结果拼进 system prompt 或者首条 messages 里。我写了一个最简单的工作流示例:

import subprocess import anthropic def get_memories(query: str) -> str: result = subprocess.run( ["claude-mem", "search", query, "--limit", "5"], capture_output=True, text=True, check=True ) return result.stdout system_prompt = ( "你是一个可靠的技术文档助手。\n" "以下是历史记忆,请优先参考:\n" + get_memories("术语表 目录命名") ) client = anthropic.Anthropic() resp = client.messages.create( model="claude-sonnet-4-0", max_tokens=2048, system=system_prompt, messages=[{"role": "user", "content": "帮我检查第三章节的措辞"}] ) print(resp.content[0].text)

这里有几个细节要注意。第一,search的查询词别太长,一两句话足矣;第二,--limit 5是取前五条,配合 token 预算刚好;第三,最好在每次对话开始前搜,不要在每轮都搜——每轮都搜会增加不少 API 延迟。

3.5 常用命令速查

命令作用常用参数
claude-mem add "约束" --tags project,style手动添加一条记忆--importance 1-5
claude-mem search "关键词"检索相关记忆--limit N、--project P
claude-mem inject将相关记忆注入上下文--format full/compact
claude-mem list --project P查看某个项目的记忆列表--sort time
claude-mem status查看库状态无
claude-mem compact压缩旧记忆,合并重复项--dry-run先看效果

compact这个命令我每两周跑一次。它会把重要度低、时间久、内容重复的记忆合并成几个大的“档案条目”,库就不会无限膨胀。

4. 进阶调整:记忆分级、团队共享与防“中毒”

4.1 记忆分级:短期、中期、长期分开管理

默认配置下所有记忆一视同仁,但用久了你会发现这不够。有些记忆是要长期遵守的规范,比如“项目命名统一 kebab-case”;有些记忆只是临时的,比如“今天下午三点要测一下接口”,过俩小时就没用了。

claude-mem 支持用importance字段做分级。我按自己的习惯分了三档:

  • 重要度 5:永久规范,项目核心约束,如命名规范、术语表。
  • 重要度 3:中期事实,如当前模块状态、负责人分工。
  • 重要度 1:临时代办,过几天就删。

配合清理策略:重要度 1 的记忆 3 天自动清理;重要度 3 的 30 天归档;重要度 5 的永不自动删。这个策略不是内置功能,是我写了个 cron 脚本调compact和delete --old实现的,效果很好。库不会泛滥,注入的质量也更高。

4.2 多项目隔离:一个库还是多个库

我一开始把所有项目的记忆都塞进同一个库,结果灾难了——搜“目录”这个词,文档项目和前端项目的结果混在一起,注入一堆无关记忆。后来改成按项目名隔离:每个项目建一个独立 SQLite 文件,或者在同一库里用project字段过滤。

更推荐后一种方案,因为search命令天然支持--project参数:

claude-mem search "目录" --project docs-site

同一个库里跨项目搜索偶尔也有用,比如查一个技术方案的往事,能搜到其他项目里的类似经验。共用库让数据可复用,按项目滤噪又保证精准,两全其美。

4.3 防“记忆中毒”:冲突与过时的处理

记忆系统最大的坑,是旧记忆跟新对话冲突。比如项目改版了,命名规范从 kebab-case 改成了 snake_case,但库里还残留旧记忆。如果 claude-mem 注入的是旧规则,模型就会犯错。

我的解决办法分三层。第一层,检索时加时间衰减:超过 90 天的记忆分数打折,降低被注入的概率。第二层,注入模板里明确写了“如果历史记忆与本次对话冲突,请明确指出冲突点”,给模型一个“抗命授权”。第三层,主动更新:每次改规范,我会手动删掉旧记忆,写一条新记忆。删旧用claude-mem delete --id,写新用claude-mem add --importance 5,一分钟搞定。

4.4 团队共享记忆:把知识沉淀下来

单人用 claude-mem 解决的是“失忆”问题;团队用解决的是“重复造轮子”问题。我把团队的 SQLite 库放在一个共享目录里,大家读写同一个文件。SQLite 对这种并发场景天然支持 WAL 模式,读写不会锁库。我实测过三个同事同时写入,没有出现问题。

如果是异地团队,也可以定期导出、合并、再导入。claude-mem 支持 JSON 导出:

claude-mem export --format json --output backup.json claude-mem import --file backup.json

团队共享的关键是给记忆打上“作者”标签,比如--tags author:zhang,这样后续看到一条陌生记忆,能知道是谁写的,也方便私下确认。

5. 常见问题与踩坑记录:我一个月里踩过的坑

5.1 高频问题速查表

现象原因解决方案
搜不到昨天的记录分词器对中文支持弱配置 FTS5 trigram 分词,或写入时额外生成英文别名标签
注入后占用太多 tokentoken_budget 设太大调低inject_token_budget,同时调高min_score
多客户端并发写入报错默认 rollback journal 模式在 SQLite 打开时设置PRAGMA journal_mode=WAL
记忆注入太旧,回答错误时间衰减不足打开时间衰减,缩短保留期
启动时注入慢数据库体量过大且无索引给project、created_at建索引,定期 compact

5.2 中文搜索匹配的坑

这个我折腾了两天。claude-mem 默认的 FTS5 分词器是 unicode61,它把中文按“整句单词”处理,所以搜“目录”搜不到“目录结构”,因为它们在词法上是完全不同的 token。解决方法是给记忆表单独建一个 trigram 分词索引。SQLite 从 3.34 开始支持 trigram tokenizer,把每一个连续三个字符当一个 token,中文搜索效果立竿见影。

CREATE VIRTUAL TABLE memories_trigram USING fts5(summary, content, tokenize="trigram");

配上 trigram 之后,英文的精确匹配依然走原表,中文的模糊匹配走 trigram 表,两个结果做合并,取并集后统一排序。数据量不大时这个“双通道”方案非常靠谱。

5.3 记忆条目爆炸式增长

人懒的时候会特别夸张,我有一阵子每天加 20 条记忆,两周就攒了 300 多条。里面混合着核心规范、临时任务、甚至还有心情记录。库一膨胀,检索速度不明显变慢,但注入质量明显下降——因为噪声多了,真正有用的记忆被挤掉。

这个问题的根源不是 claude-mem 设计不好,而是输入质量失控。现在我给自己订了个规矩:手动加记忆必须写“可执行的约束”,而不是“想法记录”。比如“我觉得目录结构可以更扁平”不算一条记忆;“目录层级不超过三层,新增文件优先放根目录”才算。质量把关是用户自己的事,工具只能帮你存,不能帮你判断哪些值得存。

5.4 API 报 400 错误:注入内容格式问题

有段时间我的 Python 脚本总是偶发报 400,查日志发现 system prompt 里混入了从记忆里带过来的非法字符。原因是我导入过一条带 HTML 标签的旧记忆,注入模板包上<memories>标签之后就变成了多级标签嵌套,导致消息格式解析出问题。

解决办法是在检索结果输出给模型之前做一个转义:把<、>转成全角字符,或者删掉记忆内容中的 XML 标签。claude-mem 后续版本在inject时可能已经处理了,但自定义接入时一定要自己加一道过滤。

6. 我的最终体会:记忆工具的边界和取舍不多说,最终聊点实在的

用 claude-mem 跑了一个多月,最大的感受是:项目连续性确实改善了。以前跨会话协作基本靠我的“复制粘贴毅力”,现在靠工具自动完成。这不代表它完美——检索准确性、注入质量、记忆的时效性之间始终存在张力,需要根据项目实际反复调整参数。我花了一天调min_score和token_budget,从最初每次对话注入 10 条、每次消耗 2000 token,收敛到注入 4~5 条、消耗 600 token,模型对历史规范的遵守率反而提升了。原因很简单:少而精的记忆更容易被模型注意到。

最后再分享一个我养成的习惯:每天结束工作前跑一次claude-mem list --sort time | tail -20扫一眼今天写了哪些记忆,发现写歪了就顺手改掉。这个动作花不了两分钟,却能在第二天开新会话之前把“记忆库”清理干净。如果你也想把 LLM 真正用进长期项目,从一件外置记忆工具开始,是性价比最高的路径。