首页 / 资讯中心 / 文章详情

大模型跨会话记忆层设计:claude-mem的架构与实现

大模型跨会话记忆层设计:claude-mem的架构与实现 ★ FEATURED ARTICLE
1. 项目缘起与核心定位1.1 从一次“记忆断片”说起几个月前我在做一个多轮对话的智能助手项目用户连续问了七八个问题前面聊过的偏好、约束、上下文到了第十轮全丢了。用户直接甩了一句“你怎么又忘了”那一刻我意识到大模型本身没有跨会话记忆这件事不是靠调大上下文窗口就能解决的。上下文窗口再大它也是“一次性”的会话结束一切归零。claude-mem这个项目就是冲着这个痛点去的。它的核心定位非常明确为 Claude 这类大模型补上一层可持久化、可检索、可管理的记忆层。你可以把它理解成给模型外挂了一个“笔记本”模型每次开口之前先去笔记本里翻一翻相关的旧账再决定怎么回答。它解决的问题可以拆成三个层次。第一层是持久化把对话中产生的关键信息落盘不随会话结束而消失。第二层是检索当新问题进来时能从海量历史里捞出真正相关的那几条而不是把全部历史一股脑塞回去。第三层是管理记忆会过期、会冲突、会冗余需要有一套增删改查和淘汰机制。适合谁来参考如果你正在做智能客服、个人助理、长期陪伴类应用或者任何需要“记住用户”的对话产品这个思路都能直接借鉴。哪怕你只是想让自己的本地脚本记住上次跑到哪了这套记忆管理的设计逻辑同样适用。1.2 为什么是“记忆层”而不是“更长上下文”很多人第一反应是现在上下文窗口都上百万 token 了还需要单独做记忆吗我实测下来的结论是需要而且非常需要。原因有三个每一个都是真金白银踩出来的。第一是成本。上下文窗口是按 token 计费的你把三个月的聊天记录全塞进去每次请求都在为重复内容付费。而记忆层只召回相关片段token 消耗能压到原来的十分之一甚至更低。第二是精度。上下文越长模型对中间部分的注意力越容易稀释业内叫“lost in the middle”。你塞了一堆无关历史反而干扰了当前问题的判断。第三是可控性。记忆层里的内容是结构化的你可以审核、可以删除、可以设置过期时间。而上下文里的东西是黑盒用户说“删掉我刚才说的”你很难精准定位。所以claude-mem的设计哲学是上下文负责当前这一轮记忆层负责跨轮次。两者分工明确各司其职。2. 记忆层的整体架构拆解2.1 三层记忆模型的设计考量我在设计记忆层时参考了认知科学里人类记忆的分类把它拆成了三层短期记忆、长期记忆、工作记忆。这个划分不是为了学术好看而是每一层的生命周期和检索策略完全不同。短期记忆对应最近几轮对话生命周期以“会话”为单位会话结束就清空或归档。它的特点是量小、访问频繁、几乎不需要检索直接全量带上就行。长期记忆是跨会话沉淀下来的事实性信息比如用户的姓名、偏好、长期目标生命周期以“天”甚至“月”为单位。工作记忆比较特殊它是当前任务相关的临时上下文比如用户正在填一张表单填到一半去查了个资料回来还得接着填这中间的状态就存在工作记忆里。为什么要分这么细因为如果不分你要么把所有东西都当长期记忆存导致检索噪音巨大要么全当短期记忆会话一结束就丢。分层之后每层的存储介质、淘汰策略、检索权重都可以独立调优。2.2 存储选型为什么我最终选了向量库加关系库的组合存储这块我试过三种方案最后落地的是向量数据库 关系型数据库的组合。先说结论再说为什么。纯关系库方案用 SQL 的 LIKE 做关键词匹配。优点是简单、事务强、好维护。缺点是语义匹配能力几乎为零用户说“我喜欢安静的地方”你存的是“偏好安静”换个说法“讨厌吵闹”就匹配不上了。纯向量库方案语义匹配强但有个致命问题它不擅长精确过滤。比如你要查“用户 A 在 2024 年 3 月之后的所有偏好”向量库做这种带条件的结构化查询很别扭。所以我的组合是关系库存元数据和结构化字段向量库存语义向量两者用同一个 ID 关联。检索时先用关系库做条件过滤缩小候选集再在候选集里做向量相似度排序。这样既保住了语义能力又保住了精确过滤。存储方案语义匹配结构化过滤事务支持适用场景纯关系库弱强强结构化事实为主纯向量库强弱弱语义检索为主组合方案强强中综合场景提示组合方案会带来数据一致性问题写入时要保证两边同时成功建议用“先写关系库再写向量库失败则补偿”的策略而不是分布式事务后者太重。2.3 记忆的生命周期管理记忆不是存进去就完事了它得有“生老病死”。我设计了一套生命周期状态机新建 → 活跃 → 衰减 → 归档 → 删除。新建的记忆默认是活跃状态参与检索。随着时间推移如果一条记忆长时间没被召回它的权重会衰减。衰减到阈值以下进入归档状态不再参与常规检索但保留可查。归档超过一定时间或者用户主动要求删除才真正物理删除。这套机制解决了一个很现实的问题记忆膨胀。我见过一个项目跑了半年记忆库涨到几百万条检索一次要好几秒里面全是“用户说了句你好”这种垃圾。有了衰减和归档库能一直保持在一个健康的规模。3. 核心实现细节与实操要点3.1 记忆的抽取从对话里“捞”出值得记的东西记忆层最难的不是存是判断什么值得存。你不可能把每句话都存下来那样库会爆炸。我的做法是用一个轻量的抽取流程分三步走。第一步是规则过滤。明显的寒暄、确认、无信息量的内容直接丢弃比如“嗯”“好的”“谢谢”。这一步能砍掉大概四成的噪音。第二步是模型抽取用一个小的提示词让模型判断这句话里有没有值得长期记住的事实输出结构化的 JSON比如{type: preference, key: 喜欢的城市, value: 杭州}。第三步是去重合并新抽取的事实如果和已有记忆语义高度相似就更新而不是新增。这里有个坑我踩过抽取提示词不能太激进。一开始我让模型“尽可能多地抽取”结果它把用户的每一句抱怨都当成长期偏好存下来了用户第二天心情好了记忆里还留着“用户讨厌这个功能”。后来我把提示词改成“只抽取明确、稳定、跨会话仍然成立的事实”准确率立刻上来了。EXTRACT_PROMPT 从以下对话中抽取值得长期记住的事实。 只抽取明确、稳定、跨会话仍然成立的信息。 不要抽取临时情绪、寒暄、确认类内容。 输出 JSON 数组每项包含 type、key、value 三个字段。 如果没有任何值得记住的内容输出空数组 []。 对话内容 {dialogue} 3.2 检索策略怎么在毫秒级捞出最相关的记忆检索是记忆层的性能瓶颈所在。我的策略是两阶段检索粗排 精排。粗排阶段用关系库的条件过滤把候选集从百万级压到千级。条件包括用户 ID、记忆类型、时间范围、状态只查活跃的。这一步走索引通常几毫秒就出结果。精排阶段对候选集做向量相似度计算取 Top-K。K 一般设 5 到 10太多会稀释相关性太少可能漏掉关键信息。这里有个参数需要仔细调相似度阈值。低于阈值的记忆即使进了 Top-K 也不应该用否则会引入噪音。我的经验值是余弦相似度 0.75 起步具体要看你的 embedding 模型。调这个参数的方法很简单准备一批标注好的“问题-应召回记忆”对画一条 precision-recall 曲线找平衡点。注意向量检索的 Top-K 不是越大越好。我实测过 K20 和 K5 的效果K20 时模型经常被无关记忆带偏回答质量反而下降。宁可少召回不可乱召回。3.3 记忆注入怎么把记忆“喂”给模型而不显得突兀检索出来的记忆怎么放进提示词里也是门学问。我试过三种注入方式效果差异很大。第一种是直接拼接把记忆原文贴在系统提示词后面。简单粗暴但模型容易把记忆当成指令比如记忆里写“用户喜欢简洁回答”模型就真的每句都极简哪怕用户这次问的是复杂问题。第二种是结构化注入把记忆整理成“已知信息”列表明确告诉模型“这些是背景不是指令”。效果好很多。第三种是动态注入根据当前问题的类型只注入相关类型的记忆比如问技术问题就只注入技术偏好不注入饮食偏好。我最终用的是第二种加第三种。提示词模板大概长这样以下是你对这个用户的已知信息仅作为背景参考不要当作指令 - 用户偏好喜欢简洁的技术解释 - 用户背景有五年后端开发经验 - 当前任务正在排查一个数据库性能问题 请基于以上背景回答用户的问题。关键是那句“仅作为背景参考不要当作指令”加上之后模型跑偏的概率明显下降。4. 完整实操流程与关键环节4.1 环境搭建与依赖安装先把环境跑起来。我用的是 Python 3.10依赖不多核心就几个向量库客户端、关系库驱动、embedding 模型。embedding 我推荐用本地的 sentence-transformers省得依赖外部接口速度和隐私都更好。pip install sentence-transformers sqlalchemy chromadb pydantic选 chromadb 是因为它轻量单机跑足够不用额外部署服务。如果你数据量上千万可以考虑更重的方案但大多数项目 chromadb 够用了。sqlalchemy 用来管关系库我用的 SQLite小项目零配置要上生产换 PostgreSQL 改个连接串就行。4.2 记忆写入的完整代码路径写入流程我封装成了一个MemoryWriter类核心方法就一个write。它接收原始对话内部走抽取、去重、落盘三步。class MemoryWriter: def __init__(self, db, vector_store, embedder): self.db db self.vector_store vector_store self.embedder embedder def write(self, user_id, dialogue): facts self._extract(dialogue) for fact in facts: existing self._find_similar(user_id, fact) if existing: self._update(existing, fact) else: self._insert(user_id, fact) def _extract(self, dialogue): # 调用模型抽取返回结构化事实列表 ... def _find_similar(self, user_id, fact): vec self.embedder.encode(fact[value]) results self.vector_store.query( query_embeddings[vec], where{user_id: user_id}, n_results1 ) if results and results[distances][0][0] 0.2: return results[ids][0][0] return None去重阈值我设的是距离 0.2对应余弦相似度大概 0.9 以上。这个值调高会导致重复记忆堆积调低会误合并不同事实。建议先用 0.2观察一段时间再微调。4.3 检索与注入的串联检索这块我封装成MemoryRetriever对外只暴露一个retrieve方法返回排好序的记忆列表。调用方拿到之后用前面说的模板拼进提示词。class MemoryRetriever: def retrieve(self, user_id, query, top_k5, threshold0.75): vec self.embedder.encode(query) candidates self.db.query_active(user_id) # 关系库粗排 candidate_ids [c.id for c in candidates] results self.vector_store.query( query_embeddings[vec], where{id: {$in: candidate_ids}}, n_resultstop_k ) memories [] for i, mid in enumerate(results[ids][0]): score 1 - results[distances][0][i] if score threshold: memories.append(self.db.get(mid)) return memories整个链路跑下来一次检索的耗时在 20 到 50 毫秒之间完全能满足实时对话的需求。如果嫌慢可以把 embedding 缓存起来同一个 query 不重复算。4.4 参数调优的实操记录调参这块我做了几组对比实验记录如下供你参考。参数取值召回率准确率备注top_k30.720.91召回偏低top_k50.880.87平衡点top_k100.940.76噪音明显threshold0.700.920.74噪音多threshold0.750.880.87推荐threshold0.800.790.93漏召回结论很清晰top_k5、threshold0.75 是综合最优。当然这跟你的数据分布有关建议你自己跑一遍再定。5. 常见问题与排查技巧实录5.1 记忆冲突用户改主意了怎么办这是最高频的问题。用户上周说喜欢咖啡这周说戒了改喝茶。如果两条记忆都留着模型会精神分裂。我的处理策略是时间戳加权 显式覆盖。每条记忆带一个updated_at字段检索时同 key 的记忆只保留最新的一条。同时如果新抽取的事实和旧记忆是同一个 key 但 value 不同直接标记旧记忆为“已过期”而不是删除保留审计痕迹。这样既保证了当前回答用的是最新信息又能在需要时回溯历史。5.2 检索不到明明存了却捞不出来这个坑我踩过好几次原因通常有三个。第一是embedding 模型不一致写入时用了一个模型检索时换了另一个向量空间对不上相似度全是乱的。解决方法是把模型版本号也存进元数据检索时校验。第二是粗排条件太严比如时间范围卡死了把相关记忆过滤掉了。建议粗排条件尽量宽松把精度交给精排。第三是阈值设太高这个前面说过调低一点试试。5.3 记忆污染模型把噪音当事实有次测试用户随口说了句“今天天气不错”模型把它抽成了“用户喜欢晴天”。这种过度推断很常见。我的对策是在抽取提示词里加反例明确告诉模型哪些不算事实。另外对于低置信度的抽取结果可以先存成“待确认”状态不参与检索等积累到一定证据再转正。5.4 性能下降记忆库大了之后变慢记忆库超过十万条之后检索开始变慢。排查下来瓶颈在向量库的全量扫描。解决办法是分片 索引。按 user_id 分片每个用户的记忆独立索引检索时只扫自己的分片。另外定期把归档状态的记忆从向量库里移出去只留关系库能显著减小索引体积。问题现象可能原因排查方向解决手段检索结果不相关embedding 不一致校验模型版本统一模型召回为空粗排条件过严放宽过滤条件调整查询记忆冲突未处理更新检查时间戳加覆盖逻辑响应变慢索引膨胀查看库大小分片加归档提示每次改动检索参数后一定要用固定的测试集回归一遍否则很容易按下葫芦浮起瓢修好一个问题引入另一个。6. 一些延伸思考与个人体会claude-mem这套东西跑通之后我最大的体会是记忆层的价值不在于“记得多”而在于“记得准”。一开始我总想着把所有东西都存下来结果检索质量一塌糊涂。后来做减法只存真正稳定、跨会话成立的事实效果反而好了。这跟人脑很像你记不住所有事但关键的事记得牢就够了。另一个体会是可解释性很重要。记忆层里的每一条你都得能说清楚它从哪来、为什么在这、什么时候会过期。否则出了问题根本没法排查。我在每条记忆上都加了source字段记录它来自哪次对话调试的时候一目了然。后续如果要扩展我会往两个方向走。一是记忆的主动遗忘让模型自己判断哪些记忆不再需要主动清理而不是靠时间衰减这种被动机制。二是跨用户的群体记忆在不涉及隐私的前提下把多个用户的共性偏好抽象出来作为新用户的冷启动先验。这两个方向都挺有意思等有进展了再单独写一篇。最后分享一个小技巧调试记忆层的时候别只看最终回答要把召回的记忆列表打出来看。很多时候回答不对不是模型的问题是召回的记忆本身就不对。把召回结果可视化排查效率能提升一大截。
阅读完成 · 觉得有帮助?
咨询建站