ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

给对话模型装上跨会话记忆:claude-mem的设计与实战

给对话模型装上跨会话记忆:claude-mem的设计与实战 用过一阵子 Claude 之后我最深的感受不是它多聪明而是它实在太容易失忆了。上午刚在对话里交代过的项目背景、写代码时的偏好、几个人名的关系第二天开启一个新会话它就像什么都没发生过一样我又得从头解释一遍。被这种重复劳动折磨了几天之后我动手做了个小工具叫 claude-mem本质上是给 Claude 这类对话模型加一层跨会话记忆对话结束之后自动把值得记住的信息抽出来存到本地下一次对话开始时再把相关记忆注入到上下文里。这套玩意儿跑了大半年中间踩了不少坑也改了好几版今天把整体设计、关键原理和实际跑出来的数据表现完整分享一下。它适合正在折腾 AI 工作流、想给对话模型接长期记忆的人参考不管是打算自己写一个还是想理解市面上记忆增强功能的底层逻辑应该都有点用。1. 为什么我要给 Claude 装一副跨会话的记忆1.1 那几次被失忆坑到的实际场景最先让我受不了的是一个模拟客户支持项目。我反复告诉 Claude回答客户时一定要先给结论、再解释原因、公司内部叫法不要用缩写它在单次会话里执行得很好但每次新开会话又打回原形。我开始以为是我提示词写得不够清楚试过把规则加到系统提示词里也试过把常问问题的答案直接贴进每次对话的开头效果都不持久。后来我意识到问题不在提示词而在架构对话模型根本没有任何机制把这段对话自动搬移到下段对话里。上下文窗口就像一张临时便签会话一结束就被扔掉了。哪怕模型当时记住了所有细节一旦窗口关闭记忆就归零了。这跟人脑的记忆机制完全不一样大多数人会本能地用它应该还记得吧去要求模型结果自然是反复失望。另一个常见场景是跨天讨论技术方案。前一天我们刚讨论出项目采用模块化结构优先保证扩展性第二天我想继续聊时它完全不记得这个结论又开始泛泛而谈。我还试过把聊天记录粘贴回去让它复习几百行历史消息塞进去Token 成本高不说模型还容易把历史里的中间讨论当成当前诉求回答变得很怪。手动给摘要也行但每次都要写、每次都要贴根本坚持不下来。1.2 我把需求拆成了四个必须满足的条件反复踩坑之后我坐下来列了一份需求清单最后收敛成四条硬性标准自动化记忆必须在对话结束后自动抽取不能靠人手动总结。人一旦需要手动维护这个方案就注定会被弃用。可检索不能把所有历史消息原样堆回上下文那样既贵又吵。必须按相关度、时间、重要性有选择地召回。可注入新会话启动时检索到的记忆要自动拼进提示词用户无感知也不需要手动粘贴。可遗忘任何一条被保存的记忆都必须能被单独删除或者批量清理。这一条一开始被我忽略后来才意识到它是整个系统的安全底线。有人可能会问为什么不直接做全文历史回放把过去所有对话都附在每次请求里我算过一笔账假设一天产生 5 轮对话、每轮 3000 Token一周下来就是 10 万 Token 级别的历史。全部回放成本高、噪音大而且模型会被大量无关上下文干扰。真正有效的记忆系统本质上是一个信息过滤器只留住那些跨会话仍然有价值的稳定信息。2. claude-mem 的信息流大部分工作在对话结束后才发生2.1 从 messages 里提取值得记住的事claude-mem 的核心处理流程是异步的它在一次对话结束后才开始干活。触发时机我设置成两种会话窗口关闭后 5 分钟或者执行claude-mem watch手动触发一次扫描。此时会把整段会话的 messages 列表交给一个抽取器抽取器本身也由对话模型担任——我用一段固定的提示词要求它从 transcript 里找出三类信息。第一类是用户偏好比如以后回复尽量控制在 200 字以内、代码注释用中文、不要使用专业术语缩写。第二类是稳定事实比如项目代号 X 当前版本基于 3.1、数据库用的是 PostgreSQL、团队有三人分别负责前端、后端和测试。第三类是明确结论比如我们决定采用方案 B、周一之前完成接口联调。抽取逻辑也很直接核心代码如下def extract_memories(transcript: list[dict]) - list[dict]: prompt f 从以下对话中抽取需要长期记住的信息。 只抽取用户明确表达过的偏好、项目相关的事实、双方达成的结论。 忽略寒暄、临时任务、一次性指令。 每一条记忆用一句话表达不超过40个字。 对话如下 {transcript} resp call_model( modellocal_extractor, system你是一个信息抽取引擎不要回答问题只输出JSON列表。, userprompt, ) return parse_json(resp)抽取这一步是纯提示词工程没有上 NLP 模型。一开始我试过用规则和关键词匹配效果很差因为值得记住的判断依赖语义理解不是几个词能覆盖的。后来换成语义抽取就稳定多了。2.2 去重、归一化与本地存储的结构设计刚开始运行 claude-mem 时爆出一个很典型的问题同一件事会在不同会话里被反复提起。比如用户每轮都说一次回复控制在 200 字以内抽取器就忠实地抽出了 20 条几乎一模一样的记忆。如果不处理记忆库会迅速膨胀最后全是没有区分度的冗余条目。去重逻辑我放在抽取之后、写库之前。具体做法是先把每条记忆文本做归一化——去掉多余空格、统一标点、繁体转简体——然后用向量相似度跟库里已有条目比对相似度超过 0.92 就认为是同一件事。命中之后不直接丢弃而是做一次合并更新last_seen_at时间戳并检查新旧文本哪个更完整保留完整度更高的一方。存储使用一个本地 SQLite 数据库目录固定在~/.claude-mem/下里面除了数据库文件还有config.json和logs目录。核心表结构是这样的CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, session_id TEXT, created_at TEXT DEFAULT (datetime(now)), last_seen_at TEXT, last_used_at TEXT, importance INTEGER DEFAULT 1, status TEXT DEFAULT active, source_messages TEXT ); CREATE INDEX idx_memories_status ON memories(status); CREATE INDEX idx_memories_last_seen ON memories(last_seen_at);importance字段在抽取阶段由模型一起给出取值 1 到 33 代表用户强调过这一点很重要。status字段只有两个值active和superseded后者表示该记忆已被新结论覆盖正常情况下不会被检索到。源消息 ID 也被存下来这样将来出了疑问可以倒查是哪一段对话产生了这条记忆。2.3 存储选型为什么最终选用 SQLite 而不是 JSON最初版本我图省事直接用 JSON 文件存记忆。用了一个星期就扛不住了每次新增记忆都要把整个文件读进来、改完再写回去量小还好量一大就慢而且并发写容易把文件写坏。后来我改成 SQLite问题基本消失。SQLite 对单用户场景非常合适。它不需要单独起服务一个文件就是全部数据备份直接复制文件即可。检索用 SQL 也灵活比如要查 7 天内被使用过的 active 记忆一行语句就出来了。另外我加上了 FTS5 全文索引用来做关键词级快速检索配合向量检索互补。向量列则直接存 embedding 的 JSON 字符串不引入额外的向量数据库依赖。这套方案对个人项目足够用如果将来记忆条目超过十万级再考虑迁移到专门的服务端数据库也来得及。3. 检索与注入它是怎么在关键时刻想起旧事的3.1 检索不只是一个向量搜索很多人一想到记忆召回就默认是向量相似度搜索实际用下来发现单靠向量远远不够。记忆库里可能有几百条内容用户当前的问题只跟其中两条相关单纯看语义相似度很容易漏掉那些重要但表达方式相差很大的旧结论。于是我设计了一个综合评分公式每条候选记忆按下面的方式打分score 0.3 × 新近度分 0.2 × 重要度分 0.4 × 语义相似度分 0.1 × 关键词命中分新近度分根据last_seen_at计算时间越近越高近 7 天的给满分。重要度分直接等于importance / 3。语义相似度分来自 embedding 向量的余弦相似度。关键词命中分是当前用户输入里出现的词是否也在记忆文本中出现出现越多分越高。四个维度加权后取 top 5 到 top 10 条作为候选记忆。权重是调出来的。语义相似度占比最高因为最终召回质量主要靠它新近度次之因为近期说过的话大概率比三个月前的旧话更相关重要度再次关键词命中是兜底防止语义模型对某些专有名词失明时完全召回不到。冷启动阶段有个特殊处理新用户第一次使用时库里没有记忆评分公式直接不生效此时 claude-mem 会注入一条引导语比如如果用户提供了偏好信息请主动确认并记住这样提示模型在对话中注意新信息方便后续抽取。没有这步的话新会话里用户说了偏好模型只是自然回复不会明确表态抽取器就很难判断这段内容是否真的值得记住。3.2 组装记忆卡片的顺序与格式召回结果要拼成一段能被模型消费的文本。这一步最考验细节。我第一版直接把几条记忆用逗号拼接塞进去结果模型经常把记忆当成用户当前说的话造成严重的上下文混淆。后来我设计了一套统一的记忆卡片格式[MEMORY SECTION] 以下是你之前与用户互动时记住的信息它们不是用户现在的发言 - [RECENT_MEMORY] 2025-05-20 用户偏好回复控制在200字以内 - [IMPORTANT] 2025-05-18 项目结论技术选型采用模块化架构 - [CONTEXT] 2025-05-10 项目事实数据库当前使用 PostgreSQL [MEMORY SECTION END]卡片排序按照 [IMPORTANT]、[RECENT_MEMORY]、[CONTEXT] 的优先级。每条记忆后面都带日期让模型知道这些信息的时间顺序。首尾的分隔符是强制要求模型的系统提示词里明确写了看到 MEMORY SECTION 内容时把它当作背景资料不要引用或重复这样能减少很多污染问题。3.3 注入位置系统提示词才是正确入口记忆块放在哪里效果差别很大。一开始我放在用户消息的开头比如每次提问前都自动贴一段历史背景...用户现在问...。实测有两个问题一是用户发出的消息变得一团糟模型经常分不清哪些是记忆哪些是当前诉求二是模型的回复风格会被记忆文本带跑因为用户消息是整个对话里指令性最强的区域。后来我把记忆块移到系统提示词的末尾在你是 Claude请... 之后补一段独立小节。系统提示词是模型最稳定的指令来源模型会把它当成全局设定来理解不会把里面的内容误当成当前用户输入。更新后的系统提示词伪代码如下你是一个乐于助人的助手。 [其他已有指令...] MEMORY SECTION 里的内容是你额外知道的背景信息 它们不是当前对话不要主动复述只在你认为必要时引用。这里有个小技巧在系统提示词中明确区分记忆和当前对话之间的逻辑边界模型会把记忆当知识背景对待。只写以下是你记住的信息是不够的因为模型区分不了记住的信息和待处理的任务要多加一句它们不是当前对话实测下来这句话能让混淆率明显降低。4. 关键参数与一次完整实测从冷启动到记忆生效4.1 配置文件与最小可用参数claude-mem 的默认配置长这样{ max_tokens_memory: 1200, top_k: 8, importance_threshold: 2, recency_days: 30, similarity_threshold: 0.82, update_on_conflict: true, privacy_mode: local }字段含义max_tokens_memory控制每次会话最多注入多少 Token 的记忆top_k控制召回条数importance_threshold是抽取阶段的最低重要度低于这个值不被写入recency_days是时间衰减的半衰期similarity_threshold是查重阈值超过这个值视为同一记忆update_on_conflict允许新记忆覆盖旧记忆privacy_mode决定 embedding 计算在哪一端进行。这些值按经验调整聊技术方案的用户会话比较长max_tokens_memory我设成 1200因为它要承载大量结论性内容如果是闲聊场景800 就差不多了。top_k设成 8 是因为再高容易把不相关记忆也带进来干扰模型判断。similarity_threshold0.82 是从实测里得到的值太高容易漏掉明显重复的句子太低则误杀太多新信息。4.2 模拟项目的实测数据表现我在一个模拟客服支持项目里跑了完整一周的验证记录了一些实打实的数据。项目场景是让 Claude 回答用户关于某软件的提问每周大约 20 轮完整会话。冷启动后第一轮对话毫无悬念记忆为空所有回答全靠模型自身能力。整个验证周期里总共抽取了 186 条原始记忆经过查重和归一化后实际写入库 73 条。平均每条记忆约 35 个中文字符这意味着全部记忆加起来也就 2555 字左右作为背景资料负担很小。第二个值得记录的数据是记忆命中率。我构造了 20 个特意依赖记忆才能答对的问题比如我之前说过希望回复简洁你觉得这个建议还适用吗。当 claude-mem 关闭时这类问题命中率是 0因为模型确实不知道用户说过什么。开启后命中率升到 92%少数未命中的原因是用户提法太委婉比如只说了那个东西缺乏可检索的关键词锚点。每次会话平均注入的记忆块大约 800 Token占总上下文量的 8% 左右。这个比例影响很小模型回答速度没有明显变慢上下文窗口也完全没有触顶。从成本角度看相当于每轮对话额外付出 800 Token 的回忆费换来的效果是用户不用再重复背景信息。4.3 从实测看到的改进空间第一版跑完数据虽好看但还是暴露出几个明显短板。最突出的是置信度阈值缺失。单次抽取器有时会从无关紧要的对话里抽出用户今天喝了杯咖啡这类完全没用的记忆重要度还是 1照样落库。后来我在抽取阶段加了一个置信度字段让模型判断这条信息是否真的值得长期记住低于 0.5 的直接不写库。第二个问题是时序冲突处理不够智能。当出现我们改用方案 C这样明显推翻旧结论的记忆时系统虽然会把旧记忆标记为superseded但完全是基于关键词规则不够可靠。比如用户说之前那个方案也不差时系统可能误以为旧方案要被撤回其实只是在讨论优缺点。这块如果想做得更好需要引入专有的矛盾检测模型但成本太高我暂时放在待办清单里。第三个可以改进的是人工确认机制。对于重要度 3 的记忆理想方案是在对话里用一句我已经记住了以后回复控制在 200 字以内让用户感知到系统在主动记住。这能大幅提升信任感但也会打断对话流畅度我目前只在非实时场景下启用。5. 遭遇过的四个坑及其排查链路5.1 重复记忆把上下文撑爆第一周跑完我检查上下文时发现提示词越来越长打开记忆列表一看同一条回复控制在 200 字以内出现了 37 次。根因是抽取器每轮对话都会从 transcripts 里重新抽到这条偏好而当时的去重模块只在写入时比对文本是否完全一致稍微改动一个标点就逃过了查重。排查链路是先看日志里insert_memory被调用了多少次再看SELECT content, COUNT(*) FROM memories GROUP BY content一眼就发现热点条目。修复分两层。第一层是写入前做向量相似度查重cosine 0.92一律合并而不是等文本完全相同。第二层是在抽取提示词里加一句如果该偏好已经在之前对话中出现过只抽取一次从源头减少重复输出。改完以后 37 条重复变成 1 条上下文长度立刻恢复正常。这里最关键的教训是去重必须发生在抽取之后、写库之前不能指望抽取器自己避免重复。5.2 中文切词与标点干扰检索一个让检索失效的问题来自中文表达习惯。测试时我问项目用什么语言按道理应该能召回项目采用 Python 生态开发这条记忆但多次尝试都失败。查了 embedding 的相似度分数发现中文标点、空格和断句方式对向量表征的影响比我预想大得多。记忆文本里全角标点和查询文本里的半角标点会让向量朝不同方向偏移尤其短文本一个标点不同就能让相似度从 0.83 掉到 0.61。解决办法是做统一归一化管道。所有进入记忆库的文本和所有查询输入先经过同一套处理全角转半角、去掉多余空格、繁体转简体、统一标点符号。这步做完之后再计算 embedding 和相似度。实测检索命中率从 68% 提升到了 85%而且效果稳定说明不是模型的问题是数据预处理不到位。后续我还给记忆文本加了同义词扩展比如语言这个词在检索时会把编程语言、开发语言一起映射进一步降低漏召回概率。5.3 旧记忆赖着不走导致结论过时系统运行到第三周时出现一个很有意思的问题用户在某个会话里明确提出之前决定用方案 B现在改成方案 C 了claude-mem 也确实抽到了这条新结论但下一次会话检索时模型仍然时不时提到旧方案 B。查了数据库才发现旧记忆状态仍然是active新记忆被写入但旧记忆没有被覆盖。根因是我没有把改写信号作为一等公民对待。用户说出改成、不再用、推翻这类词时系统应该把涉及同一主题的旧记忆标记为superseded而不是仅仅新增一条。修复方式是在抽取提示词里增加一个字段replaces_memory_id让模型判断新结论是否在取代某条旧记忆。系统收到这个字段后先将旧记忆状态置为superseded检索时默认过滤只保留最后一条有效结论。对比验证后测试问答里关于当前方案的准确率从 81% 提升到 97%。这里还有个延伸经验我给每条记忆加了一个last_seen_at时间戳超过 90 天没有在对话中出现的记忆检索时自动降权。这样做能让那些曾经重要但已经无关的信息逐渐淡出而不是永远霸占召回名额。5.4 注入模板影响模型语气记忆块格式本身也会影响模型的输出风格这是我一开始完全没预料到的。某段时间所有启用 claude-mem 的会话都出现了同一个现象Claude 的回答变得特别喜欢列清单、用书面语跟没启用记忆时差距很大。排查过程很直接我把它当成一次 AB 对比实验。开启 claude-mem 时回答变书面化关闭就恢复正常所以问题锁定在注入内容上。我一条条拆记忆卡片发现记忆文本里有大量用户偏好应该……、结论我们决定……这样的句子。模型的系统提示词里写了照顾用户偏好于是它把这些祈使句当成了风格指令开始模仿那种命令式的表达。修复方式很简单记忆卡片的措辞从祈使句改成陈述句。比如记住回复控制在200字以内改成用户曾经说过希望回复控制在200字以内。加一个曾经说过的框架模型就能把内容理解为历史事实而不是当前指令。再从系统提示词里删掉照顾用户偏好那个笼统表述改成参考历史信息但保持自然对话风格问题就消失了。这块儿提醒我提示词工程里最不起眼的一句话都可能对模型行为产生蝴蝶效应。6. 隐私边界为什么我把遗忘功能放在第一位6.1 本地优先的存储策略记忆系统天然比普通对话更容易引发隐私担忧因为它把用户说过的话跨会话地保存下来而且是在后台自动做的。很多用户对系统暗中记录我的一切不舒服这是合理的。所以我从设计之初就坚持本地优先所有记忆数据只落在一个本地 SQLite 文件里没有任何云同步、没有遥测上报。embedding 的计算可以选本地模型也可以选远程 API但如果选远程 API记忆文本会被发送到外部服务器这时候privacy_mode就必须设置成hashed也就是只上传哈希后的特征串不传原文。隐私模式和功能完整度之间是一个权衡local模式最安全但需要本地跑 embedding 模型速度和精度都略差hashed模式兼顾安全与质量但哈希后的特征依然有被逆向的风险不能认为是绝对匿名none模式会把全部原文发送给远程 embedding 服务我不建议开。这里没有完美解但对个人项目来说本地模式足够了。6.2 遗忘接口与记忆体检遗忘接口是我做得最仔细的部分因为记不住的问题是可用性问题而忘不掉的问题是信任问题。claude-mem 的遗忘命令分三种粒度# 删除单条记忆 claude-mem forget --id 42 # 删除某个会话产生的所有记忆 claude-mem forget --scope session:7a3f9b # 删除某日期之前的所有记忆 claude-mem forget --before 2025-06-01为了更好理解记忆库状态我还加了一个claude-mem health命令输出一份记忆体检报告包含活跃条目数、被废弃条目数、平均重要度、平均最后使用时间等核心指标。当我看到某条记忆超过两个月没被使用就会手动判断它是不是过时了。这里有个自己设计的小规定如果一个会话被用户主动删除那么该会话产生的所有记忆自动进入superseded状态防止删了对话、留下案底的情况。6.3 给使用者的几条安全建议最后聊几条我自己跑下来总结的安全经验。第一不要在对话里说出真正敏感的明文信息比如密钥、密码、身份证号哪怕对话只有自己能看到因为记忆系统可能会把这些内容留存很久。如果必须处理这类信息至少要定期用forget --before清理。第二不要让 claude-mem 的抽取器碰到任何你不希望被记录的话题。我在对话开始前加了一个关键词过滤器凡是命中临时、无需记录、这是机密这类前置标记者整段对话直接跳过抽取流程。第三如果记忆的内容涉及第三方的信息要确保有明确的授权这不仅是合规问题也是做事的基本底线。跑了这大半年 claude-mem我最深的体会是记忆系统真正的价值不在于能存多少而在于知道什么不该存、什么该及时忘。去重和遗忘机制的优先级应该放在检索和注入之前。后面我打算把精力花在两件事上一是给改写信号做个更可靠的检测模块二是把记忆卡片的格式改成自适应——根据当前对话的情绪和风格自动调整措辞减少对模型输出风格的隐性影响。如果你也在折腾对话模型的记忆功能上面这些坑和数据直接拿去用就行。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进