多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

AI助手记忆增强:用Markdown为CLI打造持久记忆系统

AI助手记忆增强:用Markdown为CLI打造持久记忆系统 如果你也每天都在命令行里跟同一个 AI 助手反复交接工作你迟早会冒出这个念头它为什么不能记住我昨天告诉它的那件事。我连续三周被同一个问题卡住每次都要重新解释一遍项目的前因后果——不是因为助手笨而是因为它每一次启动都是全新的一次会话模型唯一的记忆来源就是我这次打进去的提示词。受够了之后我给自己做了一个叫 claude-mem 的东西一个挂在 CLI 会话旁边的记忆模块。它负责把每次对话里值得留存的结论、决定、错误和偏好抽出来写成结构化的 Markdown 文件在下一次会话开始时把相关的记忆重新注入上下文。这篇文章不是产品宣传是我把它从零搭起来、用了半年之后的设计思路和踩坑记录如果你也在折腾类似的方向应该能直接拿走不少经验。1. 先搞清楚痛点会话没有记忆上下文再大也白搭1.1 痛点复盘上下文窗口再大也装不下昨天我最早以为只要模型的上下文窗口够大把整个项目源码都塞进去就够了。后来发现完全不是一回事。窗口再大解决的也只是这一次会话内能看到多少信息的问题它解决不了上次会话里我们做出的决定还存不存在的问题。我做一个跨端重构的时候昨天刚敲定了一个接口的弃用顺序今天新开一个会话助手一本正经地建议我用那个已经被我和它讨论过三次、决定要废弃的方案。那一刻我意识到这不是模型的智力问题是我的工作流里缺了一层基础设施。我大概估算过成本。项目背景加技术栈说明每次会话开头都要重新铺一遍大概一千字左右如果每天开十个会话光重复自我介绍这件事一周就要消耗几万字的输入。更贵的不是 token 费用而是我的时间——我必须在每次会话里重新把语境讲清楚讲漏一处后面就得返工。1.2 claude-mem 要回答的三个问题动手之前我把需求拆成了三个问题记住什么、存在哪里、什么时候用。记住什么指的是记忆的粒度。不是把整段对话录音保存下来那只是存档不是记忆。真正有用的记忆是结论、偏好、决定、错误和可复用的解法。存在哪里决定了记忆能不能被检索、备份、编辑。什么时候用决定了记忆是主动注入还是被动查询——每次会话开始前把最相关的几条记忆混进上下文效果和用户自己想起来了再手动问完全不一样。这三个问题想清楚之后claude-mem 的定位就明确了它不是一个聊天机器人的人设记忆而是一个本地优先、纯文本、可审计的工作笔记侧车。它跟 CLI 助手分开运行通过文件系统和事件钩子交互不侵入模型本身的权重也不依赖任何云端服务。1.3 设计目标像一个常驻的工作笔记我给 claude-mem 定的设计基调是它要像我桌边的一本硬壳笔记本而不是一个黑盒。模型每次会话结束它会把值得记的东西抄进笔记本每次会话开始它会把相关的条目翻出来摆在桌面上。这本笔记本必须是人能直接读、直接改、直接 git 提交的——因为这意味着记忆库本身可以被审查、被纠错、被版本化。很多 AI 记忆方案喜欢把记忆藏进向量数据库但对我来说记忆的第一读者不是数据库是人。这个判断在后面帮我避了好几个大坑。2. 存储选型为什么我把记忆库做成了 Markdown 而不是向量数据库2.1 我对比过的三种存储方案动手写第一行代码之前我先对着三种方案列了一张对比表分别是关系型数据库SQLite、向量数据库带嵌入的本地方案和纯 Markdown 文件。选型不是越复杂越好要看匹配的使用场景。方案检索能力可读性版本控制维护成本适合阶段SQLite强支持结构化查询差需要工具打开一般不适合 diff中需要高频统计、关系查询向量数据库强支持语义相似检索差不可直接编辑差高需要管索引、重嵌入记忆量大到纯文本撑不住Markdown 文件中靠 grep、名称和标签极好可直接阅读编辑极好天然适合 git低文件夹加文本早期和中期个人或小团队我当时的需求很清楚记忆量在初期撑死几百条语义检索是锦上添花但人能直接改错是硬需求。向量库和 SQLite 的编辑链路都太反人性了一条记忆错了你得先查到记录再写更新语句而 Markdown 只要打开文件删掉那一行。所以我选了 Markdown而且直到现在我也没后悔。2.2 Markdown 记忆的三个底层优势第一个优势是可读性。记忆是给模型看的但更是给人看的。模型读 Markdown 和读一段二进制编码后的向量理解的准确度完全不一样。模型在推理的时候碰到一段格式良好的 Markdown 记忆比碰到一段从向量库里召回后重新拼装的片段要自然得多。第二个优势是工具链。grep、rg、fzf、git diff、CI 检查所有这些现成的 Unix 工具都能直接作用于记忆库。我可以写一条rg -i 支付|payment ~/.claude-mem/mem/就把相关记忆全捞出来也可以在不改任何插件的情况下把记忆目录提交到代码仓库让团队所有人都能 review 每一条记忆的变更。这种生态红利任何自研存储格式都给不了。第三个优势是可纠错性。模型在生成记忆摘要的时候一定会出错这是概率问题。如果记忆存在专用数据库里错了就藏在里面用户根本不会发现。而在 Markdown 方案里记忆就是文件每次会话结束之后我看一眼 git status谁改了哪条记忆、改成了什么清清楚楚。发现模型记错了直接编辑文件比什么都快。这一点在长期使用中价值极大。2.3 什么时候我才考虑换向量库我不是反向量库。等记忆量超过两千条纯 grep 的召回就开始漏东西——同一个概念换了说法就搜不到。那时候正确的做法也不是推翻 Markdown 重来而是把向量索引作为第二检索通道加上去Markdown 文件仍然是唯一事实来源向量索引只是它的加速和模糊匹配辅助。搜索时把两边的结果做一次合并去重再按相关度排序。这个思路我现在已经预留了接口等库增长到那个量级再开也不迟。3. 从监听、萃取到召回注入claude-mem 的四个核心模块3.1 监听层怎么捕捉一次会话的有效片段claude-mem 的第一步是监听。CLI 助手通常会把自己的会话过程写成日志或留出事件钩子claude-mem 就挂在这些出口上。我实现的是在session_end的事件钩子里读取整段会话记录然后做清洗。清洗的规则很朴素只保留用户输入和助手输出的最终结果删除中间的长篇思考过程、代码 diff 噪音。按时间戳切分把一次会话切成若干段提问-回答单元。对每一段做指纹计算同一个事实如果已经在记忆库里存在就跳过避免重复写入。这一步最容易踩的坑是什么都想记。我一开始把整段高质量的回复都存下来结果记忆库迅速膨胀召回时噪音比信号还多。后来我把监听层的职责收窄只负责把会话切成干净的、可萃取的片段存进原始日志区真正的筛选交给萃取层。3.2 萃取层从对话里捞出值得记的信息萃取层是决定记忆库质量的地方。我总结了一套触发规则命中越多越值得写成记忆卡用户明确说了记住下次注意以后都这样之类的话。会话里出现为什么之前会失败和这次怎么修好的成对信息。代码被用户接受并应用到项目里且涉及一个非显然的约定。同一个主题在多次会话里反复出现说明它是长期关注点。助手给出了一个技术选型或者方案取舍的理由。命中之后按统一模板落成一张记忆卡。模板只有两部分开头的 YAML 元信息和正文。YAML 里记录类型、标签、来源会话、创建时间、最近更新时间还有一个置信度字段。正文用纯文本写结论然后再补一小段背景语境。模板长这样--- type: decision # decision / preference / lesson / skill / profile tags: [payment, api, refactor] source: session-20250112-1430 confidence: medium # high 需要人工确认medium 是模型提炼low 是猜测 created: 2025-01-12 updated: 2025-01-12 --- # 支付网关的弃用顺序 结论先切磨损率最高的旧接口观察三天后再切备用通道。 背景旧接口的签名已经冻结新接口支持 idempotency key所以可以安全灰度。这个格式最大的好处是机器可读、人也可读。置信度字段尤其重要后面讲到记忆污染的时候你就知道它有多救命了。3.3 存储层记忆文件的目录组织记忆文件不是随便扔在一堆我用了四层分类。全局用户偏好单独放项目相关的按项目隔离可复用的解法单独成档错误教训再单独成档。目录结构大概是~/.claude-mem/ ├── mem/ │ ├── user/ # 用户的偏好、工作习惯、常用术语 │ ├── project/ # 按项目分文件夹 │ │ └── payment-refactor/ │ ├── skill/ # 跨项目可复用的解法、命令片段 │ ├── lessons/ # 踩过的坑和错误教训 │ └── index.md # 一个全局索引记录每个文件的主题 ├── sessions/ # 原始会话日志保留但不参与召回 └── claude-mem.md # 给助手看的记忆使用说明这里的关键决策是项目隔离。如果所有项目的记忆混在一起做 A 项目的时候会把 B 项目的技术栈约定召回进来助手就会张冠李戴。隔离之后召回时默认只搜当前项目目录全局偏好单独走一个轻量通道。3.4 召回层怎么在下次会话前找到相关记忆召回层解决知识记得住到知识用得上之间的鸿沟。我在 claude-mem 里用了最朴素的三段式打分关键词匹配、时间衰减、标签加权。给每条记忆算一个分数取 Top N 注入上下文。打分逻辑我写成了一个小脚本核心思路大家可以参考def score(memory, query, now): s keyword_overlap(memory.tags memory.body, query) s tag_boost(memory.tags, query) # 项目名、模块名命中大幅加分 age_days (now - memory.updated).days s * 0.95 ** min(age_days, 60) # 时间衰减60 天后不再继续降权 s 2.0 if memory.confidence high else 0 return s细节不重要重要的是两个思想。第一时间衰减必须有限度不能让两年前的关键项目约定被彻底遗忘第二置信度要参与打分高置信度的记忆优先展示。这个脚本每行代码都很直白但效果出奇地好因为绝大多数场景里会话的主题词本身就是最好的检索词根本不需要上嵌入模型。3.5 注入层把记忆塞回上下文召回之后是把记忆注入到下一次会话的上下文里。我试过三种注入方式最终保留了两种。第一种是合入启动文件。很多 CLI 助手支持在会话开始时加载一个项目说明文件claude-mem 可以把召回出来的记忆按排序追加到一个生成的片段文件里通过钩子让助手在这个文件里看到上次项目相关记忆。第二种是通过事件钩子注入一条系统级前缀文本格式类似以下是过去会话中记录的与当前任务相关的记忆卡片请结合但不要盲从……。注入是有预算的。我给每条记忆设了 150 字左右的硬上限单次注入最多八条超出部分不强行塞。宁可少带不可带偏——上下文一旦被无关记忆污染模型的表现会断崖式下降这一点后面专门讲。4. 接入 CLI 助手的完整配置从初始化到验证闭环4.1 初始化把记忆库的骨架搭起来接入过程其实很短。我把 claude-mem 做成一个命令行工具初始化只做一件事在用户目录下创建四层目录骨架生成claude-mem.md使用说明并初始化 git 仓库用于版本追踪。claude-mem init # 输出 # 创建 ~/.claude-mem/mem/user # 创建 ~/.claude-mem/mem/project # 创建 ~/.claude-mem/mem/skill # 创建 ~/.claude-mem/mem/lessons # 已初始化 git 仓库~/.claude-mem然后再针对具体项目做一次绑定把它和 CLI 助手的配置文件关联起来。这一步只需要在配置里声明记忆库路径确保每个项目有独立的记忆子目录即可。4.2 事件钩子和 CLI 助手握手的关键CLI 助手通常允许在会话开始和结束时执行一些外部命令claude-mem 就是靠这两个钩子活着的。会话开始前执行回忆注入会话结束后执行监听萃取。配置结构大概类似这样{ hooks: { session_start: [ claude-mem recall --project {{project_dir}} --top 8 /tmp/claude-mem-preamble.md ], session_end: [ claude-mem ingest --session {{session_id}} --project {{project_dir}} ] }, settings: { memory_home: ~/.claude-mem } }这里的{{session_id}}和{{project_dir}}是占位符实际配置里会替换成真实的会话 ID 和项目路径。hook 的粒度决定了记忆的时效性session_start 注入的是上一次会话结束时的记忆session_end 萃取的则是本次会话产生的增量。这样一个循环接一个循环记忆库就滚动起来了。4.3 验证闭环三步确认记忆真的生效了配置完之后我建议做一个最朴素的闭环测试不要一上来就测复杂场景。第一步开一个会话明确告诉助手记住这个项目的主分支叫 main不是 master所有的合并必须走 main。第二步退出会话执行claude-mem recall --project 项目路径确认这条记忆被正确萃取和召回。第三步重开一个会话随口问一句我们项目的主分支是什么如果助手能答对闭环就通了。我第一次跑这个测试的时候第二步就翻车了记忆确实被写进去了但召回时因为关键词匹配不到分支这个词没被检索出来。后来我在萃取规则里加了别名扩展把分支、branch、git 主分支这类同义词在写入时就写进标签里问题才解决。这个细节不复杂但对召回率的提升非常明显。4.4 日常手写命令不能只靠自动流程自动流程再完善也得留手工入口。claude-mem 提供了几个我高频使用的子命令claude-mem recall 支付网关弃用顺序 # 手动召回 claude-mem add ~/.claude-mem/mem/lessons # 手动新增一条记忆卡 claude-mem search payment # 全库搜索 claude-mem prune --dry-run # 列出可合并/可删除的记忆 claude-mem stats # 各类记忆数量、最近更新列表手工入口的意义在于纠偏。自动萃取有它的盲区有些东西模型觉得不重要但用户心里知道很重要有些东西模型觉得重要其实只是一次性的临时信息。没有手工入口记忆库就会慢慢被模型的偏好带偏。我每周都会花十分钟跑一次prune --dry-run把模型误当成重要信息存下来的垃圾清掉。5. 跑了半年才暴露的问题记忆污染、预算失控和匹配失效5.1 记忆污染模型记住了自己编的解释这是我在所有坑里最想提醒大家的一个。场景是这样的某次会话里助手对一个问题给出了一个看似合理但其实是猜测的解释我也没有细究直接让它继续了。萃取层把这个猜测当作结论记进了 lessons置信度还标了 medium。下一个会话里助手读到了这条教训把它当成既定事实回答我再次没纠正于是这条错误被写回记忆库变成 high。三个循环之后一个错误结论成了项目里的铁律。排查链路是这样的我发现助手反复推荐一个明显不符合项目实际的方案第一反应以为是模型的问题后来去查记忆库发现整条错误的演进路径都留在 git 历史里。修复分两步。第一把置信度机制加强凡是模型自己提炼、没有被用户明确确认的记忆默认只能标 low不参与高权重注入。第二萃取规则加上需要用户确认标记模型提炼出结论后要在当前会话里反问一次我记录了这条结论对吗只有用户确认了才升到 medium。这个改动之后错误记忆的传播路径被切断了。5.2 上下文预算被陈年记忆吃光第二个坑是记忆库长大之后召回结果开始失控。前期只有几十条记忆的时候Top 8 条条有用半年后几百条每次召回的八条里总有两三条是陈年旧事主题沾边但时过境迁。助手的上下文里塞满了过时约束反而影响了本次任务的判断。我做了三处调整。第一调整时间衰减曲线把 30 天内的记忆权重提得更高超过 90 天的除非置信度是 high 否则直接不参与 Top N。第二给每个项目加了一个当前关注区目录这个区内放的记忆永远优先召回其他区域的记忆只有强关键词命中才进候选。第三也是最重要的引入了上下文预算监控注入的记忆总字符数一旦超过阈值就不再增加优先保证助手的主对话空间。一顿调整之后召回的准确率显著回升助手不再被陈年记忆牵着走。5.3 中文与专有名词匹配失效中文场景下的检索和英文完全不是一回事。英文按空格分词关键词匹配相对自然中文没有天然分词而且同一个概念在对话里可能有好几种叫法。我遇到过最典型的情况记忆里存的是支付网关旧接口迁移用户在新会话里说的是那个老的支付模块怎么处理两边关键词几乎没有交集匹配失败。解决思路是建立别名词表。在每个项目目录下维护一个aliases.md把常见的同义说法映射到记忆卡片的标准主题词上。召回的时候先把查询语句过一遍别名表做扩展再拿去和记忆库匹配。这个方案朴素但有效比嵌入向量更可控。当然如果项目是双语混杂的我建议直接上向量索引作为兜底纯别名表会补不过来。5.4 会话监听重复写入与文件竞争最后这个坑很工程化CLI 助手偶发崩溃或者被强制退出时session_end钩子可能执行两次或者监听脚本读到同一个日志文件的两个不同位置导致同一条记忆被写成两份。记忆库里出现重复条目后召回时同一份知识会被注入两次模型会误以为这个结论特别重要进而产生过度自信的倾向。排查链路是从重复指纹开始的我注意到stats里某些标签的数量异常随手rg一下发现内容完全一样的记忆卡出现在两个文件里。修复方案是给每条记忆在 YAML 里加一个mid字段由内容哈希加来源会话 ID 生成。写入之前先按mid查重重复的直接跳过。同时给写操作加了一个简单的文件锁避免两个进程同时写同一个记忆文件导致内容交错。这两处都是小改动但属于典型的不跑一个月发现不了的工程问题。6. 记忆库是需要维护的半年治理节奏与几条实用心得6.1 记忆库的轻量治理循环很多做记忆增强的人把精力全花在记住上却忽略了忘记也是一种能力。我现在的治理节奏是每周五下午花十分钟跑一次清理每月做一次合并。周清理主要删两类内容时效性已经过去的任务信息以及置信度 low 且超过两个月没有被任何会话命中的记忆。月合并主要处理标签重叠的碎片记忆比如三条都是关于支付网关的决策就合并成一条结构更完整的卡片。这个节奏看起来很轻但长期坚持下来效果很好。记忆库的体积始终维持在一个可控范围召回的准确率不会因为库变大而衰减。我见过一些同事的同类工具越用越迟钝多半就是因为他们只做加法不做减法。6.2 该记和不该记的边界用久了之后我对什么值得记有了一个非常明确的心智模型。值得记的是项目里非显然的约定、用户的明确偏好、经过验证的排错结论、可复用的代码片段和命令。不值得记的是一次性任务的细节、未经验证的推测、情绪化的评价、还有任何形式的密钥和隐私信息。类型例子是否值得记原因项目约定主分支是 main合并必须走 review值得非显然、长期有效明确偏好不用 Redis 做队列用本地进程值得属于用户决策已验证排错这个报错是因为证书过期不是代码问题值得避免重复排查一次性任务今天要改三个页面的文案不值得时效性过强推测结论可能是缓存导致的不值得未验证容易污染隐私信息API Key、密码绝对禁止风险极高这里特别强调隐私信息claude-mem 的定位本来就是本地优先但即便如此我也在萃取层加了一条硬规则凡是被识别为密钥、令牌、密码格式的内容直接丢弃不入库、不落盘。这条规则宁可误杀不可放过。6.3 多人共用记忆库的注意点如果是一个团队共用同一个记忆库事情会变复杂。共享记忆最大的风险不是写不进去而是改出来的冲突。两个人同时维护同一个项目记忆一个说要走 A 方案一个改成了 B 方案没有协调机制的话记忆库就会自相矛盾。我的建议是共享记忆库只写不改任何修改走 Pull Request 机制。每周固定时间由一个人轮流 review 本周新增的记忆把真正重要的合并进正式库把不重要的标记为过期。这样做虽然多了一道人工环节但避免了模型被自相矛盾的记忆搞到精神分裂。6.4 最后分享一个小技巧给记忆库写一份使用说明书我把最后一个心得放在这里是因为它往往被忽略。记忆库不仅仅是给模型读的它最好也带一份给模型看的使用说明书告诉它这套记忆体系是怎么运作的。我在claude-mem.md里写了这么几条记忆卡片里的结论是经过确认的事实可以直接引用。带有猜测背景的条目需要结合当前代码重新验证。如果当前任务与记忆中的决策冲突优先参考时间更新的那条并主动向用户确认。上下文里出现记忆片段时不要盲目复述把它当作线索而不是真理。这份说明书的本质是元记忆——让助手知道自己正在读记忆并且知道记忆也有可信度分级。加完之后我明显感觉助手对记忆的态度变了从盲目引用变成了带着判断去用。也是从那时候起我才真正觉得 claude-mem 不再是一个简单的检索工具而是一套有生命周期的工作方式。
返回列表