资讯动态

claude-mem 记忆系统实战:SQLite + 向量索引实现持久化记忆

发布时间:2026/10/26 7:31:35 来源:尧图企业网站定制
1. 项目缘起与核心定位第一次看到claude-mem这个名字我的直觉是这大概率是一个围绕 Claude 生态做“记忆层”的项目。事实也确实如此。它要解决的是一个所有长期使用大模型的人都会遇到的痛点——模型没有持久记忆。每次开新会话之前聊过的偏好、项目背景、技术栈约定、甚至你反复纠正过的错误全部归零。你得像复读机一样重新交代一遍上下文效率极低。claude-mem的核心定位就是给 Claude 这类对话式 AI 外挂一套可持久化、可检索、可管理的记忆系统。它把每次对话中值得留存的信息抽取出来存进本地或远程的存储层在后续会话中按需召回拼接到提示词里让模型“记得”你之前说过什么。适合谁来用三类人最需要一是每天高频使用 Claude 做开发、写作、研究的重度用户二是想把 AI 助手接入自己工作流、做自动化管线的工程师三是关心数据隐私、希望记忆数据掌握在自己手里、不想依赖平台内置记忆功能的人。我自己的使用场景很典型手上有好几个并行项目每个项目的技术选型、命名规范、目录结构都不一样。以前每次切换项目都要重新给 Claude 喂一遍背景现在有了记忆层切过去它就知道“哦这个项目用的是 pnpm 不是 npm测试框架是 vitest 不是 jest”。这种体验上的差别用过就回不去了。需要先说明的是claude-mem并不是官方产品而是社区围绕 Claude 能力构建的一类记忆增强方案的统称。不同实现细节会有差异但核心思路一致抽取—存储—召回—注入。下面我按这个主线把整套东西拆开讲透。2. 记忆系统的整体设计与选型思路2.1 为什么不能只靠“把历史对话全塞进去”很多人第一反应是记忆不就是把之前的对话记录全部拼到 prompt 里吗这个思路在小规模下能用但很快就会撞墙。原因有三。第一是上下文窗口成本。就算窗口够大把几千轮对话全塞进去token 消耗是线性增长的每次请求都在为重复的历史付费长期下来成本惊人。第二是信噪比问题。历史对话里大量是寒暄、试错、废弃方案真正有价值的记忆可能只占百分之几全塞进去反而稀释了关键信息模型注意力被干扰。第三是冲突与时效。你三个月前说“用 MySQL”上周改成了“迁移到 PostgreSQL”如果两条都塞进去模型不知道该信哪个。所以claude-mem这类方案的核心不是“存全部”而是“抽取精华 按需召回”。这决定了整个架构的设计方向。2.2 分层记忆模型短期、长期、工作记忆参考认知科学的分类我把记忆层设计成三层这也是多数成熟实现的共识。工作记忆Working Memory当前会话的即时上下文就是最近几轮对话直接放在 prompt 里不落库或只做临时缓存。短期记忆Short-term Memory本次会话内跨轮次需要记住的信息比如用户刚说的“接下来都用中文回答”会话结束可丢弃或降级。长期记忆Long-term Memory跨会话持久化的信息包括用户偏好、项目背景、事实性知识、纠正记录等这是claude-mem的主战场。分层的意义在于召回策略可以差异化。工作记忆永远在场短期记忆按会话生命周期管理长期记忆则要走检索。如果只有一层你要么全塞贵要么全检索慢且可能漏分层之后各司其职。2.3 存储选型为什么我最终选了 SQLite 向量索引存储层是绕不开的选型题。常见选项有纯文件JSON/Markdown、关系库、向量库、以及混合方案。我踩过的坑值得说说。一开始我用纯 Markdown 文件存记忆好处是可读、可手动编辑、能进 Git。但问题很快暴露检索只能靠关键词 grep语义相近但用词不同的记忆召回不到。比如我存了“偏好函数式写法”查询“喜欢 immutable 风格”就匹配不上。后来上向量库语义检索是好了但纯向量库做精确过滤很别扭。我想查“项目 A 的所有记忆”向量库要么不支持结构化过滤要么过滤性能差。而且单独跑一个向量数据库服务对个人用户来说太重了。最终我采用的是SQLite 向量扩展的混合方案。SQLite 存结构化字段时间戳、项目标签、记忆类型、来源会话 ID向量列存 embedding检索时先用 SQL 做结构化过滤缩小范围再在候选集里做向量相似度排序。这个组合的好处是单文件、零运维、可备份、支持事务同时兼顾了结构化查询和语义检索。对于个人和小团队场景这是性价比最高的选择。提示如果你的记忆量在几千条以内甚至可以不引入向量检索用 SQLite 的全文检索FTS5配合关键词扩展就够用。向量检索的收益在记忆量上万、且查询表达多样化之后才明显。2.4 抽取策略什么时候写记忆、写什么抽取是整套系统里最容易被低估的环节。存什么、不存什么直接决定记忆库的质量。我的原则是宁缺毋滥因为垃圾记忆的召回会污染 prompt比没有记忆更糟。我采用的抽取触发条件有三类。一是显式指令用户说“记住这个”“以后都这样”直接抽取。二是偏好与约定比如“我用 pnpm”“回答别太啰嗦”“代码注释用英文”这类是长期有效的。三是事实与决策比如“项目 X 的数据库是 PostgreSQL”“上周决定放弃方案 B”。反过来寒暄、临时性的调试过程、已经被推翻的结论都不该进长期记忆。这里有个技巧抽取时让模型输出一个置信度和时效性标记比如“这条偏好是永久的还是仅本次会话有效”后续召回时据此加权。3. 核心细节解析与实操要点3.1 记忆的数据结构设计一条记忆记录该有哪些字段这个设计决定了后续所有操作的便利性。我实际用的 schema 大致如下。字段类型说明idINTEGER主键自增contentTEXT记忆正文自然语言描述embeddingBLOB向量表示用于语义检索mem_typeTEXT类型preference/fact/decision/instructionscopeTEXT作用域global/project/sessionproject_tagTEXT项目标识用于结构化过滤confidenceREAL置信度 0-1created_atINTEGER创建时间戳updated_atINTEGER更新时间戳source_sessionTEXT来源会话 ID便于溯源statusTEXTactive/superseded/archived几个字段值得展开说。scope决定了召回范围global 的记忆在任何项目都召回project 的只在对应项目召回这个设计避免了跨项目污染。status里的superseded很关键——当新记忆覆盖旧记忆时不是删除旧的而是标记为被取代这样既保留了历史又不会在召回时产生冲突。confidence则用于排序加权高置信度的记忆优先注入。3.2 记忆抽取的提示词工程抽取质量高度依赖提示词。我反复调过很多版最终稳定下来的思路是给模型明确的分类框架 输出结构化 JSON 要求它主动判断时效性。一个简化版的抽取提示词骨架是这样的你是一个记忆抽取器。阅读以下对话片段判断其中是否包含值得长期记忆的信息。 值得记忆的类型包括 - preference: 用户的稳定偏好工具、风格、习惯 - fact: 客观事实或项目背景 - decision: 已做出的决策 - instruction: 用户给出的长期指令 对每条记忆输出 - content: 一句话描述 - type: 上述类型之一 - scope: global 或 project - confidence: 0-1 - durability: permanent 或 temporary 如果没有任何值得记忆的内容返回空数组。 只输出 JSON不要解释。这里的关键是要求模型主动判断 durability。很多实现忽略了这点导致临时性的“这次用 Python 写”被当成永久偏好存下来下次做别的任务也被召回非常烦人。3.3 召回策略怎么在正确的时候给正确的记忆召回是记忆系统的“临门一脚”。存得再好召回不对等于白搭。我的召回流程分三步。第一步是候选集生成。根据当前会话的 project_tag 和 query 语义从库里捞出候选。结构化过滤先砍掉 scope 不匹配的再用向量相似度取 top-KK 一般设 10 到 20。第二步是重排序。候选集里按综合得分排序得分公式我用的近似是score 0.6 * 语义相似度 0.2 * 置信度 0.1 * 时效新鲜度 0.1 * 类型权重。类型权重上instruction 和 preference 给高一点因为它们对行为影响大fact 次之。第三步是注入格式化。把选中的记忆拼成一段简洁的文本放在 system prompt 或对话开头。格式上我习惯用带标签的列表让模型清楚这是“记忆”而非当前指令[记忆上下文] - (偏好) 用户偏好使用 pnpm 而非 npm - (事实) 项目 alpha 的数据库是 PostgreSQL 15 - (指令) 代码注释统一使用英文注意注入的记忆要控制总量我一般限制在 500 到 800 token。超过这个量边际收益递减还挤占了正常对话空间。宁可少而精。3.4 冲突处理与记忆更新记忆库用久了必然出现冲突新偏好覆盖旧偏好新决策推翻旧决策。处理不好模型会精神分裂。我的做法是写入时检测冲突。新记忆入库前先检索同 scope、同类型的高相似度记忆。如果相似度超过阈值比如 0.85且语义相反或更新就把旧记忆标记为superseded新记忆正常入库并在新记忆里记录supersedes_id指向旧记忆。这样召回时只召回 active 的历史可追溯。对于语义相反但相似度没那么高的我会让模型做一次判断“新记忆是否与以下已有记忆冲突如果是指出哪条被取代。”这一步多花一点 token但能显著提升记忆库的干净程度。4. 实操过程与核心环节实现4.1 环境准备与依赖安装假设你从零开始搭一套。基础环境需要 Python 3.10或 Node.js看你熟悉哪套一个 SQLite系统自带即可以及 embedding 模型。embedding 可以调 API也可以本地跑一个小模型。本地跑的好处是零成本、数据不出本地代价是首次要下载模型权重。我本地用的是 sentence-transformers 系列的小模型几百 MBCPU 也能跑单条 embedding 几十毫秒对记忆这种低频操作完全够用。如果你追求更高质量可以调云端 embedding API但要考虑成本和数据流向。依赖清单大致是数据库驱动、embedding 库、以及一个 HTTP 客户端用于调 Claude API。装完之后先跑一个连通性测试确认能写库、能算 embedding、能调模型再往下走。4.2 数据库初始化建表语句我简化成核心部分CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, embedding BLOB, mem_type TEXT NOT NULL, scope TEXT NOT NULL DEFAULT global, project_tag TEXT, confidence REAL DEFAULT 0.8, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source_session TEXT, status TEXT DEFAULT active, supersedes_id INTEGER ); CREATE INDEX idx_scope_project ON memories(scope, project_tag); CREATE INDEX idx_status ON memories(status); CREATE INDEX idx_type ON memories(mem_type);索引很关键。scope project_tag的联合索引让结构化过滤走索引而不是全表扫记忆量大了之后差别明显。status索引用于快速筛出 active 记忆。4.3 抽取流程的代码实现抽取的伪代码逻辑如下我用 Python 风格写方便理解def extract_memories(conversation_snippet, project_tag): prompt build_extraction_prompt(conversation_snippet) raw call_claude(prompt) items parse_json(raw) saved [] for item in items: if item[confidence] 0.5: continue vec embed(item[content]) conflict find_conflict(item, vec, project_tag) if conflict: mark_superseded(conflict[id]) mem_id insert_memory(item, vec, project_tag) saved.append(mem_id) return saved几个实操细节。confidence 0.5直接丢弃这是过滤噪声的第一道闸。find_conflict里做向量检索加语义判断命中就标记旧记忆。整个抽取可以异步做不阻塞主对话流程用户体验更好。4.4 召回与注入的代码实现召回侧的核心函数def recall(query, project_tag, top_k15, token_budget700): candidates fetch_candidates(query, project_tag, limit50) scored [] for mem in candidates: sim cosine_sim(embed(query), mem[embedding]) score (0.6 * sim 0.2 * mem[confidence] 0.1 * freshness(mem[updated_at]) 0.1 * type_weight(mem[mem_type])) scored.append((score, mem)) scored.sort(reverseTrue, keylambda x: x[0]) selected [] used 0 for score, mem in scored[:top_k]: cost estimate_tokens(mem[content]) if used cost token_budget: break selected.append(mem) used cost return format_memories(selected)这里token_budget是硬约束防止记忆挤爆上下文。freshness函数让新记忆略微占优但权重不高避免老的重要记忆被无脑淘汰。4.5 与 Claude 对话流程的集成集成点有两个对话前召回注入对话后抽取入库。对话前拿用户当前输入作为 query 去召回把结果拼进 system prompt。对话后把这一轮的用户输入和模型回复一起送去抽取。有个细节要注意抽取不要每轮都做太频繁既费 token 又容易存进噪声。我的策略是每 N 轮或会话结束时批量抽取一次N 取 5 到 10。另外用户显式说“记住”时立即触发抽取不等批量。5. 常见问题与排查技巧实录5.1 记忆召回不相关怎么办这是最高频的问题。表现是模型突然提到一个跟当前话题八竿子打不着的旧记忆。排查顺序如下。先看候选集是不是太大。如果 top-K 设了 50里面混进不相关的很正常。缩小到 10 到 15配合重排序通常能改善。再看embedding 质量。如果用的是很弱的模型语义区分度差相似度算出来都差不多排序就失效了。换一个更强的 embedding 模型往往立竿见影。最后看scope 过滤是否生效。如果 project_tag 没正确写入或过滤跨项目记忆就会乱入。我踩过的一个坑是早期没做 scope 过滤结果做 A 项目时召回了 B 项目的技术栈约定模型给出的建议全是错的。加上 project_tag 过滤后问题消失。5.2 记忆库越来越臃肿用几个月后记忆条数可能上千检索变慢噪声变多。这时候要做记忆整理。我的做法是定期跑一个归档任务把超过一定时间、从未被召回过的记忆标记为 archived把语义高度重复的记忆合并把 superseded 的旧记忆物理删除或冷存。还有一个技巧是记忆摘要。对于同一主题的多条细碎记忆让模型合并成一条概括性的。比如五条关于“代码风格”的细碎偏好合并成一条“用户偏好函数式风格、英文注释、2 空格缩进”。这样既省空间又提升召回质量。5.3 记忆冲突导致模型行为矛盾表现是模型一会儿用这个约定一会儿用那个。根因是冲突处理没做好。检查两点一是写入时有没有做冲突检测二是召回时有没有过滤掉 superseded 的记忆。我建议在召回 SQL 里硬性加上status active从源头杜绝旧记忆被召回。如果冲突已经发生可以手动介入查库找出矛盾的记忆把旧的标记为 superseded或者直接修正内容。这也是为什么我坚持记忆要可读、可手动编辑——自动化再智能也需要人工兜底。5.4 常见问题速查表现象可能原因排查方向召回不相关候选集过大/embedding 弱/scope 未过滤缩小 top-K、换模型、检查 project_tag记忆库臃肿无归档、无去重定期归档、合并重复记忆行为矛盾冲突未处理、旧记忆被召回加 status 过滤、写入时检测冲突抽取噪声多置信度阈值低、抽取太频繁提高阈值、改批量抽取注入超长token 预算未限制加硬性 token_budget 约束检索变慢缺索引、全表扫描补 scope/project/status 索引5.5 几个我踩过的坑第一个坑是把 embedding 存成 JSON 字符串。一开始图省事把向量序列化成文本存 TEXT 字段结果每次检索都要反序列化慢得离谱。改成 BLOB 存二进制后速度提升一个数量级。第二个坑是抽取时没限制输出长度。模型有时候会把整段对话复述一遍当记忆一条记忆几百 token。后来在提示词里硬性要求“每条记忆不超过 50 字”问题解决。第三个坑是忘记处理时区。时间戳用本地时间存跨时区或夏令时切换时排序错乱。统一用 UTC 时间戳后就没这问题了。第四个坑是过度依赖自动抽取。有些关键约定模型没抽出来导致后续行为不符预期。后来我加了一个手动添加记忆的接口重要的事情自己敲进去比指望模型靠谱。6. 记忆系统的扩展方向基础版跑通之后可以往几个方向扩展。一是记忆的时效衰减让老记忆的权重随时间缓慢下降除非被反复召回。二是多用户隔离给每条记忆加 user_id支持多人共用一套系统。三是记忆的可视化面板用网页界面浏览、搜索、编辑记忆比命令行友好得多。四是跨模型复用把记忆层做成模型无关的换用别的对话模型时记忆照样能用。我个人最看重的是第四点。记忆是用户的资产不该绑定在某一个模型上。把记忆层做成独立服务通过标准接口对接不同的对话前端这样你的“AI 记忆”才真正属于你自己而不是某个平台的附属功能。这也是我当初选择自建而不是依赖平台内置记忆的根本原因——数据主权和可迁移性长期看比省事重要得多。最后分享一个小技巧给记忆加一个last_recalled_at字段记录每条记忆最后一次被召回的时间。这个字段在归档和衰减计算时特别有用能帮你识别哪些记忆是“死重”哪些是高频刚需。我靠这个字段把记忆库精简了将近一半召回质量反而提升了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑