
如果你也遇到过这种场景跟 Claude 聊了一个星期的项目换一个新会话它连我们三天前定下的技术栈都不记得了。我是在一个周五下午遇到这件事的当时对着空白的输入框愣了几秒然后决定不再当人肉上下文动手写一个给 Claude 用的持久记忆工具代号就叫 claude-mem。这个工具通过 MCP 协议给 Claude 挂上一个外置记忆库自动把对话沉淀成可检索的摘要片段等下次新会话开启时按语义召回最相关的历史记忆重新注入当前上下文。文章里会完整记录我在设计、实现和调优 claude-mem 时的思路、代码细节以及三个让我折腾到半夜的坑。如果你重度使用 Claude 做技术方案、写作、编程并且已经受够了每次重新交代背景的循环这篇内容应该能直接拿来当参考。1. 为什么 Claude 越聊越蠢一个让我决定动手做记忆工具的导火索1.1 从上下文塞满到重复交代背景问题出在会话天然无状态先说我自己的真实经历。我在维护一个内部项目涉及前端、后端、部署三个模块基本每个模块都会跟 Claude 聊上几个小时。前端方案聊完了第二天开新会话聊后端Claude 完全不记得前端已经定了什么约束。我无奈地开始粘贴背景材料从技术栈到已决策项粘贴完这几段背景上下文预算已经烧掉了不少真正的问题还没开始问。这个现象背后的原理其实很直白每一次会话都是独立的模型没有跨会话记忆。上下文窗口有长度上限对话过程中内容超了就会被截断本质上像是一个一次性的工作台东西放上去关掉会话就清空了。或许有人会觉得把上下文写长一点就完事但窗口再长也有天花板而且塞进来的每一个 token 都会增加处理成本和响应延迟。1.2 那些常见的记忆方案为什么总差一口气在决定自己写工具之前我把能想到的方案都试了一遍各有各的难受。方案做法痛点手动复制历史新会话开头粘贴上次的关键结论每开一次会话都要人工整理费时且靠自觉维护System Prompt 塞背景把项目说明写死在提示词里有长度限制内容更新后得手动改过期信息容易残留外置文档喂给 Claude每次把整个文档丢进上下文文件大了很贵而且与当前问题无关的内容会稀释注意力自己搭 RAG文档切片后做向量检索再注入需要额外维护向量库和检索流程对个人工具来说太重claude-mem对话自动沉淀跨会话按语义召回需要写代码自己搭但一次搞定后就不用再管这几个方案共同的问题是你把记忆维护者的角色交给了人。要么靠人肉复制要么靠人肉更新文档。我想要的是一个能自动运转的东西对话结束不需要我再做二次整理。这正是 claude-mem 立项时最原始的动机。1.3 claude-mem 要解决的三个核心问题所以我给这个工具定了三条硬性要求。第一自动沉淀。对话过程中产生的关键结论、偏好、决策要自动进入记忆库而不是等对话结束后我再手动整理。第二跨会话召回。新会话里用户只要正常提问工具就能根据语义检索到旧记忆把它重新放回上下文。第三低摩擦接入。我不想改变和 Claude 对话的方式哪怕有一天换一个支持 MCP 的客户端这套记忆库还能继续用。带着这三个目标我开始了 claude-mem 的设计。技术选型的时候比想象中纠结但核心思路确定之后实现路径其实很清晰。2. 整体设计与技术选型MCP、SQLite 和语义检索是怎么拧在一起的2.1 为什么选择 MCP 当记忆插槽先解释一下 MCP 是什么。MCP 的全称是模型上下文协议它定义了一套统一规范让 AI 应用可以调用外部工具、读取外部数据源。Claude Desktop 和 Claude Code 这类客户端原生支持 MCP所以我只要实现一个 MCP Server记忆功能就能像普通工具一样被 Claude 使用而不需要依赖某个特定客户端的私有没有接口。为什么不用把历史记录全部塞进 System Prompt这种笨办法因为成本高而且会稀释注意力。上下文里堆的无关历史越多模型对当前问题的判断就越容易被干扰。我的思路是把 MCP 当成一个记忆插槽Claude 在需要的时候主动调用 search_memory 工具去查而不是每一轮都把所有历史强行喂给模型。claude-mem 的 MCP Server 总共注册了三个工具save_memory写入记忆、search_memory检索记忆、delete_memory删除记忆。工具数量刻意保持精简因为对模型来说工具越多调用时的决策负担就越大。2.2 SQLite 做记忆库单文件、零运维、够用确定接入方式之后下一个问题是记忆库用什么存。我选了 SQLite理由很简单个人工具的数据量撑死就是几万条记录SQLite 单文件就能搞定备份就是复制一个文件事务可靠不容易出现写一半文件损坏的情况。不用 JSON 文件的原因是并发写入容易互相覆盖而且每次检索都得全量加载几百条可以忍上万条就会变慢。不上专业数据库则是因为没必要个人工具还要装服务端和维护数据目录属于过度设计。实际项目里我会尽量让事情简单一点能用一个文件解决的问题不值得引入一整套服务。表结构设计也比较精简核心就三张表CREATE TABLE conversations ( id INTEGER PRIMARY KEY, title TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE messages ( id INTEGER PRIMARY KEY, conversation_id INTEGER, role TEXT, content TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE memories ( id INTEGER PRIMARY KEY, conversation_id INTEGER, content TEXT, summary TEXT, embedding BLOB, created_at TEXT DEFAULT CURRENT_TIMESTAMP );messages 表存原始对话memories 表存经过提炼的记忆片段。embedding 字段用 BLOB 存向量这样暂时不需要引入独立的向量数据库。2.3 语义检索只用向量还不够必须加关键词兜底记忆检索的核心是怎么找到相关的旧内容。我采用了双路召回方案第一路是向量相似度第二路是关键词过滤。向量相似度的原理是把文本通过嵌入模型转换成一串高维向量语义相近的文本在向量空间里距离也近。我选择在本地跑一个轻量的中文嵌入模型既省 API 费用又不会把对话数据发到外部服务。向量直接存到 SQLite 的 BLOB 字段里检索时暴力计算余弦相似度并取 top_k在几万条数据的规模下完全能接受。但向量检索有一个明显短板对数字、版本号、专有名词不敏感。比如把依赖升级到 Python 3.12和测试 Python 3.11 的兼容性语义上差别很大但向量距离可能很接近导致召回一堆不相干的片段。关键词过滤就是干这个的用正则和精确匹配优先命中包含Python 3.12这类实体的记忆再参与排序。2.4 一次带记忆的对话请求是怎么流转的完整链路大概是这样的顺序用户在 Claude 里发出提问Claude 分析当前问题是否需要历史记忆需要的话会调用 search_memory 工具MCP Server 收到查询后先做查询嵌入化再去 SQLite 里做向量召回和关键词精排返回最相关的几个记忆片段Claude 把这些片段组装进当前上下文然后生成回答回答完成后消息异步写入 messages 表和 memories 表如果消息数量达到阈值再触发一次摘要生成。这里有一个关键细节检索是同步的但摘要生成是异步的。用户提问时最怕的就是每个问题都要等摘要生成完那体验会非常差。异步沉淀的好处是记忆写入不会阻塞正常对话Claude 该回答就回答沉淀的事在后台悄悄完成。3. 手把手实现核心流程从 MCP 服务器骨架到记忆自动沉淀3.1 项目骨架和 MCP Server 初始化我用 Python 来写这个项目依赖主要是 mcp 的 Python SDK、sqlite3 标准库、以及一个本地嵌入模型。项目结构比较简洁一个server.py负责 MCP 工具注册一个memory_store.py负责数据库读写一个embedder.py负责文本转向量一个summarizer.py负责调用模型生成摘要。MCP Server 的骨架长这样from mcp.server.fastmcp import FastMCP mcp FastMCP(claude-mem) mcp.tool() def search_memory(query: str, top_k: int 5) - list[dict]: 根据查询文本召回最相关的历史记忆片段。 return memory_store.search(query, top_ktop_k) mcp.tool() def save_memory(content: str, conversation_id: str, metadata: dict | None None) - str: 把一段关键决策或结论写入长期记忆库。 memory_store.save(content, conversation_id, metadata or {}) return saved mcp.tool() def delete_memory(memory_id: int) - str: 手动删除某条记忆片段。 memory_store.delete(memory_id) return deletedMCP 协议默认通过 stdio 传输所以 Claude Desktop 配置里只需要指向启动命令比如python /path/to/server.py不需要开放网络端口。这保证了工具只在本地被调用不会暴露额外攻击面。3.2 写入流程不是存聊天记录而是存决策片段在最初版本里我天真地想把原始 messages 直接灌进 memories 表结果发现检索噪声巨大。原因很好理解原始对话里大量内容属于寒暄、反复试探、废话这些都会干扰向量检索的结果。后来我调整了策略messages 表保存原始记录用于回溯memories 表只保存经过提炼的决策片段。save_memory 的内部逻辑会做三件事清洗文本去掉无意义的语气词和重复内容提取关键信息比如技术选型、偏好、结论、时间点生成结构化摘要记录会话 ID 和时间戳。这样存入记忆库的内容是压缩过的、高密度的检索时命中率明显提升。摘要生成是按主题压缩而不是逐句总结。我给 summarizer 定义了一个固定输出框架当前项目背景、确定的结论、候选方案、用户偏好、下一步待办。这样后续检索时能快速定位到用户真正需要的结论而不是一段普通聊天内容。def generate_summary(messages: list[dict]) - str: prompt f 请把以下对话内容压缩成结构化摘要包含 - 项目背景 - 确定的结论/决策 - 候选方案 - 用户偏好 - 下一步待办 --- {messages} return llm_chat(prompt)3.3 召回流程查询嵌入化、向量召回、关键词精排search_memory 的完整实现分三步。第一步对用户查询做嵌入化得到查询向量。第二步遍历 memories 表用余弦相似度计算每条记忆与查询的相似度筛掉低于阈值的片段按分数排序。第三步用关键词和正则对候选片段做精排比如查询里包含FastAPI时精确包含FastAPI的记忆应该排在前面。代码实现用到了一个关键技巧把向量存储为 BLOB 二进制数组检索时再反序列化成 numpy 数组。数据量小的时候这种暴力扫描的方法反而比维护复杂索引更可靠。import sqlite3, numpy as np def cosine_similarity(a, b): return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) def search_memory(query: str, top_k: int 5, min_score: float 0.6): q_vec embedder.encode(query) rows conn.execute(SELECT id, content, embedding FROM memories).fetchall() scored [] for row in rows: vec np.frombuffer(row[2], dtypenp.float32) score cosine_similarity(q_vec, vec) if score min_score: scored.append((score, row[1], row[0])) scored.sort(reverseTrue) return scored[:top_k]召回后返回的片段会按照模板格式拼进上下文例如历史记忆来自 3 天前用户明确说过后端优先选择 FastAPI原因是团队更熟悉 Python。Claude 看到这段记忆后回答就知道该怎么对齐用户的历史偏好。3.4 嵌入和摘要模型怎么选才划算嵌入模型我用了本地轻量的中文模型几百 MB 级别纯 CPU 就能跑。摘要模型直接用 Claude 本身因为摘要质量直接影响记忆库的价值这里不建议省。成本上本地嵌入是零 API 费用摘要按 token 计费但触发频率不高整体开销可以接受。环节方案成本特点文本嵌入本地轻量模型零 API 费用隐私友好中文表现需要实测远程嵌入云端模型按 token 计费质量高但数据要出本机摘要生成Claude 当前模型按 token 计费质量高触发频率可调低经验是摘要频率不要设太高否则 API 费用会涨得很快。我最终设定每 20 条消息或 4000 token 才触发一次摘要既保证记忆连续性又不会让后台任务频繁跑。4. 实测效果与四个关键参数让记忆记得准比记得多更重要4.1 跨会话技术选型记忆一个完整的实测场景工具跑起来之后我做了个最直接的验证。第一天我和 Claude 聊后端技术选型聊到中途明确说了不想引入重型框架倾向用 FastAPI。第二天我开了一个全新会话直接问我们后端定的是什么。如果没有 claude-mem这个问题注定需要我重新讲一遍背景但有记忆库的情况下Claude 在回答前调用了 search_memory召回了前一天的结论然后直接回答根据历史记忆后端倾向 FastAPI理由是团队熟悉 Python 且项目规模不大。这次实测的召回结果如下记忆片段相似度是否命中用户明确说后端优先考虑 FastAPI原因是团队更熟悉 Python0.82命中讨论过 Django 的重量级特性以及维护成本0.67命中前端组件打算用某个 UI 库0.31未命中相似度低于 0.6 的基本都是无关内容说明 min_score 设为 0.6 在当前场景下是合理的。4.2 四个关键参数怎么调才不翻车我重点调了四个参数这里直接给出我实测下来的推荐值和建议。top_k 是每次召回的记忆片段数量默认 5。太小容易漏掉关键信息太大又会让上下文被无关记忆占满实测下来 5 到 10 之间比较合适我最终停在 5。min_score 是相似度阈值默认 0.6。这个参数宁高勿低低阈值会让大量弱相关的记忆进入上下文反而干扰 Claude 的判断也就是所谓的记忆污染。0.6 到 0.7 是我在中文场景下的经验区间。摘要触发间隔是每多少条消息生成一次摘要我设成 20 条消息或 4000 token两个条件先到先触发。这个值影响的是记忆库的更新频率太频繁会白烧 token太稀疏又可能漏掉关键转折。max_memory_block 是每次注入上下文的记忆块最大长度我限制在 800 token 以内。记忆不是越多越好给 Claude 塞一整个记忆库进去它反而不知道哪个信息对当前问题最重要。4.3 记忆噪声和过期信息处理怎么避免旧消息误导新决策对话里会有大量临时信息比如先这样试试暂时用这个方案这类内容过几天就会过期。如果全部塞进记忆库老方案会干扰新决策。我采用了两层策略。第一层是时间衰减。检索时对记忆片段按时间做加权超过一定时间阈值的记忆分数会打折新鲜记忆优先。第二层是让用户能手动标记和删除。MCP 工具里提供 delete_memory用户发现某条记忆已经过时或错误时一句话就能让 Claude 调用工具把它删掉。这比在数据库里手动改文件方便得多。还有一个更深的原则记忆系统的目标是辅助决策不是存档。我后来梳理的时候发现真正值得进记忆库的是确定性的结论而不是过程性的讨论。所以我在摘要生成框架里刻意区分了确定的结论和候选方案两个字段检索时优先返回结论类记忆。4.4 性能开销实测普通笔记本上能不能忍受我担心过这个工具会让对话变慢实测数据打消了顾虑。在普通笔记本的 CPU 上本地嵌入模型编码一个查询大约耗时 30 到 50 毫秒SQLite 暴力扫描一万条向量并计算余弦相似度大约 80 到 120 毫秒总耗时在 150 毫秒左右。这个量级的延迟用户基本感知不到。环节耗时说明查询嵌入化30-50msCPU 推理受文本长度影响SQLite 向量扫描80-120ms一万条向量规模结果组装和注入10ms纯字符串拼接如果记忆量超过五万条暴力扫描就会开始吃力那时可以考虑换独立向量数据库或者给向量列建立索引但个人使用场景下其实很难走到那一步。5. 踩坑记录三个让我折腾到半夜的问题5.1 MCP 工具返回体不符合客户端预期工具调用一直报错跑通第一版的时候Claude Desktop 里调用 search_memory 一直报Tool execution failed看日志也只有一个笼统的错误描述。因为错误信息太模糊我最初怀疑是 MCP Server 崩溃或者网络问题排查了半小时毫无进展。后来我写了一个本地 stdio 客户端脚本直接绕过 Claude Desktop 去调用 MCP Server把返回结果完整打出来才发现问题根本不在 MCP Server 本身而是工具返回的数据结构不符合协议要求。早期版本的 mcp SDK 里工具函数直接返回了普通字符串但外部客户端期望的是一个 content 对象的列表里面每项要明确 type 为 text。修复很简单让工具函数返回正确的 content 结构或者用 SDK 提供的辅助类包装一下返回值。这个坑给我的教训是调试 MCP Server 时不要每次都用客户端测先写一个最小的 stdio 脚本在命令行里把工具调用结果打出来定位问题的速度快得多。5.2 SQLite 写锁摘要线程和主线程同时抢数据库异步摘要生成引入之后我很快遇到了 SQLite 的经典问题database is locked。原因是主进程写入消息的同时后台线程也在写入摘要SQLite 默认的 journal 模式在并发写入时会直接拒绝第二个写入请求而不是等待。排查过程比较烦因为不是每次都会触发而是和时序强相关。最终确定是写并发问题后解决方式是在连接初始化时执行两条 PRAGMAPRAGMA journal_modeWAL;和PRAGMA busy_timeout3000;。WAL 模式允许读写并发busy_timeout 让连接在遇到锁时等待三秒而不是立刻报错。顺手也改掉了另一个隐患每次操作都新建数据库连接改成使用连接池复用连接。SQLite 的单写者限制还在但对这个量级的个人工具来说已经足够稳定。5.3 中文文本向量化效果差召回结果频频跑偏最头疼的是中文召回不准。一开始我用了英文场景下表现很好的通用嵌入模型结果中文查询召回的内容经常驴唇不对马嘴比如问部署方案却召回训练数据准备语义关系八竿子打不着。根因是通用英文模型在中文语料上的分布覆盖不足。换成一个在中文语料上训练过的轻量模型之后召回率明显改善。但中文嵌入模型的 benchmark 和真实召回表现往往不一致所以我养成了一个习惯任何新嵌入模型接入前先拿实际对话数据跑一遍召回测试人工检查命中率而不是只看公开评测分数。同时也在检索环节加了关键词兜底遇到版本号、类名、专有名词这类高确定性信息时关键词匹配比向量更可靠。双路召回之后中文场景的可用性才真正到了一个能日常使用的水平。最后说一点个人体会。claude-mem 做完之后我最大的感受是记忆工具的核心难点不在技术而在该记什么和该忘什么。向量检索、SQLite、MCP 这些技术都是成熟的难的是摘要框架怎么设计才能让记忆高密度、低噪声以及阈值怎么调才能保证检索精准。我现在每天还在用这个工具目前体验比较满意。如果你也在做类似的记忆增强工具建议先从你真实的对话数据出发统计一下哪些信息是你反复强调的再倒推记忆库的摘要格式这种思路比照搬现成方案要靠谱得多。