很多人在使用 Claude 这类对话模型时都遇到过同一个尴尬:上午刚聊完的方案细节,下午换个话题再切回来,它已经完全不记得了。如果只是临时问答倒也还好,可一旦你希望它持续跟进一个项目、记住你的写作偏好、或者跨多轮会话维护一份"长期使用说明书",这种"每次对话都从零开始"的体验就很劝退。claude-mem 这个项目正是冲着这个痛点来的——它要做的事情很简单也很本质:把模型本身没有的长期记忆能力,通过外部存储和提示词注入补回来。
我不是这个项目的作者,只是在自己的一个模拟项目中完整复现了它的核心思路,顺便改了一版适配自己工作流的实现。前前后后踩了不少坑,也想清楚了很多"为什么要这样设计"的问题。这篇文章把我拆解 claude-mem 原理、动手实践的过程完整记录下来,内容包括记忆的分层建模、数据存储选型、检索策略、Prompt 注入逻辑,以及实测中出现过的各种边界问题。适合两类人看:一类是刚接触对话模型记忆增强、想搞明白"记忆到底是怎么塞进上下文里的"的初学者;另一类是已经在做类似工具、想对比一下实现取舍的开发者。
1. 先说清楚:Claude 对话中"没有记忆"到底卡在哪
很多人以为模型不带记忆是因为"模型笨",其实不是。Claude 这类大语言模型本质上是一个"无状态函数"——每次请求,你喂给它一段输入,它基于这段输入生成输出,处理完就结束了。模型内部的权重是固定的,它不会因为你昨天和它聊过什么,今天就在权重层面发生变化。
那为什么 ChatGPT、Claude 的官方 Web 界面里看起来"好像记得"?因为官方客户端替你在背后做了两件事:一是把你的历史消息全部带着(或者做了一定截断),二是把你的历史消息重新拼进新的请求里。也就是说,记忆从来不活在模型内部,而是活在每一轮请求携带的上下文窗口中。
claude-mem 解决的核心问题就在这个上下文的"搬运"环节。很多人自己调用 API 写对话脚本时,习惯性地只发送当前这一条消息,结果模型当然不记得之前说了什么。更常见的是把全部历史消息一股脑都发过去,这样短期没问题,但对话一长就会撞上上下文窗口上限,而且费用和延迟都跟着涨。
我整理了一下,实际场景中"记忆缺失"大致分三个层面:
| 记忆丢失类型 | 现象 | 原因 |
|---|---|---|
| 会话内短期失忆 | 同一个 session 聊长了,早期内容被截断 | 上下文过长被裁剪 |
| 跨会话完全失忆 | 关闭对话后重新开,什么都不记得 | 新请求未携带旧会话信息 |
| 事实性偏好失忆 | 用户说过"我喜欢简洁回答"但下次又长篇大论 | 偏好未持久化存储,只在临时上下文里存活 |
claude-mem 的解决思路不是让模型变得有记忆,而是把"记忆"当成一种可读写的外部数据层:对话结束后抽取重要信息存入数据库,下次对话开始前再把和当前话题相关的信息注入到系统提示词里。这套思路本质上和 RAG(检索增强生成)一致,但它把"记忆"从文档检索抽象成了"用户事实 + 对话摘录 + 偏好配置"三者的统一管理。
2. claude-mem 的记忆建模:分层存储与摘要提取逻辑
真正动手写记忆系统之前,最先要回答的问题不是"用什么数据库",而是"记忆长什么样"。我见过不少人一上来就往 SQLite 里塞 JSON,塞进去才发现存进去容易、取出来难——因为你不知道在未来的哪一轮对话里,哪段历史会是相关的。
claude-mem 的做法是把记忆分成三个隔离层,这个分层我非常认同。
2.1 第一层:原始对话记录
原始对话是最不该被丢掉的记忆底稿。这里存的不是摘要,而是完整的历史消息,每一条带时间戳、角色、会话 ID。这一层的作用有两个:一是为"摘要提取"提供原料;二是在检索命中时,可以把完整的上下文片段重新拼回 Prompt 里,让模型看到的不是孤零零的一句话,而是前后有语境的对话片段。
存原始记录时要考虑体积问题。一次对话可能有几十条消息,如果没有压缩策略,一个月下来数据量就很大了。我给自己的实现定了一个简单策略:原始对话只保留 30 天,超过 30 天的转入摘要层,原始记录定期清理。这样既保留了短期内的完整回溯能力,又不会让存储无限膨胀。
实现上我最开始试过直接存 JSON 文本,后来还是换成了标准化的表结构。核心表大致是这样的:
CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_messages_session ON messages(session_id, created_at);2.2 第二层:对话摘要与关键事实抽取
原始记录只能"回溯",不能"理解"。要让记忆真正可复用,必须从原始对话中抽取出更高密度的信息。这一步是 claude-mem 最核心的环节,做法是在每轮对话结束时,额外调用一次模型接口,让它执行一个"总结任务"。
总结任务的 Prompt 我调了很多版,最后固定成三个输出方向:
- 用户长期目标:这次对话里,用户想达成的核心目标是什么?有没有跨会话持续的意图?
- 关键事实:用户提到的个人信息、项目参数、偏好、决策结论。
- 当前进度:这次聊到了什么阶段,下一步计划是什么。
比如用户和 Claude 聊了一个网站重构的方案,那么这一层的记忆可能是:
用户正在计划把个人博客从静态站点迁移到基于容器部署的方案; 已决定使用某个轻量级框架; 尚未确定数据库选型; 用户偏好技术文档风格,喜欢带目录和代码示例。这些东西如果只存在原始对话里,下次查询时根本捞不出来。而抽取成结构化描述之后,就会变成非常干净的"记忆条目"。
实际抽取时要注意上下文窗口的成本问题。每轮都调用一次模型做摘要,意味着每个会话都要额外消耗一轮 token。我的做法是:短的对话(少于 10 条消息)不触发摘要;只有对话达到一定长度或用户主动说"记住这个"时才触发。省下的 token 都是真金白银。
2.3 第三层:可写入的长期偏好设置
第三层和对话数据无关,它是用户主动告诉系统的"永不过期"的信息。比如"以后回答都用中文""日报格式要包含完成项、风险项、明日计划""我的服务器环境是 CentOS"。
这些信息的特点是:明确、长期、和单次对话无关。如果混在摘要层里,每次检索还要判断相关性;但如果单独存一层,每次请求都无条件注入,反而更轻量。
claude-mem 把这层叫 preferences,我个人建议用最简单的 key-value 表来存。注入时直接拼成一段"关于用户的持久信息"放进系统提示词。这个层虽然简单,但它往往是被大多数自制脚本忽略的部分——写了不少对话管理逻辑,却忘了记忆系统最重要的客户是"人",人最在乎的是你记不记得他说过的那些固定要求。
CREATE TABLE preferences ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );三层存储各司其职:原始记录负责"回溯",摘要层负责"抽取理解",偏好层负责"长期绑定"。后面不管是做检索还是注入,思路都会清晰很多。
3. 冷启动与检索策略:如何在对话中把记忆"喂"给 Claude
存储解决了"有没有记忆"的问题,检索才决定"记忆能不能被用上"。很多自制系统死在检索这一步——不是没存东西,而是取出来的内容牛头不对马嘴,最后模型读了也白读,甚至因为噪音太多干扰了原本的回答质量。
3.1 自闭式规则匹配:最轻量也最不靠谱的起步方案
我在最早一版实现用的是简单的关键词匹配:把当前用户消息拆成几个关键词,然后去摘要库里做 LIKE 查询。效果很差,原因在于自然语言的同义表达太多了。用户上次说"网站速度太慢",这次问"页面加载时间怎么优化",关键词层面几乎没有任何重合,自然也就捞不出那条本应该相关的记忆。
如果你的记忆系统只服务一个非常窄的垂直场景(比如只记录"服务器 IP 和登录方式"),那规则匹配勉强够用。但只要场景稍微开放一点,就别指望它能干活。这一步是我踩了大概一周坑之后放弃的。
3.2 向量检索:让"语义相关"替代"字面匹配"
后来我改成了标准的向量检索方案。做法分两步:入库时把记忆条目用嵌入模型转成向量,查询时把当前用户消息也转成向量,然后算相似度取 Top-K。
实践中有几个容易忽略的细节:
第一,嵌入模型的选择直接决定检索质量。国内访问某些服务不稳定,所以我最终选择了本地运行的轻量嵌入模型。你完全不需要在最开始就追最强的模型,百来 MB 量级的本地嵌入模型在"记忆检索"这个任务上已经足够——因为记忆本身是短文本,不像法律文档那样需要极强的语义分辨力。
第二,向量要存,原文也要存。光存向量没有意义,检索回来的最终目的还是把原文文本塞进 Prompt。所以每条记忆都要有"向量列 + 文本列"两套字段。
第三,Top-K 不要贪多。我踩过的坑是:一次检索 10 条以上结果塞进上下文,结果模型被无关信息带偏,回答质量不升反降。现在我的默认值是 3 到 5 条,宁可漏一些,也不要因噪音干扰主线。
伪代码大概是这样的:
memory_candidates = memory_db.search(query_vector, top_k=5) relevant = [m for m in memory_candidates if m.score > SIMILARITY_THRESHOLD]这个相似度阈值很重要。太低了,无关记忆会混进来;太高了,很多本来有用的记忆会被过滤掉。我试下来 0.52 到 0.60 之间(余弦相似度)比较合适,但具体还是要看你的嵌入模型对文本长度的敏感度。
3.3 注入结构:记忆要以什么形态出现在 Prompt 里
检索到记忆之后,怎么把它放进 Prompt 是个非常有讲究的细节。直接把一大堆"历史上说过的话"怼在系统提示词后面,会稀释指令权重,甚至可能让模型误认为这些记忆是"新的用户指令"。
我建议的注入模板是:
以下是与当前用户相关的历史记忆,仅作为背景参考,不要直接复述,不要当作当前指令: 1. [时间信息] 摘要内容 2. [时间信息] 偏好内容这个模板有三个作用:一是用"背景参考"四个字明确告诉模型这些内容的定位;二是每条记忆带时间信息,模型可以根据时效性判断哪些信息可能过时;三是明确指令优先级——记忆是参考,当前用户消息才是最高指令。
注入的位置也有讲究。我对比过两种放法:放在系统提示词末尾和放在用户消息之前。实测下来放在系统提示词末尾的效果更好,因为系统提示词本身承载了"你是谁、你怎么工作"的角色设定,记忆作为"你了解和关心的用户信息"放进去逻辑上更顺。放在用户消息前容易被模型当成用户的新要求,出现一些奇怪的反客为主。
4. 落地实测与踩坑记录:从单机脚本到常驻服务的边界问题
理论说得再漂亮,落地才是真考验。下面这些坑全部来自我在模拟项目和个人工作流里的实际测试,每一个都真实导致过多少次"模型突然变笨"或者"系统直接报错"。
4.1 先撞上的问题:会话边界到底怎么定义
最早我天真地认为,一个会话就是一个 session ID,新开会话就是新的 session。但实际用起来发现,人的使用习惯根本不是线性的。
我经常上午用同一会话聊技术方案,下午又用它聊日报格式。如果按会话维度管理记忆,下午的日报请求会把上午的技术讨论也检索进来,干扰回答。反之,如果每个小时都新开会话,那跨会话的持续性又没了。
后来我调整了设计:不再把 session 当作记忆检索的唯一维度。每次检索时先以"用户 ID 全局检索",再用"当前会话 ID 过滤掉冲突项"。也就是说,记忆的范围是"这个用户的所有历史"+"当前会话的具体上下文",而不是拘泥于某一个 session。
实现上就是:
- 同一用户的所有摘要记忆都纳入向量索引;
- 检索时带上 user_id 过滤条件;
- session_id 只用于获取原始对话片段,不参与摘要层的隔离。
4.2 时间戳幽灵:记忆过期了,模型还在当传家宝
第二个大坑和时效性有关。系统记忆里存着一条"用户正在使用某个旧版框架",两个月后用户已经迁移到了新框架。但因为旧记忆仍然存在向量库里,并且和当前话题"框架选型"语义高度相关,每次检索它都被捞出来,模型反复基于过期信息回答。
这是记忆系统最危险的问题——它比没有记忆更糟糕,因为模型会自信地使用这些过期事实,而且你很难察觉。
我的解决办法有两道防线。第一道是"时间衰减":检索结果排序时,把记忆的时间远近作为一个加权因子,越久之前的记忆得分越低。第二道是"显式冲突标注":在同一条记忆被更新或修正时,旧记录不是物理删除,而是打上 superseded_by 标记,检索时直接排除。
具体来说,我在摘要记录表里加了两个字段:
ALTER TABLE memory_items ADD COLUMN is_superseded INTEGER DEFAULT 0; ALTER TABLE memory_items ADD COLUMN superseded_by_id INTEGER;后续如果有新摘要和旧摘要描述同一个主题,就把旧的标记为已取代。虽然简单粗暴,但实测下来非常有效。
4.3 异步写入失败:记忆没存上,你根本不知道
第三个坑出现在把脚本改造成常驻服务之后。之前是同步调用:对话结束,调用摘要 API,写入数据库,整个过程阻塞完成。改成常驻异步服务后,摘要请求经常因为网络超时、API 返回异常而失败,而我又没做重试,结果就是很多对话白白聊了,记忆却没存进去。
这个问题最恶心的点在于:它不会直接报错给你看,而是在未来某一天你发现"模型怎么又忘了上次说的那个需求",你才回头去查数据库,发现那条摘要压根没落库。
教训就是:记忆写入一定要有对账机制。我在之后实现里加了一个简单的补偿队列:摘要请求失败后进入重试队列,最多重试三次,仍然失败就丢弃并记录日志。同时,每次对话开始时定时检查"昨天的对话是否有对应的摘要记录",如果缺失就补采一次。
4.4 上下文长度预算:注入记忆也讲究一个"度"
最后一个边界问题是长度控制。单条摘要可能一两百字,3 到 5 条就是五六百字,再加系统提示词、用户消息、参考文档,上下文很快就不够用了。
我实测发现很多模型的上下文虽然标称很长,但超过一定长度后,模型对细节的把握能力会明显下降,尤其是长对话场景下"注意力的中间部"容易丢失信息。所以记忆注入不是越多越好。
我的做法是给记忆注入设一个预算上限,比如 800 字符。检索回来的记忆先按相关度排序,从高到低依次塞入,直到达到预算上限就停止。宁可牺牲一点覆盖面,也要保证注入质量。
5. 给不同使用场景的配置建议与扩展思路
这套方案我折腾了差不多一个月,到目前为止算是稳定跑在自己的工作流里了。最后聊一点配置上的心得,不同的人群可以按需调整。
5.1 普通用户场景:追求开箱即用
如果你只是想给自己用,不需要搞常驻服务。一个最简单的方式是用脚本把 claude-mem 的核心逻辑包起来:每次提问前读取记忆库、拼 Prompt、调用 API;每次对话结束后调用摘要接口、写入数据库。全部串行执行,连队列都不用。
这个场景下我的建议是:向量检索的相似度阈值可以调高一些,因为个人对话的记忆量不大,宁缺毋滥。摘要抽取的触发条件也可以放宽——哪怕是三句话的交流,只要涉及项目决策,就值得抽取成记忆。
部署环境建议是本地跑一个容器实例,存 SQLite 完全够用,没必要为了这几十兆的数据去搞一个复杂数据库集群。
5.2 团队协作场景:多账号隔离与权限
如果你要给一个团队用,复杂度会上一个台阶。首先是多用户隔离:user_id 必须显式贯穿所有表,否则 A 的记忆一旦串到 B 的会话里,后果很尴尬。我当时在模拟项目里专门写了一个中间件层,确保任何数据库查询都必须带 user_id 条件,不允许漏传。
其次是共享记忆和私有记忆的区分。比如团队成员之间共享的项目规范和某位成员的私人偏好,这两类不能混在一个池子。我的建议是给记忆表加一个 visibility 字段,取值是 private 或 shared,检索时根据当前用户的身份决定是否拉取 shared 类型的记忆。
5.3 长期演进:记忆的维护与遗忘机制
记忆系统不是"存了就完事",它需要养护。我用下来的一个真实体会是:记忆库和真实的人脑一样,会随着时间积累噪声。一条早期的记忆可能在三个月后已经完全无关,但它仍然占据检索结果的位置,干扰模型的判断。
所以我给自己定了一个定期维护的节奏:每周检查一次摘要库,把"已经明确过时"的条目标记为 superseded;每个月做一次全量盘点,删掉超过 90 天且从未被检索命中的低价值条目。这些听起来像脏活,但恰恰是它们决定了一个记忆系统能不能长期好用。
另一件值得做的小事是"用户反馈回路"。我在自己用的版本里加了一个简单的指令:如果用户对某条回答不满意,可以在消息里附带"记得修正:...",系统会把这句修正指令强制写入偏好层,覆盖原有的旧记忆。虽然只是一个小功能,但它让记忆系统不再是单向的记录器,而是可以不断纠偏。
最后再分享一个小技巧:在摘要抽取的 Prompt 里,不要只让模型输出结构化内容。可以让它同时生成一个"检索关键词列表",把这组关键词一起存进数据库。检索的时候,向量相似度和关键词命中各占一部分权重,能明显提升短文本场景下的召回准确率。这个技巧帮我解决了不少"向量模型对小众术语不敏感"的难题。
如果你正准备做一个对话模型的记忆层,我的建议是不要一开始就追求大而全。先把原始记录、摘要抽取、偏好存储这三层跑通,再逐步加检索和衰减机制。记忆系统的复杂度是随着数据量增长才显现的,早期架构如果留足扩展空间,后期会省很多事。
以上就是我折腾 claude-mem 整个过程的完整记录。代码层面的东西并不复杂,真正复杂的是对"记忆"这件事的理解——它不是一个存储问题,而是一个围绕上下文窗口的调度问题。想清楚这一点,后面的路就顺了。