ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

给Claude CLI装个“记忆库”:跨会话记忆注入的完整实践

给Claude CLI装个“记忆库”:跨会话记忆注入的完整实践 每天跟Claude的CLI工具打交道最折磨我的不是它回答得不好而是它永远不记得上一个会话里我们讨论过什么。今天问清楚了接口鉴权的完整方案明天新开一个会话它又一脸茫然地从头猜起。这个问题困扰了我很久直到我写了claude-mem——一个给Claude注入跨会话记忆的轻量扩展。简单说它做的事情就是在对话之外维护一本“备忘录”下次会话开始前自动把相关记录翻出来塞给模型看。核心链路只有三步记录、检索、注入。这篇博文我会把整个设计思路、核心实现细节、接入工作流的方法和排查问题的心得完整写出来。如果你也正在被无状态对话折磨或者纠结怎么给自己的命令行AI助手做持久化记忆这篇文章应该对你有用。1. 失忆的AI与需要记忆的我1.1 无状态会话到底是个什么状态先从模型本身聊起。大多数会话式AI尤其是通过API方式接入的模型本质上都是一个无状态函数你给我一段文本我还你一段文本。它不保存任何“之前发生了什么”。你在同一个客户端界面里看到的上下文连续是因为客户端在背后把所有历史消息一遍又一遍地重新发给模型。一旦你开了一个新会话或者切换了项目目录历史就不存在了。这听起来像是基本常识但真正用起来才会感受到它的拧巴。我之前维护一个跨平台同步项目连续几天都在通过CLI工具排查文件冲突问题。第一天确认了要用“版本向量加本地时间戳”作为冲突判断依据第二天新开会话我忘了带上下文模型就开始给我推“按修改时间覆盖”这种完全相反的方案。倒不是说它错而是它根本不知道昨天我们已经对比过三种方案最终排除了这个选择。同样的论证又重新走了一遍来回浪费了快一小时。这种场景多了之后我开始意识到问题不在模型本身而在于对话之外没有任何结构化的沉淀机制。聊天记录是流水账翻起来累也不能被自动使用。我需要的是一个独立的记忆层让模型在每次会话开始时能主动看到“这个项目之前已经确认过哪些结论”。claude-mem就是奔着这个需求去的。它不试图改变模型的工作方式也不去做微调而是在会话和模型之间加一个外部记忆中枢。这个中枢不参与推理只负责在恰当的时候把恰当的历史结论拿出来。1.2 哪些场景最适合外挂记忆不是所有使用场景都需要永久记忆我用了几个月下来觉得有几个场景收益特别明显长期项目维护。架构决策、接口约定、历史bug的根因分析这些内容一旦确认短期内不会变。与其每次重新讨论不如记下来直接复用。个性化风格保持。比如你要求代码输出严格遵守某种命名规范或者文档必须带使用示例这种偏好属于全局级记忆任何会话都应该生而具有。多步骤工作流。今天做到一半的迁移方案明天接着做模型需要知道昨天已经完成了哪几步、卡在哪个环节。脚本化批量调用。用命令行工具写自动化脚本时如果每次调用都无状态很难让模型在连续多轮任务中保持一致口径。反过来说有些内容不适合记忆比如一次性问答、跟项目无关的闲聊、时效性极强的实时信息。把这些也塞进记忆库只会让检索噪声变大反而把真正重要的结论淹没了。这个“记忆边界”的问题后面专门讲。2. claude-mem怎么设计才不失控2.1 先想清楚外挂记忆不是微调很多人一听“让模型记住历史”第一反应是做微调。这个思路方向其实不对。微调适合让模型学习一种稳定的能力模式比如学会某种代码风格、学会某种文档格式。但要让它记住“昨天我们决定用A方案不用B方案”微调既慢也不灵活而且一旦结论变化你没法让模型快速忘掉旧知识。外挂记忆最大的优势是实时可写、实时可查、实时可删。它和模型本身完全解耦今天加一条记忆下个会话立刻生效发现记错了直接改文件或者删除效果马上纠正。这比重新训练轻量得多也符合个人工具、小团队工具的定位。我把claude-mem定位成“第三层”。第一层是模型权重负责推理能力第二层是客户端上下文窗口负责当前会话的连贯性第三层就是记忆库负责跨会话的知识沉淀。前两层都不归我管我只需要把第三层做好。2.2 存储、检索、注入三段的选型整个记忆系统拆开看就是存储、检索、注入三段。存储端我一开始在纯SQLite、JSON文件、Markdown文件之间犹豫。纯SQLite的好处是查询方便、索引强大但坏处是内容不可读你没法直接打开数据库浏览一条记忆长什么样手工修改更麻烦。JSON文件结构灵活但嵌套多了以后人眼很难快速扫描。最后我选了“Markdown文件为主SQLite索引为辅”的混合方案。记忆正文用Markdown文件保存每个文件一条记忆文件名就是记忆的简短标题文件头部用YAML front matter存元数据。SQLite只用来建全文索引和标签索引方便快速检索。这个方案实测下来很舒服。日常浏览记忆时直接用编辑器打开目录扫一眼文件列表就知道记了什么。要修改一条记忆直接编辑Markdown文件。而检索时又可以利用SQLite的FTS全文搜索来提升速度。相当于同时拿到了人类可读性和机器可查性。检索端我采用两级策略。第一级是粗筛用关键词、标签、项目名过滤出一个候选集。第二级是精排对候选集做相似度打分把最相关的排到最前面。粗筛保证效率精排保证质量。后面的章节会详细展开打分公式。注入端核心矛盾是token预算。模型上下文窗口是有限的记忆不能无限制地塞进去。我采用的策略是给记忆注入分配一个固定预算比如总上下文的20%超出预算的部分按权重截断。高重要性、最近更新、与当前问题词面相似度高的记忆优先保留。2.3 整体架构与模块划分claude-mem的架构并不复杂核心就四块模块职责关键设计写入层从会话内容中提炼记忆写入文件只记结论不记流水账写入前做格式校验存储层维护Markdown记忆文件和SQLite索引元数据与正文分离文件名带关键标签便于人查检索层根据当前问题召回相关记忆关键词粗筛 相似度精排支持重要性加权注入层把记忆组装成可用的上下文块分配token预算标记块隔离记忆与常规上下文这个分层的好处是每一块都可以独立替换。比如你今天不想用SQLite想换成纯文件扫描只改存储层就行写入层和注入层完全不受影响。我第一次实现时就因为分层清晰给检索层换方案只花了不到半天。3. 核心实现细节逐个拆3.1 记忆文件一份人能看懂、机器能检索的Markdown记忆文件是整套系统的地基所以格式设计我花了很多心思。先看一下一条记忆的实际文件内容--- tags: [接口鉴权, 网关, 限流] project: cross-platform-demo importance: high status: active created: 2025-01-10T14:22:0008:00 updated: 2025-01-15T09:30:0008:00 --- # 接口鉴权统一走新网关 在模拟项目X中联调环境里所有接口鉴权统一走新网关旧网关只保留灰度流量。 2025年1月起新接入的服务必须直接对接新网关不要再单独申请旧网关权限。为什么用YAML front matter因为它能把结构化的元数据和正文分开机器解析起来非常方便。字段的设计是有讲究的tags检索时的第一道筛子多打几个标签通常不会错但要避免过度打标否则标签本身就失去区分度。project记忆的归属项目避免多项目内容互相串场。importance优先级标记high表示关键结论normal表示一般背景low表示可记可不记的内容。status记忆的生命周期状态。这个字段很关键因为不是所有结论都永远成立。方案变更了旧记忆就应该标记成superseded或者archived不再参与注入。updated排序时会用到最近的更新通常意味着当前仍然有效。我给每条记忆定的原则是“一条记忆只讲一个结论”。听起来简单执行起来容易犯浑。很多人会把一个会话里的所有讨论塞进一条记忆结果就是一条记忆里同时包含问题背景、讨论过程、三个方案、最终结论检索的时候反而抓不到重点。正确做法是拆开每个独立结论单独成文件。3.2 写入链路从对话里提炼出“值得记”的东西写入链路最忌讳的是把对话记录原样保存。如果你只是把聊天记录转存成文件那根本不叫记忆叫日志。记忆是要经过提炼的。我的写入流程分成三步第一步捕获源材料。在执行命令时把当前会话的上下文导出为纯文本我通常只保留最后几轮对话因为前面的讨论过程已经淹没在细节里了。如果一次会话非常长我会在会话结束后统一导出。第二步提炼候选记忆。有三种方式最省事的做法是给会话过程加一个自动判断当对话中出现“我们决定”“最终方案是”“结论是”这类话术时就标记为候选记忆。这个规则简单粗暴但经常漏掉重要信息。更可靠的做法是让模型自己抽导出一段文本让模型生成“可持久化的结论清单”对结论逐条打分。我实践下来两种方式结合最好规则命中直接收规则没命中的再让模型判断。第三步归并去重并写入。新记忆写入前要先检索一遍库里是否已经有相同主题的记忆。如果有判断是新结论覆盖旧结论还是补充背景。具体到实现就是比对标题和关键标签命中后用新版替换旧文件同时把旧文件移到archived/目录而不是直接删除。这个习惯帮了我大忙——后来有两次新方案被推翻需要找回旧结论归档文件正好还在。写入链路还有一个非常容易被忽视的点格式校验。模型在自动生成记忆文件时偶尔会把front matter写破或者把正文的Markdown层级弄乱。我所有的写入操作都会过一道校验器解析YAML失败或者缺少必填字段时直接拒绝写入避免脏数据污染记忆库。好的记忆库一定是有边界的宁可不记也不能记一堆解析不了的东西。3.3 检索链路如何在几百条记忆里捞出最相关的检索是记忆系统的心脏写得不舒服整个系统都用不起来。我先后试过纯关键词方案、纯向量方案、混合方案最后留下的是混合方案。先说纯关键词。SQLite自带FTS5全文搜索可以很轻松地实现关键字命中。它的优点是快、透明缺点是很死板。你说“网关鉴权”它不会自动匹配到写过“统一入口鉴权”的那条记忆因为字面不重叠。中文场景下这个问题更严重同一个意思可能有多种完全不同的表达。再说纯向量。把每条记忆和当前问题都转成向量用余弦相似度排序。优点是可以捕捉语义相似即便字面完全不一致也能命中。缺点是依赖嵌入模型的本地部署质量而且纯向量检索容易产生“假阳性”——相似度高但实际不是一回事。混合方案的具体实现可以这样描述先用关键词和标签做粗筛拿到候选集再把候选集和当前问题做向量相似度排序最后再做加权重排。这个方案在速度和准确率之间取了一个平衡。如果嵌入模型没配好就直接退回纯关键词至少还能用。重排阶段的打分权重我建议按这个思路走向量相似度贡献50%权重解决语义匹配问题。标签命中贡献20%权重快速对齐主题。重要性贡献20%权重高优先级的结论应该更容易被想起。时间新鲜度贡献10%权重最近更新的内容往往时效性更强。这个比例不是死的实际用的时候可以根据项目性质调整。比如一个快速迭代的项目时间新鲜度权重可以调高到20%一个偏历史沉淀的项目重要性权重可以再高一点。检索结果的数量限制同样重要。别贪多默认取前5条就够了。取太多注入层塞不下还会让模型看到一堆互相矛盾的上下文。判断一个检索配置合不合适最简单的标准是把检索结果打印出来看一眼如果其中一半内容和当前问题无关那就需要收紧。3.4 注入策略怎么塞进上下文而不喧宾夺主检索到记忆之后下一个问题是怎么注入。注入的位置很讲究。不能放在最前面否则模型会把记忆当成交互指令也不会放在历史消息的最后那样容易被上下文窗口截断。我采用的方案是放在系统指令块之后、用户消息之前用明显的标记块包起来让模型一眼看出“这段是记忆不是本次对话内容”。具体长这样[记忆上下文] mem importancehigh projectcross-platform-demo 接口鉴权统一走新网关旧网关只保留灰度流量。 /mem mem importancenormal 控制台日志级别默认设为info排查连接问题时临时切到debug。 /mem [/记忆上下文]标记块的作用就是隔离。模型可以理解“这里的内容是外部记忆需要结合当前对话一起思考”而不会误以为你在命令它执行某些操作。token预算这块我用了一个动态截断机制。举个例子如果模型上下文窗口是64K我会给记忆上下文最多留出8K的预算也就是12%左右。先把检索到的记忆按权重排序依次往块里填填到接近预算限制就停。如果当前对话已经非常长预算还要进一步压缩保证记忆块不会挤压到正常的对话推理空间。有一个细节值得注意注入记忆之后模型偶尔会直接照着记忆念答案缺少对当前问题的主动分析。我会在记忆块后面加一句“记忆仅供参考请结合本次对话内容判断”实测下来能明显减少模型机械复读记忆的情况。4. 把它挂进工作流跟着我做一遍4.1 初始化目录与配置拿一个实际项目来演示。假设我在做一个跨平台网络同步工具项目代号叫模拟项目X。初始化记忆库后目录结构是这样的~/.claude-mem/ ├── config.yaml ├── memories/ │ ├── global/ │ ├── projects/ │ │ └── cross-platform-demo/ │ │ ├── active/ │ │ └── archived/ ├── index.db ├── logs/ └── export/配置文件的初始内容# config.yaml storage_path: ~/.claude-mem/memories index_path: ~/.claude-mem/index.db default_project: cross-platform-demo inject_budget_ratio: 0.12 importance_default: normal retrieval: top_k: 5 vector_weight: 0.5 tag_weight: 0.2 importance_weight: 0.2 freshness_weight: 0.1初始化这一步没啥复杂的就是建目录、建数据库。真正花时间的是配置检索参数尤其是top_k和权重比例这两个值直接决定了记忆回读的命中率。4.2 三种接入CLI工具的方式接入方式我试过三种按使用体验排序方式一包装脚本。用一个本地的包装脚本包住命令行工具用户输入任何问题之前脚本先调用一次检索接口把返回的记忆拼接到当前问题上再发给模型处理。这是最稳定的一种方式因为它在进程层面就是“先查记忆再发请求”完全不需要动工具本身。我日常主要用这种方式。方式二管道过滤。如果你的命令行工具支持从标准输入读取文本可以写一个轻量管道把历史会话导出文本作为输入源处理完的结果再传给模型。这种方式的缺点是调试麻烦管道一旦某个环节输出格式不对整个链路就断了。方式三手动命令。不改变工具的调用方式而是在需要时手动执行“查询记忆”和“添加记忆”命令把结果复制到当前对话中。这种方式最灵活适合不想改任何现有配置的场景缺点是多了一步手动操作。三种方式不是互斥的。我实际的使用姿态是日常用包装脚本做自动注入遇到复杂问题需要彻查历史时再手动跑一次记忆查询把完整结果作为补充材料贴进对话。自动化负责“日常想起”手动化负责“深度回顾”。4.3 从一个完整案例看记忆的写入与回读这里记录一个实际流程能直观展现记忆的工作方式。第一次会话中我处理模拟项目X的接口鉴权问题讨论到最后明确了结论手动执行添加记忆命令claude-mem add 接口鉴权统一走新网关旧网关只保留灰度流量 \ --tags 接口鉴权,网关,限流 \ --project cross-platform-demo \ --importance high添加成功后记忆库中出现了一个新Markdown文件。第二天我新开一个会话直接问“新模块接入鉴权应该走哪里”包装脚本自动执行检索命中前一天写下的That条记忆注入到上下文里。模型给出的回复直接沿用了新网关方案并且补了一句“旧网关只保留灰度流量不应新开权限”。这个案例里让我印象最深的点是模型回答时的语气就像真的记得这件事一样完全没有“昨天我们不是聊过吗”的尴尬。这就是外部记忆的价值。再举一个踩过坑的例子。有一段时间我在记录项目的时候偷懒把对话里讨论过程也原封不动写进记忆文件结果检索时经常把“候选方案”当成“已确认结论”注入进去模型开始一本正经地推荐我们早就否决掉的方案。后来我严格给记忆文件分状态候选方案一律标记status: candidate确认过的结论才标记status: active注入时默认过滤非active状态问题才消失。4.4 定期维护修剪、重建、备份记忆库不维护半年之后就变成垃圾场。维护工作有三件第一定期清理过期内容。我习惯每周花十分钟浏览一遍项目目录下的记忆文件把已经失效的结论移到archived/目录。批量操作时可以直接改状态字段claude-mem update old-gateway-plan --status superseded第二重建索引。文件结构变动多了以后SQLite索引可能和磁盘文件不一致。写一个定时任务每天凌晨把索引重建一次保证检索结果的准确。claude-mem reindex第三备份。Markdown记忆文件是纯文本备份很简单直接把整个~/.claude-mem目录做一次归档压缩。我个人会连同SQLite索引一起打包这样恢复之后不需要重新建索引。5. 我踩过坑直接给你排查速查表5.1 记忆污染旧结论压过新事实这是记忆注入最典型的问题旧记忆写得太坚定导致模型在新情况面前仍然照着旧结论回答。我遇到一次项目里某个接口从自研方案切到了开源方案但库里还留着一条旧的“接口内部实现说明”结果模型一直在按旧实现推断完全无视当前代码已经重构。解决办法分三层第一方案变更后马上把旧记忆标记为superseded确保默认不参与注入第二在检索层过滤状态字段只把active记忆纳入候选第三重要结论变更后再补一条新记忆覆盖旧主题让检索排序时新记忆排前面。好记性不如烂笔头前提是烂笔头要常新。5.2 Token爆炸上下文塞太满反而变傻记忆注入太多会导致两个问题一个是直接撑爆上下文窗口请求直接失败另一个是模型注意力被记忆块吸走忽略当前的用户问题。我有一次配置了极高的检索数量一口气注入二十多条记忆结果模型回答越来越发散最后甚至开始复读记忆文件里的原文。现在的固定策略是先给记忆块分配一个明确预算超出就截断。宁可少注几条也要保证注入的每条记忆都和当前问题强相关。对话特别长时记忆预算还要手动再压缩一半。5.3 中文语义检索命中差纯关键词检索在中文场景下表现很不稳定。“鉴权”“认证”“授权”三个词在很多语境下说的是一件事但字面完全不重叠。如果只做关键词检索经常捞不到记忆。我的建议是配置一个本地嵌入模型做语义向量如果环境限制不能上本地嵌入模型就退而求其次手动扩充标签把同义词、上下位词都打上。比如“认证”相关记忆就同时打认证, 鉴权, 登录, token多个标签用规约法弥补语义检索缺失。5.4 隐私与边界记忆库就是隐私库记忆文件里面存了大量项目细节可能包含密钥、内部架构、个人偏好。这些文件存在本机默认不做任何网络同步。我有一次把记忆目录顺手放进了云同步盘意识到之后立刻移出来——记忆库绝不能和公共云盘绑定。本地安全方面建议把记忆目录权限设为仅当前用户可读写如果涉及高敏感内容可以给目录做一层磁盘加密。备份归档也注意不要用明文压缩包传到不信任的地方。5.5 其他高频问题速查问题典型表现排查思路记忆不生效新会话里模型完全不知道旧结论先确认检索命中再确认注入标记块格式正确注入内容重复同一条记忆反复出现检查写入链路是否做了去重看是否多个入口重复添加记忆文件解析失败命令报YAML格式错误用校验器定位具体文件修复front matter字段检索结果杂乱注入五条里三条无关收紧top_k提高向量相似度阈值检查tags是否过度宽泛多项目串场项目A的结论跑到项目B确认每条记忆的project字段注入时按当前项目过滤性能变慢检索响应时间快速上升检查索引是否过期执行重建索引5.6 给新手的三个避坑建议新手刚开始做这种记忆工具时最容易犯的思路错误有三个第一一开始就想搭一个“全自动”记忆系统希望模型自动把什么都记下来。这个目标太难自动抽取质量很难控制污染风险很高。我建议先手动添加记忆等你对什么内容该记、什么不该记形成手感之后再逐步放开自动抽取。第二记忆条目太粗。一条记忆里塞三四个结论后面检索时根本定位不到。坚持“一记忆一结论”文件多了不要怕检索层会帮你搞定排序。第三忽略记忆的时效性。今天记下的内容下周可能就过期了。状态字段不是摆设项目进入新阶段后主动把旧记忆标记成过期。记忆库小但干净比大而杂有用得多。6. 一段关于“记忆边界”的自我反思跑了大半年claude-mem之后我最大的体会是记忆工具的核心价值不是“记住”而是“在需要时想起”。把一切对话都存下来不难难的是在几百条记忆里精准捞出当前需要的那三五条。这就要求记忆库本身保持克制——重要的结论记清楚候选方案标明白过时的内容及时归档。另一个体会是记忆最好分层。全局记忆放通用的个人偏好和工作习惯项目记忆放具体的架构决策和踩坑结论临时内容放短时记忆区并按期清理。层次分开之后检索的压力会小很多注入的时候也更容易决定哪部分必须带、哪部分可以省略。最后提一个扩展方向。我目前做的claude-mem写操作基本靠手动触发和简单规则下一步我打算加一个异步学习组件每次会话结束后自动把新出现的“决策类内容”提取成候选记忆经过相似度去重和重要性打分后进入待确认队列我再批量审核。这一步如果能稳定跑通整个记忆库的维护成本还能再降一个台阶。如果你也在做类似的方向建议先从手动加记忆开始跑通一条最小闭环再谈自动化。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进