说实话,我第一次意识到必须给AI助手做“记忆”这件事,是被逼的。某天我开着十几个会话窗口做同一个项目,那边刚在A窗口告诉助手“接口用REST风格,别用GraphQL”,转头在B窗口问代码方案,它又给我推了一版GraphQL的设计——那一刻我盯着屏幕愣了三秒:这到底是助手太笨,还是我太天真?后来我反应过来,问题不在对话窗口,而在于这些会话天生没有“跨窗口的长期记忆”。于是我花了两周时间,给Claude做了一个叫“claude-mem”的持久化记忆模块,让它能在不同会话间记住我的项目偏好、技术选型、甚至一些工作习惯。今天这篇,就是把整个设计思路、核心代码、以及我在实测中踩过的坑,完整拆给你看。
如果你也遇到过类似的重复交代背景、反复纠正同一件事、或者觉得“它明明之前知道,怎么换了个窗口就失忆”的情况,那这篇文章就是给你准备的。我会从最底层的记忆分类讲起,到存储选型、到检索注入、再到隐私和遗忘机制,顺带附上可以直接抄的配置和代码。
1. 记忆不是聊天记录:claude-mem要解决的真实痛点
1.1 为什么AI助手“聊过就忘”让人抓狂
很多人以为AI助手有上下文窗口,就等于有记忆。这是个非常普遍的误解。上下文窗口更像是一张“临时便签纸”,它只保证你在当前这一段对话里,它能想起之前聊过什么。一旦你关闭会话,或者新建一个对话窗口,这张便签纸就被清空了,一切回到原点。
这种“失忆”在短对话里没什么,但在持续数周甚至数月的项目里,就是灾难级的体验。举个例子:我在维护一个内部工具库,里面有三套不同的配置规范。每当我开新会话去问代码问题,都得重新打一遍“我们用的构建工具是XX、代码风格是YY、发布流程是ZZ”。打字倒是小事,问题是每次重新解释,都可能产生信息偏差——漏一条、错一条,AI给出的方案就完全走样。后来我干脆把项目背景写在一个固定的文本文件里,每次手动粘贴。但手动粘贴也很蠢,我明明可以一劳永逸地解决它。
claude-mem的核心出发点,就是把“临时便签纸”变成“可长期翻阅的笔记本”。它不是简单地把聊天记录存下来再翻出来,而是要在合适的时候、用合适的方式、把合适的记忆重新送到AI面前。这里面每一项拆开来看,都有不少门道。
1.2 从“上下文窗口”到“持久记忆”的思维转变
理解这个转变,你只需要记住一个比喻:上下文窗口是短期记忆,claude-mem是长期记忆的“外接硬盘”。
短期记忆的特点是容量小、速度快、随对话推进不断被新内容覆盖。你聊了一百轮之后,最早几轮的内容基本就被挤出了窗口,AI想“回忆”也回忆不起来了。长期记忆则不同,它可以存几百条、几千条关键信息,并且在你需要的时候,把最相关的几条“调取”出来,重新放回短期记忆里。
所以这里的关键不在于“存了多少”,而在于“取出的那一刻”。你存一万条笔记,如果AI每次只能看到其中五条,那五条选得准不准,直接决定了它的表现。这就引出了我设计claude-mem时最重要的三条原则:
- 记忆必须结构化,不能是一坨不可检索的日志文本;
- 检索必须带相关性排序,不该把所有记忆都无脑塞给AI;
- 记忆必须能演化迭代,过时的、冲突的要能覆盖或删除。
这三条原则听起来简单,真正落地时每一步都有坑。后面我会逐个展开。
1.3 三类可记忆信息:事实、流程与偏好
在动手写代码前,我先给自己做了一个“记忆分类学”。因为不是所有东西都值得长期记住,存错了东西比不存更糟。我把记忆分成三大类,这个分类后来成了整个系统的骨架:
第一类是事实型记忆,包括具体的命令、配置、邮箱地址、API端点、项目目录结构等。这类记忆的特点是客观、准确、可验证,比如“测试环境地址是192.168.1.88:8080”。它像你的通讯录,改动频率低,但记错了危害极大。
第二类是流程型记忆,描述“这件事应该怎么做”,比如“发布前需要先跑shellcheck再打tag”“上线走灰度,每次20%流量观察10分钟”。这类记忆本质是工作流和SOP,它的价值在于让你不用每次重新解释规范。
第三类是偏好型记忆,包括你对代码风格、命名习惯、工具选择的个人倾向,比如“配置用YAML不用JSON”“注释用中文,但代码里的log用英文”。这类记忆最主观,也最容易引发误判,因为它往往是AI在对话中“猜”出来的,一旦猜错,就会持续输出你不想要的东西。
我对这三类记忆的写入策略完全不同:事实型记忆高置信度直接存;流程型记忆需要二次确认再存;偏好型记忆则设置更严格的触发条件,避免把“随口一提”当“长期规则”。这个分类直接影响了我代码里的数据结构设计。
2. 核心设计定稿:记什么、存哪里、怎么取
2.1 记忆条目统一抽象:一条记录的完整字段
想清楚分类之后,我开始设计记忆条目的数据结构。最初我想弄得很复杂,分了七八个字段,结果越写越别扭——复杂的数据结构意味着后续的维护和迁移都痛苦。最后我砍到六个核心字段,足够覆盖绝大多数场景:
{ "id": "m_3f9a2b", "type": "fact", "content": "测试环境地址是192.168.1.88:8080", "tags": ["测试环境", "地址", "接口"], "importance": 0.8, "expires_at": "2025-12-31T00:00:00Z", "source_session": "sess_8x2k9q" }字段说明:
- type:标记这条记忆的类型,fact / workflow / preference,不同类型在检索时权重不同。
- content:实际要记住的内容。
- tags:关键词标签,用于快速过滤。它和下面的向量检索互为补充,标签负责精确,向量负责模糊。
- importance:重要度打分,0到1。这个值会参与注入时的排序,重要性越高的记忆越优先被送入上下文。
- expires_at:过期时间。有些记忆是有时效的,比如“临时调试用的token三天后失效”,这类到期就该被清走。
- source_session:记录来自哪个会话,方便回溯查证。
这个结构里最难定的是importance和expires_at。importance一开始我打算让AI自己判断,后来发现它会莫名自信,经常给“今天要喝美式”这种废话打0.99的高分。后来我改成“AI打分+规则修正”:凡是content里含有时效性词汇(比如“这周”“临时”)自动降低初始重要性并设置expires_at,凡是含有关键词“永远”“必须”“不要”的,提高重要性且不设过期时间。
2.2 存储选型对比:SQLite、JSON文件与向量库的取舍
存储层我纠结了挺久。主流方案就那几种:纯JSON文件、SQLite、以及专门的向量数据库。我做了个对比表:
| 方案 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| JSON文件 | 零依赖,读写简单,人类可读 | 数据量大了之后检索慢,没有结构化查询能力 | 个人轻量使用,几百条以内 |
| SQLite | 单文件、无需独立服务,支持SQL查询,稳定可靠 | 需要写SQL,对文本相似检索无能为力 | 中量级记忆管理,配合代码做标签过滤 |
| 向量数据库 | 内置embedding检索,语义匹配能力强 | 依赖较重,需要额外管理index和embedding模型 | 大规模记忆,上千条以上 |
最终我选了“SQLite + Python代码内做embedding检索”的组合。为什么没有上专门的向量数据库?因为claude-mem的定位是“个人记忆模块”,绝大多数人的记忆规模在几百到几千条之间,这个量级用不着一套独立的向量库服务。相比之下,SQLite单文件方案安装简单、备份方便——整个记忆库就是磁盘上的一个文件,拷贝走就是备份,省心得多。
embedding检索这块,我在每条记忆写入时调用文本嵌入模型把content转成向量,存进同一个SQLite表里作为BLOB字段。检索时,把用户当前的问题也转成向量,然后做余弦相似度计算,回到Python层排序取top-k。这样既享受了语义检索的好处,又避开了引入重型组件的麻烦。
2.3 记忆写入链路的三个关键判断
存储结构定下来之后,我梳理了“一条记忆从出现到入库”的完整链路。这不是简单地“听到就记”,而是要让每一跳都有判断。我在代码里实现了三个关键判断:
第一个判断是“该不该记”。触发写入的时机有两种:系统主动提取和用户主动命令。系统主动提取是指在对话中检测到关键信息(比如出现了“密码是”“端点地址”“记住了”等信号)时自动抓取;用户主动命令则是用户对AI说“记住:……”。前者会有误报,所以我对自动写入的记忆都默认降低初始重要度,宁可不够高,不要瞎记。
第二个判断是“是不是重复”。两条内容极其相似甚至冲突的记忆,直接入库会让检索结果变乱。所以写入前我会先对全库做一次相似度检查,如果已有的记忆与新内容相似度超过0.92,就视为重复,直接跳过;如果相似度介于0.7到0.92之间,则进入“记忆仲裁”逻辑——拿新内容去覆盖旧内容,但要在旧记录上打一个“superseded_by”标记。这一步非常关键,不然你会发现同一个事实被存了五六遍,检索时AI会看到互相矛盾的版本。
第三个判断是“该记在哪个分类”。这一步我交给了规则+AI协作:type字段由AI判断,同时用关键词规则做兜底。例如content里出现“步骤”“流程”“应该”等词,规则层会把type强制改成workflow;出现“我喜欢”“我习惯”“不要”则判为preference。兜底规则的存在,是为了防止AI抽风分类错误。
3. 从零搭建可运行的记忆服务:代码与配置
3.1 最小目录结构与依赖清单
到这你估计已经有点手痒了。我直接给出我当时搭建的最小可运行版本,先把骨架跑通,再谈优化。
我的目录结构长这样,非常朴素:
claude-mem/ ├── mem/ │ ├── __init__.py │ ├── store.py # SQLite存储层 │ ├── embedder.py # 文本向量化 │ ├── manager.py # 记忆管理主逻辑 │ └── mcp_server.py # MCP工具接入层 ├── config.toml # 全局配置 └── requirements.txt依赖方面用到的东西不多:
sqlite3:Python内置,负责持久化;toml:读取配置文件;httpx:调用本地或远程embedding服务的HTTP客户端;mcp(Model Context Protocol):如果你要让Claude真正调用这个记忆服务,这一步逃不掉。
不想用MCP也可以退一步,直接用标准输入输出的脚本方式,但那样体验会差不少——Claude无法“实时”调你的记忆工具。后面我会细说。
3.2 实现记忆管理器:memo_add与memo_search
核心逻辑我封装在manager.py里,最核心的是两个方法:memo_add和memo_search。我把关键代码贴出来,并逐段解释。
先看memo_add:
def add(self, content: str, memo_type: str, importance: float): # 1. 生成embedding向量 vec = self.embedder.embed(content) # 2. 重复性检查 similar = self.find_similar(vec, threshold=0.7) for hit in similar: if hit.similarity > 0.92: logging.info(f"重复记忆已忽略: {content}") return elif hit.similarity > 0.7: self.mark_superseded(hit.id) # 3. 写入新记录 rec = MemoryRecord( content=content, type=memo_type, importance=importance, embedding=vec ) self.store.insert(rec)这段代码的意图很明确:每条记忆入库前先做相似度检查,高于0.92直接丢弃,0.7到0.92之间则标记旧记忆为“已被替代”。这里说句实在话,0.7这个阈值是我反复调出来的,太低了会把不同内容误判成重复,太高了又拦不住轻微改写过的重复记忆。你在自己的项目里应该根据实际数据分布微调。
再看memo_search:
def search(self, query: str, top_k: int = 5) -> List[MemoryRecord]: qvec = self.embedder.embed(query) hits = self.store.vector_search(qvec, top_k=top_k) # 过滤掉已过期和被替代的记录 alive = [h for h in hits if not h.is_expired() and not h.superseded_by] # 按重要度加权排序 alive.sort(key=lambda h: h.similarity * 0.6 + h.importance * 0.4, reverse=True) return alive[:top_k]检索这里我做了一个很多人会忽略的细节:相关性和重要度的加权融合。纯粹按相似度排序有个问题——高相关但低价值的碎碎念会霸占前列。比如你问“怎么部署”,一条相似度0.91的“我讨厌下雨天”不可能出现,但一条相似度0.89的“上次部署的中间件版本是v2.1”和一条相似度0.85的“生产环境部署必须双人复核”会打架。前者更相关,后者更重要。0.6比0.4这个比例体现了我更看重“是不是正在问的事”,同时又不会让重要规则被淹没。
3.3 接入MCP工具,让AI会话自动读写
模块有了,接下来得让Claude真的能用它。MCP(Model Context Protocol)是当前比较通用的接入方式,它把记忆模块包装成几个工具,AI在对话中按需调用。我注册了三个工具:
memo_add(type, content, importance):写入记忆;memo_search(query, top_k):检索记忆;memo_forget(id):删除指定记忆。
MCP server的核心代码骨架如下:
from mcp.server import Server, Tool server = Server("claude-mem") manager = MemManager() @server.tool() def memo_add(memo_type: str, content: str, importance: float = 0.5): """记住一条新信息""" manager.add(content=content, memo_type=memo_type, importance=importance) return {"status": "ok", "id": last_inserted_id} @server.tool() def memo_search(query: str, top_k: int = 5): """检索与当前问题最相关的记忆""" results = manager.search(query, top_k=top_k) return [{"id": r.id, "content": r.content, "type": r.type} for r in results]接入MCP之后,Claude在会话中会自动决定什么时候调用这些工具。它有时会在任务开始前主动查记忆,有时会在对话中突然提取信息。我的实测是:主动调用率大概在70%左右,剩下30%需要你在对话里提一句“查一下记忆里的XX信息”,它才会去检索。所以我在系统提示词里加了一条引导规则:“处理任何涉及项目配置、代码规范、历史决策的问题前,先调用memo_search。”这一句话,让整体调用率提升到了90%以上。
3.4 记忆注入模板的参数设计
工具调用只是把记忆“拿”出来,真正让它发挥作用,还需要把它注入到Claude的上下文中。这一步我设计了一个记忆注入模板,它不直接拼原始字符串,而是先把检索结果格式化,再放到系统提示词的一个专门的“记忆区段”。
实际的模板长这样:
{ "role": "system", "content": "以下是关于用户的长期记忆,参考它们来回答当前问题。\n" "如果记忆与当前对话冲突,以记忆内容为准,除非用户明确表示记忆已过时。\n" "【记忆区段开始】\n" "- [事实] 测试环境地址是192.168.1.88:8080(重要度0.8)\n" "- [流程] 发布前必须执行shellcheck(重要度0.9)\n" "【记忆区段结束】\n" "请开始本轮对话。" }注意两个细节:模板里明确写了“冲突时以记忆为准”,这是为了避免AI在已有明确记忆的情况下还按对话里的临时说法走;每条记忆后面都带着重要度,给AI一个轻重参考。注入区段安排在系统提示词的最后部,尽量靠近用户当前提问的位置——离得越近,被上下文窗口“遗忘”的概率越低。
关于记忆区段的token预算,我的经验是控制在800个token以内。超过这个值,AI反而会被一堆历史信息干扰,生成变得僵硬、保守。你在调参时可以从600开始试,根据效果微调。
4. 检索比存储更考验设计:注入优先级与上下文预算
4.1 top-k检索与相似度阈值
记忆库建好之后,最难的反而不是“存”,而是“取”。如果每次只取3条,90%的关键信息都取不到;如果每次取50条,上下文直接爆炸。我最后把top_k默认定为5,同时维护了一条动态调整规则:当问题的复杂度上升(比如用户问的是跨模块架构问题),自动把top_k提到8;而针对简单事实查询,top_k降到3就够。
有一个非常容易踩的坑是相似度阈值。embedding检索看似智能,实际比较死板。同一个意思,换一种说法,余弦相似度可能只有0.6。我就遇到过:我存了“登录接口走graphql”,后来问“用户身份验证服务在哪个入口”,两者字面几乎不重叠,相似度只有0.55,直接被过滤掉了。后来我把兜底方案设成“标签命中优先”——如果query里的关键词命中了某条记忆的tags字段,即使嵌入相似度不够,也强行放进候选集。
4.2 上下文预算怎么算:一个可以抄的公式
关于上下文预算,我后来总结出一个可以直接抄的计算公式:
记忆注入预算 = min( fixed_cap, total_context_window * injection_ratio )fixed_cap是硬上限,我取800token;injection_ratio是注入比例,我取5%。为什么是5%?如果你用Claude的长上下文模型(比如200k窗口),5%就是10000token,理论上是够的,但我不建议真用这么多。因为记忆区段是每轮对话都要加载的,它会挤占实际回复和推理的空间。实测下来,记忆区段占整个上下文窗口的3%-5%是安全区间,下限是800token,二者取小值。
这套预算要做成动态计算,而不是写死。因为不同模型上下文窗口不同,同一模型不同配置也可能不同。我在代码里维护了一个budget_provider,每次对话开始时读一次上下文窗口大小,再乘上比例得到本次session的预算。
4.3 新旧冲突时的记忆仲裁规则
记忆的更新迭代是整个系统里最容易翻车的地方。我举个例子:某个接口的地址从v1升级到了v2,AI在旧会话里记住了v1。新会话里你告诉它地址变了,那么旧记录还在记忆库里。下次检索时,新旧两条记忆同时被命中,AI就会陷入“到底听谁的”的困惑。
我的解决方案是“三态仲裁”:
- 新记忆与旧记忆的相似度高,且用户在新记忆里明确说“不用之前的”“换掉”“改成”,则旧记录直接标记superseded;
- 新记忆重要度高于旧记录,但内容没有明确冲突,则两条都保留,注入时优先展示重要度高的;
- 新旧内容只是平行补充,互不冲突,则两条都保留,分别入库。
这套仲裁逻辑在代码里就是一堆条件判断,但规则写得越明确,AI在混乱场景下的表现就越稳。你不能指望AI自己“悟”出仲裁规则,必须写死。
5. 实测效果与两个最容易翻车的坑
5.1 几轮实测的效果对比
光说不练假把式。我把claude-mem接入到自己的日常AI工作流里,连续测了两周。我做的对照很简单:同一系列任务,一周不开记忆模块,另一周开启,对比两者的平均纠正次数和成果质量。
结果挺明显的。开记忆前,我每次开新会话都要花3到5轮“背景交代+规范重申”,偶尔还会因为漏交代导致方案返工;开记忆后,首轮回复就准确包含了我长期强调的几条规则。比如我常用的一个配置场景,之前每次要单独说“不要用单引号缩进”“模板引擎要开严格模式”,现在AI上来就懂。整体算下来,单次任务平均节省了约40%的“复述时间”,而且是越复杂的任务省得越多。
不过,时间节省只是表象,更值钱的是决策一致性。跨会话的代码风格和接口规范保持一致之后,我不用再靠人去记住“当时到底定了哪个方案”,事实本身就存在记忆库里。
5.2 坑一:AI把“随口一提”当成“永久偏好”
这个坑我在前文提过几次,但实际踩的时候感受更深刻。有次我们在讨论临时工具有个选项“方便起见这次直接跳过校验”,我随口说了一嘴。结果下一周我在写正式流程时,AI居然引用记忆区里的“跳过校验”作为既定规范,直接给我产出了一份没有验证环节的方案。
问题出在写入阶段:这类带有时效性、随口性的话,不应该进入长期记忆。我加的修正规则是:内容中若出现“这次”“暂时”“先”“随便”等临时性副词,禁止自动写入preference类型,最多写成临时fact并设置短expires_at(比如24小时)。同时引入了“写入确认”机制——不确定的一条记忆,默认以“待确认”状态存下,只在AI对话中给它一个提示:“有一条不确定的记忆,是否确认记入长期偏好?”这样,长期偏好库里的内容,可信度大幅提升。
5.3 坑二:记忆越攒越多,对话反而变笨
第二个坑更反直觉。跑了两三周后,记忆库涨到了400多条。我以为越多越好,但实际测试时发现,AI回复开始慢慢变得“啰嗦而保守”——它总想把自己的回答往记忆里的旧内容上靠,反而对当前问题的新颖解法不够敏感。典型症状是:你问一个全新的问题,它非要先复述一遍“根据你之前的偏好……”然后才给答案。
根因是检索层放入了太多“命中”的记忆,挤占了上下文空间。解决办法有两条路:一是收紧检索过滤条件,把top_k从5降到4,同时把阈值从0.7提到0.75,减少边缘相关性记忆的混入;二是定期清理过期和低重要度记录。我写了一个每七天跑一次的记忆瘦身脚本:将所有importance低于0.5、且超过30天未被检索命中的记录归档到一个备份表,而不是直接删——万一哪天要找旧信息还能翻出来。这样主记忆库保持在一个“精益”状态,AI的回复也恢复到了干净敏捷的状态。
6. 隐私边界与后续演进方向
6.1 数据边界:本地存储、敏感信息隔离与遗忘机制
聊到最后必须说隐私。claude-mem本质上是在收集你与AI对话里的信息,如果边界没定好,就是给自己埋雷。我在设计时立了三条硬规矩:
第一条,默认全本地存储。记忆库就是磁盘上的一个SQLite文件,不主动上传任何第三方服务。接MCP时,AI会话会临时读取记忆注入上下文,但记忆本体始终留在本地。
第二条,敏感信息隔离。凡是content里命中密码、token、私钥等敏感模式,一律不进正常记忆库,而是进一个加密的隔离表,并且这些记忆默认不参与检索注入。检索代码里加了个硬过滤:if secret_flag: skip。这玩意儿宁可有漏网之鱼,也不能拿密码去冒险。
第三条,遗忘机制不是可选项。每条记忆都有expires_at,过期就自动降级。用户也可以主动用memo_forget命令删除。我还在配置里加了一个“记忆总条数阈值”,比如超过1000条时强制触发一次重要度重排,把低重要度、长期未命中的记录归档。
这套边界做下来,我对它的信任度才高到敢在真实工作流里长期开着。你也别嫌麻烦,作为开发者,先想想“如果这条记忆泄露出去,我会不会慌”,再决定要不要把那条记忆写进去。
6.2 几个可以继续深入的扩展方向
目前这个版本的claude-mem已经够我用,但它还远不算完满。我自己觉得有三条值得继续走下去的路:
一是记忆的分层管理。现在所有记忆都在一个大池子里,未来可以按项目、按主题拆分成多个记忆空间。比如“工作项目A记忆库”“个人学习记忆库”,检索时带上空间限定,隔离性会更好,也不会出现工作规范混入闲聊场景的尴尬。
二是记忆摘要的自动提炼。当某类记忆积累到一定数量,可以定期用AI对它们做一次聚合摘要,把零散细节提炼成一条高层次的规则。比如“测试环境地址”有三四处历史版本,摘要后只剩一条当前版本,既压缩了存储,也减少了冲突。
三是多模态记忆。现阶段只能记文本信息,未来如果能把对话中的图片、代码片段、甚至终端输出的关键部分都结构化入库,记忆的价值会大得多。但难度也会指数级上升——想想怎么给代码片段做embedding并保持可执行性,就知道不是小事。
最后,如果你准备动手做类似的东西,我能给的最实在的建议就一句话:别一开始就追求大而全,先把“记住一句话、查出一句话、忘掉一句话”跑通,再慢慢往上加东西。
我自己从有这个念头到做出第一个能用的版本,只花了四个晚上。第二周接了MCP,第三周开始日常使用,现在它已经成了我工作流里离不开的一部分。这个项目的价值不在于代码有多精巧,而在于它把“AI和你一起工作”这件事,从“每次从零开始”变成了“越用越懂你”。这种体验,一旦用上就回不去了。