
1. 从零搭建一个持久记忆系统我为什么盯上了 claude-mem第一次看到claude-mem这个名字我脑子里蹦出来的不是某个具体工具而是一个困扰了我大半年的问题为什么每次跟 AI 助手对话它都像失忆了一样你肯定也遇到过这种场景。周一你跟它聊了一个项目的架构设计把技术选型、目录结构、命名规范都敲定了。周三你回来接着聊它一脸茫然地问你请问您想做什么项目。你不得不把之前的上下文重新贴一遍贴完还得解释上次我们说的是这个意思。一次两次还行次数多了这种重复劳动消耗的耐心比写代码本身还多。claude-mem就是冲着这个痛点来的。从名字拆解claude指向的是对话式 AI 助手的交互场景mem是 memory 的缩写合起来就是给 AI 助手装一套持久化记忆。它要解决的问题很明确让 AI 在跨会话、跨时间的情况下依然能记住你是谁、你在做什么、你们之前聊过什么、做过哪些决定。这篇文章适合三类人看。第一类是重度依赖 AI 助手做开发、写作、研究的个人用户你每天要跟 AI 来回几十轮记忆断裂对你来说是实打实的效率损失。第二类是对 AI 应用层开发感兴趣的工程师你想知道一套记忆系统在工程上到底怎么落地涉及哪些存储、检索、注入的环节。第三类是做知识管理、个人助理类产品的同学记忆能力是你产品的核心壁垒你想看看别人是怎么拆解这个问题的。我不会只讲概念。下面会从整体设计思路、核心模块拆解、实操搭建过程、常见坑排查四个维度把claude-mem这类记忆系统讲透。你能拿到的是可以直接参考的架构方案、参数选择的计算逻辑以及我在实际折腾过程中踩出来的经验。2. 记忆系统的整体设计与思路拆解2.1 为什么把历史对话全塞进上下文是条死路很多人第一反应是记忆嘛简单把之前所有对话记录拼起来一起发给模型不就行了这个思路在小规模下能跑通但很快就会撞墙。原因有三个而且一个比一个硬。第一是上下文窗口的物理限制。主流模型的上下文窗口从几万到几十万 token 不等看起来很大但对话是累积的。假设你每天跟 AI 聊 20 轮每轮平均 500 token一天就是 1 万 token。一个月下来 30 万 token直接爆掉大部分模型的窗口。你不可能无限往里塞。第二是成本和延迟的线性增长。就算窗口够大每次请求都把几十万 token 的历史带上推理成本是按输入 token 计费的你的账单会以肉眼可见的速度膨胀。同时输入越长首 token 延迟越高交互体验直线下降。我实测过把上下文从 5K 拉到 50K响应明显变慢那种打字机般的流畅感就没了。第三也是最容易被忽略的信息密度问题。历史对话里 80% 是废话——好的明白了那我试试嗯嗯。真正有价值的决策、结论、偏好可能只占 20%。你把全部历史塞进去等于让模型在一堆噪声里捞信号它反而更容易抓错重点甚至被过时的信息误导。比如你上周说先用方案 A 试试这周已经改成方案 B 了但模型看到的是两条并列的信息它可能还在按 A 给你建议。所以记忆系统的核心不是存多少而是存什么、怎么取、怎么用。claude-mem这类方案的价值就在于它把全量堆砌换成了选择性记忆 按需召回。2.2 三层记忆架构短期、长期、工作记忆我在设计自己的记忆系统时参考了认知科学里对人类记忆的分层落地成了三层结构。这套结构在claude-mem这类项目里基本是标配。第一层是短期记忆Short-term Memory。它对应的是当前这次会话的上下文。你这一轮对话里说了什么AI 回复了什么都放在这里。它的生命周期就是这次会话会话结束就清空或者归档。这一层不需要特殊处理就是模型原生的上下文窗口负责维持对话的连贯性。第二层是长期记忆Long-term Memory。这是核心。它跨会话持久化存储的是那些值得记住的信息用户的身份和偏好、项目的关键决策、重要的结论、反复出现的需求。它存在外部存储里数据库、文件、向量库都行不占用模型上下文只在需要的时候被检索出来注入。第三层是工作记忆Working Memory。这一层比较微妙它是在单次请求内临时组装出来的记忆切片。系统根据当前对话的内容从长期记忆里检索出最相关的几条拼成一小段文本注入到本次请求的上下文里。它是一次性的、动态的用完即弃。三层的关系可以这样理解短期记忆是你现在说的话长期记忆是你的档案库工作记忆是根据你当前话题从档案库里抽出来的那几页纸。真正发给模型的是短期记忆 工作记忆长期记忆始终待在外部不直接进上下文。提示三层架构的关键在于检索这一步的质量。检索不准工作记忆里塞的就是无关信息反而干扰模型。所以检索策略比存储策略更值得花时间打磨。2.3 存储选型向量库、关系库还是纯文件长期记忆存哪里是个绕不开的选型问题。我把常见的几种方案拉出来对比一下这也是我在实际项目里反复权衡过的。存储方案适用场景优势劣势纯文本/Markdown 文件个人使用、记忆量小零依赖、可读可编辑、易备份检索靠关键词语义匹配弱关系型数据库结构化记忆、需要精确查询事务可靠、查询灵活语义检索需要额外扩展向量数据库语义检索为主、记忆量大语义匹配强、扩展性好引入依赖、需要 embedding 成本混合方案生产级、兼顾精确与语义两者兼得架构复杂、维护成本高我个人的建议是从纯文件起步按需升级。如果你只是个人用记忆条目几百条以内一个 Markdown 文件加简单的关键词匹配就够了别一上来就上向量库那是过度工程。等你的记忆条目上千、发现关键词匹配经常召回错误的时候再引入向量检索。claude-mem这类项目通常会提供一个可插拔的存储层让你根据规模切换后端。这个设计思路是对的——记忆系统的复杂度应该随使用规模增长而不是一开始就背上一堆依赖。2.4 记忆的写入时机什么时候该记存储方案定了下一个问题是什么信息值得写进长期记忆如果什么都记长期记忆很快就会被垃圾填满检索质量下降。如果记得太少又起不到作用。我总结了几条写入触发规则实测下来比较靠谱显式指令触发用户明确说记住这个以后都按这个来直接写入优先级最高。决策类信息出现了我们决定用 X方案定为 Y以后统一用 Z这类表述写入。偏好类信息用户表达了对风格、格式、工具的偏好比如我喜欢简洁的回答代码用 Python写入。实体类信息项目名、关键人物、重要日期、专有名词写入。高频重复信息某个信息在多次会话里反复出现说明它重要写入。反过来寒暄、确认、临时性的中间过程一律不写。这里有个实操技巧让模型自己判断是否值得记忆。在每轮对话结束后追加一次轻量的判断请求问模型这轮对话里有没有值得长期记住的信息如果有提取成一句话。这个判断请求的 token 消耗很小但能大幅提升记忆质量。3. 核心细节解析与实操要点3.1 记忆条目的数据结构设计记忆存成什么样直接决定了后面检索好不好用。我踩过的第一个坑就是一开始只存了一句话结果检索出来发现这句话是谁说的、什么时候说的、属于哪个项目全丢了根本没法用。一个合格的记忆条目至少要包含这几个字段{ id: mem_20240115_001, content: 项目统一使用 TypeScript禁用 any 类型, type: preference, scope: project_alpha, created_at: 2024-01-15T10:30:00Z, updated_at: 2024-01-15T10:30:00Z, source_session: session_20240115, confidence: 0.9, tags: [typescript, coding-standard] }逐个字段说下为什么这么设计。content是记忆正文要求一句话说清别写成一坨。type是记忆类型我分了 preference偏好、decision决策、entity实体、fact事实几类检索时可以按类型过滤。scope是作用域区分是全局记忆还是某个项目专属避免不同项目的记忆互相污染。created_at和updated_at用于时间衰减——越久远的记忆相关性可能越低。confidence是置信度模型提取的记忆不一定百分百准给个分数检索时可以设阈值。tags用于辅助检索和分类。注意scope这个字段千万别省。我见过太多人把所有记忆混在一起结果在 A 项目里聊着聊着AI 突然引用 B 项目的决策那种错乱感非常影响信任。3.2 检索策略关键词、语义还是混合检索是记忆系统的心脏。检索不准前面存得再好都白搭。我把三种主流检索方式拆开讲。关键词检索最简单把用户当前输入分词去记忆库里匹配包含这些词的条目。优点是快、零成本、可解释。缺点是同义词匹配不了——你存的是禁用 any 类型用户问的是类型检查严格吗匹配不上。语义检索靠 embedding。把每条记忆和用户当前输入都转成向量算余弦相似度取 top-k。优点是能理解语义同义表达也能召回。缺点是需要 embedding 模型有计算成本而且纯语义检索有时候会召回意思相近但实际不相关的条目。混合检索是我最终采用的方案。先用关键词做一轮粗筛缩小候选集再对候选集做语义排序。这样既控制了 embedding 的计算量又保证了召回质量。具体流程是用户输入进来先做关键词匹配召回所有包含相关词的记忆假设得到 50 条。对这 50 条和用户输入一起做 embedding算相似度。按相似度排序取 top-5。再叠加时间衰减和置信度加权重新排序。最终取 top-3 注入上下文。时间衰减的公式我用的是指数衰减final_score similarity * exp(-λ * days_elapsed) * confidence其中 λ 是衰减系数我取 0.01意味着大约 70 天后记忆权重衰减到一半。这个值可以根据你的场景调——如果是长期项目λ 调小如果是快速迭代的短期任务λ 调大。3.3 记忆注入怎么塞进上下文才不突兀检索出来的记忆怎么注入到发给模型的请求里也有讲究。直接甩一段 JSON 进去模型可能理解不了或者把它当成用户输入的一部分。我的做法是用自然语言包装成一段背景信息放在系统提示词或者用户消息的前面明确标注这是记忆。类似这样[以下是之前会话中记录的相关背景供你参考] - 该项目统一使用 TypeScript禁用 any 类型记录于 2024-01-15 - 用户偏好简洁的回答代码示例优先记录于 2024-01-10 [背景信息结束]这样模型能清楚知道哪些是历史记忆、哪些是当前输入不会混淆。同时明确标注供你参考而不是必须遵守给模型留了判断空间——如果记忆和当前需求冲突它应该以当前需求为准。提示注入的记忆条数别贪多。我实测下来3 到 5 条是最佳区间。超过 8 条模型反而会因为信息过载而抓不住重点而且 token 成本也上去了。3.4 记忆的去重与更新记忆库用久了一定会出现重复和过时。比如你三次会话里都提到用 TypeScript就会存三条几乎一样的记忆。再比如你之前记了用方案 A后来改成用方案 B两条并存检索时可能召回旧的那条。去重和更新机制必须要有。我的做法是写入前去重。新记忆写入前先跟已有记忆做一次相似度比对。如果相似度超过 0.9判定为重复不新增只更新已有条目的updated_at。如果相似度在 0.7 到 0.9 之间判定为相关但不同可以合并或者标记关联。冲突检测与覆盖。如果新记忆和旧记忆在语义上冲突比如一个说用 A一个说用 B需要判断哪个更新。通常以时间新的为准把旧记忆标记为superseded已废弃而不是直接删除——保留历史有助于追溯决策演变。定期清理。设置一个清理任务把长期未被召回、置信度低、且超过一定时间的记忆归档或删除。我一般设 90 天没被召回就降权180 天没召回就归档。4. 实操过程与核心环节实现4.1 环境准备与依赖安装假设我们从零搭一个最小可用的记忆系统。技术栈我选 Python因为生态成熟跟各种 AI 接口对接方便。存储先用 SQLite 加一个轻量的向量检索库够个人用也方便你理解原理。先建目录结构mkdir claude-mem-demo cd claude-mem-demo mkdir -p data memory安装依赖pip install openai sqlite-vec numpy python-dotenv这里解释下选型。sqlite-vec是 SQLite 的向量扩展让你在单文件数据库里就能做向量检索不用单独部署向量数据库对个人项目非常友好。numpy用于向量运算。python-dotenv管理 API 密钥避免硬编码。注意API 密钥一定要放环境变量别写进代码。我见过有人把密钥提交到公开仓库结果被刷爆额度这个坑千万别踩。4.2 数据库表结构初始化建两张表一张存记忆条目一张存向量。import sqlite3 import sqlite_vec def init_db(db_pathdata/memory.db): conn sqlite3.connect(db_path) conn.enable_load_extension(True) sqlite_vec.load(conn) conn.enable_load_extension(False) conn.execute( CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, content TEXT NOT NULL, type TEXT, scope TEXT, created_at TEXT, updated_at TEXT, confidence REAL DEFAULT 1.0, tags TEXT, status TEXT DEFAULT active ) ) conn.execute( CREATE VIRTUAL TABLE IF NOT EXISTS memory_vectors USING vec0( memory_id TEXT PRIMARY KEY, embedding FLOAT[1536] ) ) conn.commit() return connFLOAT[1536]这个维度要跟你用的 embedding 模型对齐。我用的模型输出 1536 维所以这里写 1536。如果你换模型这个数字要跟着改否则写入会报错。这是新手最容易踩的坑之一。4.3 记忆写入的完整实现写入流程分三步提取、去重、落库。import uuid from datetime import datetime def extract_memory(conversation, client): 从对话中提取值得记忆的信息 prompt f从以下对话中提取值得长期记住的信息。 只提取用户偏好、项目决策、重要事实、专有名词。 每条记忆用一句话表述输出 JSON 数组每项包含 content 和 type。 如果没有值得记忆的内容返回空数组 []。 对话内容 {conversation} resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: prompt}], response_format{type: json_object} ) return resp.choices[0].message.content def write_memory(conn, content, mem_type, scope, client, embed_model): # 1. 去重检查 existing search_similar(conn, content, embed_model, top_k1) if existing and existing[0][similarity] 0.9: # 更新已有记忆的时间戳 conn.execute( UPDATE memories SET updated_at ? WHERE id ?, (datetime.utcnow().isoformat(), existing[0][id]) ) conn.commit() return existing[0][id] # 2. 生成 embedding emb embed_model.embed(content) # 3. 落库 mem_id fmem_{uuid.uuid4().hex[:12]} now datetime.utcnow().isoformat() conn.execute( INSERT INTO memories (id, content, type, scope, created_at, updated_at) VALUES (?,?,?,?,?,?), (mem_id, content, mem_type, scope, now, now) ) conn.execute( INSERT INTO memory_vectors (memory_id, embedding) VALUES (?,?), (mem_id, emb) ) conn.commit() return mem_id这里的关键点是去重阈值 0.9。这个值我调过几轮太低会误判把不同记忆当成重复太高会漏判重复记忆没识别出来。0.9 是个比较稳的平衡点。你可以根据自己的数据分布微调。4.4 检索与注入的实现检索用混合策略先关键词粗筛再向量精排。def search_similar(conn, query, embed_model, top_k5, scopeNone): query_emb embed_model.embed(query) sql SELECT m.id, m.content, m.type, m.created_at, m.confidence, vec_distance_cosine(v.embedding, ?) AS distance FROM memories m JOIN memory_vectors v ON m.id v.memory_id WHERE m.status active params [query_emb] if scope: sql AND m.scope ? params.append(scope) sql ORDER BY distance ASC LIMIT ? params.append(top_k) rows conn.execute(sql, params).fetchall() results [] for row in rows: similarity 1 - row[5] # 余弦距离转相似度 results.append({ id: row[0], content: row[1], type: row[2], created_at: row[3], confidence: row[4], similarity: similarity }) return results def build_memory_context(memories): if not memories: return lines [[以下是之前会话中记录的相关背景供你参考]] for m in memories: lines.append(f- {m[content]}记录于 {m[created_at][:10]}) lines.append([背景信息结束]) return \n.join(lines)vec_distance_cosine返回的是余弦距离越小越相似所以要1 - distance转成相似度。这个转换关系别搞反了我一开始就搞反过导致检索出来的是最不相关的记忆排查了半天。4.5 完整对话流程串联把上面的模块串起来一次完整的对话处理流程是这样的def chat(user_input, session_id, conn, client, embed_model, scopedefault): # 1. 检索相关记忆 memories search_similar(conn, user_input, embed_model, top_k3, scopescope) # 2. 组装上下文 memory_ctx build_memory_context(memories) # 3. 调用模型 messages [] if memory_ctx: messages.append({role: system, content: memory_ctx}) messages.append({role: user, content: user_input}) resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages ) reply resp.choices[0].message.content # 4. 异步提取并写入新记忆 conversation f用户{user_input}\n助手{reply} extracted extract_memory(conversation, client) # 解析 extracted 并逐条写入略 return reply这个流程里第 4 步的记忆提取建议异步做不要阻塞主回复。因为提取本身要调一次模型如果同步做用户会感觉响应变慢。用后台任务或者消息队列处理都行个人项目用线程池就够了。5. 常见问题与排查技巧实录5.1 记忆召回不准的排查思路这是最高频的问题。用户明明之前说过AI 却想不起来或者想起来的是不相关的。排查顺序我一般这样走第一步确认记忆到底写没写进去。直接查数据库看那条记忆在不在。很多时候问题出在写入环节——提取模型没识别出来或者去重逻辑误判了。我遇到过提取模型把用 TypeScript判定为不值得记忆的情况后来调整了提取提示词明确列出技术栈选择属于决策类必须提取。第二步确认检索能不能召回。手动拿那条记忆的原文去检索看能不能命中。如果命中不了说明 embedding 有问题或者相似度阈值设太高了。第三步确认注入有没有生效。打印出发给模型的完整 messages看记忆有没有被拼进去。有时候是代码逻辑 bug记忆检索出来了但没注入。下面这张表是我整理的常见症状和对应原因可以直接对照排查症状可能原因排查方法完全想不起来记忆未写入查数据库确认条目存在想起来但内容不对检索召回了错误条目检查相似度排序调阈值时好时坏关键词匹配不稳定改用混合检索加语义排序记的是旧信息冲突未处理检查是否有 superseded 标记响应变慢注入记忆过多限制 top-k检查 token 数5.2 记忆污染与冲突处理记忆污染是个隐蔽但危害很大的问题。所谓污染就是错误或过时的记忆被反复召回导致 AI 持续给出错误建议。我遇到过一个典型案例早期测试时误存了一条项目使用 JavaScript后来项目改成 TypeScript 了但那条旧记忆一直没清理检索时经常被召回AI 就时不时建议用 JS 写法。排查了好久才发现是记忆污染。处理办法有三条一是写入时做冲突检测。新记忆和旧记忆语义冲突时把旧的标记为superseded检索时过滤掉。二是给记忆加时效。决策类记忆设一个有效期比如 30 天过期后需要重新确认。这个对快速迭代的项目特别有用。三是定期人工审查。个人项目记忆量不大我建议每周花几分钟扫一眼记忆库把明显过时的删掉。这个习惯能省掉后面很多莫名其妙的 bug。注意删除记忆要谨慎最好用软删除标记 status 为 archived而不是物理删除。万一删错了还能恢复而且保留历史有助于理解决策演变。5.3 成本与性能的平衡记忆系统会带来额外的成本主要在两块embedding 调用和记忆提取的模型调用。embedding 成本相对低但如果你记忆量大、检索频繁也会累积。优化办法是缓存 embedding——同一条记忆的 embedding 只算一次存起来复用。查询的 embedding 也可以做短期缓存相同或相似的查询直接命中。记忆提取的模型调用是成本大头因为每轮对话都要调一次。优化办法是用小模型做提取比如用轻量模型专门干这个活成本能降一个数量级。提取任务本身不复杂小模型完全够用。性能方面检索的延迟主要来自 embedding 计算和向量搜索。个人规模下几千条记忆SQLite 加向量扩展的检索延迟在毫秒级完全无感。等记忆量到十万级以上才需要考虑专门的向量数据库。5.4 隐私与数据安全记忆系统存的是你的对话历史里面可能包含敏感信息。几点必须注意本地优先。能本地存就本地存别默认上传到第三方服务。SQLite 文件放本地你自己完全掌控。加密敏感字段。如果记忆里可能包含密钥、个人信息存储时加密。提供清除机制。用户要能一键清除所有记忆这是基本的数据自主权。注意 API 传输。调用模型接口时注入的记忆会随请求发出去确认你信任这个接口的数据处理方式。我在实际使用中的一个体会是记忆系统的价值不在于技术多复杂而在于它是否真的融入了你的工作流。我见过太多人搭了一套很精巧的记忆系统结果因为写入太慢、检索不准用了两天就弃了。反而是那种简单粗暴、但稳定可靠的方案能长期用下去。所以如果你刚开始搞别追求一步到位先把写入-检索-注入这条最小闭环跑通用起来再根据实际痛点迭代。踩过几次坑之后你会发现真正难的不是技术实现而是判断什么值得记——这个判断力只能靠大量实践慢慢养出来。