news 2026/10/26 7:52:08

claude-mem:跨会话上下文持久化与语义检索实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
claude-mem:跨会话上下文持久化与语义检索实践

1. 项目概述与核心定位

1.1 这个工具到底解决什么问题

claude-mem这个名字第一次看到的时候,我下意识以为是某个 Claude 的周边小工具,实际用下来才发现它解决的是一个非常具体的痛点:跨会话的上下文持久化。

如果你日常用 Claude 做开发、写文档、做研究,肯定遇到过这种情况——昨天聊了三个小时的架构方案,今天开新会话,它完全不记得你是谁、在做什么项目、之前定了哪些技术选型。你得重新贴一遍背景,重新解释约束条件,甚至重新纠正它上次犯过的错。这种重复劳动在长期项目里极其消耗精力。

claude-mem的思路很直接:把每次会话中值得保留的信息抽取出来,存到一个本地可检索的记忆库里,下次开新会话时自动把相关记忆注入上下文。它不是简单的聊天记录存档,而是做了结构化提取 + 语义检索 + 按需注入这一整套流程。

适合谁来用?我梳理了三类:

  • 长期维护同一项目的开发者:项目背景、技术决策、踩坑记录需要跨天甚至跨周保持连贯
  • 用 Claude 做研究或写作的人:参考资料、论点脉络、已写过的段落需要持续累积
  • 多项目并行的人:不同项目之间的记忆需要隔离,不能互相污染

1.2 核心架构拆解

claude-mem的架构可以拆成四层,我用一个生活化的类比来解释:把它想象成一个私人助理的笔记本系统。

层级功能类比
采集层从会话中提取关键信息助理在会议中做笔记
存储层结构化保存记忆条目笔记归档到分类文件夹
检索层根据当前问题找相关记忆助理翻出相关历史记录
注入层把记忆拼接到新会话上下文助理在开会前给你简报

采集层是最关键的一环。它不能把整段对话原封不动存下来,那样检索时噪音太大。常见做法是用一个轻量的提取 prompt,让模型输出结构化的记忆条目,每条包含:主题标签、核心内容、时间戳、关联项目。这个提取过程本身也消耗 token,所以需要控制频率——不是每轮对话都提取,而是在会话结束或达到一定轮次后批量处理。

存储层我建议用 SQLite 配合向量扩展(比如 sqlite-vec),原因后面会详细讲。检索层是语义检索为主、关键词检索为辅的混合策略。注入层则要控制总量,不能把所有相关记忆都塞进去,否则上下文窗口很快就被占满了。

1.3 为什么值得单独做一个工具

有人可能会问:Claude 本身不是有 Projects 功能吗?为什么还要自己搞一套?

我实际对比过,差异主要在三个地方。第一,Projects 的知识库是静态的,你手动上传什么它就用什么,而claude-mem是动态增长的,每次会话都在自动积累。第二,Projects 的检索是黑盒,你不知道它到底用了哪些内容,而自建方案可以精确控制注入哪些记忆、注入多少。第三,也是最重要的——数据完全在本地,对于涉及内部项目信息的场景,这一点是刚需。

还有一点:claude-mem这类工具通常设计成可以配合 API 调用使用,意味着你可以把它集成到自己的脚本、编辑器插件或者自动化流程里,而不是被绑死在某个网页界面上。这个灵活性是官方功能给不了的。

2. 核心细节解析与实操要点

2.1 记忆提取的粒度控制

这是整个项目里最容易做砸的地方。我踩过的坑是:一开始提取粒度太细,每轮对话都存一条,结果记忆库迅速膨胀到几千条,检索出来的内容全是碎片,拼在一起反而干扰模型判断。

后来调整成按主题聚合的方式:不是按对话轮次切分,而是按话题切换点切分。具体做法是维护一个滑动窗口,当检测到当前话题与上一个话题的语义相似度低于阈值时,就把上一个话题的完整片段作为一个记忆单元存下来。

阈值怎么定?我用的是余弦相似度 0.65 作为分界。低于这个值认为话题发生了切换。这个数字不是拍脑袋来的——我拿自己过去两周的对话记录做了标注,试了 0.5、0.6、0.65、0.7、0.75 几档,0.65 在"不过度切分"和"不遗漏切换"之间平衡最好。当然这个值跟你的对话风格有关,建议自己标注二三十个样本调一下。

每条记忆条目的结构我建议包含这些字段:

{ "id": "唯一标识", "project": "项目名", "topic": "主题标签", "summary": "一两句话的核心内容", "detail": "完整片段", "entities": ["涉及的技术名词、人名、文件名"], "timestamp": "创建时间", "access_count": "被检索次数", "last_access": "最后检索时间" }

entities字段很关键,它是关键词检索的抓手。access_count和last_access用于后续做记忆衰减——长期不被访问的记忆可以降低检索权重,避免陈旧信息干扰。

注意:提取记忆时一定要带上项目标识。我见过有人把所有项目的记忆混在一个库里,结果写 A 项目的代码时检索出了 B 项目的架构决策,直接导致模型给出错误建议。

2.2 存储方案选型:为什么是 SQLite + 向量扩展

存储方案我试过三种:纯文件(JSON/Markdown)、纯向量数据库(Chroma、Qdrant)、SQLite + 向量扩展。最后选了第三种,理由如下。

纯文件方案最简单,一条记忆一个 Markdown 文件,用 grep 检索。优点是透明、可读、易备份。缺点是语义检索基本没法做,只能靠关键词匹配,召回率很低。我用了两周就放弃了。

纯向量数据库方案检索能力强,但引入了一个独立的服务进程,部署和维护成本上来了。而且向量数据库通常不擅长结构化过滤——比如"只检索 project=A 且 timestamp 在最近一周内"这种条件,在纯向量库里做起来很别扭。

SQLite + sqlite-vec 的组合刚好补上了这个缺口。SQLite 处理结构化字段的过滤和排序是强项,sqlite-vec 提供向量相似度检索,两者在同一个查询里就能完成"先按项目过滤,再按语义排序"的操作。而且整个数据库就是一个文件,备份就是复制一个文件,迁移毫无成本。

建表语句大概长这样:

CREATE TABLE memories ( id TEXT PRIMARY KEY, project TEXT NOT NULL, topic TEXT, summary TEXT, detail TEXT, entities TEXT, timestamp INTEGER, access_count INTEGER DEFAULT 0, last_access INTEGER ); CREATE VIRTUAL TABLE memory_vectors USING vec0( memory_id TEXT PRIMARY KEY, embedding FLOAT[768] );

向量维度 768 对应的是常见的嵌入模型输出维度。如果你用的模型维度不同,改这个数字就行。注意 sqlite-vec 目前对维度的支持是固定的,建表时就要确定好,后期改维度需要重建表。

2.3 检索策略:混合检索的权重分配

单纯靠向量检索有个问题:对于包含具体文件名、函数名、专有名词的查询,向量检索反而不如关键词精确匹配。比如你问"上次那个parse_config函数怎么改的",向量检索可能返回一堆关于配置解析的泛泛内容,而关键词检索能精准命中包含parse_config的那条记忆。

我的做法是双路检索 + 加权融合:

  1. 向量检索取 Top 20,按相似度打分
  2. 关键词检索(对 entities 和 detail 做全文索引)取 Top 20,按 BM25 打分
  3. 两路结果合并去重,用加权公式算最终分数

加权公式我用的是:

final_score = 0.6 * vector_score_normalized + 0.4 * bm25_score_normalized

0.6 和 0.4 这个比例也是调出来的。纯向量(1.0/0)在专有名词查询上表现差,纯关键词(0/1.0)在语义泛化查询上表现差,0.6/0.4 是我测试集上综合表现最好的。如果你的使用场景里专有名词查询特别多,可以把关键词权重提到 0.5。

检索出来之后还要做一步去重和压缩。因为同一主题可能有多条记忆,内容有重叠。我的做法是按 topic 分组,每组只保留分数最高的那条,如果组内有多条且内容互补,就做一次摘要合并。这一步能显著减少注入的 token 量。

2.4 注入策略:上下文窗口的预算管理

检索到相关记忆后,怎么注入也是有讲究的。全塞进去肯定不行,上下文窗口是稀缺资源。

我的预算分配是这样的:假设总上下文窗口是 200K token,我会给记忆预留 10% 到 15%,也就是 20K 到 30K token。在这个预算内,按检索分数从高到低填充,填满为止。

注入的格式也很重要。我试过几种,最后固定成这个结构:

[历史记忆 - 项目A] 主题:数据库选型讨论 时间:2024-01-15 内容:确定了使用 PostgreSQL 而非 MySQL,原因是需要 JSONB 支持和更丰富的索引类型... --- 主题:API 认证方案 时间:2024-01-18 内容:采用 JWT + refresh token 方案,access token 有效期 15 分钟...

用分隔符明确标出每条记忆的边界,并带上时间和主题,这样模型能清楚地知道哪些是历史信息、什么时候的信息。实测下来,带时间戳的记忆比不带时间戳的,模型在判断信息时效性时准确率高不少。

提示:注入位置建议放在系统提示之后、用户当前问题之前。放在最前面容易被模型忽略,放在最后面又可能干扰当前问题的理解。

3. 实操过程与核心环节实现

3.1 环境搭建与依赖安装

先把基础环境跑起来。我用的是 Python 3.11,主要依赖就几个:

pip install sqlite-vec anthropic numpy

sqlite-vec是 SQLite 的向量扩展,安装后会提供一个可加载的扩展文件。anthropic是官方 SDK,用来调嵌入模型和做记忆提取。numpy处理向量运算。

加载扩展的代码:

import sqlite3 import sqlite_vec db = sqlite3.connect("memories.db") db.enable_load_extension(True) sqlite_vec.load(db) db.enable_load_extension(False)

注意enable_load_extension用完要关掉,这是个安全习惯。有些环境默认禁用了扩展加载,如果报错说not authorized,需要在连接时传check_same_thread=False并确认 Python 编译时开启了扩展支持。

3.2 记忆提取的完整实现

提取逻辑我封装成一个函数,输入是一段对话文本,输出是结构化的记忆列表。核心是构造一个提取用的 prompt:

EXTRACT_PROMPT = """分析以下对话片段,提取值得长期保留的记忆条目。 要求: 1. 只提取有长期价值的信息:技术决策、项目背景、明确的偏好、踩坑记录 2. 忽略寒暄、临时性讨论、已经被推翻的方案 3. 每条记忆包含:主题(10字以内)、摘要(50字以内)、详情(200字以内)、涉及的关键实体 4. 如果整段对话没有值得保留的内容,返回空列表 对话内容: {dialogue} 以 JSON 数组格式输出,每个元素包含 topic, summary, detail, entities 四个字段。"""

这里有几个细节值得说。第一,明确要求"忽略已经被推翻的方案"很重要,否则模型会把讨论过程中被否定的选项也存下来,后续检索时造成干扰。第二,"如果整段对话没有值得保留的内容,返回空列表"这个兜底条件能避免模型强行编造记忆。

调用提取:

def extract_memories(dialogue, project): response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=2000, messages=[{"role": "user", "content": EXTRACT_PROMPT.format(dialogue=dialogue)}] ) try: memories = json.loads(response.content[0].text) except json.JSONDecodeError: return [] for m in memories: m["project"] = project m["timestamp"] = int(time.time()) return memories

JSON 解析失败的情况一定要处理,模型偶尔会输出带 markdown 代码块包裹的 JSON,或者末尾多一个逗号。我加了一层清洗:先 strip 掉json 和,再尝试解析。

3.3 向量化与入库

每条记忆的 summary + detail 拼接后做嵌入:

def embed_text(text): response = client.embeddings.create( model="voyage-3", input=text ) return response.embeddings[0]

嵌入模型的选择上,我用的是 voyage-3,768 维。选它的原因是它在中文和英文混合文本上的表现比较均衡,而且支持 32K 的输入长度,对于较长的记忆详情也能一次处理完。

入库操作要保证原子性,记忆表和向量表要么都成功要么都失败:

def save_memory(db, memory): embedding = embed_text(memory["summary"] + " " + memory["detail"]) db.execute("BEGIN") try: db.execute( "INSERT INTO memories (id, project, topic, summary, detail, entities, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)", (memory["id"], memory["project"], memory["topic"], memory["summary"], memory["detail"], json.dumps(memory["entities"]), memory["timestamp"]) ) db.execute( "INSERT INTO memory_vectors (memory_id, embedding) VALUES (?, ?)", (memory["id"], json.dumps(embedding)) ) db.execute("COMMIT") except Exception as e: db.execute("ROLLBACK") raise e

3.4 检索与注入的串联

检索函数把前面说的双路策略实现出来:

def retrieve_memories(db, query, project, top_k=10): query_vec = embed_text(query) # 向量检索 vector_results = db.execute(""" SELECT memory_id, distance FROM memory_vectors WHERE embedding MATCH ? ORDER BY distance LIMIT 20 """, (json.dumps(query_vec),)).fetchall() # 关键词检索 keyword_results = db.execute(""" SELECT id, bm25(memories_fts) as score FROM memories_fts WHERE memories_fts MATCH ? AND project = ? ORDER BY score LIMIT 20 """, (query, project)).fetchall() # 融合打分 scores = {} for mid, dist in vector_results: scores[mid] = scores.get(mid, 0) + 0.6 * (1 - dist) for mid, bm25 in keyword_results: scores[mid] = scores.get(mid, 0) + 0.4 * normalize_bm25(bm25) # 按项目过滤并排序 ranked = sorted(scores.items(), key=lambda x: -x[1]) results = [] for mid, score in ranked[:top_k]: row = db.execute("SELECT * FROM memories WHERE id = ? AND project = ?", (mid, project)).fetchone() if row: results.append(row) return results

normalize_bm25是因为 BM25 的分数范围不固定,需要归一化到 0-1 才能和向量分数加权。我用的是 min-max 归一化,在当前结果集内做。

注入函数把检索结果拼成前面说的格式:

def build_memory_context(memories, max_tokens=20000): context_parts = [] current_tokens = 0 for m in memories: entry = f"主题:{m['topic']}\n时间:{format_time(m['timestamp'])}\n内容:{m['detail']}\n---" entry_tokens = estimate_tokens(entry) if current_tokens + entry_tokens > max_tokens: break context_parts.append(entry) current_tokens += entry_tokens return "[历史记忆]\n" + "\n".join(context_parts)

estimate_tokens我用的是简单的字符数除以 2.5 的估算,中文场景下这个比例比较接近实际。要精确的话可以调 tokenizer,但每次检索都调一遍开销不小,估算够用了。

4. 常见问题与排查技巧实录

4.1 检索结果不相关怎么办

这是最高频的问题。排查思路按这个顺序走:

第一步,检查嵌入模型是否一致。入库时用的模型和检索时用的模型必须是同一个。我遇到过有人入库用 voyage-3,检索时手滑改成了别的模型,结果向量空间对不上,检索出来的全是随机结果。

第二步,检查项目过滤条件。如果检索时 project 参数传错了,或者记忆入库时 project 字段为空,过滤就会失效或过滤掉所有结果。建议在检索函数里加一行日志,打印出过滤前后的候选数量。

第三步,看查询本身是否太短。三五个字的查询,向量检索效果很差。我的做法是当查询短于 10 个字符时,自动把最近几轮对话的上下文拼进去一起做嵌入,增加语义信息量。

第四步,检查记忆质量。如果提取出来的记忆本身摘要就很模糊,检索自然不准。可以抽样看几条记忆的 summary 字段,如果都是"讨论了某个问题"这种废话,说明提取 prompt 需要调整,要求模型写得更具体。

4.2 记忆库膨胀太快怎么控制

我最初的版本两周就攒了 3000 多条记忆,检索延迟明显上升。后来加了三层控制:

去重:入库前先做一次相似度检查,如果新记忆和已有记忆的向量相似度超过 0.92,就合并而不是新增。合并策略是保留新的 detail,但把 access_count 累加。

衰减:定期(我是一周一次)跑一个清理任务,把超过 90 天未被访问且 access_count 小于 2 的记忆归档到冷存储表,不参与常规检索。

容量上限:给每个项目设一个记忆条数上限,比如 500 条。超过时按"最后访问时间 + 访问次数"的综合分数淘汰最低的。

这三层下来,我的记忆库稳定在 400 条左右,检索延迟控制在 200ms 以内。

4.3 常见问题速查表

问题现象可能原因排查方法解决方案
检索返回空项目过滤条件不匹配打印过滤前后数量检查 project 字段
检索结果随机嵌入模型不一致对比入库和检索的模型名统一模型
注入后模型答非所问记忆内容与当前问题冲突检查注入的记忆时间戳加时效性权重,旧记忆降权
提取出大量无意义记忆提取 prompt 太宽松抽样检查 summary 质量收紧 prompt,加负面示例
入库报错向量维度不匹配检查建表时的维度重建表或换模型
检索延迟高记忆条数过多统计总条数启用去重和衰减
JSON 解析失败模型输出格式不规范打印原始输出加清洗逻辑和重试

4.4 几个我踩过的坑

坑一:时间戳用了本地时间。一开始用datetime.now(),后来跨时区协作时发现记忆时间全乱了。改成统一用 Unix 时间戳存,展示时再转本地时区。

坑二:entities 字段存成了字符串。早期版本 entities 直接存 JSON 字符串,做关键词检索时匹配不到。后来改成存成空格分隔的字符串,同时对 detail 字段建全文索引,检索命中率提升明显。

坑三:忘记处理并发写入。如果你同时开多个会话,两个进程同时写 SQLite 会锁库。我的做法是加一个文件锁,写入时串行化。SQLite 本身支持 WAL 模式,开启后并发读性能好很多:

PRAGMA journal_mode=WAL;

坑四:注入的记忆没有排序。早期版本按入库顺序注入,结果最新的记忆排在最后,模型容易忽略。改成按检索分数排序,最相关的放最前面,效果立竿见影。

4.5 性能优化的几个实用技巧

嵌入调用是最大的延迟来源,每次检索都要调一次嵌入 API,网络往返 200-500ms。我的优化是加一层查询缓存:对最近 100 个查询做 LRU 缓存,命中缓存直接返回结果。实际使用中查询重复率不低,缓存命中率能到 30% 左右。

批量嵌入也值得做。如果你一次要入库多条记忆,不要一条一条调嵌入,拼成一个 batch 一次调用,能省不少时间。voyage-3 支持一次最多 128 条输入。

检索时的向量计算,如果记忆条数在几千条以内,SQLite 的暴力扫描完全够用。超过一万条再考虑加 IVF 索引或者换专门的向量库。不要过早优化,我见过有人几百条记忆就上了分布式向量库,纯属折腾自己。

最后分享一个我在实际使用中觉得最有价值的经验:记忆的提取质量比检索算法重要得多。我花了很多时间调检索的权重和排序,后来发现真正影响体验的是提取出来的记忆本身是否准确、具体、有信息量。把提取 prompt 打磨好,比换任何检索算法都管用。具体来说,在提取 prompt 里加几个正例和反例,让模型明确知道什么样的内容值得存、什么样的不值得,效果提升非常明显。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 7:52:00

GEN-1具身智能:多模态感知与动态运动控制技术解析

1. GEN-1:具身智能的范式突破当波士顿动力的机器人开始后空翻时,我们意识到具身智能(Embodied Intelligence)已经跨过了机械控制的阶段。GEN-1的出现,标志着具身基础模型(Embodied Foundation Models&#…

作者头像 李华
网站建设 2026/10/26 7:50:07

Codex上下文压缩后Goal不中断:双层记忆与目标保留机制解析

1. 先看现象:上下文压缩到底“压”掉了什么先说一个我自己的实测经历。上个月接了一个跨模块重构任务,要动十几个文件,任务描述本身就有三千多字。Codex 进入 Goal 模式之后一口气跑了几十轮,文件改了一堆,期间还穿插着…

作者头像 李华
网站建设 2026/10/26 7:50:03

OpenMontage:程序化视频合成框架,批量生成视频的自动化方案

1. 项目缘起与核心定位第一次看到“OpenMontage”这个名字,我脑子里蹦出来的第一反应是“开放剪辑”或者“开源蒙太奇”。Montage这个词在影视行业里太经典了,指的是通过镜头组接来压缩时间、制造隐喻、传递情绪的那套手法。把Open放在前面,基…

作者头像 李华
网站建设 2026/10/26 7:46:59

Dyna-2.1物理智能体:构建端到端闭环工作流的工程实践

最近我一直在折腾 Dyna-2.1 这套面向端到端工作流程的物理智能体框架,说实话,一开始我对它的预期并不高。过去两年里市面上叫“智能体”的东西太多了,大部分只是接个大模型API,会解个数学题、写段代码就敢自称Agent。但物理智能体…

作者头像 李华
网站建设 2026/10/26 7:46:59

AI Agent设计模式与架构演进实践

1. AI Agent设计模式概述最近两年,AI Agent技术正在经历从单任务处理到复杂协作的范式转变。作为一名全程参与过多个人工智能项目落地的技术负责人,我深刻体会到设计模式在AI Agent开发中的重要性。传统的面向对象设计模式已经无法完全满足AI Agent的特殊…

作者头像 李华
网站建设 2026/10/26 7:46:44

OpenClaw飞书图片消息发送问题解析与优化

1. OpenClaw飞书消息发送图片的路径陷阱解析上周在给客户部署OpenClaw对接飞书机器人时,遇到个看似简单却折腾了半天的坑:通过filePath参数发送的图片消息,在PC端和移动端显示效果截然不同。这个现象背后涉及到文件路径处理、飞书客户端渲染机…

作者头像 李华