
你有没有遇到过这种情况上午刚把一个 AI 助手的上下文喂熟约定好了术语、框架、代码风格下午开个新会话它完全像第一次见面一样问你这问你那。更头疼的是你在几个项目之间来回切换每次都要重新交代背景动不动就是几百字的“前置说明”真正想做的事反而没时间展开。claude-mem就是为解决这件事诞生的。简单说它是一个给 Claude 加“外挂记忆”的轻量方案让 AI 跨会话记住你的偏好、项目背景、关键结论和常用规范。它不是修改模型也不是复杂的微调管道而是用一套本地优先、结构化存储、按需注入的记忆系统把“反复解释”这件事从你的工作流里删掉。这适合深度使用 AI 写代码、做研究、写长文的人尤其是同时维护多个项目、需要长期连续性的人。这篇文章我会从设计思路、存储结构、检索策略到完整实操和踩坑记录一次性讲清楚。1. 为什么 Claude 需要一块“外挂记忆”1.1 没有记忆的 AI 用起来有多痛很多人都低估了“会话隔离”带来的损耗。我自己的一个很典型的使用场景是让 AI 帮我维护一个跨平台的小工具包含前端界面、后端服务和几个自动化脚本。每次开新会话我必须先花十分钟把以下信息重新打一遍项目的目录结构、代码风格偏好、当前进度、下一步要做的功能、库的选择、以及之前踩过的坑。时间一长你会发现真正让 AI 产生价值的不是单次对话里它生成了多少代码而是它对你项目上下文理解的连续性。你昨天在这个会话里和它确认过“不要用全局状态改用依赖注入”今天新开一个会话它可能又给你写一个全局单例。这种重复劳动浪费的不只是时间还有你的耐心。更隐蔽的问题是AI 在单次会话中的“长期记忆”能力其实很弱。它会记住上下文窗口里的内容但窗口之外它就是一张白纸。很多工具都宣称“记住你的一切”实际只是把每次对话的文本堆在一起丢回窗口既浪费 token又容易把搜索和代码生成的注意力带偏。这就是为什么单纯把所有历史都塞给模型并不是解决方案。1.2 claude-mem 的核心定位不改变模型只管理上下文claude-mem的核心思路很直接模型本身不需要变也不需要为某个人微调只要在“对话之前”和“对话之后”各做一件事就行。对话之前它负责把相关的记忆从本地存储中检索出来整理成一段结构化的“记忆简报”注入系统提示词或上下文开头让 Claude 在一开始就知道自己“是谁、在做什么项目、有什么规则要遵守”。对话之后它负责把刚才这段对话里值得长期保留的信息抽取、压缩、去重写回存储。这有点像你给一个新同事准备的入职手册不是把过去所有的聊天记录都丢给他而是整理出一份要点文档告诉他哪些是事实、哪些是约定、哪些是当前的待办。这里的关键词是“整理”而不是“囤积”。一套好的记忆系统应当具备几个基本素质第一本地存储数据自己掌控第二结构化分类方便检索第三按需注入只喂最相关的信息第四可遗忘、可修正避免错误记忆永远存在于你的上下文里。1.3 设计原则本地优先、结构化存储、按需注入我设计这个小工具的时候给自己定了三条硬性原则。第一本地优先。所有的记忆条目都存放在本地一个普通目录中可以是 SQLite 数据库也可以是纯文本文件加索引。不需要一个云端账号不需要把对话数据送到第三方服务去“分析”。这一点在涉及代码、业务偏好等敏感信息时尤其重要。第二结构化存储。记忆不是一坨聊天记录的堆积而是有分类、有项目归属、有创建时间、有重要度、有来源标签的条目。这样检索时才能精准定位而不是全量扫描。第三按需注入。不是每次对话把所有记忆都塞给模型而是根据当前的项目和对话主题选出最相关的若干条并且要限制注入的 token 预算。上下文窗口是很稀缺的资源你不可能把十页文档全塞进去得学会做筛选。这三条原则最终决定了存储格式、检索方式和工具链选型。接下来我展开讲为什么最终选的方案是“SQLite 关键词全文检索 轻量评分函数”而不是一上来就上向量数据库。2. 整体设计思路拆解2.1 先把记忆拆成四类事实、对话、项目、技能设计存储结构之前先要清楚一件事记忆不是同质的。不同类型的记忆生命周期、检索方式和注入优先级完全不同。我把记忆拆成四类。第一类是事实记忆fact。比如“我偏好 Python 3.11”“服务器的部署端口是 8080”“读者主要是独立开发者”。这类信息稳定、长期有效是最值得优先注入的内容。它适合在每次会话开始都出现因为错误的事实会让所有下游工作跑偏。第二类是对话记忆conversation。指的是某次对话中沉淀出来的结论比如“性能问题定位在数据库索引缺失”“用户反馈说导入功能太慢需要做批处理”。这类信息有明确的上下文可能随着项目进展而过时所以重要度需要动态评估。第三类是项目记忆project。包括项目背景、目录结构、当前进度、架构决策、遗留问题。这类记忆是“当前状态”的载体要求及时更新。比如项目从单体架构改成微服务旧的记忆如果不更新后续 AI 给出的建议会全部作废。第四类是技能记忆skill。这是你希望 AI 每次都在工作里遵守的规范和行动指南比如“提交代码前必须跑一遍类型检查”“输出中文回复时先给结论再给分析”“错误处理必须带上下文日志”。技能记忆本质上是一种“固定工作流约束”。分类的价值在于检索时可以按路由来缩小范围。比如在做“登录功能优化”这个任务时项目记忆里“当前架构”相关条目优先技能记忆里“代码风格规范”也要带上而事实记忆里“数据库端口”这种内容就没必要每次都出现。2.2 存储选型为什么用 SQLite 而不是 JSON 或向量库存储介质的选择直接影响系统的稳定性和维护成本。我最早用纯 JSON 文件试过一条记忆一个 JSON 对象看起来简单但很快就遇到问题文件越来越大读取要全量加载修改一条记录还要重新写整个文件而且多个进程并发写的时候容易冲突。后来换成 SQLite这些问题基本都解决了。SQLite 的优点很明显单文件存储备份和迁移方便一个.db文件就能带走全部记忆支持标准 SQL 查询可以做聚合、过滤、排序自带全文检索扩展 FTS5对关键词搜索有很好的支持支持并发读写配合 WAL 模式多端口同时写也不会整库锁死。作为一个本地记忆库SQLite 完全够用而且极轻量。有的朋友可能会问为什么不用向量数据库向量检索对“语义相似”的召回确实更强但代价是需要安装额外的依赖、维护向量索引而且语义检索在小规模记忆库上的优势并不明显。几百条、几千条记忆用关键词匹配加上评分函数效果已经足够好。如果你的记忆库规模真的大到几万条也可以把 SQLite 里的content字段同步给一个向量索引但那应该是后续优化而不是一开始的负担。存储方案优点缺点适合场景JSON 文件直观、易读全量读写、并发差几十条以内玩具项目SQLite FTS5轻量、查询强、并发好需要懂一点 SQL几千到几万条记忆向量数据库语义召回强依赖重、维护成本高十万条以上/复杂语义检索2.3 检索与注入策略上下文窗口是稀缺资源存储做好了检索才是关键。很多记忆系统做出来“能存不能取”就是因为只解决了写入没解决读出来之后怎么用。我的做法分两步候选召回 预算筛选。候选召回时先把当前项目和当前对话主题作为约束条件用 SQL FTS5 检索出候选条目。召回的排序分由几个部分加权组成关键词相关度、重要度、最近访问时间、访问次数。简单说越相关、越重要、越常用、越新鲜的记忆排得越靠前。预算筛选时要设定一个“本次对话可注入的最大 token 数”。比如 600 token然后依次从排序结果里取条目每取一条先估算它占多少 token如果剩余预算不够就跳过。这保证了记忆注入不会挤占真正对话的空间。另外很重要的一点是不要把记忆条目原样塞进上下文。我会先把它们渲染成一段结构化的“记忆简报”分节排列[项目] Blog 内容维护系统 [进度] 正在实现标签页功能分类页面已完成 [规范] 输出中文先结论后分析 [事实] 用户使用本地 Markdown 文件存储草稿这样的格式对 Claude 来说比“一大坨杂糅文本”好理解得多。2.4 遗忘与压缩好记忆系统必须会“忘”人脑的机制是有遗忘的记忆系统也一样。如果不加节制的把每条记忆都永久保存最终会积累大量过时、冗余、甚至互相矛盾的条目。到那时候系统不是在帮你而是在干扰你。claude-mem提供三层遗忘机制。第一层是衰减。每条记忆都有一个last_accessed_at字段长期没有被检索到的记忆在排序时的权重会逐渐降低慢慢“沉底”不再是默认候选。第二层是压缩。每隔一段时间脚本会把若干条低价值、高相似的记忆合并为一条摘要原始条目则进入归档表。第三层是显式删除。使用者可以在任何时候执行遗忘命令把某条记忆彻底移除或标记为“不采纳”。这三层机制共同保证了记忆库不会无限膨胀也避免了“旧的错误结论一直干扰新任务”的情况。后面讲实操的时候我会给出具体的压缩和删除命令以及它们的配置方法。3. 核心模块实现详解实操实录3.1 初始化与配置定义记忆库边界安装claude-mem之后第一步是初始化记忆库目录。我习惯为每个大的工作领域建一个独立的记忆库而不是把所有东西混在一起。初始化命令大概长这样$ claude-mem init --project blog-demo --root ~/.claude-mem [claude-mem] 初始化完成 - 存储引擎: sqlite (mem.db) - 记忆目录: ~/.claude-mem - 当前项目: blog-demo - 已启用: fts5 全文索引, 自动摘要初始化后会在根目录生成一个配置文件。我用的是 TOML 格式因为可读性好、注释友好。配置文件的几个关键项如下# ~/.claude-mem/config.toml [storage] engine sqlite # 存储引擎 sqlite_path ~/.claude-mem/mem.db [retrieval] top_k 8 # 候选召回条数 max_inject_tokens 600 # 注入记忆简报的最大 token 数 min_score 0.15 # 相关度分数下限 [summary] model claude-sonnet # 用于提取摘要的模型 max_turns 20 # 一次最多摘要多少轮对话 schedule session_end # 自动摘要时机会话结束时触发 [projects] default personal # 默认项目标签这里最容易忽略的是max_inject_tokens。它的作用是防止“记忆简报太长了把真正的提问挤没了”。一开始我设成 2000结果每次对话光读简报就消耗一大半窗口非常不划算。调到 600 之后效果反而好很多。初始化的过程实际上是建好了表结构。核心表大概是这样CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, -- fact / conversation / project / skill project TEXT NOT NULL DEFAULT global, -- 项目归属 content TEXT NOT NULL, -- 记忆正文 tags TEXT DEFAULT , -- 扩展标签 importance INTEGER DEFAULT 3, -- 重要度 1-5 source TEXT, -- 来源如手动 / 自动摘要 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_accessed_at DATETIME, access_count INTEGER DEFAULT 0, archived INTEGER DEFAULT 0 ); CREATE INDEX idx_memories_project ON memories(project); CREATE INDEX idx_memories_category ON memories(category); CREATE INDEX idx_memories_importance ON memories(importance); CREATE VIRTUAL TABLE memories_fts USING fts5(content, tags, content);索引的作用是让“按项目查”“按分类查”“按关键词查”都不至于全表扫描。FTS5 的虚拟表单独存了一份全文索引写入时同步更新查询时用MATCH语法就能做关键词匹配效率不错。3.2 写入链路从对话到结构化记忆写入分两种手动写入和自动沉淀。手动写入适合那些你希望即刻生效的信息比如项目的临时决定、用户偏好调整。命令的形式很直接$ claude-mem remember --project blog-demo --category fact \ 目标读者主要是有 3 年以上经验的独立开发者术语可以偏专业 $ claude-mem remember --project blog-demo --category skill \ 输出中文回答时先给结论再给原因和代码示例自动沉淀则发生在每次对话结束之后。这里的关键是不要把所有原始对话都存下来而是调用模型对最近若干轮对话做一次摘要并把结果拆成符合四类分类的记忆候选。伪代码如下def summarize_conversation(conversation, project, existing_memories): prompt f 请从最近 {len(conversation)} 轮对话中提取值得长期记忆的信息。 要求 1. 忽略寒暄、临时内容、已被纠正的错误结论 2. 输出 JSON 数组每项包含 category/content/importance 3. 避免与现有记忆高度重复的内容 现有记忆 {existing_memories[:800]} results llm_complete(prompt) return parse_json(results)自动摘要出来的条目不会直接写入而是进入一个pending状态在终端里展示给使用者确认。我实际用下来确认这一步非常必要。模型有时会把对话中的一个观点当成既定事实如果不人工看一眼就写入后面就可能带着错误信息做决策。宁可多花十秒确认也不要让一条错误记忆在库里躺一个月。写入前还要做去重。我会把新条目的content和库里已有的同分类条目做一次相似度对比如果相似度过高就把新条目降级为“对既有条目的补充”提示是否合并。3.3 读取链路让 Claude 每次开工先“述职”读取链路决定了 Claude 每次对话开场时的状态。我的策略是在开启新会话之前执行一次召回构造记忆简报然后把简报作为上下文的开头注入。实际命令和输出类似这样$ claude-mem recall --project blog-demo --query 标签页功能开发进度召回逻辑的简化版伪代码如下def build_relevant_context(query, project, top_k, max_tokens): candidates search_memories(query, projectproject, ktop_k * 3) # 按综合评分排序 ranked sorted(candidates, keylambda m: score(m), reverseTrue) selected, budget [], max_tokens for m in ranked: need estimate_tokens(m.content) if budget - need 0: continue selected.append(m) budget - need if budget 80: break return render_memory_brief(selected)其中score(m)是综合评分函数def score(mem): keyword_score mem.fts_rank importance_score mem.importance / 5.0 recency_score days_since(mem.last_accessed_at) access_score min(mem.access_count, 10) / 10.0 return (0.5 * keyword_score 0.2 * importance_score 0.2 * recency_score 0.1 * access_score)关键词相关度的权重最高因为直接命中通常意味着这条记忆和当前任务关系最紧密重要度和新鲜度各占两成防止老旧的记忆占用宝贵的注入名额访问频率作为辅助信号让高频使用的条目更稳定地出现。注入给 Claude 的格式要固定。我自己用的是这样的以下是你需要了解的上下文记忆简报 【事实】 - 目标读者是有 3 年以上经验的独立开发者 【项目】 - 正在开发 Blog 内容维护系统当前进度在标签页功能 【技能】 - 输出中文回答时先给结论再给原因和代码示例 【来源说明】 这些记忆来自本地记忆库如果与本次对话中的最新信息冲突以本次对话为准。最后那句“以本次对话为准”很重要它能避免旧的记忆压过新的明确指令给模型留一个纠错出口。这是我踩过几次坑之后总结出来的关键细节。3.4 记忆删除、归档与手工修正记忆系统如果没有退出机制迟早会被垃圾信息填满。删除命令设计得很干净# 按 id 删除一条 $ claude-mem forget --id 42 # 按项目删除所有过时记录 $ claude-mem forget --project blog-demo --category conversation --before 2025-01-01 # 归档而不是删除留一条后路 $ claude-mem archive --id 128归档和删除的区别在于归档条目不参与检索和注入但完整保留在archived_memories表里万一之后发现还需要还可以恢复。手工修正也容易忽略。自动摘要沉淀出来的内容我每周会做一次检查发现有偏差的条目直接就地改内容或者补充说明字段。修改句子本身不难难的是建立“定期维护记忆库”的习惯。我的做法是给这个动作专门设了一个备忘录提醒每个周日处理一次每次十五分钟专门看三条内容这周新增了哪些记忆、有没有互相矛盾的条目、有没有已经过时的项目进度还挂在库里。4. 常见问题与排查技巧实录4.1 检索命中率低相关记忆调不出来这是最常被问的问题表现是明明库里有一条记忆很相关但发起对话时没有出现在简报里。排查方向一般是三个第一看查询条件。如果当前项目被识别成global但记忆条目归属在具体项目下面自然就查不到。我会先检查当前 project 是否传递正确再检查记忆条目的 project 字段。第二看关键词。FTS5 默认匹配是子串级别的歧义不大但如果记忆内容和查询词完全是同义替换比如库里写的是“数据同步”而你问的是“镜像机制”那就没法命中。解决方法是在记忆条目的tags字段里补上同义词和别名。比如$ claude-mem tag --id 17 --add 同步,镜像,replica,副本第三看min_score阈值。如果设得太高一些评分稍微低一点的相关条目会被过滤掉。我自己的经验是设为 0.15 左右比较宽松然后靠排序来决定谁进简报而不是靠硬阈值一棍子打死。有一个更实用的技巧把摘要生成阶段就让模型给每条记忆补一句“触发场景描述”。比如一条“用户希望批量导入支持 CSV”的记忆触发场景可以写成“当讨论导入功能、批量操作、数据迁移时这条优先”。这样查询时即使关键词不是完全一致FTS 也能通过场景词命中。4.2 记忆条目互相冲突回答前后矛盾冲突的典型场景是库里有一条旧记忆写着“数据库使用 SQLite”后来又更新为“使用 PostgreSQL”但旧条目没有被清理导致 Claude 在不同会话里给出不同答案。解决办法至少有四个层面。第一写入新条目时主动检查冲突。自动摘要流程里新增条目前会和同分类、同项目的记忆做一次语义相似度对比如果发现两句话在核心命题上是矛盾的就把新条目标记为conflicts_with旧id并在确认界面显示出来由人裁决。第二给记忆加“有效期”或“适用条件”。不是所有记忆都是永久成立的。有些只适用于某个阶段。我给记忆表加了一个conditions字段可以填whenproject_phase in (v1, v2)这样的描述。注入时如果当前条件不匹配这条记忆就不进简报。第三时间优先原则。当两条记忆描述同一主题但结论相反时默认取created_at更新的那一条。这个原则要写进渲染逻辑里注入前做一次冲突消解。第四定期人工裁决。我每两周会把库里所有conflicts_with非空的条目拉出来逐条确认哪条作废。作废的并不直接删掉而是archived1避免未来再被检索到。4.3 上下文被记忆撑爆出现这个问题的标志是对话开始后你发现模型对你的真实问题的回答变得“敷衍”或者简报占了太多空间对话窗口的可用的上下文剩余不多。核心原因是max_inject_tokens设得太大同时召回的候选条目里塞入了大量低价值内容。解决办法有三个方向。第一个方向是硬性缩减预算。把max_inject_tokens从 600 下调到 300只保留最高分的前两三条。这个改动通常能立刻感受到对话质量的提升。第二个方向是分层注入。事实和项目这两类高频信息全部注入对话和技能类信息只注入部分。比如def build_injection_plan(selected): plan [] for mem in selected: if mem.category in (fact, project): plan.append(mem) # 必注 elif mem.importance 4: plan.append(mem) # 高重要度 elif mem.access_count 5: plan.append(mem) # 高频使用 # 其余先不注入保留为一个索引 return plan第三层是“记忆索引”模式。如果记忆库实在庞大可以把记忆简报做成两层第一层是几十条摘要第二层是完整条目链接。Claude 看到摘要后如果要深入了解可以向工具发出查询请求。这个模式在的项目里非常管用因为摘要数量可控也不会撑爆上下文。4.4 多项目记忆互相污染如果你和我一样同时维护两三个项目会发现最要命的不是记忆不够而是项目之间的记忆串场。比如在写博客系统的时候脑子里出现了“电商项目的数据表设计规范”回答里就掺杂着完全无关的技术选型。这种问题的根源是记忆写入和检索时没有严格的项目边界。对策也很明确所有记忆写入必须显式指定--project没有指定的一律落到global检索时只查“当前项目 global”两层禁止跨项目召回自动摘要流程里项目名要从当前会话上下文里自动提取保证沉淀时不会写串。我的配置里还有一个硬校验如果库里的记忆条目的source是某个项目专属会话但是它的project字段不是这个项目写入时直接拒绝并提示人工检查。这个小校验在早期帮我拦下了不少脏数据。global层的记忆也要控制数量。我会定期把global里那些明显只属于某个具体项目的内容重新归类而不是任由它长期留在全局区。5. 从工具到工作流claude-mem 的扩展玩法5.1 多设备同步把记忆库纳入同步盘或用代码仓库管理claude-mem的记忆库是一个普通目录里面有一个mem.db文件、一个配置文件和一个归档目录。这个结构非常适合放进同步盘或者一个私有的代码仓库里。放进同步盘的好处是办公室电脑、个人电脑、笔记本三端共享同一份记忆上午在公司沉淀的结论晚上回家做副项目时也能复用。需要注意的是同步冲突问题。SQLite 的写并发在单机 WAL 模式下很舒服但它不是为多端实时同步设计的。如果你只是把mem.db丢进同步盘同步工具会经常检测到文件不一致然后冲突复制出一个mem (冲突).db这就很麻烦。我的做法是把 SQLite 作为“本地工作库”再利用一个备份脚本定期导出为 JSON 快照同步盘只同步快照。恢复时用claude-mem restore导入。这样虽然牺牲了一些实时性但换来的是稳定不会被同步工具的冲突机制搞崩。5.2 对接更多 AI 工具用同一份记忆服务多个入口claude-mem的设计是底层存储和上层对话解耦的。如果你不想每次手动执行命令可以把记忆能力封装成一个模型上下文协议服务这样 Claude 的命令行客户端和桌面客户端都能调用同一个记忆库。一个很简化的工具定义长这样server.list_tools() async def list_tools(): return [ Tool( nameremember, description写入一条长期记忆, inputSchema{...}, ), Tool( namerecall, description检索相关记忆条目, inputSchema{...}, ), Tool( nameforget, description删除指定的记忆条目, inputSchema{...}, ), ]这样做的好处是模型不是在开场时被动接收一份简报而是在对话过程中发现“这个信息值得记下来”时主动调用remember工具写入。换句话说记忆从“对话前的一次性注入”变成了“对话中的持续协作”。更关键的是这套能力不绑定某一家模型。只要实现了相同的工具接口任何模型都能调用这同一个本地记忆库。你今天的记忆沉淀可以在明天换一个模型时继续使用知识资产不会被锁死在单一工具上。5.3 给自己定一套维护节奏我实际使用这段时间最深的体会是claude-mem并不复杂难的是建立维护记忆的习惯。我的维护节奏非常简单每次重要对话结束花两分钟过一遍自动摘要的候选确认无误再写入每周日花十五分钟处理冲突条目、过期进度、补充 tags每月做一次压缩归档把低价值的杂项合并或被移除。这样的节奏坚持下来记忆库的规模增长是缓慢而健康的。我的主项目记忆库运行到现在活跃条目保持在几百条量级每次注入成本都很低但 Claude 对我的项目上下文的理解已经相当稳定很少再出现“重新自我介绍”的尴尬。最后一个建议任何记忆系统都只是辅助真正的决策权始终在你手里。遇到重要结论该把核心逻辑写在专门的设计文档里就写别指望记忆库替你承担一切。claude-mem的价值是帮你节省那些“反复解释背景”的时间而不是替你建立一个永不犯错的数据库。良好的记忆管理本质上是对自己工作流的持续整理这比工具本身的技巧更值得投入。