你是不是也有这种经历跟 AI 助手聊了一大通项目背景、技术选型、踩坑记录全交代清楚了结果第二天新开一个对话它又像第一次见面一样两眼一抹黑。我这个项目的起点就是这段让人抓狂的重复劳动。claude-mem 是我自己动手做的对话记忆增强层目标很直接——让每次新对话都能继承之前的关键信息而不是每次都从自我介绍开始。这篇文章不是什么通用方案而是我基于 Claude 这类大模型接口从零搭建一套记忆外挂的完整记录。适合正在做 LLM 应用开发、想给聊天机器人加持久记忆、或者单纯被金鱼脑式对话折磨过的朋友。里面会讲清楚记忆系统该怎么分层、数据怎么写进去、检索时怎么避免把垃圾也捞出来以及我实际用下来踩过的几个印象深刻的坑。1. 从金鱼脑到长效记忆这个项目诞生的直接原因1.1 一次对话的失忆你和我都经历过很多人第一次感受到大模型的失忆是从一个很小的事情开始的。某次我调试一段脚本花了一整个下午确认某个配置参数必须用字符串而不是整数还在对话里明确说了一句这个坑记一下以后别再犯了。当时模型答得挺好表示理解了。结果第二天新开会话它又开始推荐整数写法我差点以为自己在跟一个患了短期失忆症的实习生沟通。问题的根源在于大模型在标准 API 调用模式下是无状态的。每次请求发送过去服务端只基于你这次给的内容做推理推理完就结束。所谓的上下文窗口本质上是临时便签本窗口内的内容用完即丢一旦会话关闭或者上下文被截断那些讨论结论偏好设定冲突警告就全蒸发了。生活里类比一下想象你每次见同一个朋友对方都有兴趣帮你但不记得上次你们说过什么。你说还是之前那个问题它只能回一句不好意思什么问题这种体验一次两次还能忍天天如此就非常消耗耐心。1.2 为什么不能靠堆历史对话解决最简单粗暴的思路是既然它不记得那我每次都把所有历史记录贴进去不就行了我第一次遇到这个需求时也是这么干的把整个聊天记录导出拼接成一大段扔进提示词里。结果三个问题接踵而至。上下文长度有限。对话一旦变长总会超出窗口限制系统只能从中间截断反而可能把最关键的结论丢掉。就算不超限塞进去几千行历史后模型的注意力被大量无关内容稀释回答质量肉眼可见地下降——它开始纠结于某次闲谈里的无关细节而不是你当前真正要解决的问题。还有 token 成本所有输入都要计费早先几次密集测试下来光搬运历史这件事的消耗就占了总消耗的两三成。更隐蔽的问题是对话历史里其实充满了过程性噪音。比如我一开始考虑用方案 A聊着聊着发现不行改成了方案 B。如果整个历史都贴进去模型可能还会被方案 A 的讨论影响时不时冒出要不要试试 A这种已经废弃的想法。所以真正的需求不是记住所有而是记住重要的、忘记该忘的。1.3 记忆系统要解决的真问题经过那段时间的折腾我把需求拆成了三条跨会话连续上上次聊定的结论下次新开会话能直接引用不用再说一遍。语义关联召回用户后续不会用一模一样的词提问需要系统能根据语义找到相关记忆。噪声可控不能因为想记得多就把不相关的内容也带进来污染当前对话。这三点单独实现都不难难的是放进一个系统里同时满足。很多现成方案我试用过有的只做全量历史堆叠有的只擅长关键词匹配有的部署起来太重。说到底别人的工具是为别人的场景设计的我自己需要的记忆系统并不复杂但必须贴着我自己的使用习惯来。于是 claude-mem 就开工了。2. 记忆层的整体设计三层存储与数据流动方向2.1 事实、事件、偏好三层记忆分类动手写代码之前我先花了一点时间想清楚一个问题什么信息值得记如果什么都记那和堆历史没有区别。我把值得长期记住的信息分成了三类这个分类在后面实现检索时帮了大忙。第一类是事实型记忆。比如项目使用 Python 3.11后端框架是 FastAPI部署在私有服务器。这些信息是稳定的、明确表述过的用结构化存储最合适更新时直接覆盖旧值。第二类是事件型记忆。比如上个月排查出一个超时问题根因是数据库连接池过小调大之后恢复。这类信息的特点是过程性、有因果适合用一句话摘要保存。后面检索时主要靠语义匹配来找它们。第三类是偏好型记忆。比如用户喜欢简洁的代码风格回答时要先给结论再解释细节。这类信息往往不是用户直接说出来的而是从多次交互里推断出来的。能推断但要小心不能乱猜。三种记忆在存储和处理方式上天然不同。我最早把它们混在一起存后来发现在同一张表里又做关键词匹配又做向量检索逻辑非常混乱拆开之后清爽得多。2.2 存储选型结构化数据库与向量索引的分工存储选型是另一个需要解释清楚的决策点。事实型信息我用SQLite原因很朴素个人使用场景单机跑完全够零部署成本一个文件就能带走全部数据。我见过有人在这种规模上直接上重型关系数据库属实没必要。事件型和偏好型信息则走了向量检索路线。为什么不能只靠关键词匹配举个例子库里记着之前确认过采用离线优先架构用户某天随口问那网络不好的时候怎么办。这两句话字面上几乎没有一个词重合但语义高度相关。关键词搜索完全抓瞎向量检索却能把这条老记忆捞出来。这个需求决定了必须引入 embedding 化文本。向量库的选择上我一开始考虑过部署一个独立服务但是个人项目图省事最后还是选了轻量嵌入式方案本地加载模型做向量化不依赖外部接口。这样即使网络环境不好记忆检索也照常工作。检索延迟方面本地向量化一次大约几十毫秒完全在可接受范围内。2.3 数据流设计从对话到记忆再到提示词整个系统的数据流我用文字描述一下方便有个整体感用户对话流 - 记忆提取器异步触发 ├── 事实提取 - SQLite结构化覆盖/更新 └── 事件摘要 - 向量库追加写入 新会话/新问题 - 候选记忆检索 ├── 向量相似度 ├── 时间衰减 └── 冲突消解 - 注入系统提示词中的历史记忆区块 - 模型正常回答这个设计里有一个关键原则记忆系统是助手手里的笔记本不是大脑本身。它不应该替代模型的推理能力只是给模型提供参考。所以我在提示词里特意把记忆区块设计成仅供参考的上下文而不是必须遵守的指令。这个细节一开始没注意后面踩了好几个坑会在第 4 部分详细说。3. 核心链路落地写入、检索、注入的完整实现3.1 记忆提取什么时候触发、用什么逻辑抽记忆写入的第一步是提取。如果对每个用户的每条消息都做一次提取成本会高到不可接受。我测试了几轮之后就放弃了这种暴力方案。最终我设定了三个触发条件只要命中其一才执行提取用户明显切换了话题。可以通过对话片段的嵌入向量突变来检测简单点也可以看用户是否开始问一个与前面完全无关的新问题。出现了明确的结论性表述。像决定了就用不要再考虑记住这类词说明这是一个值得落库的信息锚点。会话自然结束或到达一个较长的停顿。比如用户一段时间没有新消息系统就把刚才这一段对话做一次批量提取。提取本身由模型来完成。我用了一个专门的提示词模板让模型从对话片段中抽取值得长期记住的条目MEMORY_EXTRACT_PROMPT 从下面的对话片段中提取值得长期记住的信息。 只输出条目每行一条不要解释。 要求 1. 只提取对后续对话有价值的信息过滤寒暄和过程性废话 2. 事实用陈述句事件用一句话概括结果和关键原因 3. 不确定的信息标注为“可能”不要强行下结论 对话片段 {segment} 为什么用模型而不是正则规则早期版本我试过纯规则提取靠关键词命中句子结果碰到不要选方案 B暂时不考虑用 X这种否定信息就完全抓瞎。换成模型抽取之后准确率明显提升代价是每次提取要额外发一次子请求。所以控制触发频率就成了降本的关键只在真正有价值的时间点执行。3.2 事实层的 Schema 与写入逻辑提取出来的信息会写入存储。事实层的 SQLite 表结构如下CREATE TABLE facts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_key TEXT NOT NULL, content TEXT NOT NULL, category TEXT NOT NULL DEFAULT fact, source_session TEXT, confidence REAL DEFAULT 1.0, created_at TEXT DEFAULT (datetime(now)), updated_at TEXT DEFAULT (datetime(now)), last_access_at TEXT, access_count INTEGER DEFAULT 0 ); CREATE INDEX idx_facts_user ON facts(user_key);这个 Schema 里有两个字段后来被证明非常关键。一个是user_key用来区分不同用户或不同项目主题的隔离另一个是updated_at用于处理信息更新和时间衰减。这两个都是使用过程中踩坑之后补的不是一开始就想到的后面会展开讲。写入逻辑里我也做了一个轻量级的去重与更新合并。思路是如果同一用户下已经存在高度相似的记录就不新增条目而是更新它的updated_at和confidence避免同一事实被反复记录成多条。这里用的是前缀匹配做粗筛再结合模型判断做精过滤。3.3 检索层的向量召回与阈值检索层我封装了一个MemoryStore类底层是嵌入向量库。写入时同时保留 SQLite 里的id这样检索到向量之后可以回查原文和元数据。class MemoryStore: def __init__(self, embed_fn, top_k5, min_score0.45): self.embed_fn embed_fn self.top_k top_k self.min_score min_score self.index [] def add(self, text: str, metadata: dict): vec self.embed_fn(text) self.index.append({text: text, vec: vec, meta: metadata}) def search(self, query: str): qvec self.embed_fn(query) scored [] for item in self.index: score cosine_similarity(qvec, item[vec]) scored.append((score, item)) scored.sort(reverseTrue) return [(s, item) for s, item in scored[:self.top_k] if s self.min_score]这个类里那个min_score参数是踩坑后才加的最开始根本没有。没有阈值的结果就是什么阿猫阿狗的记忆都被召回了上下文被塞进一堆不相关的东西回答质量反而断崖式下降。具体踩坑过程会在第 4 部分展开。本地 embedding 模型的选择也有讲究。我用了一个中等尺寸的文本向量模型速度尚可语义匹配精度对记忆召回这个任务足够用。如果完全依赖云端 embedding 接口每次检索都有网络开销交互体验会差不少而且离线环境下整个系统就瘫痪了。3.4 注入格式如何让模型正确使用记忆检索完成后把记忆注入提示词。这一步的格式设计决定了模型是真正把记忆当参考还是被记忆带偏。def build_system_prompt(user_key, current_query, memories): memory_block \n.join(f- {m[content]} for m in memories) return f你是助手。下面提供一些关于当前用户的“历史记忆”全部来自用户过去的对话仅供参考。 如果与当前对话冲突以当前对话为准。 history_memory {memory_block} /history_memory 现在请回答用户的问题。这个提示词有两个刻意设计值得说一下。第一个是仅供参考这个措辞。早期版本我写的是用户要求如下结果模型把历史记忆当成了硬性指令遇到接口已经升级的情况还死抱着旧参数不放。改成仅供参考之后模型明显更懂得灵活处理。第二个是history_memory标签。把记忆区块用特定的标签包裹起来模型就能清晰地区分哪些是历史事实参考和哪些是当前指令。这个技巧看似微小但对稳定性的提升是肉眼可见的。3.5 冲突消解的保守与激进当新旧记忆内容矛盾时怎么处理比如用户三个月前说数据库用 PostgreSQL前几天说换 MySQL 了。我的策略介于保守和激进之间。轻量做法是按更新时间排序新的优先。实现上就是按记忆条目的updated_at字段比较保留较新的版本删除或忽略旧的。对绝大多数事实型记忆这个策略够用了。真正的语义级冲突检测比如项目采用离线优先架构和项目放弃离线优先、全面转在线这种需要理解语义才能发现的对立则要复杂得多。我试过用模型来逐对判断效果确实好但成本太高无法在每次检索线上都跑。目前的做法是只在写入时做一次粗粒度的相似检查检索出来之后按时间裁剪。这个方案在个人场景下已经足够稳如果要处理高并发多用户场景肯定还需要更完备的方案目前不在我考虑范围内。4. 真实使用中的翻车现场与针对性修复4.1 检索噪声污染上下文的修复第一次把完整链路跑通的时候我满怀期待地上线了。结果实测下来加了记忆之后回答质量还不如不加。排查了很久才发现问题出在检索阈值上。有个典型例子用户问今天有什么待处理事项系统把两个月前一次闲聊里提到的某首歌名也召回了模型居然还顺着这首歌展开了几句用户直接懵了。根因就是min_score不存在系统把语义沾边但完全不重要的内容也当成有价值的记忆。修复策略是组合拳min_score调到 0.45具体数值跟 embedding 模型有关需要自己校准调太低会带进噪声调太高会导致该召回的召回不到。记忆大类分开处理事实型走关键词/结构化匹配事件型才走向量检索避免向量检索把不相关事实也带进来。注入前做一次排序截断最多取 5 条多余的宁可不给。这套调整下来回答质量才真正超过了不加记忆的基线。4.2 旧接口信息被反复引用的坑第二个坑跟时效性有关。某次用户改了某个配置接口的参数名老版本已经废弃了。但记忆库里还留着旧接口的写法结果之后每次询问模型都按旧格式回答折腾了很长时间才发现是记忆在捣鬼。根因是事实型记忆没有时效性概念。一条三个月前的接口信息和一条今天的变更记录在检索里权重是一样的。修复方案分两部分。一是给事实记录加updated_at字段注入时按时间加权越新的记忆得分越高。二是对接口、版本号、配置项这类高时效信息单独建立一个当前值索引命中后直接用新值覆盖旧值。我还总结了一条经验规则凡是对话中出现了修改改用换成不要再这类强硬变更词就强制提高该条记忆的权重并标记为对旧信息的覆盖。这个规则虽然简单但处理配置类变更特别有效。4.3 token 成本翻倍与降耗方案最早实现是每条用户消息后都触发一次记忆提取。结果每个交互后面都跟着一次子请求不仅延迟大token 消耗直接翻倍。尤其是密集测试阶段账面数字非常难看。后来改成三个触发点话题切换、结论性信号、会话暂停之后提取频率降了一大截。具体幅度跟对话类型有关技术讨论类的对话提取频率高一些闲聊类的频次就很低。检索侧也有一个隐蔽的消耗每次请求都先做一次 embedding 查询。单次便宜但高频交互下积累起来也不少。我给检索加了一层会话内缓存——同一个会话内前一次已经注入过的记忆只要当前查询语义没有显著变化就不重复检索。大概缓存命中率能有 60%整体 token 消耗降下来三分之一。4.4 多会话互相串台的隔离设计这个坑印象最深。当时我同时开着两个会话一个聊个人博客改造一个聊工作项目重构两个都在往同一个记忆库写入。结果在个人博客会话里问项目思路时系统把工作项目里的技术栈也带了出来上下文变成了四不像。修复就是前面提过的user_key隔离。更进一步我用会话主题标签做了二级分区def topic_key(user_key, topic): return f{user_key}::{topic}检索时优先查当前主题分区没命中再放宽到用户全量记忆。这个策略既保证了跨会话的连续性又不至于让不同项目互相干扰。相当于同一个人的记忆库里又按笔记本分册管理检索时先翻对应分册找不到再翻全部。4.5 记忆引起的人格漂移还有一次比较乌龙的翻车。历史记忆里有一条旧对话中用户随口说的我喜欢极简风格注入后模型回答突然变得极端简洁什么都不解释。实际上用户那次语境下明显需要详细解释这条旧记忆反而帮了倒忙。这说明偏好型记忆的注入需要格外谨慎。修复方法有两个关键点在注入格式里明确历史记忆仅供参考当前对话的指令优先防止偏好型记忆喧宾夺主。只注入与当前问题语义相关的偏好记忆而不是把用户所有历史偏好一股脑全塞进去。比如当前在讨论性能优化就别把喜欢极简风格这种无关偏好调出来。这条经验的本质是记忆系统的价值密度比记忆总量更重要。宁可少给不能给错。5. 更进一步的优化过载、衰减与可解释性5.1 记忆过载的清理策略用了一段时间库里的条目慢慢积累到几百条。检索性能倒没成为瓶颈但出现了两个新问题。第一个是语义重叠。同一件事被不同会话记录了好几次表述不同但意思一样检索时全部命中白白占用上下文。第二个是过时信息残留早期的一些试验性结论已经没有参考价值却在检索里频繁出现。我的做法是加一条离线清理任务定期跑一遍按last_access_at和access_count排序把长期没被命中的低价值条目标记为休眠再跑一轮模型判断确认后删除。这个过程就像定期整理笔记本防止记录变成一堆堆灰的杂物。毕竟记忆系统的价值不在数量而在调出来的时候真的有用。5.2 时间衰减与得分合成检索的最终排序得分由三部分合成score semantic_score * 0.7 recency_score * 0.2 access_count_score * 0.1semantic_score是向量相似度。recency_score用指数衰减计算exp(-age_days / half_life_days)。access_count_score则根据条目被成功引用的次数归一化等于是用进废退机制——经常被用到的记忆权重更高。半衰期的取值需要根据记忆类型区分。接口、版本号这类高时效信息半衰期我设成 7 天两周没提到基本就该淡出视野了用户偏好、项目背景这类稳定信息半衰期设成 90 天就算很久没提也不能轻易丢掉。这个参数直接决定了多久没提过的事就该被淡忘我是用两组不同的对话测试标定出来的不建议照抄最好自己跑几轮对话观察一下。5.3 记忆可视化与调试定位记忆系统的另一个痛点是黑盒。模型回答引用了某条记忆但你不知道它引用了哪条、为什么引用。为了定位问题我在注入格式里给每条记忆加了编号history_memory [1] 项目使用Python 3.11更新时间2025-05-02 [2] 数据库连接池曾因过小造成超时已调大更新时间2025-04-18 /history_memory然后提示模型在回答中如果使用了历史记忆用括号标出编号。这样如果模型答错了我能直接看到是哪条记忆带偏的而不需要靠猜。这是一个典型的调试友好设计在记忆系统这种天然隐蔽的模块里尤其重要。5.4 后续想做的事目前 claude-mem 已经稳定跑了一段时间满足了我的日常使用但还有几个方向想继续做。跨会话摘要升级当某个主题下记忆条目超过一定数量时自动生成一篇主题页把零散记忆浓缩成结构化摘要减少检索时的重复命中。主动遗忘不只是时间衰减还要支持用户显式反馈这条记错了/删掉让记忆系统可纠错。多后端适配当前实现是面向某一类模型协议写的后面想抽出一层接口方便接不同的模型服务同时不影响记忆层。这些都是锦上添花核心的记忆链路已经足够稳固。对我个人而言真正改变使用体验的是它记得住我是谁、我做过什么决定这件事本身。我在实现 claude-mem 前后的最大感受是真正让对话工具好用的往往不是模型本身有多强而是它能不能记住你是什么样的人、做过什么决定。如果你也被大模型的金鱼记忆折磨过不妨从最小的版本做起——一个 SQLite 表、一个向量索引、一段注入逻辑加起来不过几百行代码但体验的提升立竿见影。记忆系统的精髓不在算法多花哨而在于对什么该记、什么该忘、什么时候该引用的判断。这组判断才是 claude-mem 这个项目里最值钱的部分。