ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Claude长期记忆系统实战:SQLite+向量索引构建外部记忆层

Claude长期记忆系统实战:SQLite+向量索引构建外部记忆层 1. 为什么我要自己动手做一个 Claude 记忆系统用 Claude 做长期项目的人大概率都经历过一种崩溃昨天刚跟它聊完的架构决策今天开个新会话它一脸无辜地问你“请问您想做什么项目”。上下文窗口再大也架不住会话一关就归零。官方给的 Projects、Memory 功能确实能缓解一部分但真到多项目并行、跨会话追踪决策、需要按时间线回溯“当时为什么这么定”的时候还是不够用。claude-mem就是冲着这个痛点去的。它不是官方功能而是一套围绕 Claude 构建的外部记忆层——把对话里产生的关键信息决策、偏好、待办、代码片段、项目背景抽取出来落到本地可检索的存储里下次开新会话时按需注入。说白了就是给 Claude 装一个“长期记忆硬盘”而不是每次都靠它那点短期记忆硬撑。这套东西适合谁三类人最受益一是长期维护多个项目的独立开发者二是需要跟 AI 反复对齐需求的团队三是像我这样把 Claude 当主力协作工具、每天几十轮对话的重度用户。如果你只是偶尔问两句天气和语法那确实用不上但只要你有“这件事我上周跟它说过”的困扰往下看就对了。我前后折腾了大概三周踩了不少坑也总结出一套相对稳定的方案。下面把设计思路、核心实现、实操步骤和排查经验完整拆开讲尽量让你能直接抄作业。2. 整体设计思路记忆到底该怎么存、怎么取2.1 先想清楚记忆不是聊天记录很多人第一反应是“把历史对话全存下来不就行了”。我一开始也这么干结果两周就崩了——检索出来的全是废话真正有用的决策淹没在“好的”“明白了”“让我想想”这种噪声里。所以claude-mem的第一个设计原则就是存的是提炼后的结构化记忆不是原始对话流。我把记忆分成四类这个分类直接决定了后面存储和检索的设计记忆类型内容举例生命周期检索优先级决策记录“数据库选 PostgreSQL 而非 MySQL因为需要 JSONB”长期高用户偏好“代码示例统一用 TypeScript不要 Python”长期高项目上下文“当前项目是电商后台技术栈 Next.js Prisma”中期中临时待办“下次要补上单元测试”短期低这个分类不是拍脑袋来的。决策和偏好是跨会话最需要保留的因为它们直接影响 Claude 的输出方向项目上下文会随项目结束而失效待办则是用完即弃。分开管理检索时才能按需加权避免把过期信息当成当前事实。2.2 存储选型为什么最后落在 SQLite 向量索引存储方案我试过三种最后选了 SQLite 打底、配一个轻量向量索引。说说取舍过程。第一种是纯 Markdown 文件。优点是简单、可读、能直接 git 管理。缺点是检索全靠 grep语义匹配基本没戏——你搜“数据库选型”它匹配不到“PostgreSQL 还是 MySQL”这段。项目一多文件一散找起来要命。第二种是纯向量数据库比如 Chroma、Qdrant。语义检索确实强但有个致命问题精确查询拉胯。我想找“上周三那条关于缓存的决策”向量检索给我返回一堆语义相近但时间不对的内容。而且多一个服务要维护本地跑还得占内存。最后落在 SQLite 向量索引的混合方案。SQLite 存结构化字段时间、类型、项目、标签负责精确过滤向量索引只存 embedding负责语义召回。查询时先用 SQL 缩小范围比如“最近 30 天 决策类 当前项目”再在这个子集里做向量相似度排序。这样既有精确性又有语义能力而且 SQLite 单文件、零运维备份就是复制一个文件。提示向量索引不必上重型方案。记忆条目通常就几千到几万条用sqlite-vec这类扩展或者内存里跑 FAISS 的 flat 索引完全够用别为了“技术先进”引入一堆依赖。2.3 注入策略什么时候把记忆喂给 Claude存下来只是第一步关键是在对的时候把对的信息塞进上下文。我的策略是分层注入而不是一股脑全塞。会话开始时注入“项目上下文 用户偏好”这两类稳定信息大概占 500 到 800 token。这部分相当于给 Claude 做“开机自检”让它知道现在在哪个项目、用户有什么习惯。对话过程中当检测到用户提到某个历史话题通过关键词或语义匹配再动态注入相关的“决策记录”。比如用户说“还是用之前那个方案吧”系统就去检索最近的决策记录把相关几条插进去。会话结束时触发一次记忆抽取把这一轮产生的新的决策、偏好、待办写回存储。这一步我用一个独立的抽取 prompt 完成让 Claude 自己判断“这轮对话里有什么值得记的”。这个分层设计的好处是 token 消耗可控。如果每次会话都把全部记忆塞进去几轮下来上下文就爆了而且大量无关信息反而干扰 Claude 的判断。2.4 为什么不做全自动保留人工确认我一开始追求全自动结果发现 Claude 抽取记忆时会“过度总结”——把一些临时性的讨论也当成决策记下来下次注入就误导了后续对话。后来改成关键记忆人工确认系统抽取后列一个候选清单我扫一眼勾选真正要保留的。这个改动看起来增加了操作成本但实际上省了大量清理脏数据的时间。记忆系统最怕的不是记不住而是记错了还反复用。人工确认这一道闸是保证记忆质量的关键。3. 核心实现拆解从抽取到检索的完整链路3.1 记忆抽取怎么让 Claude 吐出结构化数据抽取是整个系统的入口质量直接决定后面所有环节。我的做法是定义一个严格的 JSON schema让 Claude 按格式输出而不是自由发挥。抽取 prompt 的核心逻辑是这样的给 Claude 一段对话要求它识别出四类记忆每条记忆必须包含type、content、confidence、tags四个字段。confidence是 0 到 1 的置信度低于 0.6 的直接丢弃避免噪声入库。EXTRACT_PROMPT 分析以下对话提取值得长期记忆的信息。只提取明确的决策、稳定的偏好、 项目背景变更和未完成待办。不要提取寒暄、临时讨论和不确定的猜测。 输出 JSON 数组每条包含 - type: decision | preference | context | todo - content: 一句话描述不超过 50 字 - confidence: 0-1 的置信度 - tags: 相关标签数组 对话内容 {dialogue} 这里有个细节很关键要求 content 用陈述句且包含理由。比如不要写“选了 PostgreSQL”而要写“选 PostgreSQL 因为需要 JSONB 支持”。理由部分在后续检索时特别有用因为用户往往会用“那个因为 JSON 选的数据库”这种模糊描述来回忆。实测下来加了“包含理由”这个约束后检索命中率明显提升。因为纯结论的 embedding 区分度低加上理由后语义特征更丰富。3.2 去重与合并别让同一条记忆存十遍抽取出来的记忆如果不做去重同一个决策会在每次相关对话后被重复记录。我见过最夸张的情况一条“用 TypeScript”的偏好存了 17 遍检索时全冒出来占满上下文。去重分两层。第一层是精确去重content 完全相同或高度相似的用编辑距离判断直接合并更新时间戳。第二层是语义去重对 content 做 embedding余弦相似度超过 0.9 的视为同一条保留置信度更高的那条。合并时有个策略要定新记忆和旧记忆冲突怎么办比如旧记忆是“用 MySQL”新记忆是“改用 PostgreSQL”。我的处理是保留两条但给旧记忆打上superseded标记检索时默认过滤掉被取代的但保留历史可追溯。这样既不会用错又能回答“我们之前为什么用 MySQL”这种回溯性问题。3.3 向量化embedding 模型的选择embedding 模型我试过几个最后用的是本地跑的小模型维度 384。为什么不用 API两个原因一是记忆条目多每次检索都调 API 成本和延迟都受不了二是记忆内容涉及项目细节本地处理更放心。384 维在记忆检索这个场景完全够用。记忆条目短一句话语义相对集中不需要 1536 维那种大模型。实测 384 维的召回率和 1536 维差距在 3% 以内但速度快了将近 5 倍内存占用也小得多。向量存储我用的是内存里的 numpy 数组加暴力检索。几万条记忆做全量余弦相似度计算单次查询在 10 毫秒以内完全不需要上 ANN 索引。等记忆量真的到几十万条再考虑升级现在过早优化没意义。3.4 检索排序不只是相似度检索排序是决定“注入什么”的最后一道关。如果只按向量相似度排很容易把语义相近但时间久远、已经失效的记忆排到前面。我的排序公式是加权组合final_score 0.5 * 语义相似度 0.2 * 时间衰减因子 0.2 * 类型权重 0.1 * 置信度时间衰减因子用指数衰减半衰期设 30 天。类型权重里决策和偏好给 1.0上下文给 0.7待办给 0.5。置信度直接用抽取时的值。这个权重不是一次调好的。我拿自己过去两个月的真实查询做了个小测试集手动标注哪些记忆是“应该被检索到的”然后网格搜索调权重。最后这组参数在我的场景下命中率最高。你的场景可能不一样建议也做个小测试集调一调别直接用我的值。注意时间衰减不要设得太激进。我一开始半衰期设 7 天结果一个月前的架构决策全被压下去了反而检索不到。记忆的价值衰减比想象中慢尤其是决策类。4. 实操落地从零搭一套可用的记忆系统4.1 环境准备与依赖安装先把基础环境搭起来。我用的是 Python 3.11主要依赖就三个sqlite-vec做向量存储、sentence-transformers跑 embedding、anthropic调 Claude API。pip install sqlite-vec sentence-transformers anthropicsqlite-vec是个 SQLite 扩展需要单独加载。如果你不想折腾扩展也可以纯用 numpy 存向量就是每次启动要重新加载记忆量大时启动慢一点。我图省事前期用 numpy后来条目过万才换的 sqlite-vec。embedding 模型选all-MiniLM-L6-v2384 维模型文件才 80 多兆CPU 上跑单条编码 5 毫秒左右。第一次运行会自动下载模型记得留好网络。数据库初始化建三张表memories存主数据embeddings存向量relations存记忆之间的取代、关联关系。CREATE TABLE memories ( id INTEGER PRIMARY KEY, type TEXT NOT NULL, content TEXT NOT NULL, confidence REAL, tags TEXT, project TEXT, created_at TIMESTAMP, updated_at TIMESTAMP, status TEXT DEFAULT active ); CREATE TABLE embeddings ( memory_id INTEGER PRIMARY KEY, vector BLOB, FOREIGN KEY (memory_id) REFERENCES memories(id) );status字段就是用来标记active和superseded的检索时默认只查 active。4.2 抽取流程的代码实现抽取函数接收一段对话文本调 Claude 返回结构化记忆然后做去重入库。核心代码如下import json from anthropic import Anthropic client Anthropic() def extract_memories(dialogue: str, project: str) - list: resp client.messages.create( modelclaude-sonnet-4-20250514, max_tokens2000, messages[{ role: user, content: EXTRACT_PROMPT.format(dialoguedialogue) }] ) raw resp.content[0].text # 容错Claude 有时会包一层 markdown 代码块 raw raw.strip().removeprefix(json).removesuffix().strip() items json.loads(raw) results [] for item in items: if item[confidence] 0.6: continue item[project] project results.append(item) return results这里有个坑要提醒Claude 返回 JSON 时偶尔会带 markdown 代码块标记直接json.loads会报错。我加了removeprefix和removesuffix做容错。更稳妥的做法是用正则提取第一个[到最后一个]之间的内容防止它前后加解释性文字。入库前去重逻辑单独写一个函数先查同 project 下 content 相似的记忆再做合并或标记取代。4.3 检索注入的完整流程检索函数接收当前对话的上下文返回要注入的记忆列表。流程是先用当前对话的 embedding 做语义召回再用 SQL 过滤项目和时间范围最后加权排序取 top-k。def retrieve_memories(query: str, project: str, top_k: int 5) - list: query_vec encode(query) # 第一步SQL 粗筛限定项目和状态 candidates db.query( SELECT * FROM memories WHERE project ? AND status active, (project,) ) # 第二步语义相似度计算 scored [] for mem in candidates: mem_vec load_vector(mem[id]) sim cosine_similarity(query_vec, mem_vec) score weighted_score(sim, mem) scored.append((score, mem)) # 第三步排序取 top-k scored.sort(keylambda x: x[0], reverseTrue) return [m for _, m in scored[:top_k]]注入时把检索到的记忆格式化成一段文本放在 system prompt 或者对话开头。格式我习惯用简洁的列表每条前面标类型[决策] 选 PostgreSQL 因为需要 JSONB 支持 [偏好] 代码示例统一用 TypeScript [上下文] 当前项目是电商后台Next.js Prisma这样 Claude 一眼就能分清哪些是硬约束、哪些是背景信息。4.4 会话结束的自动归档会话结束时触发归档把这一轮的对话做一次抽取候选记忆列出来让我确认。确认后入库同时更新相关记忆的时间戳。归档这一步我做成半自动脚本跑完抽取把候选清单打印到终端我输入要保留的编号回车入库。整个流程 30 秒内完成比事后手动整理省事太多。实操心得归档时机很重要。不要等会话彻底结束才做最好在话题切换时做一次。因为一个长会话里可能包含多个独立话题混在一起抽取容易串味。我现在养成习惯聊完一个话题就手动触发一次归档。5. 踩坑记录与常见问题排查5.1 记忆污染最头疼的问题记忆污染是指错误或过期的记忆被反复检索注入导致 Claude 基于错误前提回答。我遇到过几次典型情况。一次是早期测试时把“用 Jest 做测试”记成了偏好后来项目改用 Vitest但旧记忆没标记取代结果 Claude 一直给我生成 Jest 的代码。排查时我以为是 prompt 问题查了半天才发现是记忆库里的脏数据。解决办法就是前面说的superseded机制加上定期人工巡检。我现在每个月花十分钟扫一遍记忆库把明显过期的标记掉。这个投入很值比事后 debug 便宜多了。5.2 检索不到明明存了却搜不出来检索不到的原因通常有三个。一是 embedding 模型对某些专业术语不敏感比如项目内部的代号、缩写模型没见过语义特征就弱。这种情况我会在记忆的 tags 里手动加上这些术语检索时先做关键词匹配兜底。二是时间衰减把老记忆压得太狠。前面提过半衰期别设太短就是这个原因。三是项目过滤太严。有时候用户跨项目引用之前的决策但检索限定了当前项目就找不到了。我的处理是加一个“全局记忆”的概念某些通用偏好比如代码风格不绑定项目所有项目都能检索到。5.3 常见问题速查表现象可能原因排查方向解决注入的记忆不相关相似度阈值太低看 top-k 的分数分布提高阈值或减小 top-k同一条记忆重复出现去重没生效查 content 相似度检查去重逻辑的相似度阈值检索延迟高候选集太大看 SQL 粗筛后的条数加时间范围或类型过滤抽取漏掉关键决策prompt 约束不够看抽取结果在 prompt 里加正反例记忆内容太笼统没要求包含理由看 content 字段强制要求陈述句加理由5.4 几个我踩过的具体坑第一个坑是 embedding 缓存。我一开始每次检索都重新编码所有记忆几万条下来一次查询要好几秒。后来改成入库时编码一次存起来检索只编码 query速度直接降到毫秒级。这个优化很基础但一开始真没想到。第二个坑是 JSON 解析失败。Claude 返回的 JSON 偶尔会有尾随逗号或者单引号标准json.loads直接崩。我加了个json5或者demjson做容错解析稳定性好了很多。第三个坑是并发写入。我有时候同时开两个会话归档时同时写数据库SQLite 会锁表报错。解决办法是加一个简单的文件锁或者把归档操作串行化。记忆写入频率不高串行完全够用。提示SQLite 的并发写是弱项但记忆系统写入频率低加个锁就解决了。别为了这个上 PostgreSQL杀鸡用牛刀。6. 效果评估与后续可扩展方向6.1 怎么判断这套系统到底有没有用我用了两个指标衡量。一是重复解释率统计我每周需要重新跟 Claude 解释项目背景的次数。上系统前平均每周 8 到 10 次上系统后降到 2 到 3 次。二是决策一致性看 Claude 生成的方案是否和之前的决策冲突。这个靠人工抽查上系统后冲突明显减少。这两个指标都不完美但足够说明问题。记忆系统的价值不在于技术多炫而在于省了多少重复沟通。6.2 可以继续扩展的方向现在这套系统还是偏手动后续我想加两个能力。一是记忆自动过期给不同类型的记忆设不同的 TTL到期自动标记为待确认而不是永久 active。二是跨项目关联当两个项目的记忆有语义关联时自动建立链接检索时能带出相关项目的经验。还有一个方向是记忆的可视化。现在记忆库是个黑盒我想做个简单的界面按时间线或项目维度展示记忆方便巡检和清理。不过这个优先级不高等记忆量真的到管不过来的程度再说。6.3 给想动手的人几句实在话别一上来就追求大而全。我见过有人想直接搭一套带知识图谱、多模态、自动推理的记忆系统结果两周就放弃了。先从最简单的 SQLite 加关键词检索做起跑通了再逐步加向量、加去重、加加权排序。每一步都解决一个具体问题而不是为了架构而架构。记忆质量比记忆数量重要得多。存一万条垃圾记忆不如存一百条精准的。抽取时的置信度过滤和人工确认这两道闸千万别省。最后这套系统是给自己用的不是给别人看的。参数、流程怎么顺手怎么来不用追求“最佳实践”。我的方案只是给你一个起点真正好用的记忆系统一定是你根据自己的使用习惯一点点磨出来的。
返回列表