ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

从零搭建 Claude 记忆层:上下文持久化与混合检索实战

从零搭建 Claude 记忆层:上下文持久化与混合检索实战 1. 从聊完就忘说起claude-mem 到底想解决什么如果你用 Claude 这类对话式 AI 干过稍微长一点的活儿一定遇到过这种尴尬昨天花了两个小时跟它把一套数据清洗逻辑捋得清清楚楚今天开个新会话它一脸无辜地问你请问您想处理什么数据。你只能把昨天的上下文重新贴一遍贴到一半发现 token 又超了于是开始删删减减最后干脆放弃自己动手写。这个痛点的本质是对话式 AI 的上下文窗口是会话级的而不是项目级的。每个新会话都是一张白纸模型本身不记得你上周让它帮你重构的那个函数叫什么名字、用的什么命名风格、踩过哪个库的版本坑。对短问答来说这没问题但对持续几周甚至几个月的项目协作来说这就是致命的。claude-mem这个项目从名字就能看出来它的野心——给 Claude 装一个记忆层memory。它不是去改模型本身而是在模型外面套一层可持久化、可检索、可注入的上下文管理系统。你可以把它理解成给 AI 配了一个随身笔记本每次对话里值得记住的东西它帮你记下来下次开新会话它自动把相关的记忆捞出来塞进上下文让模型看起来像是记得。这篇文章适合三类人看一是天天跟 Claude 打交道、被上下文长度折磨的开发者二是想给自己的 AI 工具链加一层记忆能力的技术负责人三是对AI 记忆系统这个方向好奇、想搞清楚它到底怎么落地的人。我会从需求拆解、架构设计、核心实现、踩坑经验几个角度把这类项目讲透让你看完能自己动手搭一个或者至少能判断市面上的方案靠不靠谱。先说结论claude-mem 这类项目的核心难点从来不是存而是存什么、怎么找、怎么塞。存东西谁都会难的是在正确的时机把正确的记忆以正确的形式喂给模型还不能把上下文撑爆。下面我们一层层拆。2. 记忆系统的三层需求存什么、怎么找、怎么用很多人一上来就想我要把每次对话全存下来这是最典型的错误起点。全量存储的结果就是检索时噪声爆炸你搜数据库连接能返回三百条无关记录。所以第一步必须把需求拆清楚。2.1 记忆的粒度从整段对话到原子事实记忆的存储粒度直接决定了检索质量。我见过三种粒度各有取舍粒度存储单位优点缺点会话级一整轮对话上下文完整不会断章取义检索命中率低一条记忆里混了太多主题消息级单条 user/assistant 消息粒度适中实现简单单条消息可能只是好的继续信息密度低事实级抽取出的原子事实/决策检索精准注入高效需要额外的抽取步骤可能丢失语境claude-mem这类项目通常走的是事实级为主、消息级为辅的混合路线。什么意思就是对话过程中用一个轻量的抽取环节把值得记的东西提炼成结构化条目比如项目决策这个项目用 PostgreSQL 而不是 MySQL因为需要 JSONB 字段代码约定所有 API 返回统一用{code, data, message}结构环境信息本地开发用 Node 18生产用 Node 20踩坑记录xxx 库 2.3 版本在 Windows 下有路径 bug锁 2.2这些条目才是真正值得跨会话保留的东西。而原始对话消息作为证据保留在底层需要时可以回溯但不默认注入。提示抽取环节不要追求全宁可漏掉一些也不要塞进噪声。记忆系统的信噪比比召回率重要得多。一条错误记忆造成的干扰抵得上十条正确记忆的价值。2.2 检索的触发时机不是每次都要查记忆第二个关键决策是什么时候去检索记忆。新手常犯的错是每次用户发消息都去查一遍记忆库结果就是延迟飙升、无关记忆乱入。合理的触发策略是分层的会话启动时加载项目级的基础记忆技术栈、约定、环境这部分是常驻的量小但必查。话题切换时检测到用户提到新的模块/文件/概念按关键词或向量检索相关记忆。显式请求时用户说还记得上次那个 bug 吗触发针对性检索。这里有个工程上的细节会话启动时的记忆注入要控制预算。假设你的上下文窗口是 200K token你不能一上来就塞 50K 的记忆进去那样留给实际对话的空间就太少了。我的经验是常驻记忆控制在总窗口的 5% 到 10%按需检索的记忆单次不超过 15%。2.3 注入的形式让模型自然记得而不是被喂资料记忆注入最忌讳的是生硬。如果你直接把一堆 JSON 塞进 system prompt模型会表现得很分裂——它知道这些信息但用起来很别扭经常冒出根据提供的记忆...这种出戏的话。更好的做法是把记忆转写成自然语言的项目背景混在 system prompt 里比如你正在协助一个持续开发的项目。以下是已知的项目背景 - 后端使用 PostgreSQL 16主要考虑 JSONB 的查询能力 - API 响应统一格式为 {code, data, message} - 本地 Node 18生产 Node 20注意版本差异这样模型读起来就像自己本来就知道回答时也会自然地引用而不是机械复述。3. 架构落地一个可跑的记忆层长什么样讲完需求我们来搭骨架。一个能用的 claude-mem 类系统核心组件其实就四个存储层、抽取器、检索器、注入器。下面逐个说清楚并给出可落地的技术选型理由。3.1 存储层为什么我推荐向量库 关系库双写存储层最容易踩的坑是只用向量库或只用关系库。只用向量库的问题向量检索擅长语义相似但不擅长精确匹配。用户问那个parseConfig函数向量检索可能返回一堆配置解析相关的记忆但就是找不到名字精确匹配的那条。而且向量库对结构化过滤比如只要最近 30 天的、只要某个项目的支持往往很弱。只用关系库的问题语义检索做不了。用户说数据库连不上你没法用 SQL 匹配到PostgreSQL 连接超时这条记忆。所以我的推荐是双写每条记忆同时写入关系库存结构化字段id、项目、类型、时间、原文、向量 id和向量库存 embedding。检索时先做向量召回再用关系库做过滤和重排。# 记忆条目的数据结构示意 memory { id: mem_20240115_001, project: my-app, type: decision, # decision / convention / env / pitfall content: 后端使用 PostgreSQL 16主要考虑 JSONB 查询能力, source_msg_ids: [msg_123, msg_124], # 回溯用 created_at: 2024-01-15T10:30:00Z, embedding: [...], # 向量库那份 tags: [database, postgresql] }关系库用 SQLite 就够单机场景向量库可以用 FAISS 本地跑也可以用现成的向量数据库服务。选型的关键不是哪个最强而是你的部署环境能不能扛住运维成本。个人项目我强烈建议 SQLite FAISS零运维一个文件搞定。3.2 抽取器用便宜模型干脏活别用贵的抽取记忆这件事不需要用最强的模型。它的任务是把对话里的关键信息提炼成结构化条目这是个相对机械的活儿用便宜的小模型完全够用而且速度快、成本低。抽取的 prompt 设计有几个要点明确类型枚举告诉模型只能输出 decision/convention/env/pitfall 这几类不要自由发挥。要求可独立理解每条记忆必须脱离原始对话也能看懂不能出现那个函数上面说的方案这种指代。给反例明确告诉模型好的继续谢谢这类不要抽取。输出 JSON方便程序解析别让它输出自然语言。EXTRACT_PROMPT 从以下对话中抽取值得跨会话保留的记忆条目。 只抽取以下类型 - decision: 技术选型、架构决策 - convention: 代码规范、命名约定、接口约定 - env: 环境信息、版本、配置 - pitfall: 踩过的坑、已知问题 要求 1. 每条记忆必须能独立理解不能有那个上面等指代 2. 忽略寒暄、确认、无信息量的内容 3. 输出 JSON 数组每项含 type 和 content 字段 4. 没有值得记的就返回空数组 [] 对话 {conversation} 实测下来抽取环节最大的问题是过度抽取。模型总想把什么都记下来导致记忆库迅速膨胀。解决办法是在 prompt 里加一句如果一条信息只对当前会话有意义、对未来会话无用不要抽取并且定期做记忆库的清理和去重。3.3 检索器混合检索比纯向量检索稳得多前面说了双写检索自然也要混合。我的做法是向量召回 关键词召回 重排三步向量召回用 query 的 embedding 在向量库找 top 20。关键词召回用 jieba 或简单分词在关系库做 LIKE 或全文索引找 top 20。合并去重后用一个轻量重排模型或者干脆用规则向量分 关键词命中加权 时间衰减排序取 top 5 注入。时间衰减这个点很多人忽略。记忆是有保质期的。三个月前的一条临时用 xxx 方案可能早就过时了如果它和最近的记忆得分一样高就会误导模型。我的做法是给每条记忆算一个时间衰减因子final_score base_score * exp(-λ * days_since_created)λ 取值看项目节奏快速迭代的项目 λ 大一点记忆衰减快长期稳定的项目 λ 小一点。这个参数没有标准答案得根据你自己的使用体感调。3.4 注入器预算控制是生命线注入器负责把检索到的记忆转成自然语言拼进 system prompt。这里唯一的硬约束是token 预算。我的实现里有一个MemoryInjector类核心逻辑是def build_context(self, memories, budget_tokens2000): # 按分数排序逐条累加超预算就停 selected [] used 0 for m in sorted(memories, keylambda x: -x.score): t count_tokens(m.content) if used t budget_tokens: break selected.append(m) used t return self.render(selected)注意这里是超预算就停而不是截断因为截断一条记忆会让它变得不可理解还不如不要。另外常驻记忆和按需记忆要分开预算别让按需检索把常驻记忆挤掉了。4. 实操中真正会咬人的几个坑架构讲完了但真正让你熬夜的是细节。这一节我挑几个最典型的坑把排查链路完整走一遍你遇到类似问题时能直接对照。4.1 记忆污染一条错误记忆如何毁掉整个会话现象某次会话里模型突然坚持一个错误的结论你怎么纠正它都不听换个新会话又正常了。排查链路先怀疑 prompt——检查 system prompt 里注入了什么记忆。发现有一条记忆写着项目使用 MongoDB但实际项目用的是 PostgreSQL。回溯这条记忆的来源发现是某次对话里用户随口说了句如果当初用 MongoDB 就好了被抽取器误判成了 decision。根因抽取器把假设性、否定性、对比性的表述当成了事实。修复方案在抽取 prompt 里明确排除假设语气如果假如要是本来可以开头的表述不抽取。给记忆加一个confidence字段抽取时让模型自评置信度低于阈值的进待确认区不直接注入。提供记忆的人工审核和删除接口。这点极其重要别指望全自动系统不出错一定要留人工干预的口子。注意记忆系统一定要有遗忘能力。不只是时间衰减还要支持显式删除和覆盖。当用户说之前那个方案改了系统要能把旧记忆标记为失效而不是两条并存让模型精神分裂。4.2 检索延迟为什么你的记忆层让对话变卡了现象加了记忆层之后每次对话首字延迟从 1 秒涨到了 5 秒。排查链路打点计时发现检索环节占了 3 秒多。进一步拆解embedding 计算 0.5 秒向量检索 0.3 秒关键词检索 2 秒多。关键词检索慢的原因关系库没建索引每次都在全表 LIKE 扫描。另一个隐藏问题每次对话都重新计算了所有常驻记忆的 embedding而它们根本没变。修复方案关系库给content和tags建全文索引LIKE 改成全文检索。常驻记忆的 embedding缓存起来只在记忆更新时重算。检索改成异步预取用户消息一进来就并行发起检索和模型的其他准备工作重叠而不是串行等待。优化后延迟回到了 1.2 秒左右用户基本无感。4.3 上下文膨胀记忆越攒越多窗口越来越挤现象项目跑了两个月记忆库攒了上千条每次会话启动加载的常驻记忆越来越多留给对话的空间越来越少。根因常驻记忆没有做聚合和压缩。一百条零散的 convention 记忆其实可以合并成几条概括性的。修复方案定期比如每周跑一次记忆聚合把同类型、同主题的零散记忆合并成一条更概括的。常驻记忆设硬上限比如最多 20 条超了就按重要性和新鲜度淘汰。引入记忆层级核心记忆技术栈、关键约定常驻次要记忆具体踩坑按需检索。这个坑的本质是记忆系统不是仓库是缓存。缓存就要有淘汰策略不能只进不出。4.4 跨项目串味A 项目的记忆跑到 B 项目里现象你在做项目 B模型却引用项目 A 的约定让你莫名其妙。根因检索时没有做项目隔离或者隔离字段没设对。修复方案每条记忆强制带project字段检索时硬过滤只返回当前项目的记忆。如果确实需要跨项目共享比如个人偏好、通用工具配置单独设一个global项目空间显式标记。会话启动时明确当前项目 id别依赖模型自己判断。这个坑看起来低级但在多项目并行开发时非常常见而且一旦发生就很难排查因为模型不会告诉你它引用了哪条记忆。所以记忆注入时最好带上来源标记至少在日志里方便回溯。5. 让记忆系统真正好用的几个进阶思路基础版跑通之后还有一些能让体验上一个台阶的思路这部分偏探索性质但我觉得方向是对的。5.1 记忆的主动遗忘比主动记忆更难也更重要大部分记忆系统的精力都花在怎么记住上但实际使用中怎么忘掉才是决定长期体验的关键。一个用了半年的记忆库如果全是过时信息那它带来的干扰远大于价值。我的做法是引入记忆的访问计数和最后命中时间。一条记忆如果三个月没被检索命中过就降级到冷存储不再参与默认检索。如果一条记忆被频繁命中说明它确实有用提升其权重。这套机制本质上是把 LRU最近最少使用思想搬到了记忆管理上。更进一步可以让模型在会话结束时自评这次对话里有哪些之前的记忆被证明是错的或过时的然后自动标记失效。这个闭环能让记忆库自我净化。5.2 把记忆和代码库索引打通如果你的项目是代码项目记忆系统和代码索引结合起来威力会大很多。比如记忆里提到parseConfig函数有 bug检索时能直接关联到代码库里这个函数的当前位置。用户问上次改的那个配置解析逻辑系统能同时召回记忆和对应的代码片段。这需要记忆条目里存代码引用文件路径 符号名检索时做二次解析。实现上不难但需要你的代码索引和记忆系统共享一套符号表。5.3 多模态记忆的想象空间现在大部分记忆系统只处理文本但实际项目里截图、架构图、报错截图都是重要信息。把图片也纳入记忆存图片 描述 embedding能让系统在上次那个报错长什么样这类问题上也能帮上忙。技术上图片可以用多模态模型生成描述文本再对描述做 embedding 存进向量库。检索时文本和图片描述一起召回。这个方向目前还比较新但我觉得是迟早的事。6. 我踩过的坑和几条实在建议最后分享几条纯经验性的东西都是我自己搭这类系统时真金白银换来的。第一条先跑通最小闭环别一上来就追求完美。我最初想做一个全自动、多模态、带知识图谱的记忆系统结果两周过去连个能用的版本都没有。后来砍到SQLite 存事实 关键词检索 手动注入一天就跑通了用起来之后才知道真正需要优化的是什么。记忆系统的需求是长出来的不是设计出来的。第二条抽取质量决定一切值得花时间调 prompt。我前后改了十几版抽取 prompt每改一版就拿真实对话测一遍看抽出来的东西是不是真的有用。这个过程很枯燥但收益巨大。判断标准很简单抽出来的记忆你愿意在三个月后的新会话里看到它吗不愿意的就是噪声。第三条一定要有记忆查看的界面或命令。哪怕只是个命令行工具能让你随时list和search记忆库你才能知道系统到底记了什么、检索到了什么。没有这个出问题时你就是盲人摸象。我现在的习惯是每次觉得模型回答不对劲第一件事就是查这次注入了哪些记忆。第四条token 预算要留余量别卡太死。我一开始把记忆预算设成窗口的 20%结果经常和长对话冲突。后来降到 10% 并加了动态调整对话越长记忆预算越小体验稳定多了。记忆是辅助不能喧宾夺主。第五条别迷信向量检索。向量检索在语义相似上很强但在精确匹配、结构化过滤、时间范围查询上很弱。混合检索是必须的纯向量方案在真实项目里迟早会翻车。这套东西搭下来你会发现 claude-mem 这类项目的价值不在于技术多高深而在于把上下文管理这件脏活累活工程化了。它让 AI 协作从每次重新开始变成持续积累这个体验差异是质变的。如果你也在被上下文问题折磨强烈建议自己动手搭一个哪怕最简陋的版本用起来之后你对AI 记忆的理解会完全不一样。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进