claude-mem 源码分析:记忆如何在对话中体现

06 — 记忆如何在对话中体现

核心问题:claude-mem 装好后,我在正常对话时"记忆"究竟是怎么发挥作用的?它怎么知道何时搜索、搜什么、如何注入到上下文里?


两条完全独立的路径

记忆在对话中通过两条机制体现,分工明确:

路径一:自动注入 路径二:主动搜索
触发者 Claude Code Hook 机制(机器触发) Claude 自己(LLM 判断后调用 MCP 工具)
时机 会话开始时,一次性 对话中途,按需多次
内容 近期会话摘要 + 项目相关 observations 精准历史记录(由搜索词决定)
Claude 的角色 被动接收(醒来时记忆已在上下文里) 主动调用 MCP 工具
数据密度 有限(避免撑爆 context window) 3 层递进:索引 → 时间线 → 全文

路径一:自动注入(你不感知,Claude 也不选择)

触发时机:每次 Claude Code 开始新会话(claude 启动),或 /clear/compact 之后。

关键点:Claude 不需要"决定"搜不搜,记忆已经在那里了。你看到 Claude 说"上次我们在做 X",就是这条路径的效果。

注入的内容来自 src/services/context/ContextBuilder.tsgenerateContext() 函数,它从 ContextConfigLoader 读取限额配置,然后调用 ObservationCompiler 拼装输出。


路径二:主动搜索(Claude 被"教会"的行为)

触发时机:你问了涉及历史的问题(“上周那个 bug 怎么解决的”、“之前的方案是什么”)。

为什么 Claude 会主动搜索

安装时注册了 mem-search MCP 技能。其中的 __IMPORTANT 元工具描述本身就是一段指令文本,注入进 Claude 的工具列表说明里:

1
2
3
当用户询问过去的工作、历史决策、之前的代码时,
必须先用 search → timeline → get_observations 三层工作流,
不要直接回答——先查。

💡 Tip 这是一个"prompt injection via tool description"技巧 MCP 工具描述字段对 Claude 来说和系统提示的效力等同。__IMPORTANT 工具的 description 就是工作流规范本身——架构设计即使用规范,API 即文档,无需额外指导。

搜索结果进入上下文的方式,是 MCP 工具调用的标准返回值路径,不走 hook 注入。


记忆里存的是什么

三类数据,层级递进:

Observations — 工具调用的语义提炼

每次 PostToolUse 后,原始的 {tool_name, tool_input, tool_output} 被发给 AI 压缩,产出结构化 XML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<observation>
  <type>discovery | change | bugfix | decision ...</type>
  <title>发现了 X</title>
  <subtitle>在 Y 文件里</subtitle>
  <facts>
    <fact>具体事实1</fact>
    <fact>具体事实2</fact>
  </facts>
  <narrative>更完整的叙述(可选的详细描述)</narrative>
  <concepts><concept>auth</concept></concepts>
  <files_read><file>src/xxx.ts</file></files_read>
  <files_modified><file>src/yyy.ts</file></files_modified>
</observation>

存的不是原始对话,是 AI 从工具调用里提炼的语义摘要。代码见 src/sdk/prompts.tsbuildObservationPrompt()

Session Summaries — 会话级五段式摘要

Stop Hook 触发后,buildSummaryPrompt() 把最后一条 assistant 消息喂给 AI,产出五段式摘要,存入 session_summaries

字段 含义
request 这次会话用户要做什么
investigated 调查了哪些模块/文件
learned 关键发现(这是下次最有价值的字段)
completed 完成了什么
next_steps 下次应该继续做什么

没有完整的 user/assistant 对话历史,只有这五个字段的提炼。

User Prompts — 轻量问题记录

只存用户每次输入的问题文本,用于 timeline 展示,不存 assistant 回答。


记忆会不会失控膨胀?

注入时有硬上限,从 src/shared/SettingsDefaultsManager.ts 的默认值读出:

1
2
3
4
CLAUDE_MEM_CONTEXT_OBSERVATIONS   = 50   # 注入最多 50 条 observations
CLAUDE_MEM_CONTEXT_SESSION_COUNT  = 10   # 会话摘要最多 10 条
CLAUDE_MEM_CONTEXT_FULL_COUNT     = 0    # 默认只注入摘要,不注全文 narrative
CLAUDE_MEM_SEMANTIC_INJECT        = false # 语义注入默认关闭

ObservationCompiler.ts 里的查询永远带 LIMIT

1
2
3
4
SELECT ... FROM observations
WHERE project = ?
ORDER BY created_at_epoch DESC
LIMIT ?   -- = totalObservationCount = 50

结论:注入到 Claude 上下文里的记忆是有界窗口,不随使用时间线性增长。

⚠️ Warning 数据库本身不会自动清理 SQLite 里的历史数据会一直积累(磁盘慢涨),但这不影响注入效率——每次只取最近 N 条。目前代码里没有自动 TTL 或定期清理机制。这是一个有意为之的取舍:存储廉价,查询有上限,永久保留而不主动清理。如果在意磁盘,需要手动管理 ~/.claude-mem/claude-mem.db


不想被记忆的内容怎么处理

方法一:<private> 标签

1
<private>这段内容不会被存入记忆</private>

在 hook 层(最靠近用户输入处)就被剥离,不进 Worker,不进 DB。见 src/utils/tag-stripping.ts

方法二:项目排除

summarize.ts 开头有项目过滤判断:

1
2
3
4
// src/cli/handlers/summarize.ts
if (input.cwd && !shouldTrackProject(input.cwd)) {
  return { continue: true, suppressOutput: true };  // 整个会话都不被记录
}

某些目录(个人文档、敏感项目)可以配置排除,Stop hook 遇到这些目录直接跳过,不生成任何摘要。


核心收获

  1. 记忆体现有两套机制,不要混淆:自动注入(session start 时塞进去,被动接收)和主动搜索(MCP 工具调用,Claude 主动发起)。前者无感,后者可观测(你能看到 Claude 在调用 search/timeline/get_observations)。

  2. 存的不是聊天记录,是语义提炼:observations 是工具调用的 AI 摘要,summaries 是会话的五段式结构化总结。原始对话内容不被持久化。

  3. 上下文窗口里的记忆是有界的:默认最多 50 observations + 10 session summaries,不会随使用时间膨胀。注入的 token 量由配置控制,可以调整。

  4. <private> 标签是边缘防线:在数据产生的最源头就被切断,是隐私保护的正确姿势。