claude-mem:用一个Claude监视另一个Claude,值得吗?

claude-mem:用一个Claude监视另一个Claude,值得吗?

最近在调研AI编程助手的记忆持久化方案时,看到了claude-mem这个项目。它试图解决一个真实的痛点:Claude Code每次会话结束后丢失所有上下文。但仔细研究源码后,我发现它的架构设计存在一个根本性的矛盾——为了记住东西,它付出的代价可能比遗忘本身更贵。

claude-mem做了什么

一句话概括:它在你用Claude Code编码的同时,启动一个并行的Claude子进程作为”观察者”,实时监视你的每一步操作,将观察结果压缩成结构化记录,存入SQLite和ChromaDB,下次开会话时自动注入相关历史上下文。

架构长这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
用户编码会话(主Claude进程)

├─ SessionStart Hook → 注入历史上下文
├─ UserPromptSubmit → 初始化会话,启动观察者
├─ PostToolUse Hook → 每次工具调用都捕获
├─ PreToolUse(Read) → 读文件前注入该文件的历史
├─ Stop Hook → 生成会话摘要
└─ SessionEnd Hook → 完成会话生命周期


Worker服务(port 37777)

├─ SDK Agent(观察者Claude子进程)
│ → 接收工具调用数据
│ → 生成XML结构化观察
│ → 严格只读,禁用全部12个工具

├─ SQLite(原始存储 + FTS5全文搜索)
└─ ChromaDB(向量语义搜索)

亮点在哪

公平地说,这个项目有几个设计是不错的:

1. Observer-Only的安全边界

观察者Agent显式禁用了全部12个工具(Bash、Read、Write、Edit、Grep、Glob等),确保记忆Agent不会对你的项目产生任何副作用。这是防止”Agent套娃”失控的关键:

1
2
3
4
5
6
// SDKAgent.ts:55-68
const disallowedTools = [
'Bash', 'Read', 'Write', 'Edit', 'Grep', 'Glob',
'WebFetch', 'WebSearch', 'Task', 'NotebookEdit',
'AskUserQuestion', 'TodoWrite'
];

2. 渐进式信息披露

查询端的3层搜索设计确实精巧:

  • search → 紧凑索引(~50-100 tokens/条)
  • timeline → 时间线上下文
  • get_observations → 完整详情(~500-1000 tokens/条)

先过滤再获取细节,查询时能省约10倍token。

3. 优雅降级

所有Hook在Worker不可用时返回exit 0,永远不阻塞用户的编码流程。隐私标签(<private><system-reminder>等)在Hook层就被剥离,数据进入Worker之前已经过滤。

4. 多IDE适配器模式

通过PlatformAdapter抽象,用一个适配器接口支持了Claude Code、Cursor、Gemini CLI、Windsurf、Codex CLI等六个平台。新增IDE只需要写一个适配器。

核心问题:写入端是个token黑洞

claude-mem宣传的”10x token节省”只是查询端的故事。但写入端呢?

SDKAgent.ts的核心流程:每次你在Claude Code里执行一个工具调用(Bash、Read、Write、Edit、Grep……),PostToolUse Hook就会把tool_nametool_inputtool_response全部捕获,JSON序列化后发给观察者Claude子进程:

1
2
3
4
5
6
7
8
// prompts.ts:114-123
return `<observed_from_primary_session>
<what_happened>${obs.tool_name}</what_happened>
<parameters>${JSON.stringify(toolInput, null, 2)}</parameters>
<outcome>${JSON.stringify(toolOutput, null, 2)}</outcome>
</observed_from_primary_session>

Return either one or more <observation>...</observation> blocks, or an empty response...`;

每一次工具调用都是一次API请求。一个正常的编码会话可能有几十上百次工具调用。

更要命的是,SDK Agent保持会话上下文,这意味着:

  • 第1次观察:input tokens = 系统prompt + 观察1
  • 第2次观察:input tokens = 系统prompt + 观察1 + 响应1 + 观察2
  • 第N次观察:input tokens = 系统prompt + 全部历史 + 观察N

Input tokens线性增长。它自己也知道这是个问题,所以代码里做了token追踪:

1
2
3
4
5
6
// SDKAgent.ts:210-232
session.cumulativeInputTokens += usage.input_tokens || 0;
session.cumulativeOutputTokens += usage.output_tokens || 0;
if (usage.cache_creation_input_tokens) {
session.cumulativeInputTokens += usage.cache_creation_input_tokens;
}

还把discoveryTokens存进了每条observation——说明作者知道这是个ROI问题。

具体成本估算

假设一个中等活跃的编码会话:80次工具调用,每次观察prompt约2000 tokens,观察者响应约500 tokens。

阶段 Token消耗
Init prompt ~1500
80次观察(累积上下文) ~1500 + 2500×1 + 2500×2 + … + 2500×79 ≈ 7.9M input
80次响应 ~40K output
Summary ~3K
总计 ~8M tokens

这还没算ChromaDB那边的embedding成本(虽然chroma-mcp用本地模型,但也要CPU/内存)。

为了”记住”这个会话做了什么,你可能额外花了8M tokens。而下次查询时省的那点token,可能只有几千。

对比:graphify的做法

graphify试图解决类似的”理解代码库”问题,但写入端的设计思路完全不同:

维度 claude-mem graphify
代码提取 全靠Claude做语义压缩 tree-sitter AST解析23种语言(免费
写入触发 每次工具调用实时触发 一次性扫描,增量更新
音视频 不支持 faster-whisper本地转录(免费
向量存储 ChromaDB + 额外embedding 不用向量,Leiden拓扑聚类
写入频率 O(n),n=工具调用次数 O(1),首次构建+增量更新
缓存策略 无(每次都调API) SHA256缓存,只处理变更文件

graphify的核心洞察是:代码的结构信息可以完全在本地提取。函数名、类定义、import关系、调用图——这些都是确定性的,tree-sitter解析就够了,不需要LLM。只有文档、图片这些非结构化内容才需要调Claude。

本质区别:graphify是”花一次钱建索引”,claude-mem是”每秒都在烧钱做笔记”。

如果让我重新设计

claude-mem的核心问题不是功能设计,而是在错误的层面使用了LLM。工具调用的输入输出本身就是结构化数据,用Claude去”理解”JSON格式的tool_input和tool_output,这是最大的浪费。

第一原则:能本地做的绝不调API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
用户编码会话

├─ PostToolUse Hook
│ │
│ ▼
│ ┌─────────────────────────────┐
│ │ 本地结构化提取(0 token) │
│ │ │
│ │ Bash → 命令 + 退出码 │
│ │ Read → 文件路径 + 行范围 │
│ │ Write → 文件路径 + 大小 │
│ │ Edit → 文件路径 + diff指纹 │
│ │ Grep → 搜索模式 + 匹配数 │
│ │ │
│ │ 全部是确定性的字符串解析 │
│ └───────────┬─────────────────┘
│ │
│ ▼
│ SQLite 原始事件流

├─ SessionEnd(会话结束时,一次性)
│ │
│ ▼
│ ┌─────────────────────────────┐
│ │ 批量语义压缩(花一次钱) │
│ │ │
│ │ 本地预压缩200个事件 │
│ │ → 几百token的紧凑描述 │
│ │ → 一次Claude调用做摘要 │
│ └───────────┬─────────────────┘
│ │
│ ▼
│ 结构化摘要存入DB

└─ SessionStart(下次会话开始)


SQLite查询 → FTS5全文搜索 → 注入上下文

本地提取:工具调用本来就是结构化的

这是关键洞察。Claude Code的hook给你的数据已经是JSON了——tool_nametool_inputtool_response都有明确的schema。用Claude去”理解”这些数据,就像用ChatGPT去解析一个已经格式化好的CSV。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function extractEvent(hookData: PostToolUseData): RawEvent {
const { tool_name, tool_input, tool_response } = hookData;

switch (tool_name) {
case 'Bash':
return {
type: 'command',
command: tool_input.command?.slice(0, 500),
exit_code: tool_response.exitCode,
output_lines: countLines(tool_response.stdout),
has_error: tool_response.exitCode !== 0,
};

case 'Read':
return {
type: 'file_read',
file: tool_input.file_path,
lines: `${tool_input.offset || 0}-${(tool_input.offset || 0) + (tool_input.limit || 0)}`,
};

case 'Edit':
return {
type: 'file_edit',
file: tool_input.file_path,
old_length: tool_input.old_string?.length,
new_length: tool_input.new_string?.length,
// 可选:用tree-sitter提取修改了哪个函数(本地,免费)
scope: detectScope(tool_input.file_path, tool_input.old_string),
};

case 'Write':
return {
type: 'file_create',
file: tool_input.file_path,
size: tool_input.content?.length,
};

// Grep/Glob 类似...
}
}

80次工具调用?80次字符串解析,0 token,< 10ms。

会话结束时的批量摘要:把200个事件压成一次API调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
async function summarizeSession(sessionId: string): Promise<Summary> {
const events = db.getEventsBySession(sessionId);

// 本地预压缩:200个事件 → 几百token的紧凑描述
const condensed = condenseEvents(events);
// 输出类似:
// "读了 src/auth.ts 3次,改了2次(函数 validateToken, refreshSession)
// 运行 npm test 5次,3次失败后2次成功
// 创建了 src/middleware/rate-limit.ts(新文件,约200行)
// 搜索了 'CORS' 相关代码,在3个文件中找到匹配"

// 一次Claude调用,输入可控(几百~几千token)
const summary = await claude.complete({
prompt: `基于以下编码活动,生成结构化摘要:\n${condensed}`,
max_tokens: 1000,
});

return parseSummary(summary);
}

同样80次工具调用的会话:

  • claude-mem当前设计:~8M tokens(80次API调用 + 上下文累积)
  • 我的设计:~3K tokens(1次API调用,输入是本地预压缩的几百token)

成本差距:约2600倍。

存储层:砍掉ChromaDB,SQLite一把梭

1
2
3
4
5
SQLite(唯一存储)
├── raw_events -- 原始工具事件(结构化,确定性提取)
├── session_summaries -- 会话结束时的AI摘要
├── file_index -- 文件路径→最近操作的倒排索引
└── FTS5虚拟表 -- 全文搜索(SQLite内置)

为什么不需要ChromaDB:

  • “我上次改了哪个文件”→ 文件路径精确查询
  • “上次处理CORS问题时做了什么”→ FTS5全文搜索
  • “这个项目最近的工作进展”→ 按时间排序取最近N条

这三类查询覆盖了跨会话记忆90%的使用场景,全部是SQLite原生能力。

如果真遇到需要语义理解的模糊查询,在查询时用Claude做rerank就够了——按需付费,不是预付费。一次查询花几百token做rerank,远比每次写入都做embedding划算。

查询时的按需处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户开始新会话


SessionStart Hook

├─ 1. SQLite精确查询(免费,<10ms)
│ - 同项目最近N个会话摘要
│ - 当前文件相关的历史操作
│ - FTS5全文搜索匹配关键词

├─ 2. 结果足够?→ 直接注入,结束

└─ 3. 结果不够或需要语义匹配?
→ 此时才调Claude做rerank(按需)
→ 只在大项目+模糊查询时触发

设计哲学的本质差异

1
2
3
4
5
claude-mem的哲学:  实时观察,持续理解,先花钱后省钱
我的设计哲学: 延迟处理,批量压缩,不花钱直到必须花

claude-mem: Write-Heavy → 每步都调API,写入贵,查询便宜
我的设计: Read-Heavy → 写入免费,查询时按需付费

对于编码助手的记忆场景,大部分事件根本不需要语义理解。”改了哪个文件第几行”这种信息,字符串解析就够了。”这次会话整体在做什么”这种高层语义,会话结束时总结一次就够了。

不是每一步都值得被”理解”,但每一步都值得被”记录”。记录是廉价的,理解是昂贵的。好的架构应该把廉价的事情做到极致,把昂贵的事情推迟到不得不做的时候。