多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

大模型长期记忆实战:给Claude补上跨会话记忆的完整方案

大模型长期记忆实战:给Claude补上跨会话记忆的完整方案 做AI应用开发的人迟早会撞上一个很具体的痛点和同一个大模型助手聊了两小时方案都敲定了第二天打开新对话它一脸茫然地问你“我们这次要解决什么问题来着”。标题里这个claude-mem光看名字就能猜到一半——给Claude这类对话模型补上跨会话的长期记忆。真正动手做的时候会发现记忆模块远比想象中复杂不是简单存个聊天记录就叫记住了也不是塞进向量库就算召回里面涉及信息抽取、结构化存储、动态注入、冲突更新、遗忘机制一堆环节。这篇文章我会把这个记忆模块从设计思路、存储选型、实际代码、踩坑记录到效果评估完整捋一遍。内容不是我凭空想的方案而是把我在类似项目里反复试错后的经验沉淀下来。适合正在用Claude API做自动化工具、开发个人知识库、或者给团队机器人加“记性”的开发者参考。1. 动手之前先把“记忆”这个词拆清楚1.1 上下文里的临时记忆不是我们要做的大模型自带的上下文窗口本质上是一种临时记忆。只要会话不关它能记住前面聊的每一句话文件夹位置、命名习惯、上一步做到哪儿全都清清楚楚。但窗口是临时的会话一结束这个记忆就归零了。claude-mem要补的不是这个临时区间而是跨越会话、长期有效的那部分信息。这个区别如果没想清楚工程很容易做歪。有人一上来就给每个新会话疯狂塞历史记录结果上下文越塞越长单次请求的token成本翻了几倍回答质量反而下降因为模型被大量寒暄、确认、废话干扰了判断。长期记忆要做的是“提炼”不是“搬运”。1.2 长期记忆至少可以拆成两层我在实际设计时把长期记忆分成两层事实层和偏好层。事实层是客观状态比如“用户正在做数据分析平台当前进度是数据管道已跑通遗留问题集中在接口鉴权”。这种信息更新频率低、结构固定适合用结构化字段存每条记录就是一个明确的键值对或者一张小表。偏好层是主观倾向比如“用户希望方案先给结论再展开细节”“重要参数用表格呈现”“术语能少就少”。这类信息没有固定结构更像一条条规则适合按文本条目存并且要靠使用频次来判断这条偏好是否稳定。一条偏好如果只出现过一次我不会让它进正式记忆出现了三次以上才值得长期保留。这两层更新频率、存储结构、召回方式全都不一样设计表结构时就要分清楚别全塞在一个字段里。1.3 判断一条信息能不能进记忆我用三个标准第一个标准是会不会复用。今天记的东西一周后还能不能用得上。“正在做的项目名”肯定能复用“今天午饭吃了什么”大概率不能。记忆是为未来对话服务的不是为当下服务的。第二个标准是够不够稳定。一次性的情绪信息、临时状态信息不要进长期记忆。用户说“这个方案今天必须出”是临时状态过了今天就失效了但“用户负责的模块是支付系统”是稳定事实必须存。第三个标准是代价意识。每一条写进长期记忆的信息都会在后续对话里被召回、被注入到上下文中等于是一笔持续开销。所以写记忆的门槛应该比读记忆高得多宁缺毋滥。还有一点容易被忽略每条记忆都要带时间戳和来源会话ID。这两样东西平时用不上但一旦记忆冲突、你想追溯这条信息是哪次对话里产生的没有它们就只能抓瞎。我见过不少半路接手的项目记忆表里只有“内容”一个字段后面根本没法维护。2. 核心设计把记忆做成一张会进化的表2.1 为什么不能直接把聊天记录当记忆最简单的记忆实现方案是全量存对话下次检索时往里翻。这个方案问题非常明显成本高、噪声大。对话里大量内容是“嗯”“对”“好的我再看看”全塞进上下文既浪费token又会把模型注意力带偏。而且历史对话是按时间线性排列的用户想找的是“上次确认的技术选型”不是“上次你和我讨论技术选型时的完整对话”。claude-mem代表的设计思路是中间加一道提炼层对话完成后把聊天记录送给模型做抽取产出几条干净的、离散的记忆条目然后只存这些条目。矿石炼成金属锭再存储而不是把整座矿搬进来。这个设计决策是整套系统的基石后面所有功能都是围绕它展开的。2.2 记忆抽取让模型自己总结但加上约束让大模型从对话里抽取记忆本身不复杂难的是让抽取结果稳定、可解析。我的做法是用输出格式约束要求模型产出一个JSON数组每条记录包含固定字段。下面是常用的字段结构MEMORY_FIELDS { id: 唯一标识uuid或自增id, type: user_fact | project_state | preference | decision, content: 不超过120字的结论性描述, confidence: 0到1之间的浮点数模型自评置信度, keywords: 字符串数组用于检索不超过5个, session_id: 来源会话ID用于追溯, created_at: 创建时间, updated_at: 最后更新时间 }confidence是模型自评的置信度这个字段很容易被忽视但它非常关键。低于0.6的记录默认进待定区不直接写入正式记忆表等同一事实在后续对话里再次出现、累计证据之后才转正。这一步能显著降低幻觉记忆入库的概率。抽取时的温度参数我会设到0.2左右温度太高模型会自由发挥抽取结果不稳定温度太低又可能漏掉隐含信息。这个值不算敏感但建议固定下来别每次对话都碰它。2.3 存储从SQLite开始别急着上向量库记忆项目最容易犯的错是一上来就上向量数据库。这里我强烈建议先冷静一下早期记忆量可能只有几百上千条SQLite一个文件完全够用而且备份、迁移、查日志都极其方便一个文件拷走就完了。向量检索解决的是“语义相似召回”问题但在记忆场景里真正高频的查询其实是结构化查询比如“这个人项目现在进展到哪了”“他之前定的技术栈是什么”。这些用字段精确匹配就能做到根本用不着向量。等记忆量确实大了再给content列加一个单独的向量索引两种检索方式混合用才是务实路径。我实际用的存储结构大致是这样CREATE TABLE memory ( id INTEGER PRIMARY KEY AUTOINCREMENT, owner_id TEXT NOT NULL, type TEXT NOT NULL, content TEXT NOT NULL, confidence REAL NOT NULL, keywords TEXT NOT NULL, session_id TEXT, status TEXT DEFAULT active, fingerprint TEXT UNIQUE, hit_count INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime(now)), updated_at TEXT DEFAULT (datetime(now)) );owner_id字段是给记忆加归属的哪怕现在只有你一个人用也要加。后面如果多人共用、或者做多Agent共享这个字段就是命根子。fingerprint字段做唯一约束用于去重后面踩坑部分会细说。2.4 召回不是所有记忆都值得注入召回分两步候选筛选加动态取舍。候选筛选用类型和关键词匹配比如当前对话涉及“数据管道”就把带pipeline标签的记忆捞出来然后动态取舍看活跃度得分。我用的是一个加权公式score relevance * 0.5 recency * 0.3 hit_count * 0.2relevance来自模型对当前对话主题的判断recency按最后更新时间做半衰期衰减hit_count是这条记忆被实际成功引用的次数。三者的权重是我在实验里调出来的你的场景不一定相同但方向是对的既要和当前话题相关又不能全是陈年旧事。最终只取前5条总字符控制在1800字符以内塞进系统提示词的固定位置。这样每次对话的记忆开销是可控的不会出现上下文爆炸。没有匹配到任何记忆时直接不注入记忆区块让模型保持纯净开场。3. 实操搭一个最小可用的记忆模块3.1 数据流和目录结构先看整体数据流会话结束后把原始消息发到记忆抽取服务产出记忆条目写入SQLite下次新会话开始时通过召回服务把相关记忆注入系统提示词对话结束后再对新产生的信息做一次抽取更新。目录结构不用复杂按职责划分三个模块就行mem_project/ ├── ingest.py # 记忆抽取与写入 ├── recall.py # 记忆召回与注入 ├── update.py # 记忆更新与合并 ├── store.sqlite # SQLite数据库文件 └── prompts.py # 系统提示词模板逻辑上就三段写、读、改。别一上来就拆成微服务一个进程跑起来验证思路没问题再考虑扩展。3.2 写入流程异步、幂等、防抖对话一结束就立刻阻塞着调大模型做抽取体验会很差一次抽取要几秒甚至十几秒。正确做法是放到后台队列处理。我的做法是先把对话日志写进一个待处理表定时任务每30秒扫一次把新产生的会话日志送去抽取。这样用户完全感知不到记忆模块的存在。这里有一个容易被忽视的坑用户可能同时开几个会话同一句话、同一个偏好可能被并发抽取多次。解决办法是给记忆条目生成指纹按规范化后的content算哈希。写入前先查指纹已存在就走更新而不是新增。更新时把updated_at刷新、hit_count累加这样既不产生重复记录又保留了记忆的使用热度。3.3 读取流程注入的位置和格式每次请求前拼接系统提示词时在固定位置放一个记忆区块。格式必须清晰否则模型会把记忆和当前用户消息混在一起。我用的是带标签的区域你是用户的AI助手。 memory - [项目状态] 用户正在开发数据分析平台当前完成数据管道遗留接口鉴权问题。 - [偏好] 用户喜欢先结论后细节重要内容用表格呈现。 - [决策] 已于2025-06-10确定使用Python 3.12和FastAPI框架。 /memory标签本身不是给模型执行什么特殊指令而是做一个视觉上和语义上的隔离让模型知道这块内容是背景资料不是当前对话的新消息。没有匹配到记忆时就不放这个区域给模型一个干净的开场。还要注意一点记忆区块的注入位置一定要固定不要一会儿放系统提示词开头一会儿放用户消息后面。模型对位置的敏感度比我们想象的强位置不固定偶尔会出现“记忆被当成最新指令”的怪问题。3.4 关键参数到底怎么定参数不能拍脑袋但我可以给出一个合理起点你在这个基础上做实验。参数初始值说明记忆条目最大长度120字符超过截断让模型重写活跃记忆数top_k5从5试到8对比效果注入长度上限1800字符超了就踢掉排序最末的记忆confidence阈值0.6低于此值进待定区抽取温度0.2保持输出稳定召回relevance权重0.5当前主题匹配召回recency权重0.3时间衰减召回hit_count权重0.2历史命中次数这些参数没有普适最优值。我的调参方法是准备一组固定测试问题比如“你还记得我上次的项目进度吗”“我说过不要用缩写”“我的框架选定是什么”然后一次只改一个参数记录命中率跑完对比。一次改两三个参数出了问题根本没法定位是哪个环节导致的。4. 踩坑实录这些坑我基本都踩过一遍4.1 记忆污染旧信息比新信息还抢眼现象是用户上个月说倾向于A方案这周确定改用B方案结果系统启动时还是优先输出旧方案。查数据库一看updated_at根本没更新因为抽取模型没把“方案改成B”识别为对已有记忆的更新而是写成了新条目旧条目还在active状态。这个问题的根子是冲突检测缺失。解决思路是写入前先做一次语义匹配找到同类型、同主体的旧条目把“新增”操作变成“更新”操作并在content里保留最新决策和更新时间。比如更新为“最新决定B方案2025-06-10更新此前为A方案”。旧信息不是洪水猛兽只要加上时间上下文模型就能正确判断该信哪个。4.2 上下文爆炸记忆区越长回答越糊涂有一阵子我把top_k调到了15想着召回的候选越多越不容易漏。结果对话开始几轮还算正常后面模型开始混淆“记忆里的旧内容”和“当前新内容”语气都变得怪怪的。后来打印每次请求的实际prompt一看记忆区块占了差不多三千字符比用户消息还长。解法是加硬上限超了直接截断。还有一招更有效召回前先用一个轻量分类模型或关键词规则判断当前主题再按主题召回而不是把所有相关记忆一股脑捞出来。记忆宁可少塞几条也不能让模型把记忆当成正在进行的对话内容。4.3 重复记忆像复读机用户连续几次提到同一个偏好比如“我不喜欢邮件里用感叹号”每次对话结束抽取都会生成一条新记录不到两周表里就出现四五条相似的。原因很简单抽取是逐段做的没有跨会话去重。指纹去重能解决一部分但对语义近似但措辞不同的条目没用。我加了定期合并脚本每周跑一次把content向量相似度超过0.85的条目合并保留更新时间最新的一条hit_count累加其他标记为archived。这个脚本一开始就要写在计划里不然数据多了再处理特别费劲。4.4 多人共用一套记忆的串味我自己的工具是个人知识管理所以没有这个坑。但有几个朋友做团队场景同一个服务后端、同一个记忆库几个人的人格特征、项目偏好、时间习惯全混在一起。一个人说“要用敏捷方式推进”另一个人说“要严格按期交付”模型一会儿这个风格一会儿那个风格。解法很简单就是前面说的owner_id字段。所有读写都带这个ID召回时第一过滤条件就是owner_id。如果以后做多Agent共享记忆这个字段还要扩展成agent_id加owner_id两级隔离。4.5 抽取模型“演”出一条不存在的记忆有一次对话里用户提了一句反话“我真是受够了这个方案”语气明显是自嘲或抱怨结果抽取模型把它总结成一条高置信度偏好“用户希望推翻当前方案重新讨论”。这就是抽取场景下的幻觉模型遇到矛盾或情绪化表达时会按自己的理解补一段“合理解释”。我的解法分两层。第一层是模式校验识别身份证号、手机号、银行卡号这类敏感信息发现就直接丢弃第二层是交叉验证同一个事实至少要在两次独立对话中都出现才允许进入正式记忆单次出现只能进待定区。这个策略虽然保守但对降低幻觉入库特别管用。4.6 记忆模块拖慢了主流程加了记忆功能后每次对话请求多了几百毫秒到一两秒体验明显变差。原因不难查召回时用大模型做主题判断抽取时又调大模型两次调用都排在用户请求的同步链路上。解决方法是把链路里“可异步”的部分全部拆出去。主题判断先用关键词和规则匹配规则命中不了再让模型兜底记忆抽取全部进入后台队列用户不需要等它完成就能继续发消息。记住一个原则记忆模块是后台服务不是请求链路上的必要环节它的质量可以异步提升但它的延迟绝不能让用户感知到。4.7 备份只备份了数据库文件忘了关联结构有次重装环境库文件还在但标签表、会话映射表丢了旧记忆全部变成孤儿数据。因为当时只导出了一份主表关联结构全丢了。这个教训让我明白记忆库的备份必须整库操作SQLite直接拷贝整个文件就行别只挑“看起来重要”的表导。最好配合定时快照每天把整个文件打包带时间戳存起来。记忆数据没有业务数据那么强的交易性但恢复起来一样费时间。等你真需要追溯半年前某条决定的来源时你会感谢当时多存了一份完整备份。4.8 测试集是自己构造的上线效果打对折离线阶段我构造了20组测试问题命中率85%感觉稳了。上线后用户反馈“它根本不记得我”。回看数据才发现测试问题是自己写的风格和信息密度跟真实对话完全不一样。后来把真实对话日志回放进去抽取模型产出的记忆质量掉了一大截。从那以后我的评估流程多了一步每周抽一天的真实会话记录重新做抽取然后人工标注抽取结果的正确率。这个标注集就是回归基准后面所有参数调优都拿它对照。这一步很费时间但它是效果真实性的唯一锚点。5. 效果评估和调优思路5.1 该关注什么指标记忆模块的核心指标是命中率但命中要分两段看。第一段是召回命中相关信息是否出现在注入的记忆区块里第二段是回答命中模型拿到记忆后最终答复里是否真的用上了。只测第一段很容易被“召回了但模型没当回事”骗过去。我实际操作时用20组问答做快速回归每组问一个明确的、与已存记忆相关的问题。比如“我之前说过项目进度卡在哪”“你记得我偏好什么汇报风格”。然后人工判断两件事召回结果里有没有对应条目、最终回答是否体现了这条记忆。两个都要打钩才算一次完整命中。5.2 参数调优不要拍脑袋记忆功能的参数像一团毛线top_k、置信度、权重、注入上限每个之间都存在耦合。我的方法特别朴素固定测试集跑基线版本记录命中率然后一次只改一个参数对比后再改下一个。举例来说想验证置信度阈值0.6和0.4的区别就固定其他参数不动跑同样的20组问题分别记录命中率和误召回数。这方法听起来不够“智能”但在概率性系统里是最可靠的。调参过程本身也是在发现瓶颈如果降低置信度命中率没涨说明问题不在抽取端可能在召回阶段如果召回结果很准但回答没用上说明注入格式或者位置有问题。一句一句追下去瓶颈自然浮出水面。5.3 记忆的遗忘机制不能少只写不删时间长了库就膨胀旧偏好会和最新偏好打架无关记忆会挤占注入空间。我后来给每条记忆加了一个last_hit_at字段超过60天没被命中且confidence低于0.5的进入“休眠区”不再参与召回。休眠不是删除当用户重新提到相关话题时抽取模块会识别到旧条目并自动唤醒同时刷新updated_at。如果一条记忆休眠被唤醒后又连续休眠两次才真正物理删除。这个机制不复杂但用处非常大。它相当于给记忆做了一次简易的衰减曲线让活跃记忆始终是用户最近关注的而不是让几年前的陈年偏好一直占据宝贵的注入位置。6. 如果还想继续扩展几个方向值得试6.1 让记忆变成多Agent共享的单机的记忆模块价值有限一旦过渡到多Agent协作场景记忆就成了公共资源。比如一个Agent负责收集信息另一个负责生成方案收集Agent写下的记忆必须能被生成Agent读到但同时两个Agent不能互相覆盖对方的记录。我的建议是数据库一开始就带owner_id和agent_id两级字段先做隔离再划一个共享区域。隔离和权限的复杂度远大于单纯加记忆所以基础字段务必提前留好。6.2 用记忆做“会话前摘要”新会话启动时除了注入记忆条目还可以自动生成一段“上次对话简要总结”上次聊了什么、定了什么、下一步要做什么。这个摘要和记忆条目的区别在于摘要是叙事性的记忆条目是离散事实性的。两个都用上用户开场体验会好很多。特别是隔了好几天再回来一句“你上次聊到数据管道卡在鉴权当时准备用FastAPI重构”能让用户瞬间回到状态里。6.3 记忆的安全与合规边界记忆数据本质上是用户隐私。写入前做敏感信息脱敏过滤存储时做加密界面上提供一键导出和一键清空。个人小工具阶段很容易忽略这些等数据量大了再补特别麻烦。技术上没有太多复杂的关键是要有这个意识把隐私保护写进设计的第一版而不是最后一版补丁。最后说点我折腾下来的体会。最早我也心气很高上来就设计了完整的记忆体系字段、标签、向量索引、加权权重、多Agent规划全上了一遍跑了两周发现真正高频用到的功能没几个——记住项目进度、记住用户偏好、记住已定决策仅此而已。后来把架构砍了一半用SQLite加几百行Python重新实现反而稳定得多。claude-mem这类工具本质上就是给模型配了外置硬盘但硬盘再大也得先想清楚哪些该存、哪些该扔。从最小可用版本开始别让记忆模块本身变成新的维护负担这是我在这条路上被坑了无数次之后最想跟你说的一句话。
返回列表