恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
给大模型装上长期记忆:claude-mem跨会话记忆层实现指南
首页
资讯中心
/
给大模型装上长期记忆:claude-mem跨会话记忆层实现指南
给大模型装上长期记忆:claude-mem跨会话记忆层实现指南
发布时间:2026/10/12 11:34:32
每次新开会话我都得像重新入职第一天那样把名字、技术栈、正在做的项目、甚至喝咖啡还是喝茶的习惯事无巨细再交代一遍。跟对话模型聊天时间越久这个感觉就越强烈——它明明几分钟前还跟你讨论到某个方案的关键细节换个会话再打开又变成了初次见面的陌生人。这不是谁的错而是对话模型天生的架构限制。我做了个小项目代号就叫 claude-mem思路是在模型外面套一层记忆层把每次会话里值得留下的信息抽出来、存起来下次对话开始前再按需找回来、塞回上下文里。这个项目算不上复杂但整个链路走下来踩过的坑和想明白的道理比我想象中多得多。这篇文章就是把我的完整实现思路和实测经验记录下来给正在做 AI 助手、客服机器人或者个人助理这类需要记住用户的应用开发者做个参考。1. 为什么对话模型记性差是个真问题1.1 无状态请求与上下文窗口的现实约束对话模型本身是没有状态的。每次你发一条消息它拿到的只是一段从开始到现在的文本模型根据这段文本生成回复生成完就算完事。它既不保存历史也不主动维护关于你的任何信息——所谓的多轮对话本质上是把之前的对话内容一遍遍拼进请求里假装模型还记得而已。这个设计带来了两个硬约束。第一上下文窗口有上限。你现在用的对话服务上下文窗口可能很宽但再宽也经不住无限聊天一旦超出限制最早的内容就会被截断或者被摘要压缩细节自然就丢了。第二会话之间是隔离的。窗口关掉那段拼起来的文本就没了下次重开模型只能看到你新发的一条消息它对你一无所知。这就像一个人每天上班都要重新入职不知道工位在哪、不知道项目背景、不知道你上次交代过什么。用一个例子来感受一下你让助手帮你追踪一个跨三周的开发任务第一天它知道你要做用户登录模块第五天你问上次说登录校验要改成双因子具体方案定下来了吗如果它没有记忆这句话就是无源之水它只能凭感觉猜甚至一本正经地编一个方案出来。1.2 真正需要跨会话记忆的场景清单不是所有应用都需要跨会话记忆但下面这几类场景没有记忆几乎是没法用的。个人助理类需要记住用户的日程偏好、常用地址、工作习惯。比如用户说会议尽量安排在下午这种偏好如果每次都要重新说一遍助理的价值就大打折扣。客服与问答类用户之前提交过工单、反馈过 Bug、问过某个功能如果每次都要用户从头描述问题背景体验会非常割裂。项目协作类团队成员在对话里确认过需求变更、讨论过方案取舍跨会话之后这些结论必须可追溯。学习辅导类给学生的答疑助手需要知道学生当前学到哪、哪类题容易错、上次布置了什么任务。我自己的场景更朴素我在做一个私人助手希望它能记住我是谁、我在做什么、我对哪些技术方案有偏好。它不需要真的像人一样理解我只要能在合适的时机把之前说过的话重新拿出来就已经能解决 80% 的痛点。1.3 claude-mem 的定位在模型外面套一层记忆claude-mem 这个名字很直白它服务的是以 Claude 为代表的一类对话模型助手。项目不做任何模型层的改动只在应用层加一个中间件。核心闭环是四步抽取、存储、检索、注入。抽取从每一轮对话中提炼出值得长期保存的信息比如用户的偏好、确认过的决定、进行中的任务。存储把抽取到的信息按类型写入不同的存储结构本地 SQLite 加向量索引。检索新会话开始或对话进行中根据当前问题召回相关的历史记忆。注入把召回结果格式化成一段记忆卡片拼接到系统提示词或对话上下文中让模型在生成回复时看到这些历史信息。这个思路不新鲜很多商业产品已经在做了。但自己做一遍的价值在于你能真正理解每一步的取舍知道为什么记忆不能一股脑全存、为什么检索不能只看相似度、为什么遗忘和记住同样重要。2. claude-mem 的记忆分层三类信息三套存法动手写代码之前我先把记忆做了分类。原因很简单所有信息都用同一种方式处理一定会出问题。偏好这种稳定的事实和昨天说过某句话这种临时事件生命周期完全不同它们被查询的方式也不一样有的是精确匹配有的要靠语义模糊匹配。2.1 事实记忆身份与偏好的结构化存储事实记忆是用户是谁、用户喜欢什么、用户的基本属性这类稳定信息。典型例子用户叫 A、偏好 Python、住在南方城市、不喜欢太长的回复。这类信息的特点是结构清晰每条记录都可以拆成三元组主体、属性、值。我把这类记忆存成一张简单的 SQLite 表CREATE TABLE fact_memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity TEXT NOT NULL, -- 主体比如 user、某个项目名 attribute TEXT NOT NULL, -- 属性比如 preferred_language value TEXT NOT NULL, -- 值比如 Python confidence REAL DEFAULT 0.0, -- 置信度0~1 source TEXT, -- 来源会话 ID方便追溯 created_at TEXT DEFAULT (datetime(now)), updated_at TEXT DEFAULT (datetime(now)), UNIQUE(entity, attribute) );这里有个关键设计UNIQUE(entity, attribute)意味着同一个主体同一个属性只保留最新值旧值不直接删而是作为修订历史留着。这样既保证查询时拿到的是当前有效值又不会丢失演变过程。2.2 事件记忆带时间戳的会话行为时间线事件记忆是发生过什么、什么时候发生的这类有时效性的信息。比如2025-06-10 用户确认了登录模块的改造方案2025-06-11 用户提到正在准备部署文档。这类信息不一定需要长期有效但在一段时间内有很强的参考价值特别是当用户问上次我们说到哪了的时候。事件记忆用一张事件表存储CREATE TABLE event_memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_type TEXT NOT NULL, -- 事件类型比如 decision、progress content TEXT NOT NULL, -- 事件描述 happened_at TEXT NOT NULL, -- 发生时间 importance REAL DEFAULT 0.5, -- 重要性权重 session_id TEXT -- 来源会话 );注意事件记忆不做唯一键约束因为同一类事件会反复发生。查询时按时间和事件类型过滤再按重要性排序。它的生命周期比事实记忆短后面讲遗忘机制时会专门处理。2.3 语义记忆可检索的经验与结论第三类是语义记忆也是最初觉得最玄乎的一类。它指的是从对话里提炼出的经验、结论、方法论比如用户认为接口文档应该和代码放在同一个仓库维护用户倾向于先做原型验证再做完整实现。这类信息既不是单纯的事实也不是单纯的事件而是一种带观点的结论。语义记忆没法用简单的键值对存储因为它没有一个固定的属性名用户搜索它的时候也不会用完全一样的话。更适合的方式是向量化存储把记忆文本转成向量查询的时候把当前问题也转成向量用语义相似度召回。这部分我用的存储介质是 SQLite 加一个向量扩展或者单独跑一个轻量级的向量数据库。核心思路是把文本嵌入成 768 或 1024 维的向量同时保留原文和元信息CREATE TABLE semantic_memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, -- 原始记忆文本 embedding BLOB, -- 向量数据 importance REAL DEFAULT 0.5, tags TEXT, -- 逗号分隔的标签 created_at TEXT DEFAULT (datetime(now)) );三类记忆各有各的用途也各有各的存储方式。我把它们的差异整理成了下面这张表后面所有设计都是围绕这张表展开的记忆类型典型例子存储方式查询方式生命周期事实记忆用户偏好 PythonSQLite 键值表精确匹配长期事件记忆6月10日确认改造方案事件日志表时间类型过滤中短期语义记忆用户倾向于先做原型向量存储语义相似度中长期3. 记忆构建管线从对话记录到结构化记忆存储结构定好了之后真正花时间的是怎么把一段对话变成一条条可用的记忆。这个环节做不好后面检索再精准也是白搭——存进去的是垃圾查出来的自然也是垃圾。我实现了一条四步管线每一步都有明确目标和坑点。3.1 回合切分与低质量内容过滤第一步是把对话按回合切分同时过滤掉噪音。对话里不是所有内容都值得记像嗯好的稍等这类应声词以及寒暄客套都属于低质量内容。我维护了一个简单规则集内容长度少于两个有效字符的丢弃包含大量语气词但无实义的丢弃明显是打招呼或者结束语的丢弃。切分时以一条用户消息加后面对应的模型回复为一个基本单元这样做的好处是抽取时能同时看到问题和回答上下文更完整判断这条信息是不是用户真实偏好会更准。这一步不需要任何模型参与纯正则加规则就能处理掉大部分噪音成本很低。3.2 记忆抽取提示词与 JSON 输出第二步是核心把切分好的对话单元丢给对话模型让它抽取候选记忆卡片。这里我踩了一个很重要的坑——一开始我让模型自由输出自然语言结果一百条记忆有一百种格式后续根本无法解析。后来我改成强制模型输出严格 JSON每个条目固定字段解析问题才彻底解决。我的抽取提示词大致长这样你是一个记忆抽取器。请从下面的对话中抽取值得长期保存的信息输出 JSON 数组。 每条记忆必须包含以下字段 - memory_type: fact | event | semantic - content: 记忆内容一句话只包含一个事实或观点 - entity: 记忆涉及的主体如 user、project - attribute: 属性名仅 fact 类型需要 - value: 属性值仅 fact 类型需要 - confidence: 0~1 之间的浮点数表示你对该记忆真实性的把握 - importance: 0~1 之间的浮点数表示该记忆对后续对话的价值 要求 1. 不要重复抽取对话中已有的明显常识 2. 不要编造对话中没有出现过的信息 3. 如果一条信息不确定confidence 给低分 4. 输出纯 JSON不要输出任何解释 对话 {对话内容}这里有个细节值得展开confidence置信度和 importance重要性是两个不同的维度。置信度衡量的是模型有多确定这条信息是真的重要性衡量的是这条信息对未来的价值有多大。一个置信度 0.9、重要性 0.1 的记忆可能是用户今天穿了件蓝衣服虽然很确定但没价值一个置信度 0.6、重要性 0.8 的记忆可能是用户最近在评估某个数据库方案虽然不太确定但对后续推荐很有参考意义。两个维度分开打分后续过滤和排序才有的放矢。3.3 置信度门槛与冲突合并模型抽取出来的记忆不能全信否则会出现严重的幻觉记忆问题后面专门讲。我设了一个双重门槛confidence 0.6的记忆默认不进长期库只留在当天的临时缓存里importance 0.3的记忆即便置信度很高也不进库因为不值得为它消耗存储和检索成本。写入时还要处理冲突。对新抽取的事实记忆用(entity, attribute)作为唯一键如果库里没有就插入如果已有旧值就比较新旧置信度。新值置信度更高或者来源时间更新就覆盖旧值并把旧值存到历史表否则保留旧值新值存为待确认状态。这个策略简单有效能避免对话中随口说的一句话直接推翻之前明确表达过的偏好。冲突处理的核心原则是宁可慢一点更新也不要被一条低质量新消息带偏。后面实测部分我会举一个真实例子说明。4. 检索与注入怎么让记忆恰好出现在该出现的地方存储阶段完成之后下一步就是检索。这个环节直接决定了记忆系统的体验检索多了模型上下文被无用信息挤占检索少了该记住的事情想不起来。我在 claude-mem 里用的是双路召回 综合打分 动态注入的策略。4.1 双路召回关键词加向量互补单靠向量语义检索有个典型问题相近但不相关的记忆会被误召回。比如用户问上次说的那个线上事故后来怎么处理的向量检索可能会把用户反馈线上页面加载慢这条召回来因为字面和语义都很接近但它更可能是另一次事故的记录。反过来光靠关键词检索又会漏掉那些换了一种说法但本质相关的记忆。所以我做双路召回。一条路是 SQLite 的 FTS5 全文索引按关键词匹配召回事件型记忆和事实型记忆另一条路是向量索引按语义相似度召回语义记忆。两条路的结果做合并去重后进入打分环节。双路的召回数量可以放宽一些比如每路召回 30 条反正后面还有排序过滤宁可召回偏多也不要在召回阶段就把有用的记忆漏掉。4.2 排序打分相关性之外还要看什么召回结果合并后通常是几十条必须有一套打分机制排出先后。我给每条记忆算一个综合分score 0.5 * relevance 0.2 * importance 0.2 * recency 0.1 * confidence相关性的权重最高但重要性、时效性、置信度都会影响最终排序。这里的关键认知是相关不等于有用。一条高度相关但早已被推翻的旧方案排在一条相关性稍低但在进行中的当前决策前面是明显的错误排序。加入 time decay 之后最近确认过的信息天然更容易排在前面模拟了真实记忆中近期信息更活跃的特点。我用的时效性衰减很朴素recency_score exp(-days_since / 30)30 天内的事件保留约 37% 的时效权重超过 90 天基本就只剩不到 5%这时候它必须靠高度相关或高重要性才能排到前面来。4.3 注入格式与 Token 预算控制排序完成后再考虑注入。注入不是简单的把记忆文本拼在对话后面模型能不能正确使用这些信息很大程度上取决于注入格式是否清晰。我的注入模板大概这样下面是从历史对话中检索到的、与当前话题可能相关的记忆。这些内容来自过去的会话不一定都仍然有效请结合当前对话内容判断是否使用。 [记忆 1]类型fact置信度0.9时间2025-06-10 用户偏好使用 Python 进行数据处理 [记忆 2]类型event置信度0.8时间2025-06-11 用户确认了登录模块的双因子改造方案 [记忆 3]类型semantic置信度0.7时间2025-06-05 用户倾向于先做原型验证再写完整代码注入数量必须动态调整不能写死每次注入 20 条。我在每次对话前先估算剩余 Token 预算预算充足就注入 Top-10预算紧张就只注入 Top-3同时把每条记忆压缩成一句话甚至一个短语。我见过太多记忆系统因为注入过量把整个上下文撑爆最后模型连当前对话都处理不好——这就本末倒置了。5. 遗忘机制与记忆维护不是所有信息都值得永远记住很多人做记忆系统第一反应是记住一切但实际操作一段时间后就会意识到不做遗忘机制的记忆库用不了几周就变成一个堆满过时信息的大杂烩。检索精度直线下降无用记忆干扰判断甚至出现新旧信息互相打架的混乱局面。5.1 信息过时、冲突与隐私遗忘的三种理由为什么要刻意遗忘第一个理由是信息会过时。用户去年偏好某个方案今年可能已经转向另一个过时的记忆如果还以较高权重注入上下文会误导模型的推荐和回答。第二个理由是冲突累积。用户在不同时间说过互相矛盾的话比如先决定用 A 方案后推翻 A 方案如果两条都留着且权重差不多模型会无所适从。第三个理由是隐私。有些敏感信息用户只是顺口提过一次并不代表希望被永久保存定时清理是负责任的做法。claude-mem 设计遗忘机制时参考的是人类记忆的工作方式不是所有信息都一视同仁地衰减被反复提及和引用过的记忆强度会维持甚至增强长期没被提及的逐渐淡化直到被清除。5.2 重要性评分、衰减与合并策略我给每条记忆维护一个strength值初始值等于抽取时的importance。每次这条记忆被检索命中并实际注入到上下文中就在后台给它的strength加一个增量比如加 0.1上限 1.0。同时有个定期衰减任务strength strength * 0.95 # 每 7 天衰减一次这样设计的结果是真正有用的记忆会被反复检索、反复加强保持活跃无人问津的边角料记忆则会持续衰减最终掉到阈值以下被清理掉。合并策略针对的是冲突记忆。当两条事实记忆的(entity, attribute)相同但value不同且两条的strength都超过某个阈值时我倾向于保留时间更新的一条把旧的一条标记为已废弃而不是物理删除。标记废弃的好处是如果用户之后又提起旧方案系统还能追溯到这条信息是从什么时候开始失效的。5.3 一套可落地的低成本清理流程遗忘机制说起来容易落地成代码要控制成本。我最终实现的是一个每天运行一次的轻量任务对strength低于阈值比如 0.2且超过 90 天未命中的记忆做软删除对标记为已废弃超过 30 天的历史记录做物理清理导出当前所有活跃记忆到一个可读文件方便用户确认模型记住了什么。这个流程每天跑一次SQLite 上几千条记录的清理耗时基本在毫秒级完全不用上任务队列或者分布式调度。记忆系统维护的复杂度大部分靠设计规避而不是靠更多组件去解决。6. 实测踩坑一些比文档更疼的经验代码写完不等于能用。我把 claude-mem 接到一个模拟项目上跑了将近两个月每天真实对话、真实注入、真实记忆更新。这段时间遇到的坑每一件都让对记忆系统的理解更深入一层。挑几个最有代表性的分享一下。6.1 模型编造记忆幻觉记忆的过滤最大的坑是幻觉记忆。有一次用户只是随口说了一句今年夏天好像不太热空调都没怎么开抽取器竟然生成了用户不喜欢使用空调这样一条事实记忆置信度还给了 0.8。问题在于模型看到这句带否定词的话很可能在抽取时把它当作了一种偏好表达而不是一次天气吐槽。这类问题没法靠调整提示词完全解决我用了三重防线。第一重是置信度门槛低于 0.7 的事实记忆直接不进库。第二重是语义校验对抽取出的内容做一次极性判断如果原文包含明显否定或反讽语气同时抽取结果把否定转成了肯定就自动降低置信度。第三重是人工兜底——记忆面板给用户提供一键删除和修正入口。理想情况当然是以自动处理为主但保留一个低成本的的修正入口能在关键时刻救回被错误信息带偏的对话。6.2 冲突记忆互相打架先想好覆盖规则第二个坑是冲突处理。模拟项目里用户先说我最近在学 Go准备用 Go 写后端几天后又提到其实 Go 的生态还是不太成熟我想再看看 Rust。两条记忆如果同时存在模型在后续对话里一会儿按 Go 假设、一会儿按 Rust 假设表现非常分裂。我的解决方式是给事实记忆增加有效时间窗概念。新写入的记忆默认有效旧记忆如果被新记忆覆盖不会立刻生效失效判定而是进入 24 小时的观察期。观察期内如果旧记忆还被频繁检索命中说明它可能是用户同时维持的多个状态之一那么不覆盖而是把两个值都保留并标记为多值并存如果观察期内旧记忆几乎没有被触发就正常覆盖。这个机制成本不高但效果立竿见影很多模棱两可的冲突场景都被它消化掉了。6.3 Token 预算被记忆静默吃掉的陷阱第三个坑和上下文资源有关。有段时间我为了提高记忆覆盖度把注入条数从 10 调到了 20结果对话质量反而明显下降。排查后才发现20 条记忆即使每条压成一句话也要占掉 4000 到 6000 个 Token模型的有效推理空间被挤占了将近三分之一回复开始变得冗长且抓不住重点。后来我把注入策略改成了分层预算第一优先注入事实记忆因为它的信息密度最高第二优先注入与当前话题关键词重叠度高的语义记忆事件记忆除非被明确提及时间或阶段否则默认不注入。同时把 Top-K 从固定值改成根据当前上下文剩余空间动态计算。改完之后同一段对话的响应质量恢复到正常水平而且没有漏掉任何关键记忆。6.4 记忆面板与被遗忘权用户应能看得到删得掉最后一个经验是给记忆系统加一个可读、可编辑的记忆面板。claude-mem 的简单实现是一个本地 Web 页面把三类记忆分别列出来每条后面有置信度、重要性和最后命中时间。用户可以直接删除某条记忆、手动修正内容、或者把某条记忆标记为不再使用。这个功能一开始被我当成非核心功能排在最后实际用下来反而是反馈价值最高的部分。不是每个人的都信任记忆系统能看见模型记住了我什么、能亲手删掉不想被记住的内容那种掌控感比任何说明文档都有效。而且作为开发者面板也是调试记忆系统的利器——每一条记忆是不是抽取准确、命中时机是否合理全部一目了然。如果重新做一遍这个项目我最想分享的经验是不要一上来就追求记住所有东西先把知道哪些该忘想明白。一个好的记忆系统不是大容量的垃圾桶而是一个有筛选、有排序、有更新的工作台。把记忆条目做得越原子化、越单一事实越好能用一句话讲清楚的绝不用一段话。这条原则贯彻下来抽取、存储、检索、注入每一个环节都会轻松很多。