资讯详情

给Claude装上长期记忆:SQLite+关键词召回实现跨会话上下文

📅 2026/10/11 13:00:50 | 华诺云谱 👁 阅读
给Claude装上长期记忆:SQLite+关键词召回实现跨会话上下文
1. 为什么需要 claude-mem聊聊 Claude 的“金鱼记忆”问题如果你和我一样把 Claude 当成日常工作中的半个搭档你一定遇到过这种场景上午让它帮忙搭了一个项目脚手架下午想让它继续优化某个模块结果它一脸茫然仿佛从来没听过这个项目。你要么重新粘贴一遍上下文要么自己翻聊天记录把关键信息手动喂回去。折腾几次之后你会意识到一件事Claude 本身并不具备真正的长期记忆它只是在单个会话窗口内临时“记得”你聊过的东西。很多人把“上下文窗口”理解成“记忆容量”其实是两回事。上下文窗口只是允许当前请求携带多少 token 内容就像一个人一次性能听进去多少句话。窗口再大会话一结束它就全忘了。你关闭页面再开一个新会话它对你的了解只剩你这次输入的内容。这种设计对简单问答没问题但对连续项目、长期偏好、知识积累型任务来说体验非常割裂。于是我开始琢磨能不能在 Claude 之外做一个轻量记忆层把对话中值得记住的信息抽出来、存起来等下次对话开始前再自动注入给它这个项目我管它叫claude-mem。简单说它就是一个附着在 Claude 上的“外挂记忆”每次对话结束后自动提炼关键信息存入本地数据库每次新对话开始时根据当前主题把相关记忆捞出来混在系统提示里交给 Claude。这样既不需要为每个会话重复解释背景又能让 Claude 在跨会话场景下表现得像“记得你”。这篇文章主要面向两类人一是天天用 Claude 做开发、写文档、做分析但苦于反复喂上下文的普通用户二是想自己写点小工具来增强 AI 工作流的同学。我会把这套东西的完整设计思路、关键代码实现、以及我实际踩过的坑都梳理出来你可以直接照着搭一套。2. claude-mem 的核心设计把记忆这件事拆成三个环节2.1 记忆链路采集、存储、召回任何记忆系统不管做到多复杂本质上只有三件事采集、存储、召回。人脑也是这个流程——眼睛耳朵接收信息海马体编码存储下次遇到相似场景时再提取。claude-mem 要做的事情也一样但它是用代码实现一个简化的版本。采集环节负责从对话流中提取“值得记住”的信息。这里有个容易犯的错误把整个对话历史一股脑存下来。那样做既不经济也没用因为 Claude 以后真正需要的是“关键事实”和“偏好结论”而不是你俩当时说的每句闲话。合理做法是让 Claude 自己在对话结束后生成一份结构化摘要或者你写规则从消息列表里抽取关键信息。我更推荐前者因为 Claude 的总结能力本身就很强让它自己告诉自己什么重要效果比规则抽取稳得多。存储环节决定记忆以什么形态存在。我对比过几种方案最终选了SQLite 简单的关键词索引后面会细说。原因是这套东西应该足够轻、足够本地化、足够可控我不想为了一个记忆功能去搭一个专门的向量数据库服务。如果你有几百条记忆以内的需求文件型数据库完全够用。召回环节是决定体验好坏的关键。每次发起新对话前claude-mem 会根据当前用户输入和项目上下文从库里捞出一批最相关的记忆条目拼进系统提示词里。这里要考虑三个问题按什么维度判断“相关”每次注入多少条注入内容膨胀了怎么办我后面会给出我的参数选择。2.2 为什么选 SQLite 关键词索引而不是一上来就上向量数据库第一次做记忆系统时很多人会直接上向量数据库因为“语义检索”听起来很高级。但现实是对于个人使用、项目级记忆这么小的规模向量检索带来的收益往往抵消不了它的运维成本。我做过一个简单对比大概是这样方案优点缺点适用场景纯文本文件最简单可读性最强基本无法检索内容一多就乱几十条记忆以内想手动维护SQLite 关键词匹配轻量、可靠、查询灵活语义理解弱同义改写会漏个人/小团队记忆量在几千条以内本地向量库能处理语义相似支持模糊匹配需要额外服务嵌入模型耗时调参多记忆量很大对召回质量要求高我的选择是折中用 SQLite 存储结构化记忆条目同时保留一个独立的 keywords 字段用来做关键词匹配如果日后记忆量涨到需要语义检索再加一层向量索引也来得及。实际用下来这个方案有一个额外好处SQLite 是一个单文件数据库记忆可以被轻松地复制、备份、迁移。我可以把某一天的项目记忆单独导出给同事也可以把整个记忆库写进 Git 仓库做版本管理。这对个人工具来说非常重要——你不想依赖一个需要单独部署的服务。2.3 记忆条目设计给每条记忆留好“归档位”存储层做得好不好先看表结构设计。我设计的记忆表主要字段如下id自增主键唯一标识content记忆正文一句话或几句话尽量自包含keywords逗号分隔的关键词用于匹配召回project项目/主题命名空间避免不同项目记忆互相干扰created_at记忆创建时间last_access_at最近一次被召回的时间access_count被召回次数status记忆状态active / archived / deprecated为什么必须要有 project 字段这是我吃了亏才补上的。最开始我没有做项目隔离结果聊 Python 项目时Claude 会把上星期另一个写作项目的记忆也掏出来搞得非常混乱。有了 project 字段之后召回时可以先用WHERE project ?过滤再按关键词匹配精度一下子高了很多。access_count和last_access_at也不是摆设。它们用来实现“记忆衰减”——越久没被用到的记忆召回的优先级就越低。这个点在后面“记忆污染”里会详细说。3. 核心实现从零搭一套 claude-mem3.1 环境准备目录结构与依赖先交代一下我用的技术栈Python 3.11SQLite3 标准库外部依赖只用了requests和jieba。jieba是用来做中文分词的如果你只处理英文内容可以不用。没有引入任何重量级框架整个项目跑起来就是几个文件的事情。目录结构非常直接claude-mem/ ├── mem.db # SQLite 数据库运行时自动生成 ├── mem.py # 记忆模块采集、存储、召回 ├── claude_client.py # Claude API 接入与记忆注入 ├── config.json # 配置文件 └── memory/ # 可选存放摘要草稿的目录config.json里面主要放这些配置{ db_path: mem.db, max_inject_count: 5, max_inject_chars: 1200, recall_score_threshold: 0.1, project: default }这几个参数我后面会逐步解释。先记住max_inject_count控制最多注入几条记忆max_inject_chars控制注入内容的总字符上限这两个参数是防止记忆注入撑爆上下文的关键。3.2 记忆采集逻辑让对话自然沉淀采集逻辑分成两步先调 Claude 生成对话摘要再写进数据库。为什么不让用户手动记录因为手动记录坚持不了几天。我之前试过让用户在每个对话结束前总结几句结果效果很差几乎没人会坚持。所以 claude-mem 的做法是在每次会话结束、拿到完整对话列表后程序自动构造一个“总结请求”发给 Claude然后清洗结果写入数据库。这里有一个重要的设计选择摘要请求本身也是消耗 token 的所以不要让每次对话都生成全量摘要。我设置了两个触发条件当前对话的轮次超过 6 轮对话中出现明显可记忆的信息比如用户说了“记住这个/以后都用这个”等指令。如果只是两三句闲聊就不要浪费 token 去总结。实践中我会把“用户主动说记住”这种信号直接作为强制采集指令处理def trigger_summarization(messages): if len(messages) 6: return False user_text \n.join(m[content] for m in messages if m[role] user) if any(kw in user_text for kw in [记住, 记一下, 以后都用, 别忘了, 重要]): return True return False真正做摘要的函数也不复杂核心是给 Claude 一个明确的输出格式要求。这一点非常关键因为如果你不限定格式它可能给你一段散文而不是结构化的记忆条目。我要求的格式是每行一条记忆前缀-后面跟一个完整的陈述句尽量包含项目名、关键结论、用户偏好。def summarize_and_save(messages, project): text \n.join(f{m[role]}: {m[content]} for m in messages[-20:]) prompt ( 下面是一段用户与 AI 的对话请提炼出 3-5 条值得长期记住的信息。\n 要求\n 1. 每条信息必须单独一行以 \- \ 开头。\n 2. 内容要自包含不依赖上下文也能看懂。\n 3. 如果某条信息只在当前任务中有效不要写入。\n 4. 务必包含用户表露出的偏好或决策。\n\n f对话内容\n{text} ) resp call_claude_api(prompt) parse_and_save_memories(resp, project)parse_and_save_memories会把每一行-开头的文本提取出来用jieba分好词再连同时间戳一起插入数据库。分词是为了后面召回时的关键词匹配。3.3 记忆召回逻辑给 Claude 的“考前小抄”召回是 claude-mem 里最有意思的部分。目标是在发起 Claude 请求之前先把相关记忆捞出来作为系统提示的一部分。召回流程分三步用当前用户输入结合project从 SQLite 里做关键词匹配对结果做相关性打分取前 N 条把命中的记忆拼接成一段文字插入到系统提示词的末尾。代码大概长这样def recall_memories(query, project, limit5): tokens [t for t in jieba.cut(query) if len(t.strip()) 1] if not tokens: return placeholders ,.join(? for _ in tokens) sql f SELECT content, keywords, access_count, last_access_at FROM memories WHERE project ? AND status active AND (keywords LIKE % || ? || %) ORDER BY last_access_at DESC LIMIT ? rows [] for token in tokens: rows.extend(db.execute(sql, (project, token, limit)).fetchall()) # 去重 简化相关性打分 seen set() scored [] for content, keywords, access_count, last_access_at in rows: if content in seen: continue seen.add(content) score keywords.count(query) min(access_count / 5, 2) scored.append((score, content)) scored.sort(reverseTrue) return \n.join(f- {content} for _, content in scored[:limit])这段逻辑其实不完美因为keywords LIKE %token%的方式不能命中同义词。但在“个人本地工具”这个规模下效果可以接受。如果后面想提升召回质量就按我之前说的给 SQLite 加一列 embedding 向量再配一个本地向量索引。先跑通再优化性能是这类工具最务实的路径。这里想强调一个容易被忽略的细节召回时不要只按关键词匹配还要考虑记忆新鲜度。如果一条记忆在三个月前被创建之后从未被用到那它大概率已经过时了。我在ORDER BY里用了last_access_at DESC就是为了让近期活跃的记忆优先浮出来。如果你用的是向量检索方案也应该在打分公式里加入时间衰减因子。3.4 与 Claude 的接入把记忆注入系统提示词有了召回结果接下来就是把它接进 Claude 的请求流。我封装了一个claude_client.py对外暴露两个函数chat(messages, project)和chat_with_memory(messages, project)。后者会自动调召回把结果作为 system prompt 的一部分。具体的注入方式要看调用方式。如果你用的是 Messages API直接在 system 字段里追加一段“你需要记住的已知背景信息”。代码结构如下def build_system_prompt(base_prompt, project): mem_text recall_memories(current_user_text(), project) if not mem_text: return base_prompt return f{base_prompt} 你将使用以下已知记忆来辅助回答不要重复向用户确认这些内容 {mem_text}这里有一个非常关键的实测经验让 Claude“不要重复确认”比单纯给背景信息更奏效。最开始的版本我只把记忆拼上去结果 Claude 每隔几轮就会问“您之前是不是说过……”用户体验非常割裂。加上这句提示之后它会把记忆当成默认前提直接继续往下推进对话顺畅很多。另一个细节是 token 预算。我设置了max_inject_chars: 1200也就是注入的记忆文本一般控制在 1200 字符以内换算成 token 大概 400-600 个。这既不会明显拉高成本又能覆盖大多数场景下需要的信息量。如果召回结果超过预算就按分数从高到低截取绝不让记忆注入反客为主。3.5 完整验证流程从第一次对话到跨会话记忆光看代码还不够我把一次完整的验证过程写出来方便你照着测试自己的 claude-mem。第一步创建两个会话。会话 A 里告诉 Claude“我们在做 claude-mem 这个项目技术栈是 Python SQLite你后面记住我不喜欢冗长的代码注释。”然后正常聊几轮确保对话轮次超过 6 轮触发摘要。第二步等摘要入库后用 sqlite 命令查一下sqlite3 mem.db select id, content, project, status from memories;正常情况下能看到类似这样的记录id1 content用户正在做 claude-mem 项目技术栈是 Python SQLite。 projectclaude-mem statusactive id2 content用户不喜欢冗长的代码注释更偏好精炼风格。 projectclaude-mem statusactive第三步新开一个会话 B直接问“claude-mem 的技术栈是什么代码注释风格我应该注意什么”如果你只发这一句话不带任何项目背景Claude 依然能答出“Python SQLite”“精炼注释”这些信息说明记忆召回成功了。如果它答不上来不要急着怀疑代码。先跑一下recall_memories函数看返回字符串是不是空。如果函数本身返回了记忆但还是没生效那问题大概率出在 system prompt 拼接逻辑上如果返回为空则要检查分词和关键词匹配是否正常。这种分层排查思路能帮你节省大量时间。4. 实操中踩过的坑问题排查与避坑实录4.1 召回质量差聊 A 的事情翻出 B 的记忆这是我遇到的第一个大坑。最开始没做project隔离结果记忆库混进了多个项目的条目召回时所有项目的记忆都参与打分经常把完全不相关的内容带进对话。排查过程非常简单查询一下当前库里都有哪些项目select project, count(*) from memories group by project;果然发现多个项目混在一起。修复方式是给所有表操作加project过滤并且把project字段变成索引从根本上杜绝跨项目污染。之后我再接新项目时会先在 config.json 里改project值或者直接在调用chat_with_memory时传入项目参数。这个设计让我想起文件系统里的目录隔离虽然多一步配置但长期收益巨大。4.2 记忆污染过期信息和错误结论变成了“既定事实”比召回质量差更危险的是记忆污染。比如用户某天说“我准备用 PostgreSQL”隔两天又决定“还是用 SQLite”如果摘要逻辑不够精细库里可能同时存在两条互相矛盾的记忆。更麻烦的是Claude 会把旧记忆也当成事实甚至引用它来回答当前问题误导性很强。我目前的处理方案分三层在摘要生成时特意要求 Claude 判断信息是否“仍然有效”如果对话中出现了“改成”“不用了”之类的转向词要求它标注旧结论失效在memories表加status字段手动执行update memories set statusdeprecated where content like %PostgreSQL%来让旧记忆退出召回引入基于last_access_at的自动衰减超过 90 天没有被召回过且访问次数小于 2 的记录自动标记为 archived。这三层方案不是完美的但能大幅降低矛盾记忆浮出水面的概率。如果你对这个要求更高可以考虑在注入记忆时让 Claude 先做一遍“记忆冲突检测”把新记忆与已有记忆比对发现冲突就主动生成一条“该记忆已更新”的提示。4.3 上下文被撑爆记忆注入是开销国记忆注入天然会多占 token。如果每次对话都固定注入 5 条记忆每条 200 字那就是 1000 字左右的额外开销。日积月累也是一笔不小的成本。更麻烦的是有些记忆虽然被召回但对当前问题毫无帮助纯属浪费。我做了两个优化动态注入数量。不是每次固定取 5 条而是先根据当前文本长度估算可用 token 预算。如果用户输入已经很长就只注入 top 2 条记忆如果输入很短可以注入 top 5 条甚至 8 条。相关性阈值过滤。关键词匹配很容易出现“匹配但无关”的情况。比如用户输入“Python 列表性能”库里有条记忆是“用户在 Python 项目中不喜欢类型注解”虽然关键词有重叠但对当前问题没有价值。我给打分公式设了一个最小阈值低于阈值的记忆宁可不用也不要强行注入。代码里体现为FINAL_SCORE keyword_score * 0.6 recency_score * 0.25 access_bonus * 0.15 if FINAL_SCORE 0.15: continue这个 0.15 是我反复试出来的经验值在不同数据量下可能需要微调。4.4 隐私与并发本地记忆也不是完全无害的claude-mem 的记忆明文存储并且内容包含用户个人偏好甚至敏感信息。这意味着如果你的 mem.db 文件泄露相当于把你和 AI 的长期对话摘要全部暴露出去。我采取了几项措施数据库文件放在项目目录外或者写入.gitignore避免误提交到代码仓库对 content 字段做可选加密比如利用 SQLite 的加密扩展或者应用层 AES 加密代价是召回时需要在内存中解密采集摘要时在提示词中加一句“如果对话内容涉及密钥、口令等信息不要在摘要中记录”。并发问题则没那么复杂。SQLite 支持多个进程读但写操作需要小心。我用的方案是写数据库前加一个简单文件锁或者使用 SQLite 的BEGIN IMMEDIATE事务。个人使用场景下并发量很低这个方案足够稳。5. 后续还能怎么扩展把 claude-mem 做得更聪明目前这套 claude-mem 的定位还比较“朴素”采集摘要、存数据库、召回注入。但它已经足够证明一个观点给大语言模型加外部记忆并不需要特别复杂的架构关键是流程闭环。如果你想继续扩展我建议从这三个方向入手。1. 多项目、多用户隔离。把现在的project字段升级成一张projects表记录项目名、用户 ID、创建时间甚至可以为不同项目设置不同的召回策略。这样 claude-mem 就能从一个单人的小工具变成一个供小团队共享记忆的轻量服务。2. 自动记忆整理与合并。目前的记忆条目是流水账容易存在冗余。比如“用户喜欢简洁注释”和“用户不喜欢长注释”其实是同一件事但被存成两条。可以定时让 Claude 对记忆库做一轮压缩合并把相似条目归并成更抽象的原则。这个功能很像人脑的“睡眠巩固”非常有价值。3. 主动遗忘与记忆反馈。记忆不应该是只进不出的。当用户多次在某个话题上纠正 Claude 时说明相关记忆可能有误系统应该降低那条记忆的权重。甚至可以让 Claude 主动提出“这条记忆可能要更新了”请求用户确认。这样 claude-mem 就从“被动记录”变成了“主动管理”。我个人在实际使用中最大的体会是记忆工具的价值并不取决于它存了多少内容而在于它在恰当的时候把恰当的信息送到自然语言模型面前同时不让它觉得突兀。这种“存在感很低但效果很实在”的体验才是记忆层该有的样子。后面我大概率会把 claude-mem 的向量检索补上再把记忆合并的定时任务跑起来。如果你也在折腾类似的本地 AI 工作流希望这篇文章能给你省掉几个晚上的调试时间。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑