犀牛鸟 2026 研究笔记 estelledc.github.io

精读: Claude Code 记忆系统

编排/Harness 视角见 Agent 框架 · Claude Code 架构精读

调研元信息

项目
调研日期 2026-06-22
来源 Claude Code 泄露源码包 (claude-code-main)
主代码路径 src/memdir/ + src/services/extractMemories/
证据等级 [源码](泄露包,可能过时)+ [引用](feature gate 剥离的模块)
局限 大量功能受 tengu_* feature gate 控制,实际灰度状态未知

一句话定位

Claude Code 的记忆系统是一个文件 index + 按需 LLM 检索的两层架构:MEMORY.md 作为索引全量注入 system prompt,每个用户 turn 再由 Sonnet 从最多 200 个记忆文件中选出 ≤5 个注入上下文——整个读路径是 LLM 驱动的语义匹配,而非向量检索 [源码]

日常类比

想象一个图书馆管理员。图书馆有一本很薄的目录册(MEMORY.md),最多 200 行、25KB——每本书只占一行,写着书名和一句话简介。你进入图书馆时,管理员把整本目录册摊在桌上(system prompt 注入)。当你提出具体问题时,管理员(Sonnet)从目录中挑出最多 5 本相关的书翻开给你看(每本最多 200 行/4KB)。与此同时,后台有个”记录员”(extractMemories)在你每次对话结束后悄悄记笔记,把值得保存的内容写成新的书放回书架。

类比边界:真实图书馆的目录不会自动截断,也不需要一个 AI 来决定给你看哪本书。

记忆类型标注

本文主要讨论 User memory~/.claude/projects/*/memory/ 下的自动记忆文件),兼及 Agent profile(CLAUDE.md)与 User memory 的边界,以及 Working memory(session transcript)如何通过 extractMemories 转化为持久记忆。


架构总览

Claude Code 的记忆系统分为三个子系统:memdir(记忆目录管理与 prompt 构建)、extractMemories(后台提取代理)、和 autoDream(后台整合/做梦) [源码]

flowchart TB
    subgraph read_path["读路径"]
        direction LR
        BOOT[Session 启动] --> LOAD["loadMemoryPrompt()<br/>读 MEMORY.md<br/>截断 200行/25KB"]
        LOAD --> SYS[注入 System Prompt]

        TURN[用户发送消息] --> PREFETCH["startRelevantMemoryPrefetch()"]
        PREFETCH --> SCAN["scanMemoryFiles()<br/>扫描 ≤200 个 .md"]
        SCAN --> SELECT["findRelevantMemories()<br/>Sonnet 选 ≤5 个文件"]
        SELECT --> READ_MEM["readMemoriesForSurfacing()<br/>每文件 ≤200行/4KB"]
        READ_MEM --> INJECT["附件注入上下文"]
    end

    subgraph write_path["写路径"]
        direction LR
        STOP["stopHooks<br/>模型最终响应后"] --> EXTRACT["extractMemories<br/>forked agent, maxTurns=5"]
        EXTRACT --> WRITE["写 topic .md + frontmatter"]
        WRITE --> UPDATE["更新 MEMORY.md 索引"]

        STOP --> DREAM["autoDream<br/>≥24h & ≥5 sessions"]
        DREAM --> CONSOLIDATE["整合/去重/优化"]
    end

    subgraph storage["存储层"]
        MEM_DIR["~/.claude/projects/*/memory/"]
        INDEX["MEMORY.md (index)"]
        FILES["topic-name.md (content)"]
        MEM_DIR --> INDEX
        MEM_DIR --> FILES
    end

    SYS -.-> INDEX
    INJECT -.-> FILES
    UPDATE -.-> INDEX
    WRITE -.-> FILES

读路径: 双阶段注入

Claude Code 的读路径有两个注入时机,覆盖了”广度 + 深度”两种需求 [源码]

阶段 1: System Prompt 注入(广度)

flowchart LR
    START[Session 开始] --> CHECK{auto memory<br/>启用?}
    CHECK -->|是| LOAD["readFileSync(MEMORY.md)"]
    LOAD --> TRUNC["truncateEntrypointContent()"]
    TRUNC --> BUILD["buildMemoryPrompt()"]
    BUILD --> CACHE["systemPromptSection('memory', ...)"]
    CACHE --> SYSPROMPT[System Prompt]
    CHECK -->|否| SKIP[不注入]

MEMORY.md 截断规则 [源码]

限制 原因
最大行数 200 防止索引膨胀
最大字节数 25,000 (25KB) 观察到 p100 有 197KB 但不到 200 行的案例
截断策略 先按行,再按字节(在最后一个换行符处切) 保证 Markdown 格式完整
截断提示 追加 WARNING 文本告知 Claude 让模型知道索引不完整

每行建议长度:约 150 字符。MEMORY.md 的设计意图是”每条记忆只占一行的目录”——正文存在独立的 topic .md 文件中 [源码]

阶段 2: 每 turn 按需检索(深度)

flowchart LR
    USER[用户消息] --> PREFETCH["startRelevantMemoryPrefetch()"]
    PREFETCH --> EXTRACT_TEXT[提取用户文本]
    EXTRACT_TEXT --> CHECK_BUDGET{session 累计<br/>≤ 60KB?}
    CHECK_BUDGET -->|超限| SKIP[跳过]
    CHECK_BUDGET -->|未超| SCAN["scanMemoryFiles()"]
    SCAN --> FORMAT["formatMemoryManifest()"]
    FORMAT --> SONNET["Sonnet sideQuery<br/>json_schema output<br/>max_tokens=256"]
    SONNET --> FILTER[过滤已展示过的]
    FILTER --> READ["readMemoriesForSurfacing()"]
    READ --> ATTACH["作为 relevant_memories 附件注入"]

scanMemoryFiles 的工作方式 [源码]

参数 作用
MAX_MEMORY_FILES 200 最多扫描 200 个 .md 文件
FRONTMATTER_MAX_LINES 30 只读每个文件前 30 行解析 frontmatter
排序 mtime 降序 最近修改的排在前面
排除 MEMORY.md 本身 索引文件不作为候选

findRelevantMemories (Sonnet 选择器) [源码]

Sonnet 收到的输入是格式化的记忆清单(- [type] filename (timestamp): description),输出是 { selected_memories: string[] } 的 JSON。关键规则:

读取限制 [源码]

限制 作用
每文件最大行数 200 防止单个记忆占用过多上下文
每文件最大字节 4,096 (4KB) 同上
每 session 累计 61,440 (60KB) 防止记忆注入失控

与 mem0 检索的对照

维度 Claude Code mem0 [已有]
检索模型 Sonnet LLM 语义判断 向量相似度(无 Cross-Encoder)
候选集 ≤200 个文件的 frontmatter 全量向量库
召回上限 5 个文件 可配置 top-k
成本 每 turn 一次 Sonnet 调用 一次向量查询
精确度 语义理解强,但黑盒 依赖 embedding 质量
误漏风险 Sonnet 可能遗漏相关文件 语义漂移导致低召回

写路径: extractMemories — 后台提取

extractMemories 是一个后台 forked agent,在每个完整查询循环结束时(模型产生无工具调用的最终响应时)通过 handleStopHooks 以 fire-and-forget 方式触发 [源码]

执行流程

flowchart TB
    HOOK["handleStopHooks()"] --> CHECK{auto memory 启用<br/>+ 非远程<br/>+ 非子代理<br/>+ extract mode active}
    CHECK -->|否| SKIP[跳过]
    CHECK -->|是| THROTTLE{节流检查<br/>每 N 轮一次}
    THROTTLE -->|跳过| WAIT[等待下次]
    THROTTLE -->|通过| MUTEX{主代理已写<br/>memory?}
    MUTEX -->|是| ADVANCE[推进游标, 跳过]
    MUTEX -->|否| SCAN["scanMemoryFiles()"]
    SCAN --> PROMPT[构建提取 prompt]
    PROMPT --> FORK["runForkedAgent()<br/>maxTurns=5<br/>共享 prompt cache"]
    FORK --> NOTIFY["appendSystemMessage()<br/>通知主线程"]

关键设计决策

Forked Agent 模式 [源码]:提取代理与主对话共享 prompt cache,极大降低成本(cache hit 可能 > 50%)。这是一个重要的工程优化——如果每次都独立构建完整 prompt,记忆提取的成本会翻倍。

主代理/提取代理互斥 [源码]hasMemoryWritesSince() 检查主代理是否已经手动写了记忆文件。如果主代理写了(比如用户说”记住这个”),提取代理直接跳过本轮并推进游标。这避免了两个 agent 同时写同一个文件的竞态。

游标机制 [源码]lastMemoryMessageUuid 作为游标,确保每次运行只处理新增消息。加上 pendingContext 暂存机制——当提取正在运行时有新请求到来,完成后会执行一次 trailing run。

工具权限限制 [源码]:提取代理只能用 Read/Grep/Glob(不限路径)、只读 Bash、以及仅限 memory 目录的 Edit/Write。

提取 Prompt 设计

提取 prompt 的核心指令 [源码]

四类型分类法

Claude Code 定义了一套封闭的记忆类型分类 [源码]

类型 用途 scope(团队模式)
user 用户角色、目标、偏好 默认私有
feedback 用户给的工作方式指导(纠正 + 确认) 默认私有
project 非代码可推导的项目上下文 倾向团队共享
reference 外部系统指针 通常团队共享

feedback 类型要求包含 **Why:****How to apply:** 结构——这不是装饰性要求,而是确保记忆有 actionable 信息 [源码]

什么不该保存

这可能是整个记忆系统中最有教育意义的部分 [源码]

不应保存的内容:代码模式/架构、文件路径、git 历史、调试方案、CLAUDE.md 已有内容、临时任务状态。即使用户明确要求保存 PR 列表,也应询问”什么是意外或非显而易见的”。

核心原则是不保存 derivable 信息——代码能 grep 到的就不记。这与 DB-Agent-Memory 的 L0→L3 提炼思路不同:DB-Agent-Memory 保存所有层级并做语义晋升,Claude Code 直接拒绝记住可推导的东西 [已有]


写路径: autoDream — 后台整合

autoDream 是 extractMemories 的补充:前者写新记忆,后者整合已有记忆 [源码]

触发条件(便宜的先检查)

条件 默认值
时间门 距上次整合 ≥ minHours 24 小时
Session 门 自上次整合后 ≥ minSessions 个 session 有活动 5 个
无其他进程正在整合

与 Codex Phase2 的对照

维度 Claude Code autoDream Codex Phase2
触发 24h + 5 sessions root session 启动,6h 冷却
执行者 forked agent(共享 prompt cache) 独立 consolidation agent(沙箱)
输入 所有 memory 文件 usage-based selection (top-256)
锁机制 文件系统 mtime SQLite jobs 表 + ownership_token
失败恢复 回滚锁 mtime,节流退避 retry_remaining=3,1h 重试间隔

Freshness 与 Staleness 管理

Claude Code 对记忆新鲜度的处理比大多数开源库更细致 [源码]

memoryAge 模块将 mtime 转换为人类可读格式(”today” / “yesterday” / “47 days ago”),因为模型不擅长日期算术但理解相对时间。

Freshness caveat:超过 1 天的记忆自动附带警告文本——”This memory is N days old. Memories are point-in-time observations…“。这通过 <system-reminder> 标签包裹注入。

设计动机(代码注释):用户报告说陈旧的 code-state 记忆(含 file:line 引用)被当作事实——引用格式反而让过时信息更权威。加上 staleness caveat 是对这个问题的直接回应 [源码]

与 Graphiti 时序管理的对照

维度 Claude Code Graphiti [已有]
时间模型 单维(mtime) 双时间轴(valid_at + created_at)
失效处理 caveat 提示,不强制失效 显式 invalid_at 标记
回忆性描述 不支持 valid_at 可回溯
精确度 文件级 实体/关系级

团队记忆

Claude Code 支持团队记忆——多用户共享的记忆目录 [源码]

安全设计

团队记忆的安全措施非常重 [源码]

安全层 防御内容
sanitizePathKey null byte、URL 编码遍历(%2e%2e%2f)、Unicode 正规化攻击(全角 ../
realpathDeepestExisting 逐级向上解析 symlink,检测悬空 symlink
validateTeamMemWritePath 两阶段验证:path.resolve + realpath 前缀检查
前缀攻击防护 teamDir 末尾强制分隔符,防 /foo/team-evil 匹配 /foo/team
projectSettings 排除 autoMemoryDirectory 设置源排除 projectSettings——恶意 repo 不能设 ~/.ssh

这套安全设计的深度远超开源记忆项目——因为团队记忆意味着文件写入路径可能被外部输入影响 [源码]


与 CLAUDE.md(Profile)的边界

维度 CLAUDE.md (Profile) memory/ (User memory) session transcript (Working)
写入者 人工维护(或 /init 生成) extractMemories 自动 + 用户手动 系统自动
生命周期 随项目存在 跨 session 持久 单 session
内容 行为规则、项目结构说明 偏好、反馈、项目上下文、引用 完整对话记录
进入 memory 不应存(”CLAUDE.md 已有内容”在 WHAT_NOT_TO_SAVE 中) 是记忆系统本身 通过 extractMemories 转化
注入方式 system prompt 直接拼接 index 全量 + Sonnet 按需选型 上下文窗口

关键洞察:CLAUDE.md 中的内容被明确列为”不应保存到 memory 的东西”——这在代码层面划清了 Profile 和 User memory 的边界 [源码]


关键常量汇总

常量 位置 用途 [源码]
MAX_ENTRYPOINT_LINES 200 memdir.ts MEMORY.md 最大行数
MAX_ENTRYPOINT_BYTES 25,000 memdir.ts MEMORY.md 最大字节
MAX_MEMORY_FILES 200 memoryScan.ts 扫描记忆文件上限
FRONTMATTER_MAX_LINES 30 memoryScan.ts frontmatter 读取行数
Sonnet 选择上限 5 findRelevantMemories.ts 每 turn 最多注入文件数
MAX_MEMORY_LINES 200 attachments.ts 每文件注入行上限
MAX_MEMORY_BYTES 4,096 attachments.ts 每文件注入字节上限
MAX_SESSION_BYTES 61,440 attachments.ts session 累计注入上限
extractMemories maxTurns 5 extractMemories.ts 提取代理 turn 硬限
autoDream minHours 24 autoDream.ts 整合最小间隔
autoDream minSessions 5 autoDream.ts 整合最小 session 数
drainTimeout 60,000ms extractMemories.ts 提取完成等待超时

局限性

  1. Sonnet 选型是黑盒:findRelevantMemories 的召回质量完全取决于 Sonnet 对 query 和 frontmatter 的理解——没有可调参数或 fallback 策略。
  2. 200 文件上限:超过 200 个记忆文件后,旧文件直接不进入候选集,即使它们仍然相关。
  3. MEMORY.md 截断是硬截断:被截断的索引条目对模型完全不可见,可能导致某些记忆”消失”。
  4. 每 turn 的 Sonnet 调用成本:在高频交互场景下,记忆检索的 LLM 调用开销可观。
  5. Feature gate 密度极高:几乎每个功能都有 tengu_* flag 控制——实际用户体验可能与源码逻辑有差异。
  6. extractMemories 的 5-turn 限制:复杂的记忆更新(涉及多文件读取和关联)可能在 5 turn 内完不成。

证据等级汇总

论点 证据等级
MEMORY.md 是 index 不是知识库 [源码]
读路径双阶段:index 全量 + Sonnet 按需 [源码]
200行/25KB 截断规则 [源码]
Sonnet 选 ≤5 个文件 [源码]
extractMemories forked agent 模式 [源码]
主代理/提取代理互斥 [源码]
四类型分类法 (user/feedback/project/reference) [源码]
autoDream 24h+5sessions 触发 [源码]
团队记忆多层安全设计 [源码]
freshness caveat 机制 [源码]
feature gate 密集(tengu_* [源码]
--bare 关闭全部记忆 [源码]
KAIROS 日志模式(助手模式,append-only) [引用](feature gate 剥离)
skipIndex 实验(tengu_moth_copse [引用](实验状态未知)
/remember skill(仅 ant 用户) [源码]

与开源记忆项目对照总表

维度 Claude Code mem0 [已有] DB-Agent-Memory [已有] Letta [已有]
组织范式 index + 独立 topic 文件 扁平事实列表 L0–L3 语义金字塔 core/archival 虚拟内存
写入触发 stopHook → forked agent add() API add() + 层级晋升 Agent 工具调用
检索方式 index 全量 + Sonnet 选型 向量相似度 混合 + MMD recall 向量
存储后端 本地 Markdown 文件 向量数据库 (25+) SQLite + sqlite-vec PostgreSQL
外部依赖 无(内建) 需向量 DB 零外部 API 需 LLM + PG
冲突处理 主代理/提取代理互斥 LLM 二选一 同层去重 core_memory_replace
遗忘 无显式遗忘(autoDream 做整合)
可观测性 记忆文件可人工查看/编辑 API 列表 DB 查询 API 查询
新鲜度管理 freshness caveat + mtime MMD 稳定区