每次打开新的对话窗口我都要重新向AI助手交代背景——我是谁、在做什么项目、上次聊到哪、有哪些未决事项。这种事情重复了十几次之后我开始认真考虑一个问题为什么不能给大模型装一个长期记忆正好那段时间我高频使用Claude做开发辅助和内容规划每次对话稍长一点就开始遗忘前文细节而官方原生的记忆能力又还没有完全覆盖我的使用习惯。于是我开始动手写一个正式项目就是后来命名的claude-mem——一套给Claude助手增加持久记忆能力的本地工具。经过三个多月的迭代这套方案已经稳定跑完了我的日常开发流、内容创作流和项目管理流今天把这套从架构到实现的完整思路拆开讲。如果你是重度AI助手用户经常被“每次都要重新自我介绍”折磨或者你正在纠结怎么给大模型搭一套靠谱的长期记忆这篇文章值得你认真看完。里面没有那种纸上谈兵的架构师废话全是我自己踩坑踩出来的实操结论。1. 为什么大模型需要记忆外挂1.1 上下文窗口是水杯长期记忆才是冰箱很多人对上下文窗口有一种误解觉得窗口越大越好。确实现在的模型动辄给到几万甚至几十万token的窗口看起来能容纳海量信息。但你要理解一个本质问题窗口是瞬时的对话结束它就清空了。它就像一个一次性水杯水倒进去能喝但杯子本身不会替你保存任何东西。更深一层的问题是上下文窗口还不是越大越能装。窗口越大注意力分散的问题就越明显。我做过一个粗测把一份三万字的项目文档全部塞进上下文之后模型在回答问题时引用关键细节的准确率比只塞五千字核心内容时低了差不多15个百分点。这就好比你在一个堆满杂物的仓库里找一把螺丝刀明明东西都在反而更难找到。所以核心矛盾就在这里你需要的不是更大的水杯而是一个能把关键信息保存起来、在需要时再取出来的冰箱。claude-mem做的事就是这个——把对话历史里的关键信息提取出来做结构化存储然后在每次新会话启动之前把相关内容“回填”到上下文里。1.2 重复喂背景的隐性成本你可能觉得“多打几行背景说明”没什么大不了但实际算一笔账就清楚了。以我自己的日常为例一个进行中的软件项目包含目标描述约200字、技术栈约束约150字、当前进度约200字、下次要处理的事项约150字。这些背景信息加起来700字左右每次新开会话都要重新粘贴。一天开10次会话就是7000字。一周就是接近5万字。这些重复输入的Token费用并不高但浪费的时间是实打实的——我统计过每次重新组织背景语言平均要花1到2分钟一天就是十几分钟。更麻烦的是信息损耗。人不是机器每次重新描述背景都会漏掉一些细节。可能这次忘了说某个约束条件下次忘了说某个已确认的决策而AI助手又不会知道你漏了什么。这就是我坚持做记忆系统的直接原因——不是省那点Token费而是确保每次对话都带着完整的上下文开场。1.3 官方能力与第三方工具之间的缺口我最初的想法是最懒的方案等官方原生记忆更新。现实是官方记忆在某些场景下确实有效但它的交互方式偏“被动”和“黑盒”——你不知道它记住了什么、没记住什么也没法主动管理某条记忆的优先级。而市面上的第三方对话管理工具要么太复杂要么绑定特定平台想插进自己的命令行工作流里很费劲。所以claude-mem走的是中间路线本地优先、文件可审计、接口极简。底层依赖公开的API能力上层做自己的对话数据管理和检索增强。这样既不会因为官方接口变动而彻底失效又能完全掌控记忆的存取逻辑。2. 整体架构与设计思路2.1 三条主链路的划分整个系统第一次设计时我差点把记忆逻辑和对话逻辑揉在一个脚本里结果代码越写越乱改一个功能牵一发动全身。后来我推倒重来按数据流向把系统拆成了三段采集链路负责监听对话导出文件把原始对话文本解析成结构化的消息记录存储链路负责把消息记录切分、摘要、向量化最后存进本地索引库注入链路负责在新会话启动时检索相关记忆拼装成系统提示词这个划分的好处是任何一段出问题都可以单独调试。比如存储链路索引坏了不影响采集链路继续写原始文件注入链路想换一套检索策略也不需要动存储结构。采集链路的具体实现上我利用官方导出或者接口转发的方式把对话记录落到本地格式统一为JSONL文本每个消息一行。这个设计规避了二进制格式的兼容性问题任何语言都能直接解析。2.2 技术选型JSON、SQLite还是向量数据库记忆存储用什么是我花时间最多的一次选型。我对比了三个方案用JSON文件存储最简单但检索只能靠关键词精确匹配。问题在于对话里的信息往往是语义关联的。你昨天说“帮我留意预算500元以内的送礼方案对方喜欢喝茶”今天问“我昨天说的那个事情有什么要求”。关键词“茶”“预算”可能还在但如果你换一种问法“昨天那个人喜欢什么来着”关键词就全对不上了只有语义匹配才能找回。SQLite能做结构化查询和全文索引但SQLite自带的FTS全文索引本质上是分词匹配解决不了同义改写的问题。“茶叶”和“茶”能匹配“关注”和“留意”就不一定了。向量数据库是解决这个问题的最直接方案。先把文本用嵌入模型转成高维向量然后通过余弦相似度计算语义距离哪怕关键词完全不一样只要语义接近就能命中。我在选型时重点评估了本地优先的方案因为对话数据涉及隐私我不想把所有内容都传到第三方服务去。最终我选了Qdrant的本地模式作为向量索引元数据放在SQLite里。这个组合的好处是向量检索交给专精的引擎结构化查询交给SQLite两者各管一段数据通过索引ID关联。实测在几万条记忆记录下单次检索延迟稳定在80毫秒左右。2.3 记忆注入的时机与力度记忆不是越多越好。我一开始犯过“能注入就注入”的错误每次把顶上十条记忆全部塞进系统提示词结果模型经常在记忆堆里打转当前任务反而被冲淡了。后来我把注入策略改成“按需取用宁缺毋滥”会话开始时只注入与当前话题相关性最高的前三条记忆每条记忆自带时间戳超过30天未激活的内容优先级自动降档用户在当前会话里明确说了“记住”的内容强制置顶这个策略的核心逻辑是系统提示词里每多一条弱相关记忆都会占用模型的注意力预算。尤其是长上下文中模型对中段内容的注意力本来就比较弱你塞进去的弱势记忆只会让模型把权重分给无关信息。2.4 模块边界与扩展性设计时我特意把三块做成相对独立的服务相互之间只通过标准数据格式交互。这意味着未来如果官方API的调用方式变了我只需要替换采集链路那一段如果换了更好的嵌入模型也只需要重跑存储链路里的向量化步骤其他代码完全不用动。3. 核心实现三块硬骨头逐个啃3.1 会话切分与记忆摘要生成对话原始文件是一长串消息组成的流水你不能直接把它全部塞进向量库。原因有两个一是单条消息太短切成单条存会导致向量碎片化二是一整段长对话太长直接向量化会丢失局部重点。我的切分策略基于两级维度按时间间隔切分相邻两条消息之间间隔超过30分钟判定为一次话题切换。这个阈值我调了好几版太短会导致一次完整讨论被拦腰截断太长又会让不同话题混在一起。按语义相似度合并时间上连续但内容相关性很低的消息也不该归为同一个记忆片段。我用嵌入模型计算相邻消息的余弦相似度低于0.55就切断。切分之后每个片段都要生成一个摘要再入库。摘要生成依赖Claude自身的文本总结能力Prompt模板大致是这样请将以下对话片段压缩成一段不超过150字的摘要 保留用户的核心目标、当前进度、明确的约束条件、下一步行动。 丢弃客套话、重复表述、与主线无关的闲聊。这一步非常关键。如果直接存原文检索命中时注入的是冗长的原始文本既浪费Token又让模型难以快速定位重点。存摘要的好处是每个记忆片段的体积浓缩到原来的十分之一检索命中后直接注入摘要信息密度反而更高了。3.2 向量化与相似度检索的细节向量化模型我选了一个轻量的中文嵌入模型输出维度768。选它的原因很直接体积小单机CPU推理单条文本几十毫秒足够用了没必要上重模型。相似度检索部分我写了大概这么一段逻辑def search_memories(query_text, top_k3, threshold0.6): # 1. 先将查询文本向量化 query_vec embed(query_text) # 2. 在Qdrant中检索候选结果 hits qdrant_client.search( collection_nameclaude_memories, query_vectorquery_vec, limit20 ) # 3. 按阈值过滤弱相关结果 filtered [h for h in hits if h.score threshold] # 4. 按分数排序截断返回 filtered.sort(keylambda x: x.score, reverseTrue) results filtered[:top_k] return results这里最值得记住的是阈值过滤这一环。Qdrant检索返回的前20条结果里有大量得分在0.5到0.6之间的弱相关片段。如果不过滤直接取前三条很可能混进一些“字面相关但语义无关”的内容。我调试时遇到过一个典型case聊“Python异步编程”时检索出了之前聊“Python环境配置”的一段记忆两者都有“Python”这个词但实际讨论的内容完全不同。加阈值过滤之后这类误命中明显减少了。阈值本身也值得细调。我一开始定0.7结果很多明明相关的记忆被拦在门外。后来又往下降到0.55误命中率又升高了。综合体验下来0.6是一个相对平衡的点大部分场景下既不会漏掉关键记忆也不会带回太多噪声。3.3 系统提示词的注入模板记忆检索出来了最终要以什么形式进入模型这一步决定用户体验。我经过几版迭代最终把注入格式固定成下面这种结构[记忆库内容] - 用户概述{用户自我介绍或画像信息} - 进行中事项{与当前主题相关的任务进度} - 关键背景{历史对话中的约束条件或明确偏好的} - 最近讨论{最近一次相关话题的摘要} [当前任务说明] 请结合以上记忆回答用户当前的问题。 若记忆内容与用户当前表述存在冲突一律以当前表述为准。这段提示词里最后一条规则是我反复打磨出来的。模型容易出现“历史记忆比当前话语更权威”的错误认知比如用户之前说过“周末跑步”今天说“这周太累不跑了”如果记忆里还留着“用户周末喜欢跑步”模型可能反而会建议用户坚持跑步。加这句冲突处理规则后模型的回答会优先尊重用户的即时表达。注入位置也需要注意。我测试过放在系统提示词最前面、中间、最后三种位置综合来看放在最前面效果最好。因为系统提示词本质上是一种强引导记忆内容作为“背景事实”放到最前面模型会更倾向于把它当作已知信息来引用。3.4 调用链的完整落盘流程把前面几段串起来一次完整的“唤醒记忆”流程是用户在新会话里说了第一句话系统拿到这句话作为检索query从向量库里检索出相关记忆片段把记忆片段和检索query拼装成系统提示词连同用户的第一句话一起发送给模型模型带着历史记忆返回回答回答内容和检索出的记忆片段一起写回对话记录这里面一个很容易被忽略的细节是即使没有命中相关记忆也要走一遍检索流程只是最终注入为空。这样做的好处是确保每次会话的行为路径是一致的不会因为“没有记忆”而走不同的代码分支给后续排查省了不少事。4. 实操记录与参数调优4.1 记忆条数和分数阈值的调优我在前文提过最初每次注入五条记忆效果不好。后面改成动态分配才稳定下来具体规则是分数大于0.8的记忆最多取两条用户明确标注“重要”的内容强制占一条名额剩余名额按分数从高到低补足上限三条为什么这样组合高分段记忆通常与当前话题强相关但一次来两条就够了再来第三条边际收益会急剧下降。用户明确标注“重要”的记忆不一定是当前话题最相关的但它对用户的意义很特殊值得占一个固定名额。剩下的弱相关记忆只做补充严格控制数量。这套规则在不同使用场景下的表现都还不错。做技术写作时它能准确回忆起历史讨论过的文章结构偏好做开发排期时它能稳稳带出上次确认过的优先级排序。4.2 摘要生成的触发时机与增量更新摘要生成最忌讳的是等对话结束之后再做一次全量解析。一旦会话异常中断或者导出文件损坏中间积累的对话就全丢了。我把摘要生成拆成两步对话导出之后立即做一次全量解析把每段对话切成记忆片段并生成摘要之后每新增一轮对话只对最后一个片段做增量更新不重复全量计算增量更新的实现思路是记录每个记忆片段的最后一条消息ID如果新消息属于当前片段的时间窗口和语义范围就重新生成该片段的摘要并覆盖旧版本如果新消息切出了新话题就为新话题建一个新的记忆片段。这个方案的成本优势很明显。以一天50轮对话为例全量解析一次需要嵌入向量上百条文本增量更新只需要处理新增的十来回消息整体耗时能压缩到原来的三分之一左右。4.3 存储格式与迁移踩坑记录存储层踩过最大的坑是把所有数据堆在一个文件里。最早我把SQLite元数据和向量索引放在同一个目录下结果有一次索引目录损坏向量数据没丢但元数据表对应的会话关联字段乱了一部分导致记忆检索结果张冠李戴。后来整改成现在的方案SQLite只存元数据包括会话ID、时间戳、摘要文本、激活状态四个字段向量索引单独放在/memory/vector目录下和元数据目录彻底隔离。memory/ ├── meta.db # SQLite元数据 ├── vector/ │ └── collection/ # Qdrant向量索引文件 └── raw/ └── dialogs/ # 原始对话JSONL文件迁移时只需要打包整个memory目录目标机器上重新指定路径即可。再也不用担心某个文件损坏影响全部功能。4.4 端到端的效果对比给这项目做了个小范围的A/B对比同样一组闲聊型问答和历史背景回顾问题没有记忆注入的情况下模型完全无法回答“我上周说过什么”这类问题注入记忆后连续十次测试有八次能准确引用历史细节另外两次属于数据库里压根没有对应历史记录的情况。对于记忆系统来说这个效果已经足以支撑日常使用了。5. 常见问题与避坑指南5.1 上下文污染检索到不相关记忆这是整个项目里最常见也最难缠的问题。表面上看是检索算法不够好实际原因往往是记忆片段切分得太粗糙。一段对话里可能聊了三件事但摘要时只提取了其中一件检索时自然容易召回偏差。我的解法是给每个记忆片段增加多个标签域话题标签如“开发排期”“内容创作”、实体标签如“项目X”“某跨平台系统”、状态标签如“进行中”“已完成”。检索时先按标签做一次粗过滤再对剩下的候选做向量排序。有了这层标签筛选误召回率降了不止一半。5.2 隐私边界哪些内容不该进记忆库记忆系统不该做一个“什么都记住”的应声虫。有些内容一旦被存入向量库未来可能在完全无关的场景下被意外激活。比如用户在对话里无意中说了身份证号、银行卡号、家庭住址这些信息如果被向量化未来某次检索可能就会把它们捞出来并注入给模型这个风险无法接受。我在采集端加了一道指纹过滤规则匹配手机号正则或身份证号的片段直接不写库包含地址、银行账号等强隐私特征的内容只保留“用户提到了某项隐私信息”这个元信息不保留具体内容用户主动加“不要记住”的对话片段强制从库里删除删除是物理级删除不是标记删除这条规则我从一开始就写了之后再也没有出现过隐私信息被意外注入的情况。5.3 性能瓶颈与资源占用向量数据量小的时候体感不明显但一旦超过几万条记录实时性就会肉眼可见地下降。我在五万条记忆记录下做了一次压测单次检索延迟80毫秒左右但加上摘要生成之后整个链路耗时被拉到3秒以上。如果用户是在交互式对话场景里等待这个延迟体感很明显。优化手段做了三个给SQLite里的会话ID字段加索引高频记忆集的结果做本地缓存命中缓存直接跳过检索向量检索的limit值从20调到10减少不必要的计算量。三轮优化之后单次检索的延迟降到30毫秒以内说明瓶颈主要在海量候选排序那一步值得优先优化。5.4 中文语义的歧义问题最后想提一个中文环境特有的坑。中文没有空格做分词边界“钥匙”和“药匙”这种同音词更容易让向量模型产生语义混淆。我有一次测试对话里提到“想去钓鱼”结果检索出了一堆关于“鱼缸养护”的旧记忆明显是“鱼”这个字的语义空间太宽导致的。处理办法是给向量库增加一个自定义停用词表把高频且容易产生歧义的词做加权惩罚同时配合标签过滤减少这类误命中。这不算完美方案但在实测里已经能把同音词误命中降低到可接受范围。写在最后的体会这个项目做到现在我最大的感悟是给大模型做记忆真正的难点不是“存得下”而是“取得好”。存任何一段信息都很容易难的是判断哪些信息值得长期保留、哪些信息会随时间贬值、如何在正确的时间点把正确的内容送回对话流里。这些问题没有标准答案只能靠反复测试和体验积累。这个项目后续我还在继续迭代当前在尝试给记忆增加时间衰减权重让很久没有提及的信息自动降级而不是永远保持在最高优先级。这套思路不仅适用于Claude如果换其他支持函数调用的大模型只要把采集和注入的代码适配一下整条链路基本可以照搬。如果你也想给常用的大模型装一套记忆系统我的建议是从最小可用版本开始——先把对话记录落库再写检索注入别一开始就奔着完美架构去。先跑通再调优这条路我亲自验证过走得通。