
如果你经常用 Claude 写代码、梳理方案或者整理知识大概率遇到过同一个让人抓狂的场景上一个会话里刚说完的背景、偏好、技术选型换一个新窗口再打开它就像失忆了一样什么都得从零开始讲。这个痛点持续了大半年之后我做了一个叫claude-mem的小工具。它本质上是一套跨会话的“记忆外挂”在对话结束时自动抽取关键信息保存到本地文件下一次新会话启动时再把这份记忆作为上下文的一部分自动注入。这样一来AI 会表现出“还记得你”的状态之前敲定的方案、你习惯的表达风格、项目当前进行到哪一步都被保留了下来。这个东西挺适合这么几类人连续几天盯着同一个项目、需要让 AI 维持固定输出风格、以及经常在多个主题之间切换的用户。这篇博文就把我的设计思路、核心实现、踩过的坑全部摊开讲代码和配置可以直接照着抄。1. 为什么需要给 AI 接上记忆1.1 会话断裂让长线工作变得低效对话式 AI 默认是无状态的这句话有两层意思。第一层是技术层面每次请求都是独立的上下文服务器不保留你上一个请求说了什么。第二层是体验层面所有需要“前后一致”的任务都会因为这种断裂而变得非常别扭。我举个例子。之前我在做一个内部工具的项目需要 AI 帮忙写某个模块的接口设计。第一个会话里花了大概二十分钟对齐了数据库表结构、鉴权方案和接口命名规范AI 给了很具体的建议我也都确认了。第二天继续弄的时候我开了个新会话想把前一天的结论作为基础继续往下推进结果它直接给我设计了一套完全不同的表结构名称、字段全对不上等于前一天白干了。这种场景在写代码、写文档、做方案设计这些场景里太常见了。对话轮次一多上下文窗口很快就被撑满人们习惯性地新开一个会话然后重新喂一轮背景。沟通成本被白白浪费在这些重复劳动里。1.2 人靠笔记AI 也靠“笔记”人是怎么解决这个问题的靠笔记。项目经理不可能把整个项目的所有细节都记在脑子里他会写文档、写周报、更新进度表。换了一个人接手先看文档再问几个关键问题就能快速上手。AI 也是一样的逻辑。它本身没有记忆但我可以给它造一个“笔记系统”。让它在对话结束时把当前会话里出现的关键决策、用户偏好、项目状态沉淀成结构化记录下次对话开始前再把这些记录翻出来作为上下文的一部分喂回去。这个思路听起来简单难点在于几个细节记录什么、存成什么格式、如何避免记忆膨胀、怎么处理冲突。我会在后面的部分逐一说明。2. 整体方案设计五步闭环2.1 从“对话-遗忘”到“对话-沉淀-回忆”我的claude-mem在设计上围绕一条链路展开采集、抽取、存储、注入、更新。五个环节形成闭环每次会话都会让记忆变得更完整。第一步采集。在每次对话的自然结尾我通过提示词引导 AI 输出一段简短的结构化总结包含用户偏好、项目状态、决策记录、下一步计划这几类信息。第二步抽取。我需要从对话里过滤出真正值得长期记住的内容。比如用户说“以后接口返回尽量用统一的错误码格式”这属于永久性偏好应该沉淀下来而“今天把登录接口写完”这类一次性信息属于短期任务记录一下即可。第三步存储。抽取出来的信息被写入本地文件。这里我选择的是 Markdown 加 JSON 混合的方案Markdown 方便人眼阅读JSON 方便脚本做后续的合并和更新。第四步注入。新对话开始前运行一个预处理命令把记忆文件渲染成一段固定格式的上下文作为系统提示词的一部分发送给 AI。第五步更新。每次对话结束后重新执行采集和抽取把新信息合并进存储文件。合并过程需要处理“旧记忆和新记忆冲突”的情况。这五步里面最容易做砸的是“存储格式”和“合并策略”如果设计得不好记忆文件很快就会变成一堆互相矛盾、难以解析的乱码。我在 2.2 和 2.3 里具体讲我踩过的坑和最后的方案。2.2 存储格式Markdown 与 JSON 的取舍存储格式的选择上我经历过好几个版本的迭代。最开始我用的是纯文本一个memory.txt把所有内容堆在一起。好处是简单坏处是完全没有结构。过了两个星期文件里同时存在“用户偏好写中文注释”和“代码注释尽量统一用英文”这两条内容AI 每次都人格分裂。后来我改成纯 JSON结构是有了但有个问题人看起来非常不友好。我想在开会时快速翻一下 AI 到底记得我什么偏好打开 JSON 文件看到一堆花括号阅读效率极低。最终我采用的方案是 MD JSON 双轨制。Markdown 文件按主题组织比如project_state.md、user_preferences.md、decisions.md人类直接阅读修改也很方便。每个 Markdown 文件顶部带一个 YAML 小字段块维护元信息如updated_at、version脚本也能快速解析。核心决策类的关键条目同时以 JSON 行.jsonl的形式追加到独立文件里便于做统计、搜索、回滚。这套方案兼顾了两个诉求人能读懂、机器能处理。启动注入时脚本同时读取 Markdown 和 JSON合并成一段统一的上下文块需要人工介入纠偏时直接编辑 Markdown 就行。2.3 目录结构与文件组织项目隔离是我很早就定下来的原则。不同项目之间不应该共享记忆否则做 A 项目时会莫名其妙带出 B 项目的上下文。我的记忆目录结构大概是这样的~/.claude-mem/ projects/ project-alpha/ profile.md # 用户偏好、角色设定、常用术语 project_state.md # 当前进展、待办、下一步 decisions.md # 重大决策及理由 archives/ 2025-01-01.md # 按时间归档的历史摘要 project-beta/ ... global/ global_preferences.md # 不区分项目的通用偏好 cache/ memory_block.md # 最近一次构建的注入块global目录我用来存一些跨项目的偏好比如“回复时先给结论再给理由”“代码示例偏好 TypeScript 风格”。archives用来做版本归档每个会话结束生成一份当日快照方便回滚。cache里的memory_block.md是每次预处理生成的“最终注入产物”新对话直接把整个文件塞进上下文即可。3. 核心实现拆解3.1 会话结束时的记忆采集采集这步是整个系统的输入源头做不好后面全都白搭。我的做法是在每轮对话的末尾追加一句约定好的“收尾指令”让 AI 把这段对话中的重要信息整理成结构化内容。使用的提示词大致如下会话即将结束。请基于本次对话生成一段记忆记录只保留值得长期记住的内容。 要求 1. 用 JSON 格式输出字段固定为 {timestamp: ..., preferences: [...], project_state: ..., decisions: [...], next_actions: [...]} 2. preferences 只放用户表达的稳定偏好临时性指令不放进去。 3. decisions 必须包含决策内容和决策理由。 4. 如果本次对话没有值得记录的内容直接输出空数组。 5. 不要输出 JSON 之外的任何文字。这一步看似简单实际有个很大的坑AI 有时候会把“过渡性内容”当成“长期偏好”。比如用户随口说“这次用 pandas 处理数据”它可能就把“用户偏爱 pandas”记进偏好里了。所以我在提示词里反复强调“临时指令不记录”同时在保存端的脚本里添加一层过滤规则。我还在脚本里加了一个过滤关键词列表凡是包含“这次”“临时”“暂时”“先用着”这类字眼的条目会被二次筛选要么丢弃要么降级为短期任务而非长期偏好。3.2 记忆写入脚本采集到 JSON 之后写入逻辑我用 Python 实现核心代码不长关键的合并逻辑就在这里。import json import os from datetime import datetime MEMORY_ROOT os.path.expanduser(~/.claude-mem/projects) def load_jsonl(path): if not os.path.exists(path): return [] with open(path, r, encodingutf-8) as f: return [json.loads(line) for line in f if line.strip()] def append_jsonl(path, record): os.makedirs(os.path.dirname(path), exist_okTrue) with open(path, a, encodingutf-8) as f: f.write(json.dumps(record, ensure_asciiFalse) \n) def merge_record(project, record): base os.path.join(MEMORY_ROOT, project) # preferences 合并去重保留稳定条目 prefs_path os.path.join(base, profile.md) existing_prefs read_md_section(prefs_path, preferences) new_prefs record.get(preferences, []) merged merge_dedupe(existing_prefs, new_prefs) write_md_section(prefs_path, preferences, merged, updated_atdatetime.now()) # decisions 做追加归档 append_jsonl(os.path.join(base, decisions.jsonl), record.get(decisions, []))这里我必须解释一下merge_dedupe的逻辑它不只是简单去重还会做语义相似度判断。比如旧条目是“代码注释用英文”新条目是“注释统一用英文”两条字符串不一样但意思一样。我引入了一个轻量级的相似度检查基于公共词比例计算超阈值则用新的覆盖旧的。这个覆盖策略很重要因为记忆不应该无限累积。如果一条偏好被用户反反复复用不同方式表达说明它是真实偏好的概率很高应当被保留为最新版本但旧版本需要让位。3.3 启动时注入记忆块新会话启动时我会先运行一个准备命令渲染记忆块claude-mem build --project project-alpha核心逻辑是读取该项目下所有相关文件拼接成一段固定格式的上下文字块。def build_memory_block(project): base os.path.join(MEMORY_ROOT, project) sections [] for fname in [profile.md, project_state.md, decisions.md]: path os.path.join(base, fname) if os.path.exists(path): sections.append(open(path, encodingutf-8).read()) block 以下是此前的跨会话记忆是已经和用户确认过的信息请默认全部已知\n\n block \n\n.join(sections) # 控制长度 tokens_estimate len(block) / 1.5 # 中文粗略估算 if tokens_estimate 1800: block compress_block(block, max_tokens1800) return block然后我把它保存在cache/memory_block.md在真正和 AI 对话时把这个文件的内容复制到系统提示词里。如果你用的是 API更优雅的做法是直接在 system prompt 中动态拼接你正在继续一个跨天进行的项目。以下是你此前的记忆这些信息默认成立不要重复向用户确认 {{memory_block}}这个注入方式的效果非常明显。之前需要用户重新讲述的上下文现在 AI 会在第一句话里就表现出“心里有数”。比如你问它“之前我们设计接口的时候鉴权方案确定用哪种了吗”它能直接答出你上次的结论而不是一脸茫然反问你“什么鉴权方案”。3.4 记忆压缩与清理记忆无限增长是必然遇到的硬问题。上下文窗口就那么大如果记忆块占掉一半留给真实对话的空间就小了。我采用的压缩策略分三层。第一层按 token 预算硬截断。我设定一个上限比如 1800 token。超出部分按照“重要度权重”排序截断。权重规则是用户偏好 决策记录 项目状态 历史归档。第二层定期做记忆摘要。每周跑一次脚本对过去七天的决策归档自动生成一份“周总结”压缩成几条高度凝练的结论替代那些零散的记录。这个操作利用 AI 本身做准确率基本够用。第三层主动遗忘。项目完成后把project_state.md里的内容归档到archives/然后把当前状态清零。这一步很多人忽略导致旧项目的记忆一直残留新项目启动时被过时信息干扰。我现在的习惯是每个项目收尾时自动清理只保留 profile 级别的长期偏好其他全部归档。4. 实际使用效果与参数调优4.1 使用前 vs 使用后的差异我截取一个实际工作中的对比大家感受会更直观。使用claude-mem前我帮我看看之前说的那个用户统计接口现在要实现按时间段筛选改一下。 AI好的可以帮我提供一下接口的完整定义以及现在筛选逻辑的具体位置吗 我……开始翻之前的代码然后把上下文重新描述一遍使用claude-mem后我帮我看看之前说的那个用户统计接口现在要实现按时间段筛选改一下。 AI好的根据记忆里你之前的偏好接口路径保持 /api/users/stats支持日期范围参数。当前接口在 src/api/user.ts 第 80 行。你希望我直接改这个文件还是先把改动方案列出来给你确认第二种体验的差别在于AI 不再像第一次见面的陌生人而像一个确实接手过这个项目、随时可以继续干活的同事。4.2 关键参数配置参考下面是我在config.yaml里沉淀下来的一组参数经过几轮项目验证目前比较稳定memory: max_token_budget: 1800 # 注入上下文的最大 token 预算 compress_priority: # 压缩时的保留优先级 - preferences - decisions - project_state - archives auto_collect: true # 会话结束是否自动触发采集 dedupe_threshold: 0.82 # 相似度阈值超过则视为重复 archive_on_complete: true # 项目完成时自动归档dedupe_threshold是个很有意思的参数。设太高比如 0.95几乎不会合并重复记忆文件会膨胀设太低比如 0.5两句不同含义的话会被误判成同一句话导致重要的差异被覆盖掉。0.8 左右是我试下来比较平衡的区间。max_token_budget也不是越大越好。曾经我把它调到了 4000结果 AI 的回答风格变得非常啰嗦高频记忆来回引用挤占了本该用于推理的空间。调到 1800 之后既能覆盖核心背景又能留足上下文给当前任务。5. 常见问题与排查技巧实录5.1 记忆文件里出现互相矛盾的条目这个是最先遇到的问题。AI 在某个会话里记录了一条偏好“代码注释尽量用中文”过了两天另一个会话里又记录了一条“注释统一用英文”两条同时存在AI 就开始随机挑选一条执行。根源在于我在 3.2 里说的语义相似度合并没有做好。两个不同会话产生的记录如果没有在写入前做交叉比对就会并存。我现在采取的办法是所有 preferences 的新增条目都先和已有的全部条目做一次相似度计算。超过阈值就用新条目替换旧条目同时把旧条目移入archives/以备回溯。这个方案运行之后矛盾条目的出现频率大幅下降。5.2 会话一长AI 越来越“沉浸”在记忆里有一段时间我发现注入记忆之后 AI 的回签质量反而下降了。它会在回答中频繁引用记忆里的内容甚至编造“记忆里提到过某细节”而实际上那个细节根本不存在。排查后发现问题出在提示词措辞上。原来的写法是“以下记忆信息可能对你有帮助”这种表达给 AI 留下了过度自由发挥的空间它会把记忆当作素材进行脑补。改法很简单把提示词改成严格约束形式以下是真实记忆只可引用不可虚构。若记忆与用户本次提供的信息冲突以用户本次提供的信息为准。同时加上一条兜底提示“若记忆中没有相关内容明确告知用户缺少信息不要编造。”实测下来幻觉问题大幅缓解。5.3 隐私与敏感信息的边界记忆文件是纯文本落在本地磁盘的如果里面存了不该存的密钥、客户隐私信息风险就是实打实的。我的建议是在采集阶段做一层屏蔽。在记忆采集提示词里加一句如果对话中出现密码、密钥、个人信息不要写入记忆记录用 [敏感信息已屏蔽] 代替。同时我在脚本层面加了一个正则黑名单对常见的 token、密钥格式做二次过滤。这样即使 AI 偶尔遗漏脚本也能兜底拦截。5.4 不同项目间记忆串味这个问题的触发场景是上午还在写 Python 数据分析项目下午切到另一个 TypeScript 项目。如果记忆目录结构不隔离AI 可能在下午的会话里参考上午的偏好导致输出风格完全跑偏。解决方式就是我 2.3 里设计的项目隔离。每个项目拥有独立子目录claude-mem build时只读取当前项目。全局记忆只放跨项目通用偏好并且设定最多 5 条的限制避免全局文件变成大杂烩。5.5 记忆整理脚本本身出 bug 怎么办脚本也是代码也会出 bug。最怕的是记忆文件被错误合并数据覆盖后找不回原来的内容。我的保护措施是每次写入前自动把当天修改前的原始文件备份到一个带时间戳的快照目录。这样即使写入逻辑写错也能恢复到上一版。这个成本很低收益却非常大。我强烈建议所有做类似工具的人都保留这层快照机制。6. 我的一点实操心得做这个工具的核心体会是AI 的“记忆力”其实是个工程问题而不是模型能力问题。你不需要训练模型只需要做好信息管理——什么东西值得记、记在哪里、什么时候翻出来用、什么时候主动忘掉。把这四个问题想清楚几十行脚本就能带来非常明显的体验提升。如果你也想动手做一个类似的工具我的建议是不要一上来就追求复杂。先用一个 Markdown 文件做记忆存储手动复制到新会话里验证效果。等确认这套流程真的能帮到你再逐步加入自动采集、语义去重、快照回滚这些机制。另外记忆工具长期用下来你会发现自己越来越依赖它固定的输出结构和记录条目。我目前最满意的反而是它的“低存在感”——启动时静默注入结束时静默记录平时完全感觉不到它的存在。好的工具就应该是这样的。