ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

给 Claude 装上长期记忆:跨会话上下文持久化方案解析

给 Claude 装上长期记忆:跨会话上下文持久化方案解析

Claude 用久了都会遇到一个尴尬时刻:前两天还在聊的项目方案,今天打开新会话,它一脸茫然,好像什么都没发生过。API 调用本身就是无状态的,每次对话都从零开始,而 claude-mem 这个项目的初衷,就是把这层“失忆”补上——给 Claude 一个真正能跨会话使用的长期记忆层。

这个项目做的是什么事?简单说,它在 Claude 的外面加了一个记忆中间层:对话结束后自动提炼关键信息并持久化存储,下一次新会话开始时把相关记忆检索出来、拼进上下文。适合谁参考?如果你正在做会话型 AI 应用、个人知识助手,或者被上下文窗口 10 万、20 万 token 的物理上限折磨过,这篇笔记值得看完。

先说清楚边界。claude-mem 不是给 Claude 换大脑,也不改变模型的推理能力,它只解决“信息持久化”和“跨会话连续性”这两个工程问题。我用它处理日常开发问答、项目笔记归档、客户需求跟踪这三类场景,效果还算理想。整个实现拆成三层:记忆提取、向量存储、检索注入,下面一层层讲。

1. 项目缘起:Claude 的记忆困境

1.1 无状态 API 与上下文窗口的上限

大模型 API 的本质是“一次性计算”。你把 prompt 发过去,模型根据这段输入生成回复,然后一切都结束了。服务端不会帮你记住上一条消息是什么,更不会在下次请求时把你之前聊过的内容翻出来。这就意味着,任何想要“跨对话记忆”的应用,都得自己做存储、自己做检索、自己把记忆塞回上下文中。

另一个硬约束是上下文窗口。Claude 的上下文窗口虽然已经很大,但 token 总量依然有限。你不可能把一个月的聊天记录全部塞进每次请求里,成本上不允许,模型的处理质量也会随着上下文变长而下降。我实测过一个场景:把连续五天的开发日志拼成一个大 prompt,回复速度明显变慢,而且模型开始漏掉早期信息——这就是注意力分散。窗口再大,也不是无限日记本。

所以 claude-mem 要解决的核心矛盾是:对话历史无限增长,而上下文窗口有限。解法不是“全部塞入”,而是“按需召回”。类似人脑的记忆机制,不记得所有细节,但记得关键结论和重要事实,需要用的时候再翻出来。

1.2 真实使用中的“失忆”代价

我最初做 claude-mem,是被一个真实场景逼的。某次我让 Claude 帮忙设计一个跨平台系统的数据同步模块,连续讨论了四天,产出了完整的表结构、接口定义和异常处理方案。第五天我开新会话想继续调优,结果它完全不记得之前的约定,重新问了一遍需求,还给出了截然不同的方案。那一刻我意识到,对话的“连续性”不能靠模型自觉,必须靠工程手段保证。

类似场景还有不少:客户在聊天里确认了一个报价,第二天换个会话问细节,Claude 答不上来;个人知识库里整理了三十篇笔记摘要,问模型的时候它只能基于当前 prompt 回答,完全无法引用历史整理结果。这些问题的本质都一样——对话没有记忆层。

1.3 目标与边界:claude-mem 到底解决什么

claude-mem 的设计目标可以拆成四条:

  • 跨会话记忆:上次聊过的关键结论,下次能自动带回来
  • 按主题召回:不是所有历史都有用,只提取相关的记忆片段
  • 低侵入接入:不改 Claude 本身,只在外围做一层封装,调用方几乎无感知
  • 可控可删:用户能查看到底存了什么,能手动清理敏感内容

同时我也给自己划了边界:不做事实库的实时更新、不做全局知识图谱、不做多用户权限系统。范围一旦扩大,项目复杂度会翻好几倍,核心价值反而会被稀释。先把“记下来、找得到、拼得进去”这三件事做好,比什么都重要。

2. 架构设计:从三层记忆到一条管线

2.1 三层记忆模型

参考人类记忆的分层方式,我把记忆分成三个层次,每一层对应不同的持久化粒度和生命周期:

  • 短期记忆(Short-term):当前会话内的完整消息列表,直接存在于上下文中,不做额外存储
  • 中期记忆(Mid-term):单次会话结束后生成的摘要,包含主题、结论、未决事项,存储为结构化文本
  • 长期记忆(Long-term):跨会话沉淀下来的原子化事实,如“用户偏好 Docker 部署”“项目 deadline 是下月十号”,每条独立存储并向量化

这样分层的原因是:不同信息的复用价值不同。会话内的事务性细节(“刚才你贴的那段代码我改了第三行”)最多活几天,不值得进入长期存储;而“项目的认证方案选型是 JWT”这种结论性信息,几个月后依然重要,必须沉入长期层。

每一层我都对应了一条数据处理规则。短期层只有一个操作:透传,模型自己处理。中期层靠一次“总结调用”生成,成本低、速度快。长期层则是核心,要做信息抽取、去重合并、向量化三步连做,这是整个项目工程量最大的地方。

2.2 存储与检索选型

长期记忆的存储我对比过三条路线:普通关系型数据库、键值存储、向量数据库。直接结论是:向量数据库在“语义召回”这个需求上有着不可替代的优势,最终选了它作为主存储,同时配合一个轻量级元数据库做辅助过滤。

为什么必须用向量数据库?因为记忆召回不是精确匹配。比如用户问“上次说的那个异步任务方案是什么”,而历史记忆里存的是“消费者组处理消息堆积的思路”,两者没有共同关键词,但语义上是同一件事。用词匹配会漏,用向量相似度才能捞回来。

嵌入模型的选择上,我用的是本地的通用文本嵌入模型,按句子切开后进行向量化,维度 768,距离度量用余弦相似度。实测下来,针对中文技术对话,召回 Top 5 的准确率在七成左右,对记忆增强场景足够用。如果后续需要更高精度,可以考虑换更大规模的嵌入模型,但成本会明显上升,目前这个平衡点是可以接受的。

2.3 数据流全景:一次对话怎么变成一条记忆

完整流程我用一条链路来描述:

  1. 用户在应用层发起对话,Claude 正常生成回复,短期层工作
  2. 应用层调用 claude-mem 记录消息对(用户输入 + 模型输出),暂存在内存队列
  3. 会话结束后,触发记忆提取:先调用一次“总结模式”生成中期摘要,再对摘要做信息抽取,得到多条原子化事实
  4. 每条事实经过清洗、去重、合并后,生成唯一 ID,写入向量存储,同时把来源会话、创建时间、主题标签写入元数据库
  5. 用户发起一次新对话时,应用层先调用 claude-mem 的检索接口,基于当前输入做向量查询,召回相关记忆
  6. 召回结果按相关度和时间排序,组装成“记忆上下文块”,拼接到用户消息前,再交给 Claude

这套链路的关键在于“召回发生在生成之前”,也就是说,每一次模型调用都先经历一次记忆检索。这一步骤的时延我控制在 50 毫秒内,对最终用户体验几乎没有影响。

3. 核心实现:记忆的写入、存储与召回

3.1 记忆提取:规则与模型结合

记忆提取是最容易做坏的一步。如果什么都存,向量库里塞满噪音,召回准确率直线下降;如果存得太少,又等于没记忆。我的方案是规则与模型结合,分两步走。

第一步是规则过滤:把消息对中的纯闲聊、重复提问、机械应答先过滤掉。判定规则很简单,看消息长度、问句比例、代码块出现次数。比如只有一句话的寒暄,或者模型千篇一律的“有什么可以帮助您的”,直接丢弃。这一步能砍掉约四成的无用数据。

第二步是模型抽取:保留下来的消息对,批量送入 Claude,用专门的指令模板做主题提取和事实抽取。我用的 prompt 大概是这样的:

从以下对话中提取可以长期保存的事实。 要求: 1. 每条事实必须是一个完整的原子句,主语明确 2. 只保留有长期参考价值的结论、偏好、决策、任务状态 3. 忽略临时性细节和寒暄内容 4. 输出格式为 JSON 数组,每项包含 fact、topic、importance 三个字段

抽取结果示例:

[ { "fact": "数据同步模块确定使用增量同步方案,依赖 binlog 监听", "topic": "数据同步方案", "importance": 0.9 }, { "fact": "客户要求界面支持中英文切换,英文文案待提供", "topic": "产品需求", "importance": 0.8 } ]

importance 是重点设计。它不是一个简单数字,而是后续召回排序时的重要权重。我设置了了三个档位:0.9 是结论性事实,必须长期保留;0.7 是阶段性进展,保留但允许过期;0.5 以下是参考性信息,召回优先级最低。这一招能有效避免“昨天确认的方案”和“随口说的一句猜测”在召回时地位相同的问题。

3.2 向量化与元数据设计

抽取出来的原子事实,要做两件事:向量化、打标签。向量化没什么好说的,调用嵌入模型把句子变成一个浮点数组,关键在切分策略。一条事实可能包含多个信息点,比如“用户要求性能测试并发 500,且要压测 30 分钟”,拆成一条存还是两条存?我建议拆成两条。因为后续检索可能是“性能测试并发多少”,也可能是“压测时长要求”,拆开后两条都能被精准召回,合在一起颗粒度太大,容易漏。

元数据设计上,我用的字段包括:fact_id、topic、session_id、created_at、importance、access_count。其中 access_count(召回次数)是一个很有意思的字段,它衡量这条记忆的活跃度。访问次数高的说明是高频事实,可以缓存在内存中,减少每次向量查询的开销;长期没有被召回的记忆,后台任务可以降级归档,避免存储无限膨胀。

写入策略上,我没有每产生一条事实就立刻写库,而是攒批处理。会话结束后统一写入,每批不超过五十条。原因很朴素:向量库的写入有索引构建成本,频繁小批量写会导致索引碎片化,影响查询性能。批量写还能顺带做一次全局去重,用事实文本的相似度判断,和已存记忆相似度超过 0.92 的就视为重复,只更新重要度和时间戳,不新增。

3.3 检索注入与上下文组装

检索是整个链路里最需要调优的部分。我的召回策略是“向量相似度 + 元数据过滤 + 时间衰减”三管齐下,缺一个效果都有明显下降。

具体流程是:拿到用户的新输入,先生成输入向量,然后做一次向量查询,同时用元数据字段过滤掉过期的主题和时间窗口。排序公式是一个加权和:

score = 0.7 * cosine_similarity + 0.2 * importance - 0.1 * time_decay

time_decay 是一个随时间增长的惩罚项,超过 30 天的记忆分数开始递减。这样设计是想在相关性和新鲜度之间取得平衡,避免一个月前的旧方案压过今天的最新进展。

召回的 Top 5 条记忆,我会拼成一个“记忆上下文块”,放在用户消息之前。格式是:

[长期记忆参考] - 数据同步方案:确认使用增量同步(创建于 5 天前) - 客户偏好:界面需支持双语切换(创建于 2 天前) --- 用户新消息 --- (原始输入)

这个格式信息密度高,Claude 能一眼识别出哪些是记忆、哪些是当前问题。还有一个细节:如果某次召回的 Top 5 里没有一条超过基础相似度阈值,我就不注入记忆,直接让模型裸跑。强行塞不相关内容反而会干扰当前问题判断,这个阈值我设为 0.65,低了会引入噪音,高了会漏掉该召回的信息。

4. 实操落地:部署、配置与联调

4.1 环境准备与依赖

整个 claude-mem 可以独立部署成一个微服务,和主应用之间通过 HTTP 通信。这样做的好处是语言无关,主应用不管是用 Python 还是别的语言写的,都能轻松接入。依赖也不算多,三块:

  • Python 3.10+,主要运行环境
  • 向量存储服务,负责持久化和相似度查询
  • 一个轻量级关系库,保存元数据和配置

安装我就直接用虚拟环境管理:

mkdir claude-mem && cd claude-mem python3 -m venv venv source venv/bin/activate pip install claude-mem-core

装完之后建议先跑一遍内置健康检查,确认基础路径没问题:

claude-mem-cli health-check

如果输出全部 OK,说明依赖和网络都正常;如果卡在向量存储连接那一项,优先检查对应服务的地址和端口配置。

4.2 配置文件的关键参数

claude-mem 的配置我集中在一个 YAML 文件里,初版配置不需要改太多,但有两个参数一定要亲手调。第一个是 embedding_model,默认指向本地模型地址,如果你的部署环境需要走远程模型调用,要改成对应的 API 端点;第二个是 top_k,控制每次召回的条数,默认 5。我试过改成 3,召回的上下文太薄,模型经常因为信息不足而答非所问;改成 8,效果提升有限,但 token 消耗明显增加,所以 5 是性价比最高的值。

另一个容易被忽略的参数是 summary_threshold,它控制会话多长才值得生成总结。默认是 0,意思是不管多短的会话都会触发总结。但真实使用中,只有三五句话的对话也总结,会浪费大量 token。我调成了 0.6,即会话轮次超过六轮才做总结,短对话直接跳过,成本省了大约两成。

配置文件的完整结构大概是这样的:

memory: vector_store: host: 127.0.0.1 port: 19530 meta_store: db_path: ./claude_mem_meta.db embedding: endpoint: http://127.0.0.1:8080/embed dimension: 768 retrieval: top_k: 5 min_similarity: 0.65 summary: min_rounds: 6

4.3 与 Claude 调用链集成的具体做法

最省事的接入方式不是修改 Claude 的调用代码,而是在调用之前插入一个“记忆装饰器”。我习惯在原有的 Claude API 封装外面包一层,这样业务代码几乎不用动。伪代码逻辑如下:

def chat_with_memory(user_input: str, session_id: str) -> str: memories = memory_retriever.search(user_input, top_k=5) augmented_input = assemble_context(user_input, memories) response = claude_api.chat(augmented_input) memory_ingestor.record(user_input, response, session_id) return response

每次请求先做检索,再拼上下文,最后在返回前记录这次的问答对。注意 record 这一步是异步落库的,不要阻塞请求响应。我一开始是同步写库,结果每次对话平白多了几百毫秒的延迟,改成异步队列后彻底解决。

会话生命周期这边也有讲究。我定义了一个会话结束的显式信号,而不是靠超时判断。比如用户在前端点了“结束对话”,或者后端检测到任务目标达成,就调用finalize_session(session_id),触发记忆提取流程。如果依赖超时,会话状态会一直悬着,记忆迟迟不沉淀,用户体验很差。

4.4 运行验证:怎么确认记忆真的生效了

写完集成,我习惯用一个“跨会话询问”的用例验证。第一次会话告诉 Claude 一个虚构事实,比如“项目的认证方案确定用 JWT,不用 Session”。结束会话后,第二次会话直接问“认证方案现在是什么方案”。如果第二轮的回复包含 JWT,说明记忆链路是通的;如果答不上来,我会按这三步排查:查向量库里有没有对应记录、查检索结果有没有召回、查注入后的完整 prompt 有没有丢掉记忆块。九成问题出在第三步,即上下文组装的变量顺序被搞错了,记忆块放在了系统提示词之外,导致模型没有把它作为参考信息对待。

为了便于复查,我给每条注入的记忆都加了一个前缀标记[MEM],这样在日志中能看到某次请求具体注入了哪些记忆,方便快速定位是“没存进来”还是“没搜出来”还是“没拼进去”。

5. 避坑清单与工程优化

5.1 高频踩坑点与排查实录

第一个高频问题是“记忆串味”。跨用户访问时,A 用户的记忆被 B 用户看到。根因是元数据过滤器没加上用户维度,向量查询漏设了过滤条件。我踩过一次之后,把所有检索请求强制要求携带一个 scope 参数,可以是用户 ID、项目 ID 或会话组 ID,元数据过滤条件里必须带上,否则查询直接报错。这个设计虽然增加了一点调用成本,但换来的是安全底线。

第二个问题是总结重复。同一个话题聊了两天,每天结束时都会生成一条“关于 X 的结论”,如果不去重,向量库里会有五条语义几乎一样的记录。我加了一步全局相似度比对,新增记忆前先和库里的 Top 1 做相似度计算,超过 0.92 就合并而不是新增。这个阈值不是拍脑袋定的,我调过好几轮,低于 0.9 会把不同细节的内容误并,高于 0.95 又拦不住同一结论的不同说法。

第三个问题是 embedding 模型切换后,新旧向量维度不一致,导致查询报错或召回异常。我在向量写入时会把模型版本写进元数据,检索时按当前版本过滤,确保只用同版本向量做相似度计算。切换模型前,我会先跑一次全量重嵌入脚本,把旧向量统一升级,不然新旧混着用,召回质量会非常玄学。

5.2 成本、延迟与容量优化

成本大头在总结调用和向量化。总结这块,我上面提到过用min_rounds控制触发频率。向量化这边,我做了缓存:同一个会话内的高频句子,只算一次向量,后面的直接复用。实测下来缓存命中率大概有四成,嵌入服务的调用量显著下降。如果预算更紧,还可以降级为只对事实抽取结果做向量化,不对原始对话做,成本能再省一半,但召回精度会打折扣,适合小流量验证阶段用。

延迟优化主要在做三件事。一是检索结果加进程内缓存,像用户 ID 和项目 ID 这类高频检索维度,缓存命中后延迟能从五十毫秒降到几毫秒。二是批量召回策略,如果连续提问十几轮,与其每轮都查向量库,不如每三轮合并一次,把相近的对话压成一个查询任务,减少服务端压力。三是把耗时的记忆写入和下一轮的请求并行,写入放在后台任务里,完全不影响对话响应速度。

数据容量也要提前想。向量库不是无限存就完事,我加了容量监控,当记录数超过预期上限时,触发归档逻辑:超过九十天且access_count为零的记忆转入冷存储,不参与在线检索。这相当于给记忆系统做了“遗忘机制”,保证长期运行后检索延迟不会持续恶化。

5.3 隐私与安全设计的一点实践

记忆系统里全是用户的敏感信息,安全性必须前置考虑。我做的第一件事是敏感信息过滤。在记忆提取之后、落库之前,加了一道正则和规则过滤,把明显的手机号、邮箱、卡号这类信息打码。更高阶的做法是用一个脱敏模型做实体识别,但对于个人项目,正则加白名单已经能覆盖大多数场景。

第二件事是可删除性。我提供了一个forget(session_id)接口,调用后该会话相关的所有记忆,包括向量和元数据,全部物理删除。这个接口必须提供,否则用户一旦发现记忆系统记住了不该记的东西,又没有途径清理,信任就没了。

第三件事是访问控制。虽然 claude-mem 目前是单机部署,但我从第一天就把 scope 参数作为必填项,不给后面多用户扩展留技术债。等真要接多用户场景,只需要把 scope 和用户体系关联起来,存储和检索逻辑都不用大改。

最后一件事跟 API 密钥有关。Claude 的密钥不要存在配置里,放进环境变量,或者用密钥管理服务统一持有。claude-mem 本身不保存任何密钥,只通过注入的方式从应用层获取,避免配置文件和密钥绑定导致泄露。

写在最后的一点个人体会

整个 claude-mem 从想法到稳定运行,大约花了两周。我最大的体会是:给大模型加记忆,本质上是把一个“纯函数式”的模型调用,改造成了一个“有状态”的系统,这个改造过程中所有复杂度都来自状态管理——什么时候存、存什么、怎么取、取完了怎么拼。这些问题没有标准答案,全看你的具体场景。

如果你也想动手做类似的东西,我建议不要一上来就搭完整的向量存储和复杂的抽取管线。先用手写 JSON 文件存记忆,把“跨会话把结论带回来”的最小闭环跑通,再逐步替换成向量库,你会发现每一步优化都有清晰的目标感和验证方法。我的 claude-mem 能跑成现在这个状态,靠的就是这个循序渐进的过程。最后再分享一个小技巧:给你的记忆系统加一个“查看原始库”的调试接口,所有写入的记忆都能一览无余。有了这双眼睛,调试效率和翻仓库排查问题的速度会快非常多。

RELATED READING

延伸阅读

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