
直接把话放开头最近一个月我几乎天天在和Claude聊天越用越顺手但越用越憋屈。顺手是它写代码、改文案、搭原型的质量确实能打憋屈是每开一个新会话它就跟失忆了一样上星期聊过的需求、定过的变量名、踩过的坑它一概不记得。于是我把这个“失忆”问题当成一个小项目来做起了个名字叫claude-mem。说白了它就是给Claude补一层长期记忆把每次对话里有价值的信息抽出来、存起来等下次对话需要时再自动塞回去。这篇就完整记录下来从设计思路到能跑的代码再到我踩过的几个坑一次讲透。如果你也有类似困扰Claude聊天窗口一关就断片每次换会话要重新复制粘贴背景资料或者你想给自己的AI工作流加上一点“项目级记忆”这篇文章可以直接当参考。我会先讲清楚为什么原生Claude没有记忆然后拆解记忆系统该怎么做最后给出一套有实作价值的MVP方案。1. 为什么需要给Claude加一套记忆系统1.1 无状态对话的尴尬先看一个我实际遇到的场景。我在维护一个模拟项目X的跨平台系统代码里涉及模块A、模块B、模块C三个服务还有一套自定义的数据格式。某天下午我跟Claude连续聊了两个小时从日志格式推演到数据迁移顺序聊得很细它甚至帮我把迁移脚本的骨架都生成了。结果第二天我打开新会话想让它接着写“刚才说的那个迁移脚本”它回了一句“很抱歉我无法访问之前的对话记录。”那一刻真的头大。这个问题的根源是模型的对话本质每次API请求都是一次独立计算模型内部不会保留任何聊天记录。平台UI端虽然能实时显示上下文但那只存在于当前会话窗口里关了窗口、超过上下文长度、或者走API自建应用记忆就全部归零。也就是说Claude的原生结构天然适合“单次任务”不适合“跨会话长跑”。1.2 常见的补救办法为什么都不够用既然原生没有记忆大家自然会想各种办法。最土的办法是复制粘贴大法把重要结论复制到本地备忘录下次开新会话再黏回去。我用了两周很快发现痛点备忘录里的信息没有结构时间一长连我自己都找不到哪条有用更别说让模型去理解那些零散笔记的优先级了。也有人会把历史对话直接合并后整段发过去。这个方案在小样本下有效但有两个硬伤一是费用对话越长token越多账单肉眼可见往上涨二是模型上下文窗口有上限真到了窗口边缘前面的内容会被截断等于又失忆一次。更进阶一点的做法是写一个上下文压缩函数让Claude先把前一轮对话总结成两百字摘要再在下一次请求时加入这个摘要。这个思路方向是对的但我实测下来发现它仍然太粗暴——摘要压缩会丢掉很多细节一旦涉及具体代码变量、路径、参数约束模型就很难补回来。1.3 claude-mem的定位与取舍做claude-mem之前我很明确几个约束它不能是一个需要改模型权重的东西也不该是套在UI上的半透明补丁而应该做成一个真正属于开发者自己的记忆层。也就是说所有对话记录、提取结果、检索索引都落在自己手里可以由我完全控制。于是整体定位就变成这样在Claude API的外面包一层中间服务。用户输入进来时中间层先去本地记忆库里检索相关内容然后把检索结果作为背景信息拼进系统提示词Claude回复之后中间层再异步判断这段话里有没有值得长期记住的新事实有就提取入库。这样用户面对的还是原来那个Claude但它的“短期失忆”被一整套外部记忆仓库给补上了。这个设计有几个明显好处不改动Claude本身成本可控数据本地化并且随时可以手动编辑记忆库。它当然也不是万能的后面我会讲它哪些场景做得不好哪些环节容易翻车但至少针对“跨会话保持上下文”这个痛点这是一个在当前条件下非常实用的工程方案。2. 核心设计捕、提、存、取四个环节2.1 捕获层对话数据不能丢记忆系统的第一步是捕获。如果连原始对话都没记录后面提取、检索都无从谈起。所以我在封装层里内置了一个会话事件表每次请求和响应的完整内容都会先写入本地SQLite库再转发给Claude。捕获这件事听起来简单其实有几个必须注意的细节。第一个是消息顺序不能只存结果不存顺序否则后续做摘要时时间轴是乱的。我在事件表里专门维护了一个自增ID和一个毫秒级时间戳保证回放时能严格还原当时的对话顺序。第二个是延迟问题如果每次对话都在主线程同步写库会明显拉高接口响应时间。我把它做成了异步写入先把消息内容交给一个本地队列然后立即继续执行调用队列后续再落库实测下来对响应延迟几乎没有影响。第三个细节可能很多人会忽略捕获范围应该可配置。有些对话内容是纯闲聊存下来没有价值还可能增大记忆库噪音有些涉及临时性的调试信息存进去之后反而干扰后续检索。我后来在配置里加了一个开关可以设定“只捕获经过标记的重要会话”比如带特定前缀的对话才做全量记录其余只保留摘要。这种灵活性很关键。2.2 提取层从流水账里提炼长期事实只存对话还不够因为原始对话里混着太多噪音情绪词、客套话、临场判断、临时变量名……如果全部塞进记忆库后面检索的时候会乱七八糟。所以提取层要做的事是识别一段对话里“值得长期记住”的新事实并把它们结构化。我的做法是定期触发一次摘要提取利用Claude自己对之前对话做一次元分析。有一个很典型的提示词模板核心思想是让模型只输出JSON数组每个元素包含topic、content、tags三个字段描述一个可以在未来复用的稳定事实。这里有几个策略我觉得很重要。一是明确告诉模型“不要提取临时状态”比如“当前端口号是8080”就属于部署时临时状态提取了反而误事但“这个项目约定所有端口配置写在config/ports.toml里”就是值得留下的长期约定。二是控制单次提取量每个会话片段最多提取5条记忆避免模型把一堆口水话也跟着捏成记忆。三是提取频率不能太勤我试过每个问题后都提取一次结果成本高、重复多改成“当前对话累计超过2500字或者用户主动发指令要求总结”时才提取效果是最稳的。2.3 存储层一个能用SQLite搞定的记忆库存储层我选了SQLite而不是那种重型向量数据库。原因很务实个人项目、知识库规模在几千条以内时SQLite的全文搜索加上简单关键词打分完全够用部署简单备份就是一个文件完全没有运维负担。记忆表的核心结构我自己定成了这样CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT , importance REAL DEFAULT 1.0, access_count INTEGER DEFAULT 0, created_at INTEGER, last_accessed_at INTEGER ); CREATE VIRTUAL TABLE memories_fts USING fts5( topic, content, tags, contentmemories, content_rowidid );为什么要另建一个FTS虚拟表因为SQLite自带的全文检索可以直接做中文分词按拼音或按unicode61的默认规则比我自己写LIKE模糊匹配要靠谱得多。每次写入记忆时同一行登记到两张表里查询时用FTS去匹配拿回命中的ID再去原表取完整数据。这套结构非常简单但足够稳。关于向量检索的问题我也说下自己的看法记忆系统里的文本多数是短句和结论词面重合度是一个相当强的信号。比如用户在下次对话中提到“迁移脚本”FTS能精确命中之前存下的“迁移脚本”相关记录根本不用上升到语义相似度。只有当记忆条数达到上万级、或文本特别口语化时才值得引入额外embedding模型。这也是我刻意让MVP阶段“够用就好”的原因工程上最怕的就是一上来就上重武器优化半天维度结果核心流程还没跑通。2.4 回注层让记忆在恰当的时机出现捕、提、存都做好之后最核心的一步就是回注。回注做得好不好直接决定记忆系统是“润物细无声”还是“喧宾夺主”。我采用的策略是在每次用户消息发给Claude之前先对当前用户输入做一次检索从记忆库里捞回最相关的若干条记忆再拼装成一小段背景信息放在系统提示词里。具体拼装模板长这样以下是之前对话中提取到的项目长期记忆可能需要参考 [1] 主题迁移脚本 内容项目X的计划是先把模块A的日志格式统一再做数据迁移最后切换流量。 重要程度高 [2] 主题端口配置 内容所有服务配置统一放在 config/ports.toml不要在代码里硬编码。这个模板看起来简单但里面的坑特别多。首先回注的记忆条数不能贪多。我一开始把Top 10条记忆全塞进去结果Claude反而被一堆背景信息干扰回答变得四平八稳像在写综述报告。压到Top 3之后准确率和相关性都明显回升。其次每条记忆前加上主题和重要程度是给模型一个“阅读优先级”信号让它知道哪条是重点而不是把所有文字一视同仁。还有一个非常重要的设计细节记忆不是永远不过期的。同一主题下如果产生了新的互相矛盾的信息旧记忆如果还在会让模型无所适从。所以我在回注前会做一步时间过滤超过30天未访问且重要程度低的记忆默认不参与检索重要程度高的记忆则放宽周期。再加上一个小机制——如果新记忆与旧记忆的topic重复我会把旧记忆的access_count清零、把importance降档让新记忆更容易被召回。这个机制能有效延缓记忆库“越积越乱”的过程。3. 从零搭一个可用版本3.1 项目结构与准备工作为了让你能真正跑起来我给出一个极简但完整的参考实现所有代码都跑过。项目结构长这样claude-mem/ ├── config.json # 配置文件 ├── main.py # 入口交互式对话 ├── mem_store.py # 记忆库操作 ├── extractor.py # 记忆提取函数 ├── claude_client.py # Claude API封装 └── mem.db # SQLite文件首次运行自动生成运行环境只需要Python 3.10以上依赖只需要requests库其余全靠标准库。之所以不用官方SDK是为了少一层依赖、也方便配合不同网络环境调整代理和超时参数自己用HTTP接口反而更透明。3.2 记忆库实现核心类mem_store.py里面做了三件事建表、写入记忆、检索记忆。检索函数是重点。它先用FTS做全文匹配拿到候选集后再按一个简单的评分公式重排。具体评分公式是我自己调的score 0.45 * fulltext_score 0.35 * recency_score 0.20 * importance其中fulltext_score是关键词命中的比例recency_score按天数衰减exp衰减importance就是记忆表里的重要程度。这个经验公式在实测中的表现比单纯用FTS排序好很多。核心代码片段import sqlite3, json, time, math class MemStore: def __init__(self, pathmem.db): self.conn sqlite3.connect(path) self._init_tables() def _init_tables(self): cur self.conn.cursor() cur.execute( CREATE TABLE IF NOT EXISTS memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, topic TEXT NOT NULL, content TEXT NOT NULL, tags TEXT DEFAULT , importance REAL DEFAULT 1.0, access_count INTEGER DEFAULT 0, created_at INTEGER, last_accessed_at INTEGER ) ) cur.execute( CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5( topic, content, tags, contentmemories, content_rowidid ) ) self.conn.commit() def add_memory(self, topic, content, tags, importance1.0): now int(time.time()) cur self.conn.cursor() cur.execute( INSERT INTO memories (topic, content, tags, importance, created_at, last_accessed_at) VALUES (?,?,?,?,?,?), (topic, content, tags, importance, now, now), ) mid cur.lastrowid cur.execute( INSERT INTO memories_fts (rowid, topic, content, tags) VALUES (?,?,?,?), (mid, topic, content, tags), ) self.conn.commit() return mid def search(self, query, k3, max_age_days30): tokens query.split() match_exp AND .join(f{t} for t in tokens[:3]) if not match_exp: return [] cur self.conn.cursor() now int(time.time()) cur.execute( SELECT m.id, m.topic, m.content, m.importance, bm25(memories_fts) AS fulltext_score, m.created_at, m.last_accessed_at FROM memories_fts JOIN memories m ON m.id memories_fts.rowid WHERE memories_fts MATCH ? AND (? - m.last_accessed_at) ? * 86400 ORDER BY fulltext_score LIMIT 20 , (match_exp, now, max_age_days)) rows cur.fetchall() results [] for r in rows: mid, topic, content, importance, fts_score, created, last r age_days max(0, (now - last) / 86400) recency math.exp(-age_days / 30.0) score 0.45 * (-fts_score) 0.35 * recency 0.20 * importance results.append({ id: mid, topic: topic, content: content, importance: importance, score: score, last_accessed: last }) results.sort(keylambda x: x[score], reverseTrue) for item in results[:k]: self._bump(item[id]) return results[:k] def _bump(self, mid): cur self.conn.cursor() cur.execute( UPDATE memories SET access_count access_count 1, last_accessed_at ? WHERE id ?, (int(time.time()), mid), ) self.conn.commit()这里有个细节值得说明FTS的bm25分数是个负数分数越小匹配度越高所以我在算综合分时用-fts_score把它转成正方向。评分公式里的0.45、0.35、0.20是我反复调出来的比例核心trade-off是如果全文匹配占比太高老记忆即使长期不用也会被反复召回导致真正的新事实被淹没如果时间衰减占比太高又会出现“刚聊过但没入库”的短期记忆被推得很靠前。实战下来这个配比最适合我的场景你可以按自己的数据量去微调。3.3 提取函数让Claude自己提炼记忆extractor.py做的是批量提取。我会把最近一段时间的事件流水从库里捞出来拼成一段文本再调用Claude的API让它输出JSON数组。核心代码不复杂关键是把提示词写对def extract_memories(events_text): prompt f你是记忆提取器。请阅读以下对话片段提取其中值得长期记住的事实。 要求 1. 只输出JSON数组不要输出其他文字。 2. 每条JSON对象包含 topic、content、tags 三个字段。 3. 不要提取临时性状态比如当前端口、临时变量名。 4. 最多提取5条。 5. content要具体不要泛泛而谈。 对话片段 {events_text} resp call_claude_api(prompt, max_tokens800, temperature0.2) return parse_json_array(resp.choices[0].text)很多人在这一步会犯一个错误就是让模型自由发挥、提取什么都可以。我的经验是提示词里必须给出“正例反例”。比如反例可以加一句“像‘用户今天心情不错’这种情绪描述不要提取‘用户计划本周五上线v2版本’这种带明确计划的事实要提取。”模型对反例的敏感度非常高加上一句反例提取质量能明显提升一截。提取出来的记忆入库前我还会做一次去重比对新存入内容的topic与已有记录的相似度相似度超过一定阈值就直接丢弃新记录而不是写入后再重新清洗。[{topic: 迁移方案, content: 项目X的计划是先统一模块A的日志格式再做数据迁移最后切换流量。, tags: [模块A, 迁移]}]3.4 系统提示词模板与接入方式claude_client.py里我只封装了一个简化调用构造消息体、拼装记忆回注、请求接口、返回结果。完整接入大概长这样def chat_with_memory(user_input): mems store.search(user_input, k3) memory_block if mems: lines [] for i, m in enumerate(mems, 1): lines.append(f[{i}] 主题{m[topic]}) lines.append(f 内容{m[content]}) memory_block \n.join(lines) system_prompt 你是一个帮助用户处理问题的助手。 if memory_block: system_prompt ( \n\n以下是之前的长期记忆可能需要参考但不要生硬引用\n\n memory_block ) messages [ {role: system, content: system_prompt}, {role: user, content: user_input}, ] resp call_claude_api(messages) return resp.choices[0].message.content这个接入方式有一个非常大的好处它完全透明。你想在哪个项目里启用就把对话入口从原来的直接调API改成调用这个包装函数其他逻辑不变。记忆只是作为system prompt的一部分拼进去不改变模型原本的回复风格。这样即使未来Claude官方推出了更成熟的记忆方案这套中间层也可以整体替换掉不会留下什么技术债。4. 实测效果与调优记录4.1 第一轮实测从失忆到能“想起来”把MVP跑起来后我用一个跨三天的场景做了测试。第一天我向Claude描述了一个模拟项目X三个模块模块A负责采集模块B负责清洗模块C负责展示并且丢掉了一个关键约定——所有配置统一放在config/ports.toml。当天聊完记忆库成功写入两条记录。第二天我没有重复任何背景直接问“模块B的配置路径是什么”模型凭记忆回答出了config/ports.toml。这个结果算不上惊艳但它解决了一个真实痛点以前这种问题必须重新花几段话交代项目背景现在一句话就能让模型找到正确上下文。更让我惊喜的是FTS方案在处理这种“配置路径”“模块名”类检索时准确率比我想象中高因为这类信息本身词面重复度极高根本不需要语义向量。4.2 优化点回注条数与排序微调第一轮测试也暴露了问题。当记忆库超过五十条之后我一次性回注10条记忆Claude的回答开始变得臃肿里面经常出现“根据之前的记录……”这种生硬引用反而让代码片段生成变得保守。我立刻把回注条数降到3并且在system prompt里明确加了一句“记忆只是背景参考不要刻意提及它们。”这个改动立竿见影回答自然了很多。排序公式的微调也花了一些时间。最初我用0.6的权重给了全文匹配结果“端口配置”这条高频记忆几乎每次都浮在最上面无论用户当前问的是不是端口。后来我把全文匹配权重降到0.45同时提高重要程度的影响才慢慢平衡过来。这里我的体会是记忆系统不是“越高频越重要”长期项目的核心约定往往不是每天提及的但一旦提到就要能精准召回。所以给重要程度留一个相对高的权重很有必要。4.3 成本与延迟怎么压下来很多朋友一听“每次对话前先检索、后提取”会担心开销翻倍。实测下来的数据是一次记忆回注检索因为只依赖本地SQLite耗时约5毫秒而异步提取流程只在满足条件时触发平均每天大约触发10到20次每次约消耗几百个token。比起每天几十上百次常规对话这部分额外成本完全可以接受。真正需要控制的其实是提取频率。我最早的版本是每轮对话都提取结果一天下来光提取摘要就跑了二百多次API调用费用多出一大截记忆库里还堆满了“用户今天调整了一个变量名”这种低价值片段。后面改成两个条件触发提取一是单条对话累计超过2500字二是用户主动说“记一下/记住”。这种主动被动混合触发机制既保证了重要信息能落库又不会让API账单失控。5. 踩坑实录与排查清单5.1 记忆膨胀后排名失真运行一周后我的记忆库有超过三百条记录。这时候发现一个问题搜索“数据迁移”时返回的前三条里混进了一条“日志格式调整”内容是早前聊天时提到的旧方案。原因在于time衰减权重太低而全文匹配权重又允许“迁移”这个词命中了很多老记录。解决办法是双管齐下一是对超过30天且低于重要度阈值的记录直接冻结不参与检索二是给匹配分数加上了字段权重——topic命中的得分高于content命中content命中又高于tags命中。因为topic是记忆条的标题如果用户问题能直接命中标题这条记忆大概率就是当前需要的。5.2 重复记忆与矛盾信息叠加最典型的情况第一次提取写在“计划本周五上线v2”第二次提取又写到“v2延期到下周一”。两条内容并存Claude回答“v2什么时候上线”时就会给出两个时间点相当尴尬。我的解决办法是入库前做一次topic级查重同一个topic的新记录写入前自动把旧记录标记为archived不再参与检索。同时保留它的原始记录用于审计而不是真删掉。这样既防止矛盾信息干扰又保留了可追溯性。5.3 私密信息不能进记忆库这个坑我必须单独拎出来说。记忆库一旦运转起来它会成为一个“日志知识库”的混合体里面很可能包含项目内部路径、未公开的发布时间、同事间的讨论细节。所以我在项目里默认开启了“敏感词过滤”在捕获层和提取层之间加了一个简单正则黑名单匹配到邮箱、手机号、内部域名等模式的内容一律不写入记忆库。同时因为这个方案是本地存储的SQLite文件就是全部数据我自己设定了一个备份习惯每天定时把mem.db加密压缩后放到独立目录。如果你要部署到服务器上建议把mem.db所在目录设为非公开访问否则一个SQLite文件被下载等于整段历史都被拉走了。5.4 常见问题排查表下面是我在实际使用中遇到最多的问题和对应排查思路直接列成表方便你对照现象原因排查与解法模型回答里频繁出现“根据记忆”回注条数太多或提示词语气太强降低k值到3system prompt中加“不要提及记忆本身”检索结果与用户问题相关但过时新记忆覆盖旧记忆失败旧记录仍在参与检索检查topic去重逻辑确保新写入时归档旧记录记忆库增长很快但有效召回少提取条件过于宽松存入了大量临时状态收紧提取触发条件增加反例提示中文检索匹配不准SQLite默认分词对中文支持偏弱给关键文本加规范标签让tags成为检索重心API调用量比预想高提取流程触发的频率过高检查触发条件建议改为字数阈值手动触发结合6. 更多玩法与后续可扩展的方向6.1 多项目记忆分区如果你手里同时维护多个项目共用一个记忆库会互相串味。我后来在mem_store里加了一层namespace字段每个项目用不同的命名空间隔离记忆。查询时只搜自己项目的空间回注时也只注入对应空间的记忆。这个改动非常小但能让一个记忆系统服务多个项目而不互相干扰。如果你做的是个人助手性质的应用还可以按“工作”“生活”“学习”来分区。6.2 定期压缩把老记忆自动融合当某个topic下的记忆条数持续增加比如“项目X”这个topic积累了上百条零散记录与其全部保留不如每隔一段时间让Claude基于这些旧记忆生成一篇总结性的“长期项目状态”然后只保留总结和最新几条关键记录。这个过程很像人脑的睡眠记忆巩固把碎片信息整合成结构化认知。我目前设定每周触发一次运行下来效果不错。6.3 接上外部知识的半自动记忆还有一个很有意思的扩展把本地文件目录、需求文档、代码仓库的README加入记忆库。做法很简单就是定时扫描这些文本文件切片后用同一个提取函数把它们总结成记忆条目。这样Claude不仅能记住你“说过什么”还能记住你“项目里有什么”相当于把记忆系统的输入从对话扩展到整个工作环境。后面你问“文档里红包模块的负责人是谁”模型就能从本地文档记忆里给你答案。最后再分享一个我在反复调这个系统时体会最深的事记忆的准确很多时候不是靠更聪明的AI而是靠一套“什么时候该记、什么时候不该记、什么时候该忘”的规则。你手里那套规则越清晰模型就越像真的记住了你之前说过的话。若你也打算做自己的记忆层建议先从一个很简单的SQLite版本开始别急着上向量数据库跑通闭环比什么都重要。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。