
我把这个项目从立项到落地从头捋了一遍目标是做一个会话型AI的长期记忆增强方案代号就叫claude-mem。一句话说清楚它解决的问题当你和AI对话时它一关窗口就失忆每次都要重新介绍自己的背景、偏好、之前聊到哪了。claude-mem做的事情简单讲就是把对话历史沉淀成可检索的长期记忆在下次对话开始时自动把相关记忆重新喂给AI让它看起来像“记得你”。这套思路适合两类人。一类是被AI的“金鱼记忆”折腾到崩溃的深度用户想用一个本地方案解决个性化问题另一类是对上下文工程、检索增强、提示词管理感兴趣的开发者想看看一个完整的记忆系统是怎么从零搭起来的。它不需要你怎么改动模型本身完全通过外部存储和弹性的上下文注入来实现改造成本很低。1. 项目整体设计与思路拆解1.1 先搞清楚AI为什么“记不住”做过会话型AI应用的人应该都被同一个问题卡过模型本身不维护任何对话状态每次请求都是无状态的。你发一句话过去它只看得到你放在上下文里的内容之前的对话如果没放进去它就当从来没发生过。很多人一开始的解决办法简单粗暴把所有历史对话一股脑塞进上下文。但很快会撞到几个现实问题。一是上下文窗口有上限长对话很容易超限二是token成本跟着历史长度线性上涨聊到后面每次请求都在烧钱三是无关历史会干扰模型对当前问题的判断比如你上周问过菜谱这周问工作日报模型却还在惦记着你怎么做红烧肉。所以claude-mem的核心设计思路不是“记住所有”而是“记住该记住的并在对的时间拿回来用”。这背后其实借鉴了人脑的记忆分级短期记忆处理当前对话中期记忆保留最近几轮上下文长期记忆沉淀那些真正重要的事实和偏好。1.2 记忆系统拆成三个模块我把整个系统拆成三个模块存储层、索引层、注入层。存储层负责把对话落盘索引层负责把自然语言转成可检索的结构注入层负责在构造请求时把相关记忆塞进提示词。三个模块各管一摊互相解耦后续想换存储或者换检索策略都不用推倒重来。存储层我选的是本地文件加SQLite的组合。对话原文按时间戳存成JSON行一个人一个文件结构化信息用户偏好、关键事实抽出来存SQLite表。这么做的原因很朴素数据是你自己的不依赖任何云端服务调试的时候直接翻文件就能看到存了什么出了bug也好排查。索引层是这个系统的技术核心。对话原文不可能整段塞进向量库太长、太杂、噪声大。我的做法是先把对话按语义切块每块生成两条索引一条是文本倒排索引管关键词匹配一条是向量索引管语义相近的召回。两条索引做混合检索最后用加权得分排序取前几名。注入层是纯工程活但坑最多。它要做的事情是拿到当前对话的上下文算出哪些记忆和这个话题相关再把记忆改写成一段自然语言挂在系统提示词后面。改写这一步很关键直接拿原文拼进去会显得很生硬模型读到乱七八糟的碎片语气容易跑偏。1.3 为什么不做“全量记忆”有人会问为什么不干脆把用户画像和对话历史存下来每次全部塞给模型我实际测试过效果很差。一是token开销撑不住长对话用户可能每次要烧掉几万token二是模型在大量冗余上下文里找重点的能力其实有限塞太多不相关的东西它甚至会把之前错的结论当背景信息输出出来。后来我改成按需召回只喂最相关的记忆片段。实测下来对话质量反而提升了因为注入的记忆是经过筛选和改写的模型看到的是一份干净的“人物背景卡”不是一团乱麻。这也算是一个重要经验给模型的记忆质量比数量重要得多。2. 核心细节解析与实操要点2.1 记忆分级的落地策略记忆不能不分轻重缓急全塞一起否则会出现一个很尴尬的场景AI记住了你三年前随口说的一句玩笑话却忘了你昨天强调的工作习惯。所以我在设计时把记忆分成了三个等级。第一级是短期记忆就是当前会话内的对话轮次这个不需要额外存储模型上下文天然带着。第二级是中期记忆同一会话主题下的最近若干轮摘要我会在对话暂停超过一定时间后把这段对话压缩成一个摘要存下来。第三级是长期记忆包括用户的基本画像、明确表达的偏好、反复出现的需求这些会被单独抽出来长期保存不受会话清理影响。三个等级的记忆在注入时有不同策略。短期记忆直接放在对话历史最前面中期记忆作为背景信息拼在系统提示词里长期记忆只在触发相关话题时才调出来。这样既不会浪费token又能保证关键信息不丢。注意长期记忆不是越多越好。存了100条偏好模型真正用得上愿意用的可能只有十几条。建议定期清理不活跃的记忆条目我一般三个月跑一次清理任务把半年都没被召回过的条目降权或删除。2.2 存储选型的对比与取舍做存储选型的时候我对比了三种方案纯JSON文件、SQLite、完整的向量数据库。简单说下我的结论。纯JSON文件最灵活写起来也最快适合原型验证。但数据量上来之后想按时间范围查询、按用户过滤就会变得很别扭索引和检索都得自己手写。SQLite在结构化数据上更稳查询能力强很多而且单文件部署非常方便不需要额外起服务。向量数据库比如本地跑的轻量级方案在语义检索上有天然优势但你得先解决向量化问题还要处理额外服务的运维成本。我的最终方案是SQLite存结构化记忆Lucene风格的倒排索引加上轻量级向量索引做检索。SQLite负责“记忆了什么”索引负责“怎么找回来”。两套数据通过对话ID和时间戳关联查询时先走索引拿到候选ID再回SQLite拿原文。表格对比一下三种方案方案灵活性查询能力部署成本适用场景JSON文件高低低原型验证、少量数据SQLite中中高低结构化记忆、单机部署向量数据库中高高大规模语义检索、多人共用坦白讲如果只是给自己一个人用SQLite完全够了。向量数据库的优势在大规模用户场景才明显单人单机的数据量根本喂不饱它。2.3 向量化与混合检索的设计细节检索这块是重头戏我踩过的坑比存储多得多。最早我只用向量检索很快就发现一个问题向量检索在中文场景下对精确名词匹配很不友好。比如用户之前说过“单位附近那家兰州拉面馆”你得先分词切出“兰州拉面馆”再去找但向量模型经常把这句话的语义混成一团召回结果不尽人意。后来我改成混合检索BM25倒排索引负责精准匹配关键词向量索引负责语义泛化召回两边各取topN结果然后用一个加权公式合并排序。实测下来的效果是本来找不着的记忆能找着了而且相关度排序也比单路检索顺眼很多。加权公式不复杂就是给两路得分分别乘一个权重再相加。关键词命中的记忆权重稍微高一点因为既然用户这次用了同样的词大概率还是同一件事向量召回的权重稍低一点但可以保底找到那些换了说法却没换意思的记忆。这两个权重我调了很久最后稳定在0.6和0.4大家可以根据自己的数据风格微调。3. 实操过程与核心环节实现3.1 会话记录落盘怎么做整个系统第一步就是把对话原文老老实实存下来。这一步看似简单却有两个容易忽略的点一是什么时候写盘二是存什么格式。写盘时机我没用实时写入而是做了缓冲。每轮对话结束后把这一轮的消息放进内存队列攒够一定条数或者超过一定时间再批量写入。这么做的原因是实时写入不仅频繁触发磁盘IO而且很容易在AI生成中途把半截话存进去导致记忆内容不完整。批量写盘配合会话结束标记能保证存下来的都是完整轮次。存储格式用的是滚动文件加SQLite双写。滚动文件按天拆分每条记录保存消息ID、会话ID、角色、内容、时间戳SQLite里存同一份数据但额外维护一个对话摘要表负责记录每个会话的概要信息。双写会带来一点存储冗余但换来了调试便利性我个人觉得很值。3.2 摘要与事实抽取的实现存储只是第一步真正让记忆“有用”的是从原文里抽摘要和事实。我跑了一个独立的处理任务在对话空闲时对每个会话做三件事生成对话摘要、抽取用户偏好、标记待办事项。摘要生成直接调用语言模型的prompt完成核心提示词是一句话固定的——“把这段对话压缩成50字以内的内容摘要保留人物关系、关键决定、明确的任务”。这里的要点是控制摘要长度太长了后面检索反而被噪声淹没。偏好抽取比摘要更精细。我不只存一条“用户喜欢什么”而是拆成结构化字段偏好对象、偏好程度、出现次数、最近确认时间。比如用户说“我喝咖啡不加糖”我会存成一条偏好记录下次检索到这条时模型就知道该避开糖这个选项。待办事项的抽取类似但额外标注了截止时间如果超时会自动提醒。3.3 检索索引的核心代码实现索引部分的代码不长但设计需要认真推敲。核心流程是三步切块、向量化、建立索引。切块不能固定长度硬切中文对话按句子边界和段落主题来切更合理。我用的策略是先按标点符号分句再把相邻几句合并成一个块块的大小控制在200到300字之间。太短了语义不完整太长了向量化效果会钝化。向量化我一开始用了几种公开的embedding模型比对最终选了一个在中文对话数据上表现均衡的轻量模型。一个经验向量化耗时并不长但如果你跑的是CPU环境几百个块的向量化可能需要半分钟建议做成后台任务不要阻塞主流程。构建索引的伪代码大概长这样实际代码里也用到了类似逻辑def build_index(conversation_blocks): index {} for block in conversation_blocks: tokens tokenize(block.text) for token in tokens: if token not in index: index[token] [] index[token].append(block.id) return index这个倒排索引只做粗筛后面接一个精确的重排序。向量检索的部分我用的也是本地计算余弦相似度不依赖外部服务——数据不出本机这是这个项目的一个底线。3.4 运行时注入的完整流程等索引建成真正到“用”的时候核心逻辑就是一个注入函数。流程大概是先取当前对话最近的几轮内容提取关键词和语义向量然后分别走倒排索引和向量检索拿到候选记忆块再按得分排序截取topK个结果最后把这些记忆块改写成自然语言片段拼进系统提示词。这里有一个细节注入不是把记忆块原封不动贴进去而是经过一次改写。改写模板我用的是“关于你的长期记忆你了解以下信息……”后面逐条列出记忆点。改写的好处是让模型在层面把它们当作背景知识而不是对话上下文回答时会更自然不会突然“跳戏”。还有个关键参数是注入条数。我默认取top3这个数字不是拍脑袋定的我做过实验注入1条的时候很多有用信息没用到注入5条以上模型的输出开始变散可能是因为记忆噪声多了它反而不知道该侧重哪个。top3在多数场景下是性价比最高的。下面是一个注入逻辑的示意代码def build_prompt(current_query, memories, system_prompt): memory_lines [- m.rewritten_text for m in memories[:3]] memory_section \n.join(memory_lines) if memory_section: system_prompt \n\n【长期记忆】\n memory_section return system_prompt \n\n用户问题 current_query这段代码看起来简单但实际部署时还要加一层缓存。同一个问题短时间内重复触发直接走缓存返回不重新检索能省一大块成本。4. 常见问题与排查技巧实录4.1 检索不到记忆问题出在哪很多人在跑通流程后第一个困惑是明明记得存了那条对话可检索时就是召不回。我复盘过几次原因一般有三个。一是切块切得太碎导致语义被拆散向量化后跟原意差异很大。解决办法是切块时引入句子边界感知不要让一句话被拦腰截断。二是embedding模型在中文上的表现和英文差距明显有些专有名词、网络流行语根本不在模型词表里向量化后都是近似的向量很难精准召回。这种情况就得靠倒排索引兜底所以混合检索不是可选优化而是必备设计。三是时间衰减策略太过激进把旧记忆降权降得太狠。我的经验是把时间权重设成缓衰减半年内的记忆权重差距别超过两倍不然新记忆一直占前排旧记忆直接音讯全无。排查时有个高效方法把单条记忆原文拿出来直接搜一遍关键词如果倒排索引能搜到而混合检索不能那问题大概率出在排序权重合并上如果倒排也搜不到那就是索引构建时切块或入库环节出了偏差。4.2 注入记忆后AI说话变别扭了这个坑也很典型不加记忆时AI的语气很正常加上记忆之后它说话变得特别生硬甚至会把记忆内容当成说教材料复述出来。原因在于原样注入的碎片记忆缺乏上下文语境模型不知道该怎么把记忆“用起来”。解决办法是把记忆改写成一种背景知识描述而不是散装的对话摘录。比如原来存的是“用户上周三说他不吃香菜”改写成“用户饮食偏好中明确回避香菜”。模型读到的是信息不是对话原文就不太会复述原话而是参考这些信息组织回答。这一条我强烈建议实际体验一下效果差异非常明显。注意不要在系统提示词里注入带有时间敏感性的记忆比如“昨天说好今天下午开会”过几天再对话模型会把它当现状造成误导。时间敏感信息应该放在对话历史里而不是长期记忆里。4.3 记忆冲突怎么处理当用户在不同时间说出了互相矛盾的偏好比如上个月说“我最近在减肥”这个月说“还是正常吃吧”系统把两条都存进了长期记忆结果AI下一次回答时左右互搏。这个问题我用时间戳权重加置信度解决了。每条记忆都带一个更新时间戳和来源轮次检索返回后同一个对象的记忆会做一次合并取时间更新的那条作为有效值旧的那条降到更低的置信层级。实践下来绝大多数矛盾场景都能正确收敛到最近一次表达的意思。还有一类冲突是显式纠正比如用户说“我上次说的是错的重新来”。遇到这种表述系统会把对应的旧记忆直接标记为失效而不是压低权重。失效记录保留在库里用于追溯但不再参与检索。4.4 隐私与数据安全的考虑这个项目的数据全部存在本机不上传任何云端服务这是我做技术选型时坚持的一条底线。但这不意味着就完全没有隐私风险。本地数据库文件如果被其他程序读到记忆内容一样会泄露。我的做法有几个第一SQLite文件和数据目录设置严格权限只允许当前用户读写第二可选加密对长期记忆表中的敏感字段做加密存储第三清理策略用户主动删除的对话记录不仅删除正文还会同步清掉索引和摘要确保不留后门。如果你打算长期使用建议定期备份数据目录但备份文件同样要加密存放别把明文记忆和代码一起扔到网盘。5. 实操过程中的进阶调优经验5.1 让记忆真正“有用”的提示词设计记忆注入的最终效果不取决于存了多少而取决于模型怎么理解这些记忆。我在提示词设计上反复磨过很多次最终摸索出一个比较稳定的版本。记忆区域的引导语不能太绕模型不是人它不会自动区分“这是背景信息”和“这是要我执行的指令”。我用了很直白的说明“以下是关于用户的长期记忆供你回答问题时参考。如果你认为某条记忆与当前问题无关请忽略该条。”这样模型就清楚了两件事这些是参考材料不是硬性任务有冲突时可以做取舍。另外一个细节记忆条目之间的分隔符要统一我用短横线加空格模型对这种格式的解析是最稳定的。不要花哨地搞成表格或JSON块模型解析JSON块时经常丢掉内容格式越复杂出错概率越大。5.2 成本控制与性能优化心得跑了一段时间后我意识到记忆系统的运行成本和CPU开销远比预想的高。几个主要的成本点一是每次对话结束后的嵌入向量计算二是每次请求触发检索时实时向量化和相似度计算三是定期摘要任务调用语言模型的token开销。优化思路大体是三点尽量复用向量对话文本不变的情况下嵌入向量直接缓存不要重复计算检索结果做时间敏感的缓存同样的查询在五分钟内不重新检索摘要任务挪到低峰期批量处理不要卡在用户对话的高频时间段。还有一个容易忽略的优化点控制单条记忆的体积。摘要和偏好抽取尽量精炼不要存储完整文章只存浓缩后的信息。这个习惯能显著降低向量化和检索的成本让整个系统干净很多。5.3 从单用户到多用户的结构改造这个项目最初只有我一个人用数据表上没有任何用户标识逻辑简单不少。但后来我把它部署给团队同事用时就发现单用户结构撑不住。多用户场景下所有人的对话都混在一起检索时不可能把别人记忆里的内容拉出来。改造思路很简单所有表增加一个owner字段所有查询强制按owner过滤索引文件按用户分目录存放向量缓存键也加上owner前缀。结构上不复杂但要小心别漏掉过滤条件一旦检索时忘了带owner过滤隐私问题就很严重。我建议写一个统一的数据访问层把用户隔离逻辑收敛到一处不要散落在各个业务代码里。改造之后每个用户看到的都是自己独立的记忆空间互不干扰。这个版本在实际使用中稳定性很高也验证了这套记忆方案在多人场景下仍然成立。6. 最终效果与实际体验项目跑通之后我个人的实际感受是AI对话的持续性和个性化程度有了很明显的变化。之前打开一个新会话它连我做什么工作、习惯什么表达方式都不知道现在它会在回答里自然带上我之前的偏好和背景看起来更像是一个“记得我”的助手了。稳定性方面混合检索的设计帮了大忙。关键词匹配保证精确命中向量召回兜底泛化表达两者结合让记忆的检索成功率明显提升。时间衰减和冲突处理机制又保证了记忆库不会因为积累时间太长而变杂乱。整个系统跑了几个月数据量几千条对话检索延迟仍然能控制在毫秒级没有出现明显的性能衰退。扩展空间也留出来了。以后如果要用到更复杂的记忆结构可以把SQLite替换成更完整的数据存储方案检索策略也可以加入重排模型让排序结果更精准。但对大多数使用场景来说现有的轻量设计已经足够稳定不需要为了“显得高大上”而引入过重的组件。保持简单保持在本地这套方案就很好用。