☰
claude-mem:为大模型构建长期记忆系统的工程实践
2026/10/8 11:23:30 网站建设 项目流程

如果你用过Claude超过一个星期,大概率会碰到同一个尴尬场景:每次开新对话,都得把职业背景、项目进展、技术偏好重新敲一遍,更别提那些散落在几天前对话里的关键结论。我最初做claude-mem这个项目,就是因为实在受不了这种"每次从零开始"的割裂感,想给Claude装上一套真正可用的长期记忆系统。

claude-mem的定位很明确:它是一个运行在Claude之上(也可以配合其他模型)的轻量级记忆层,负责把对话中值得记住的信息抽取出来、持久化存储,并在合适的时机重新注入到最新的上下文中。它解决的痛点是"多轮对话有记忆,跨天跨项目也有记忆",让AI从一个"每次都假装认识你"的新同事,变成"记得你上周说过什么"的老搭档。

这篇文章不会只讲概念,我会把整个项目的设计动机、架构思路、核心管线的实现细节,以及我在真实使用中踩过的坑一起拆开说清楚。适合正在做大模型应用集成、做Agent记忆系统,或者单纯被"AI没记忆"搞烦了想自己动手的开发者参考。

1. 为什么需要一套独立的记忆层:大模型的"失忆"是结构性的

先说一个反直觉的事实:你让Claude"记住"某个信息,它记住的那个东西,和你想的完全不是一回事。大模型的记忆机制只有两种,都极其有限。第一种是上下文窗口内的临时记忆,这个不用多说,窗口一关就没了。第二种是模型权重里的参数化记忆,那属于预训练阶段学到的通用知识,跟你的个人偏好没有半点关系。

这就意味着,所谓"让AI记住你的习惯",本质上是个系统工程问题:你需要在外部的存储介质里把信息存下来,再在每次对话前"帮它回忆起来"。这套外部存储加检索加注入的机制,就是记忆层。

我见过不少团队试图用"越来越大的上下文窗口"来解决问题,方向是错的。就算Claude的上下文窗口能塞下几百万token,把历史记录全量堆进去,带来的问题比解决的还多:

  • 检索噪声淹没有效信息:相关的事实藏在上万行无关日志里,模型反而更难准确回答。
  • Token成本线性增长:对话拉长之后,每次请求光历史就烧掉大把额度,成本完全不可控。
  • 权重的稀释:几千条历史堆在一起,每条信息的"注意力"都被摊薄,模型会抓不住重点。

所以我从一开始就认定,记忆系统的核心不是"存得多",而是"存得准、找得到、用得对"。这套思路跟搜索引擎很像——你不是直接把整个互联网塞进用户眼前,而是根据查询条件挑出最相关的那几十条结果。

1.1 记忆到底要存什么:事实、偏好与意图的分类思路

在设计抽取规则之前,我得先想清楚一个问题:什么样的信息值得进入长期记忆?总不能每句话都存。我最终把记忆分成三个大类,这也是后来整套系统的骨架。

第一类是硬事实。包括"我用的技术栈是React和Node.js""我在一家做跨境电商的公司做前端负责人""项目D的截止日期是下周五"。这类信息的特点是确定性高、可验证,适合直接抽取后存放,召回时也几乎不会出错。

第二类是偏好与习惯。"我写代码喜欢函数式风格""回复不要太长,重点说结论""汇报工作时先讲风险再讲进度"。这类信息短期看不出价值,但长期积累之后,是让AI输出风格向"你自己"靠拢的关键。

第三类是进行中的意图和上下文线索。"我正在排查支付回调的延迟问题""昨天和产品经理确认了新版首页的设计稿"。这类信息有一定时效性,但时效过了之后并不是彻底没用,它们构成了你在一个时间段内的工作主线,适合用时间衰减的方式管理。

这三类信息在存储和召回策略上各有不同,后面详说。先记住这个分类,它能帮你把"记忆"这个模糊的概念变成可以落地的数据结构。

1.2 为什么选本地存储优先:隐私、成本与可控性三者兼顾

记忆系统的存储方案我纠结过很长时间。最初的想法是直接调现成的向量数据库云服务,省事。但仔细想下来有几个问题绕不过去。

首先是隐私问题。记忆数据是你跟AI对话的原始素材提炼,里面可能有商业机密甚至个人信息。如果走云服务,数据就落在了第三方手里,这在很多企业内部是合规红线。其次是成本。向量数据库的托管服务一般按存储量和查询量双重计费,我是拿它当个人工具用的,一个月的账单很容易就超过一杯咖啡钱。第三是可控性——如果你自己不能随时翻出数据库里的内容检查"模型到底记了什么",那你实际上就是把记忆的控制权交给了别人。

所以我最终决定:默认走本地优先,所有记忆数据存在本地文件里,用一个轻量的向量索引做检索。只有当用户明确配置了远端数据库时,才把数据同步出去。这个设计也带来额外的好处——由于数据全部落在本地,我可以在调试时直接打开存储文件查看每条记忆的原文、提取时间、置信度,哪个环节出了问题一目了然。

2. claude-mem的架构设计:提取、存储、召回、注入四条链路

如果你在GitHub上翻过类似的记忆项目,会发现绝大多数实现都很"薄"——本质上就是在系统提示词里塞上一堆历史记录,然后听天由命。这个做法的问题在于它没有把"记忆"当作一个独立的基础设施来设计,而是当作文案的拼接。

我的架构思路是把记忆系统拆成四个独立的处理阶段,各管一段,之间用清晰的数据结构衔接。

阶段一:提取(Extraction)。在每一轮对话结束后,把对话内容发送给一个专门的提取模块(可以用Claude自身,也可以换更便宜的小模型),让它按规则产出结构化记忆条目。这一步的关键是产出格式必须稳定——你需要定义一个严格的JSON Schema,而不是让模型自由发挥。

阶段二:存储(Storage)。提取出的记忆条目经过判重、标准化、写入本地存储。这里我最开始用了纯JSON文件,后来数据量上来之后换成了SQLite,原因后面讲。每条记忆在写入时都会生成一个向量表示,用来支撑后续的语义检索。

阶段三:召回(Retrieval)。每次用户发出新消息时,系统会把这个消息转化为查询向量,去记忆库里找最相关的历史记忆。这里我做了混合召回,不只看语义相似度,还叠加了时间衰减因子和记忆重要性权重,最终按综合评分取TopK条。

阶段四:注入(Injection)。把召回的TopK条记忆排版成一段结构化的"记忆上下文",插到系统提示词和用户消息之间。这一步最考验功力——记忆条目的措辞要尽量精简,同时保留足够信息量,避免一大段文字直接挤爆上下文预算。

这套四阶段设计最大的好处是每一环都可以独立测试、独立替换。如果觉得召回不准,就只调召回模块;如果觉得提取的信息太碎,就只改提取的Prompt。你不会因为改了一个步骤而把整个系统弄崩。

2.1 对话边界的切分:不是所有turn都值得触发提取

既然要把对话内容送去提取,那"什么时候触发提取"就成了一个绕不开的问题。每个turn都提取一遍,除了浪费token之外,还容易抽出大量无关信息——比如"帮我打开浏览器"这种一次性指令,根本没有长期价值。

我采用的策略是把对话按语义边界切分成块。具体来说,每次检测到以下信号之一就触发一轮完整提取:

  • 连续对话超过3轮,且其中出现了明确的结论性表达("那就这么定了""确认没问题"等);
  • 用户主动提到了时间相关的信息("下周""明天""月底"),这通常意味着会产生日程类记忆;
  • 对话主题发生了明显切换,比如从技术讨论转到了午饭吃什么;
  • 用户主动要求记忆,比如说了"记住这件事""这个很重要"。

这个切分策略本质上是做了一个"什么时候值得记忆"的预判,把精确触发和批量提取结合起来。相比粗暴地每轮都处理,能省下接近一半的API调用量,同时记忆的密度反而更高。

2.2 提取模块的Prompt设计:重剑无锋,讲清楚比讲花哨重要

提取模块能不能产出高质量的记忆条目,七分靠Prompt,三分靠模型。我的经验是:宁可用朴素直白的指令,也别写花里胡哨的角色扮演Prompt。

以下是我在多次迭代后沉淀出的提取Prompt核心模板,你可以直接拿去改:

extraction_prompt = """你是记忆抽取器。请从下面的对话中抽取值得长期记住的信息,并严格按照JSON格式输出。 对话内容: {conversation_text} 抽取规则: 1. 只抽取有长期价值的信息,忽略寒暄、一次性指令和临时状态。 2. 信息分为三类: - fact:确定性的事实陈述,如职业、技术栈、项目信息、时间节点。 - preference:用户的行为偏好、表达风格、做事习惯。 - intent:进行中的目标、正在跟进的事项。 3. 每条记忆必须独立、明确,在什么上下文下都成立。 4. 输出格式: {"memories": [{"type": "fact|preference|intent", "content": "一句话描述", "confidence": 0.0-1.0, "timestamp": "ISO时间"}]} 只输出JSON,不要输出任何解释。"""

这个Prompt有三个关键设计。第一,明确指定了"只输出JSON",从格式上约束了模型的自由度,避免出现"好的,我已经记住了以下内容..."这种噪音。第二,confidence字段给后续的筛选提供了依据,低置信度的条目要么不写库,要么标记为待确认。第三,分类强制模型去思考"这条信息属于哪一类",这本身就提高了提取的质量。

2.3 存储层选型:从JSON文件到SQLite的演进

项目早期,我图省事直接用JSON文件存记忆条目。数据量在几百条以内时一切安好,打开文件就能看,调试体验极佳。但数据量爬到几千条之后,问题就来了:

  • 每次写入都要整个文件读入内存、修改、再写回,IO开销迅速膨胀。
  • 没有原生的索引能力,召回时得全量遍历,笨重但还能忍。
  • 更麻烦的是并发——如果两个异步任务同时写文件,很容易互相覆盖。

后来换成了SQLite,整个体验一下就顺了。SQLite是个被严重低估的组件——它虽然是个嵌入式数据库,但单机场景下的性能和可靠性完全不输那些重型的数据库服务。

我的存储表结构大致长这样:

CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, type TEXT NOT NULL, -- fact / preference / intent content TEXT NOT NULL, -- 记忆内容 confidence REAL NOT NULL, -- 置信度 0.0 ~ 1.0 importance INTEGER DEFAULT 1, -- 重要性权重,后期可手动调整 created_at TEXT NOT NULL, -- 创建时间 last_accessed TEXT, -- 最近召回时间,用于时间衰减 access_count INTEGER DEFAULT 0, -- 召回次数 embedding BLOB -- 预计算的向量,二分法或暴力检索用 );

这里有两个小设计值得说一下。一个是last_accessed和access_count字段,它们一起构成了"记忆被使用情况"的画像——如果一条记忆存了一年从来没被召回过,那它的重要性评分就该往下调,甚至可以考虑归档。另一个是embedding字段,我直接存的是二进制的向量数据,取出来做cosine similarity计算非常方便。

2.4 检索与注入:如何让模型"恰好想起来"又不被噪声干扰

召回和注入是整个系统里performance feeling最强的部分,做得好不好直接决定用户体验。

召回阶段我用了混合评分公式,不是单纯看向量相似度。计算公式大致是:

final_score = 0.6 * semantic_similarity + 0.25 * importance + 0.15 * recency_score

其中recency_score是一个随时间衰减的函数,7天内的记忆得分较高,超过30天开始显著下降,但不会降到零。这个设计是为了兼顾"近期信息更相关"和"重要事实不该被遗忘"两个诉求。

召回之后是注入。这里有个取舍问题:一次性注入多少条记忆合适?我实测下来的结论是:5条以下太少,模型经常get不到关键设;15条以上太多,会明显干扰模型对当前问题的注意力;8到12条是一个比较舒服的区间。

注入的格式我试过很多种,最后稳定在下面这个模板:

[长期记忆参考] 以下是用户之前提到过的重要信息,供你在回答时参考(按相关性排序): - [记忆类型] 内容 - [记忆类型] 内容 ... 请自然地运用这些信息,不要主动提及"根据你的记忆"之类的话术。 [长期记忆参考结束]

注意最后一句话很关键——我测试中发现,如果模型意识到自己是在"读取记忆",回答时会不自觉带上一种"作为AI我翻看了历史记录"的机械感。去掉这句话之后,输出会自然很多,就像真的是个了解你的老朋友在跟你聊天。

3. 实测中的效果与翻车现场:哪些问题真的让人头大

看完上面的架构设计,你可能觉得这套系统已经挺完整了。但真实的工程落地永远比架构图残酷。我在几周的真实使用中遇到了不少问题,挑三个最有代表性的讲。

3.1 记忆膨胀导致的"泛记忆化":AI把你的临时想法当成了长期信条

最早期版本的提取模块对"确定性"判断不敏感,导致一个很尴尬的现象:我在某次对话里随口说了句"我觉得用MongoDB比PostgreSQL好,主要是灵活",过两天新对话里模型就直接把我的偏好定性为"MongoDB优先,PostgreSQL排除"。可实际情况是,我那个判断只针对一个特定场景,全局来说我大部分项目还是用的PostgreSQL。

这个问题的本质是样本不足导致的过度泛化。我临时说了句气话或者基于局部信息的判断,被提取成了一条长期的事实性偏好。

解法分两层。第一层是在提取的时候加一个"上下文限定"判断,如果信息是在特定语境下给出的(比如带了"在这个项目里""这次我觉得"这类限定词),就标记为"情景性记忆",召回时的权重调低。第二层是在注入时做"软化"——对于preference类型的记忆,注入文案可以写成"用户在某些情况下偏好MongoDB",而不是"用户偏好MongoDB"。这个简单的措辞调整效果非常显著。

3.2 向量检索的同义表达问题:换个说法就找不到了

另一个让我头大的问题是召回率。比如我记忆里存着"用户对Rust的异步运行时Tokio比较熟",下次我聊"async runtime在Rust里怎么选型",向量相似度确实能算出来。但我说"用tokio处理并发"的时候,由于语义表达的差异,召回的排序分数不高,可能就排不进TopK。

我先后试过两个方案。第一个方案是关键词扩展,把记忆内容和查询内容做一次关键词映射,把同义词和相关性高的词放进查询向量里。体感有一点提升,但收益有限。

第二个方案效果更好:增加一次LLM辅助的重排。在向量召回Top20之后,把这些候选交给Claude做一次快速排序,让它挑出和当前问题最相关的8到12条。这一步多了大概几十毫秒的延迟和少量token消耗,但召回准确率的提升非常可观。

这个方案也顺便解决了另一个问题:向量相似度高的记忆,有时候语义上恰恰是最不该被注入的。比如用户之前说过"我不喜欢Redis",这句话和当前讨论"Redis的持久化机制"在向量空间里距离很近,但把"不喜欢Redis"这个偏好注入到"技术问题讨论"的上下文里完全不合适,甚至可能误导模型。重排阶段可以结合对话语境把这些干扰项过滤掉。

3.3 注入时机与系统提示词的冲突:记忆多了反而变蠢

还有一次很诡异的调试经历。某次我把记忆注入量调大到了20条,结果Claude在回答一个简单问题时的表现反而比不注入记忆时更差。反复排查之后发现问题出在系统提示词和注入记忆之间的"注意力竞争"上。

Claude的系统提示词本身承担了"你是助手、要准确回答问题"这些核心职责。当我塞入20条记忆时,这些记忆占据了大量注意力权重,导致模型在生成回复时"忙于回忆"而"疏于推理"。这个现象在长上下文中尤其明显。

我把这个现象叫"记忆注意力税"。解法很简单粗暴——严格控制注入上限,同时给记忆区块设置明确的位置。我的经验是最佳位置在系统提示词之后、用户消息之前,并且要让记忆区块在格式上和系统提示词区分开(如上文展示的标记块)。这样模型既能注意到记忆的存在,又不会把它们误当成必须遵守的指令。

4. 进阶玩法:让claude-mem从"个人玩具"变成"团队基础设施"

如果你只是自己用,前面几节的内容已经足够支撑你搭一套好用的记忆系统了。但如果想更进一步,让记忆系统在团队协作、多Agent协作的场景发挥价值,还有几个方向的坑值得提前踩一踩。

4.1 多账号与记忆隔离:千万不能做成"大一统"

我一开始天真地以为,记忆库当然是共享的,大家一起往里面写,召回的时候谁都能用。结果在两个人同时用的场景下立刻翻车——同事A问了个关于用户增长策略的问题,召回来的记忆全是我之前聊技术架构时留下的,回答显得驴唇不对马嘴。

问题的根源在于记忆库缺少**主体维度(owner)**的隔离。每个用户、每个项目、甚至每种对话场景,都应该是一个独立的记忆空间。我把存储结构升级成了带命名空间的设计:

namespaces/ ├── user_alice/ │ ├── memories.db ├── user_bob/ │ ├── memories.db ├── project_ecommerce/ │ ├── memories.db

每个命名空间独立存储、独立召回。如果你想做跨命名空间的记忆联动(比如用户A和用户B在同一个项目下的协作记忆),那就走显式的"共享"机制,而不是靠全库无差别检索。

4.2 定时清理与遗忘策略:好记忆的标准是"该忘的能忘"

在真实使用中,我慢慢意识到一个被很多人忽略的原则:记忆系统的能力边界不只在"记住",更在"遗忘"。如果什么信息都往里存,时间长了记忆库会变成一锅粥,召回的准确率会随着数据量增长而持续下降。

我的做法是引入了多层遗忘策略。第一层是时效性失效——带明确时间节点的intent类型记忆(比如"下周完成项目Poc"),越过截止日期后自动降权,再过一周就移入冷归档区。第二层是低价值回收——访问次数为零或者低于某个阈值、且创建超过90天的记忆,系统会主动提醒你是否要删除。第三层是显式遗忘——我做了条命令,用户可以直接说"忘了之前关于XX的事情",系统会按语义召回目标记忆并将其删除。

这条命令的实现很有意思,本质上就是用一次"反向提取":把用户的遗忘指令也走一遍向量检索、找出相关记忆、标记删除。有了遗忘能力之后,记忆系统的"信噪比"才真正稳定下来。

4.3 从命令行到API化:记忆系统的下一步

虽然claude-mem最初是围绕Claude对话设计的,但用了一段时间之后,我很自然地想把它从"对话场景"里解放出来。因为记忆并不只属于对话——你在写代码、写文档、开会的时候产出的信息,同样值得沉淀。

所以我把记忆的核心能力(提取、存储、检索、遗忘)封装成了一个独立的CLI工具和Python API。这意味着你可以在任何需要"上下文"的地方调用这套记忆:

# 手动写入一条记忆 claude-mem add --type fact --content "项目X的生产环境在AWS us-east-1" # 语义搜索记忆 claude-mem search "我们的数据库部署在哪里" # 查看全部记忆 claude-mem list --type preference --limit 20 # 遗忘一条记忆 claude-mem forget "关于旧项目的部署细节"

做成CLI之后有个意想不到的收益——我开始把记忆系统接入了自己的自动化工作流。比如每周五跑一次脚本,自动总结本周的技术决策,写入记忆库;每天早晨的新对话启动前,自动召回"最近一周在做什么"来初始化上下文。当记忆系统从"对话的附属品"变成"工作流的基础设施"之后,它产生的价值完全上了一个量级。

这个方向上的下一步,我打算让claude-mem支持多模型共享记忆库——现在大家都在同时用多个模型工具,如果能让每套模型都读同一个记忆库,那套"一次沉淀、处处受益"的体验,才是记忆系统真正成熟的样子。

5. 复盘:如果重新做一遍claude-mem,我会改掉什么

项目做到现在这个阶段,我觉得最有价值的产出其实不是那几千行代码,而是我从这个项目里沉淀出的几条判断。如果能给当初的自己几条建议,我会这样说。

第一,先定义"记忆的质量标准",再动手写代码。我一开始就是被"给AI加记忆"这个模糊的念头驱动着往前走,没有先想清楚"什么样的记忆是好记忆"。回头看,"准确、精简、可召回、可遗忘"这几个标准应该是一开始就定下来的,它们决定了后续所有设计的方向。

第二,Prompt的重量被严重低估了。这个项目最核心的智能其实不在代码里,而在提取模块和重排模块的Prompt里。同样的代码框架,换一组更精细的Prompt,效果能差出一大截。我会建议新手别急着优化代码逻辑,先把提取流程的输入输出盯细——拿50条真实对话去测试、统计识别率、逐条分析漏了什么、多了什么,然后迭代Prompt。这个过程的ROI远高于写一堆花哨的工程结构。

第三,别在早期过度设计。我刚起步时花了不少时间研究分布式存储、多机同步这类高级话题,事实证明完全用不上。等数据量真的到了需要横向扩展的程度,你早就知道自己需要什么了。先把单机版跑通、跑稳、跑出体感,再考虑规模问题。

我用这个系统已经有一段时间了,最直观的感受是:Claude从"每次对话都要重新认识的陌生人"变成了"一个记得住我技术偏好、项目进度、办事风格的长期协作者"。每次回复里带着我之前提过的上下文的时候,那种体验确实回不去了。

如果你也想动手做一套类似的记忆层,我的建议很简单:先别急着做大规模设计,把提取Prompt找对、把存储结构定好、把回调和注入的格式调顺,这三件事做好,你八成已经超过大多数同类项目了。

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

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

立即咨询