☰
Claude长期记忆实现指南:上下文压缩、向量检索与记忆分层
2026/10/10 17:17:59 网站建设 项目流程

1. 项目背景:为什么我会专门做一个 claude-mem

先说结论:claude-mem 是一个给 Claude 这类大语言模型补上“长期记忆”的独立记忆层。很多人在实际用 Claude 做自动化任务、个人助理、客服机器人时,都会遇到同一个尴尬:上一轮刚聊完的事情,下一轮再从零开始解释。这不是模型笨,而是它的会话上下文机制决定的。普通的对话请求是无状态的,每次调用都要把前面的内容重新传一遍,传不完就截断,截断了就丢记忆。

这个项目最早是我想解决一个非常具体的问题:我自己的知识库问答机器人,每次用户问完一个问题之后,我再追问细节,它就像失忆了一样,把刚才给出的结论忘得一干二净。后来我发现问题不在模型,而在我的调用方式——我只传了当前这条消息,没有传历史。但传历史也有瓶颈,上下文窗口再大也有上限,几千轮对话不可能全塞进去,于是 claude-mem 的想法就冒出来了:把聊天内容在进入窗口之前先处理好,能压缩的压缩,能存下来的存下来,下次用到的时候再把最相关的那部分捞回来。

如果你也在做基于 Claude 的智能助手、自动化流程、群聊机器人或者个人知识管理工具,并且被“上下文窗口有限”“跨会话无记忆”“每次都要重复喂背景”这几个问题折磨过,那 claude-mem 这套思路应该能直接帮到你。它不是某个现成插件,更偏向一套可以自己实现的记忆管理方案:分短期、长期两层,短期负责当前对话的连续,长期负责跨会话的调用,核心就是记忆提取、压缩、存储、检索这四件事。

1.1 上下文窗口的硬限制,是记忆问题的根源

大语言模型的对话能力确实强,但它的“记忆力”和人的记忆根本不是一回事。你发一条请求过去,模型只能看到请求里的文本;你关闭页面、断开连接,这段会话对模型来说就不存在了。上下文窗口这个概念,可以理解成模型一次性能放进工作台的材料数量。窗口越大,它能“同时想到”的内容越多,但依然有限,不可能无限堆。

用一个生活化的例子:上下文窗口就像是你的办公桌,桌面够大能摊开更多文件,但桌面再大也有边界。你同时打开一百份文档,桌面上堆满了,后面的文件就只能压到最底下,或者干脆放不下。对话一轮轮继续,消息数量增长,总 token 数也在涨,超出窗口边界之后,最早的对话要么被裁剪,要么被丢弃。在真实使用中,大概十几轮比较密集的问答之后,模型就开始“忘记”你最开始提的要求了。

所以记忆问题的本质不是模型能力,而是上下文管理能力。如果你的应用场景只是偶尔问几个问题,那没有记忆系统问题不大;但如果你要做一个持续运转的助手,那记忆层就是刚需。claude-mem 解决的就是这一层:在消息抵达模型之前,先由记忆系统决定“哪些话要说给模型听,哪些话留在仓库里”。

1.2 会话记忆系统到底要解决哪几类问题

第一类问题是跨会话记忆。用户昨天向你报备过项目背景,今天新开一个会话,模型不应该再问一遍“你的项目背景是什么”。这需要一种机制,把历史会话中提炼出的用户意图、偏好、关键事实沉淀下来,在后续会话中主动携带。

第二类问题是长会话的上下文压缩。单次会话聊了几百轮,不可能每次请求都把全部历史带上。claude-mem 的做法是阶段性总结:聊到一定轮数,就用一次额外的模型调用把前面的对话浓缩成摘要,之后的请求携带摘要而不是原始记录。这样既保住了关键信息,又控制住了 token 消耗。

第三类问题是主动召回。不是所有历史信息都需要在每轮对话里出现,很多背景事实只在特定话题下有用。比如用户上次聊的是“部署方案”,这次聊的是“费用预算”,上次的细节对这次帮助不大。这时候需要把历史记忆向量化存起来,每次对话前根据当前问题做相似度检索,只把最相关的几段记忆取出来注入上下文。

第四类是消息持久化和检索。记忆系统本身要有一套存储结构,最好能存下会话、消息、摘要、向量索引。很多人在对话应用里做了记忆功能,但只是简单拼字符串,存到内存里重启就没了。claude-mem 在这方面更接近正经的工程化方案:有数据库表,有写入接口,有查询接口,能独立于模型运行。

1.3 什么人适合参考这套方案

如果你只是想拿 Claude 临时写个文案,那 claude-mem 对你来说太重了,直接聊就行。但如果你满足下面任意一条,建议认真看后面的实现部分:

  • 正在开发客服机器人、AI 助理,希望用户下次进来还能记得自己是谁。
  • 在跑自动化分析流程,多轮对话之间需要共享上下文。
  • 做知识库问答,希望模型能引用“上次对话提到的那份文档”。
  • 已经在用原始消息拼接历史,但发现上下文窗口经常爆。

我发现很多开发者的第一版记忆方案都是从“拼接全部历史”开始的,我也是这么过来的。问题是,这条路在会话次数少的时候能跑,量上来之后必然撞墙。早期设计时就把记忆分层和存储规划好,后面省下来的调试时间非常可观。

2. 整体设计思路:claude-mem 如何组织记忆

claude-mem 不打算把“记忆”做成一个黑盒,而是拆成几个相互独立、又能自由组合的模块。我用的是三层记忆结构:工作记忆、摘要记忆、长期记忆。

工作记忆就是当前会话窗口里正在用的内容,最接近模型原生的上下文;摘要记忆是从历史对话中提炼出的压缩信息,负责“保存大概发生了什么”;长期记忆则是从对话中抽取出来的具体事实,比如用户偏好、项目信息、决定结论,以结构化或向量化的形式入库。三层各司其职,召回的优先级也不同。

先看一张模块功能的对比,后面每部分单独展开:

模块输入输出存储方式触发时机
工作记忆当前用户的输入最近几轮对话原文内存/上下文列表每次请求
摘要记忆一段较长的历史会话结构化摘要文本SQLite 文本字段达到 token 阈值或固定轮数
长期记忆会话、用户信息、结论向量化记忆片段向量索引 + 元数据表对话结束后批量处理

2.1 记忆分层的取舍:为什么不全塞给模型

刚开始设计的时候,我试过最简单粗暴的方案:把所有历史消息原封不动地拼在一起,塞进 system prompt,让模型自己处理。小规模测试没问题,到了真实数据量之后立刻暴露三个毛病。第一是 token 消耗高得离谱,每轮请求都在给模型全文背诵一遍历史;第二是噪声太大,模型被大量不相关内容分散注意力,反而忽略当前问题;第三是成本线性增长,用户越多越吃不消。

分层记忆的思路就是承认一个事实:模型不需要记住所有东西,它只需要在当前上下文里拿到最关键的部分。工作记忆解决近期连续性的问题,摘要记忆解决长会话遗忘的问题,长期记忆解决跨会话召回的问题。每一层的信息密度不同,时效性不同,所以处理方式也不一样。

这个结构其实很像人的记忆方式。你早上和同事开的会,晚上还能清楚记得讨论细节,这是短期记忆;一个月后你再回想那次会,大概只能想起当时定下的几个结论,这是经过压缩的抽象记忆;而那些每次合作都会出现的偏好,比如“你习惯用 Python 写脚本”,早就沉淀成了长期记忆,不用刻意回忆也能用上。

2.2 记忆生命周期:从写入到召回的完整链路

claude-mem 的记忆生命周期分五个阶段:采集、提炼、存储、检索、注入。

采集阶段发生在每一轮对话结束后,系统拿到新的用户消息和模型回复,判断这段内容值不值得进入记忆流程。提炼阶段由两个动作组成:一个是把连续对话切片做摘要,另一个是从摘要和原文中抽取可长期保留的结构化信息,比如用户偏好、项目名称、决策结论。存储阶段把摘要写入关系表,把结构化信息转成向量存入向量索引。

检索阶段在每次用户发起请求之前触发。claude-mem 会先做两路查找:一路拿当前最近几轮对话作为工作记忆,另一路把当前问题编码成向量,在长期记忆库里做相似度搜索,取出 Top K 条记忆。注入阶段把找出来的记忆按照优先级拼装成最终的 prompt 上下文,再交给模型。

整个链路的设计原则是“先检后写”。很多记忆系统把重心放在写入上,写了很多数据但查不准,结果模型还是用不上。claude-mem 把检索质量当作核心指标,因为从应用效果看,注入到 prompt 里的记忆片段是否精准,比库里有几百条历史重要得多。

2.3 为什么不用全历史拼接,而要用摘要加向量

全历史拼接是大多数人起步时的默认选择,因为它最简单。但随着历史变长,模型输出质量会明显下降。我实测下来,当历史消息超过总 token 长度的三分之一时,模型开始出现“注意力稀释”现象:前面的背景它还记得,但已经很模糊,回答的重点也开始偏离当前问题。

摘要压缩的核心作用不是节省 token,而是强制信息提纯。让模型用一次额外调用,把几十轮对话压缩成几个要点,相当于请一个助理先把会议纪要写好,再让另一个人做决策。只不过这个助理也是模型自己。代价是每次摘要也要消耗 token,但相比每轮都带着全文跑,整体开销小得多。

向量检索的作用则是解决“从大量历史里找对内容”的问题。把记忆片段转换成高维向量,再用余弦相似度或者内积计算相似度,本质上是在做语义匹配。只要 embedding 模型选得够好,即使查询词和历史记忆没有重合的字面词,也能找到语义相近的内容。claude-mem 的默认策略是摘要 + 向量双通道,摘要保证主干不丢,向量保证细节能召回。

3. 从零实现 claude-mem 的核心环节

设计讲完了,接下来是实操。这里我不会给一个完整的成品代码,而是把最关键的几个环节拆开讲清楚:存储结构怎么建、摘要任务怎么触发、向量检索怎么接、最终怎么把记忆注入请求。你拿到这套逻辑之后,完全可以用自己喜欢的语言重新实现一遍。

3.1 存储结构:单库表设计,别一上来就上大数据组件

记忆系统不是大数据系统,大部分场景用 SQLite 就能扛住。我见过的很多个人项目,一上来就规划了 MySQL、Redis、Elasticsearch,结果部署复杂度比功能本身还高。claude-mem 的存储层用一张元数据表加一张向量表就能覆盖绝大多数需求。

下面的建表语句是一个可落地的起点:

CREATE TABLE conversations ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, title TEXT, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id INTEGER NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, token_count INTEGER DEFAULT 0, created_at TEXT DEFAULT (datetime('now')) ); CREATE TABLE memories ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, content TEXT NOT NULL, memory_type TEXT NOT NULL, -- summary / fact / preference source_message_ids TEXT, embedding BLOB, metadata TEXT, created_at TEXT DEFAULT (datetime('now')) );

conversations 和 messages 是基础数据,记录完整的会话原始内容。memories 表是记忆核心,memory_type 区分这条记忆是摘要、事实还是用户偏好。embedding 字段存向量二进制数据,metadata 存 JSON 格式的附加信息,比如来源时间、话题标签。

为什么要把 embedding 直接存在同一个库里?因为个人项目的记忆规模通常在十万条以内,SQLite 配合内存加载完全跑得动,没必要额外维护一套向量数据库。向量库是搜索专用引擎,它强在大规模分布式检索,但代价是架构复杂度。先用一个库解决问题,等服务真正成长之后再拆分,这个顺序我认为是对的。

3.2 摘要压缩的触发与执行:设置好阈值,别每条消息都去总结

摘要太频繁浪费 token,摘要太少又容易丢关键内容。claude-mem 的触发条件是两个维度:消息条数和 token 数。我常用的配置是:当一个会话的累积消息数超过 20 轮,或者最近一次请求的总 token 数超过 8000,就对这段历史做一次摘要。

这里有个实操细节:摘要应该基于会话分段做,而不是对全体历史反复总结。比如聊到第 20 轮时,把第 1 到 20 轮总结成一段摘要;继续聊到第 40 轮,把第 21 到 40 轮总结成另一段摘要,而不是把前面已有的摘要再和后面混合重新总结。这样做的原因是防止摘要信息在反复压缩中失真。每次摘要都是信息损耗的过程,叠两次还可以,叠十次就会把关键结论磨没了。

摘要的 prompt 可以精简成下面这样:

请根据以下对话记录生成摘要,要求: 1. 保留用户的核心目标、已作出的决定、明确的偏好。 2. 忽略寒暄、话题反复和无关内容。 3. 使用中文,控制在 200 字以内。 4. 输出格式为要点列表,每行一个要点。 对话记录: {conversation_text}

实际调用时,我会用一个独立的辅助函数来封装摘要请求,和正常对话请求走同一个模型接口,但 temperature 调得更低,让输出更稳定。摘要结果写回 messages 表旁边的 memories 表,memory_type 标记为 summary。

3.3 向量化存储与相似度召回:embedding 接入的关键点

长期记忆要支持语义检索,就必须做向量化。claude-mem 的做法是提供一个 embedding 接口,你把它替换成任何可用的 embedding 服务。代码如下:

import numpy as np import sqlite3 def get_embedding(text: str) -> list[float]: # 这里替换成你自己的 embedding 接口 # 返回一个 1024 维的向量列表 ... def cosine_similarity(a: list[float], b: list[float]) -> float: a_arr = np.array(a) b_arr = np.array(b) return float(np.dot(a_arr, b_arr) / (np.linalg.norm(a_arr) * np.linalg.norm(b_arr))) def search_memories(session_id: str, query: str, top_k: int = 5): conn = sqlite3.connect("claude_mem.db") query_vec = get_embedding(query) rows = conn.execute( "SELECT content, embedding, metadata FROM memories WHERE session_id = ?", (session_id,) ).fetchall() scored = [] for content, embedding_blob, metadata in rows: if embedding_blob is None: continue saved_vec = np.frombuffer(embedding_blob, dtype=np.float32) score = cosine_similarity(query_vec, saved_vec.tolist()) scored.append((score, content, metadata)) scored.sort(key=lambda x: x[0], reverse=True) return scored[:top_k]

写入记忆时,把 embedding 结果转成 bytes 存进 BLOB:

vec = get_embedding(content) embedding_blob = np.array(vec, dtype=np.float32).tobytes() conn.execute( "INSERT INTO memories (session_id, content, memory_type, embedding, metadata) VALUES (?, ?, ?, ?, ?)", (session_id, content, "fact", embedding_blob, json.dumps({"time": created_at})) ) conn.commit()

这里最容易踩的坑是维度不一致。不同的 embedding 模型返回的向量维度不同,有的 384 维,有的 1024 维,如果中途换模型,老数据会直接检索不出来。所以项目启动时就要记住向量维度,或者在表里把维度也存下来,做兼容迁移。

3.4 把记忆注入请求:组装最终上下文的顺序有讲究

记忆检索出来后,不是简单地把几条内容往 system prompt 里一丢就行。注入顺序和格式直接影响模型对信息的重视程度。claude-mem 的默认组装顺序是:

  1. 当前任务指令(固定 system 内容)。
  2. 长期记忆片段,标注“这是此前对话中你了解到的信息”。
  3. 最近一轮摘要。
  4. 当前对话最近几轮原始消息。

为什么要这样排?因为模型对 prompt 开头和结尾的注意力更强。任务指令放在最前面,保证模型不会偏离当前目标。长期记忆放在靠前的位置,相当于在进入正式对话前先做背景同步。摘要放在中间,作为衔接。最后面的原始消息是当前工作记忆,模型回答时需要重点参考,放末尾能获得较强注意力。

组装逻辑的代码大概是这样的:

def build_prompt(user_input: str, session_id: str, recent_messages: list[dict]) -> str: memories = search_memories(session_id, user_input, top_k=5) memory_block = "\n".join( [f"- 过往记忆:{mem.content}" for mem in memories] ) system_content = f"""你是一位智能助手。以下是从历史对话中提取的长期记忆: {memory_block} 请结合这些记忆,尽量自然地回答用户的问题。如果记忆与当前问题无关,不要强行引用。""" # 把 system_content、摘要、recent_messages 按顺序拼成最终请求 ...

这里有个容易被忽略的细节:注入的记忆应该带“可忽略”的许可。如果记忆检索结果不准确,模型强行引用只会让回答变得更怪。所以在 system 提示里要有“如果记忆与当前问题无关,不要强行引用”这句。这个看似多余的设置,在实际体验中能显著减少胡说八道。

4. 常见问题与排查实录:claude-mem 实战中的坑

这部分是我自己调试时真实踩过的坑,挑几个高频的写出来,每一个都花了不止一个晚上才搞清楚。

4.1 上下文窗口还是爆了:摘要触发不及时

现象:已经做了摘要压缩,但长对话进行到中段还是偶尔出现超限报错。

排查思路:先看摘要的触发条件有没有优化。我最初只在“消息数超过 20 轮”时触发摘要,但消息数和 token 数不是一回事。有人一条消息发两千字,有人一条消息就一个“好”。结果就是消息数到了 20,token 数已经悄悄冲到三万,再叠加系统指令和检索回来的记忆,直接顶到模型限制。

解决办法是把触发条件改成双阈值:只要消息数超 20,或者当前累积 token 数超 8000,就执行摘要。同时摘要后要清理 messages 表中的历史原始内容,把已经被摘要覆盖的消息标记为 archived,不参与后续的上下文组装。这一步不做到位,原始消息和摘要会同时占着窗口,等于没压。

另外还有一个细节:摘要任务本身也会消耗 token,如果当前请求已经接近窗口上限,就不能再发起摘要请求。所以摘要要放在“上一轮对话结束后、本轮请求开始前”执行,而不是卡在请求中间去做。否则摘要还没完成,主请求就已经因为超限失败了。

4.2 检索出来的记忆完全不相关:top_k 和打分策略都有关

现象:向量检索 Top 5 返回的记忆和当前问题完全无关,模型有时还会强行引用,导致回答质量下降。

排查思路:这一类问题多半不是 embedding 模型不行,而是打分策略太单一。claude-mem 早期版本只用余弦相似度排序,结果发现对话型记忆里有些内容天然相似。比如用户聊“部署”、聊“费用”时,都涉及“服务器”这个词,向量距离很近,但它们在当前问题上并不重要。

解决办法是给记忆加上权重。我给每条记忆加了一个“重要性分”和“时效衰减”。重要性分在写入时由模型对记忆片段打分,时效衰减则让三个月前的记忆自动降权:

def compute_final_score(similarity: float, importance: float, age_days: int) -> float: time_decay = 0.9 ** (age_days / 7) return similarity * (0.6 + 0.4 * importance) * time_decay

这样排序出来的结果会更贴近真实需要。另外,如果有多条记忆的相似度接近 0.85 以上,只保留其中一条即可,避免重复内容反复出现,稀释注意力。

还有一个经验:检索的时候把 memory_type 作为过滤条件。用户当前问的是“上次我的部署结论是什么”,那就只搜 fact 类型;用户问“上次我们聊到哪了”,就搜 summary 类型。混在一起检索,返回的东西往往两边都不精准。

4.3 记忆写重复:同一个事实被存了好几条

现象:长期记忆库越来越臃肿,同一个项目名、同一个偏好,在库里有 7、8 条内容几乎一样的记录。

排查思路:写入时没有做去重。claude-mem 需要在写入前增加一个相似度检查:把待写入的文本向量和库内已有记忆做一遍比较,如果相似度超过 0.95,就认为是重复内容,丢弃新写入,或者把新旧记录合并。

合并时有一个技巧:保留信息更完整的那条,同时更新它的来源时间和最后引用时间。不要简单地删旧留新,因为旧的可能包含一些已经消失的细节。折中方案是:把新内容的要点补充进旧记录,然后更新 embedding,因为旧记录的向量已经不能完全代表扩充后的内容了。

我后来把写入去重做成了异步任务,每批处理 10 到 20 条待写入记忆,批量计算相似度,再决定入库、合并还是丢弃。这样写虽然增加了一点延迟,但长期记忆库的卫生状况明显改善,检索准确率也稳定下来。

4.4 成本翻倍:摘要调用太频繁

现象:加了记忆系统之后,模型的 token 消耗比之前暴涨了 80%,甚至更多。

排查思路:记忆系统本质上是用额外的模型调用换更好的记忆,如果每轮对话都触发一次摘要、每次都把所有记忆重新向量化,成本必然失控。控制成本有几个具体手段:

  • 摘要只对超阈值的会话段执行,平时不触发。
  • 不使用大模型做 extra embedding,用专用嵌入模型,成本低很多。
  • 缓存 embedding:相同或高度相似的文本直接复用已有向量。
  • 把摘要模型切换成更便宜的配置,摘要任务对模型能力的要求远低于正式对话。

我实际跑下来的经验是:记忆系统的额外成本能控制在主对话成本的 15% 以内,才算合格。超过这个数字,就需要检查是有太多无效摘要还是重复向量化。

4.5 本地存储与隐私边界:记忆不是越全越好

记忆系统有一个容易忽略的问题:它会把所有对话内容变成可持续调用的数据。如果这个系统跑在个人助手场景还好,但如果是给真实用户用,就必须考虑“遗忘”机制。

claude-mem 的做法是给每条记忆加一个 expires_at 字段,定期清理超过有效期的记忆。用户也可以主动删除会话,级联删除相关记忆。这个功能看起来简单,但在真实产品里极其重要。别等用户来投诉“你们把我三个月前的对话记太清楚了”,再补功能就晚了。

我还养成了一个习惯:记忆库的备份用 SQLite 自身的 backup 机制,每天一次增量,每周末一次全量。因为这个库承载的是整个助手的行为基础,丢一次比丢代码还难受。

5. 一些我认为值得再琢磨的扩展方向

如果上面这套核心逻辑你已经跑通了,有几个方向可以继续深化。

第一个方向是给记忆加“主动回顾”能力。现在的 claude-mem 还在被动检索状态,用户问什么就找什么。更高级的形态是模型自己判断“当前话题牵扯到一个月前的一个决定”,然后主动去检索。实现思路也不复杂:把检索动作本身变成一个 tool call,让模型在需要时调用 search_memory 工具,而不是每次请求前都盲目注入。

第二个方向是记忆的图谱化。向量检索擅长找相似文本,但不擅长表达“A 和 B 之间的关系”。比如用户说过“我喜欢用 Python”,又说过“我最近在做数据分析”,这两条记录在向量空间里距离可能不远,但真正有价值的是数据之间的关系。把记忆抽成实体和关系,存入图结构,再结合向量检索做混合召回,是我正在尝试的下一代结构。

第三个方向是多用户隔离。如果你不是只给自己做一个工具,而是给一个团队或一群用户用,记忆必须按用户维度严格隔离。目前的表结构里只存了 session_id,多用户场景需要再增加一层 user_id 维度,所有读写都强制带用户条件。这个改造不难,但一旦开始做,权限、备份、删除都跟着变复杂,建议早期就规划进去。

第四个方向是我认为最实用的小优化:在摘要压缩时加入“用户目标追踪”。普通的摘要只是压缩事实,但对话中用户的目标往往在慢慢演变。比如一开始说“我要搭一个博客”,聊着聊着变成“先用静态站方案”。如果不把目标变化单独记录下来,模型后期只记得一堆零散事实,忘了当前主线。我目前的做法是在摘要 prompt 里强制要求输出“当前目标”和“待办事项”两栏,效果比单纯要点列表好很多。

回到 claude-mem 这个项目本身,我最深的体会是:记忆系统和技术模型是两个完全不同的工程问题。模型的能力决定了回答质量的上限,而记忆系统决定了这个上限能不能持续发挥。做记忆层,难点不在 AI,而在工程细节——什么时候写入、什么时候压缩、什么时候丢弃、检索排序怎么调,每个环节都是经验堆出来的。

写到这里,最后分享一个很小的实战技巧:调试记忆系统时,别只盯着最终对话效果,最好给每条注入记忆打上来源标签。比如注入的内容用特殊标记包裹,让模型回答时能说明出处。这样一旦回答出现问题,你能立刻判断是检索错了、记忆本身错了,还是模型理解错了,排查效率能快上一倍。这一点在我维护 claude-mem 的过程中帮了最大的忙。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询