Claude Code 长短记忆机制分析

基于 @anthropic-ai/claude-code@2.1.88 源码还原项目

核心文件:services/SessionMemory/, services/extractMemories/, services/teamMemorySync/, services/compact/sessionMemoryCompact.ts, utils/attachments.ts


概述

Claude Code 的记忆系统不是单一的”记忆”功能,而是一个多层、多作用域、多生命周期的记忆体系。整体分为:

层次 系统名称 作用域 生命周期 文件格式
短期(秒级-分钟) Query 内上下文 单轮对话 一次 API 调用 消息数组
短期(会话级) Session Memory 单次会话 会话结束 .claude/session-memory.md
中期(项目级持久) Auto Memory / Durable Memory 项目目录 持久化到磁盘 .claude/projects/<path>/memory/*.md
长期(团队级持久) Team Memory Git 仓库(全组织) 云端同步+本地持久化 ~/.claude/team-memory/<repo>/*
系统级(配置级持久) CLAUDE.md 项目/用户目录 静态文件,每次加载 CLAUDE.md

整个系统的核心设计思想是:不同层次的信息在不同时间尺度上积累,在不同的触发点被提取,按需注入到 AI 的上下文中——而不是一次性全塞进去。


一、短期记忆:Session Memory(会话记忆)

Session Memory 是会话级别的临时笔记,记录当前会话进行到了哪里、做了什么、遇到了什么问题。它本质上是一个结构化的 Markdown 文件,由后台 AI 定期更新。

1.1 存储位置与格式

.claude/
└── session-memory.md         # 会话记忆文件

文件模板(9 个固定段落):

# Session Title
_A short and distinctive 5-10 word descriptive title_

# Current State
_What is actively being worked on right now?_

# Task specification
_What did the user ask to build?_

# Files and Functions
_What are the important files?_

# Workflow
_What bash commands are usually run and in what order?_

# Errors & Corrections
_Errors encountered and how they were fixed._

# Codebase and System Documentation
_What are important system components?_

# Learnings
_What has worked well? What has not?_

# Key results
_Exact output the user requested (table, answer, etc.)_

# Worklog
_Step by step, what was attempted, done?_
  • 每个段落有上限MAX_SECTION_LENGTH = 2000 字符/tokens
  • 文件总上限MAX_TOTAL_SESSION_MEMORY_TOKENS = 12000 tokens
  • 超过上限时提取 AI 会自动压缩过长段落

用户可以自定义模板:~/.claude/session-memory/config/template.md

1.2 触发机制

Session Memory 作为 Post-Sampling Hook 运行——每次 AI 响应完成后异步触发。但不是每次都触发,有三重门槛:

// 默认配置
DEFAULT_SESSION_MEMORY_CONFIG = {
  minimumMessageTokensToInit: 10_000,       // 上下文达到 10K token 时才初始化
  minimumTokensBetweenUpdate: 5_000,        // 每次更新后至少增长 5K tokens
  toolCallsBetweenUpdates: 3,               // 至少 3 次工具调用
}

触发条件(满足以下任一即可):

const shouldExtract =
  (hasMetTokenThreshold && hasMetToolCallThreshold) ||  // 条件 1: token 和工具调用都超标
  (hasMetTokenThreshold && !hasToolCallsInLastTurn)     // 条件 2: token 超标 + 最后一轮无工具调用(对话自然停顿点)

设计精妙之处:

  • Token 阈值 始终 是硬性要求——即使工具调用很多,如果上下文没增长也不触发
  • 条件 2 捕获 “对话自然停顿点”——当 AI 最后一轮没有调用工具(可能是回答了用户),这是提取记忆的好时机
  • 只在 repl_main_thread(主 REPL 线程)运行,不在子 agent 中运行

1.3 更新机制

更新由一个 forked agent(分叉 AI 代理) 执行——完美拷贝父级对话的上下文,共享 prompt cache,但拥有独立的工具调用权限:

// 提取代理的权限极其严格:
// 只允许 Edit 工具操作 session-memory.md 这一个文件
function createMemoryFileCanUseTool(memoryPath) {
  return async (tool, input) => {
    if (tool.name === 'Edit' && input.file_path === memoryPath) {
      return { behavior: 'allow' }
    }
    return { behavior: 'deny' }  // 其他一切拒绝
  }
}

更新流程:

  1. 检查是否满足触发条件(token 门槛 + 工具调用次数)
  2. 读取当前的 session memory 文件
  3. 启动 forked agent,发送更新 prompt
  4. Forked agent 用 Edit 工具更新文件
  5. 记录 lastSummarizedMessageId——标记记忆覆盖到了哪条消息

1.4 关键数据流

每次 AI 响应 (PostSamplingHook)
      │
      ▼
shouldExtractMemory(messages)?
      │  No → 直接返回
      │  Yes ↓
      ▼
runForkedAgent(
  prompt: "请基于最新对话更新 session notes",
  tools: 只允许 Edit → session-memory.md
)
      │
      ▼
更新 .claude/session-memory.md
      │
      ▼
记录 lastSummarizedMessageId ← 知道记忆覆盖到了哪里

二、中期记忆:Auto Memory / Durable Memory(自动持久记忆)

Auto Memory 是项目级别的持久化知识,记录跨会话的经验教训、用户偏好、代码结构理解等。与 Session Memory 不同,它不会在会话结束后丢失。

2.1 存储位置与格式

.claude/projects/<project-path>/
└── memory/
    ├── MEMORY.md              # 索引文件(每次启动必加载)
    ├── user_role.md           # 用户角色记忆
    ├── feedback_testing.md    # 反馈记忆
    ├── project_auth_flow.md   # 项目特定知识
    └── ...                    # 按主题组织的独立文件

** MEMORY.md —— 索引文件(非记忆内容):**

- [User Role](user_role.md) — 用户是数据科学家,专注于 observability
- [Feedback on Testing](feedback_testing.md) — 集成测试必须用真实数据库
- [Project Auth Rewrite](project_auth_flow.md) — 认证重写由法律合规驱动
  • 每行 ~150 字符以内
  • 超过 200 行会被截断
  • 只存储指针,不存储内容

单个记忆文件格式:

---
name: feedback_testing
description: 集成测试必须用真实数据库
type: feedback
---

集成测试必须访问真实数据库,而不是 mock。

**Why:** 上季度因为 mock/生产不一致导致通过的测试在生产迁移时失败
**How to apply:** 编写涉及数据库的测试时,使用真实连接

2.2 记忆的四种类型

enum MemoryType {
  'user',      // 用户角色、偏好、知识背景
  'feedback',  // 用户给出的指导(做什么/不做什么)
  'project',   // 项目目标、里程碑、技术决策
  'reference'  // 外部资源指针(Linear 项目、Grafana 面板等)
}

每种类型有不同的 when_to_savehow_to_use 指南,训练 AI 判断何时保存、如何使用。

2.3 提取机制:Extract Memories

与 Session Memory 的主动更新不同,Auto Memory 是通过 后台提取代理 自动积累的:

// 注册时机:每个完整 query loop 结束后(AI 没有 pending 工具调用时)
// 通过 handleStopHooks 触发
export function initExtractMemories() {
  // ...
}

export async function executeExtractMemories(context) {
  // fire-and-forget,不阻塞主流程
}

节流控制:

// 不是每轮都提取,默认每隔 N 轮提取一次(N 由 GrowthBook 远程配置)
const turnsBetweenExtraction = getFeatureValue('tengu_bramble_lintel', 1)

// 如果 AI 自己已经写了记忆文件(main agent 直接执行了 save 操作),
// 则跳过 forked agent 的提取(避免重复劳动)
if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
  return // 跳过,推进游标
}

提取代理的权限模型(比 Session Memory 宽松得多):

工具 权限 说明
Read / Grep / Glob 完全开放 只读工具,无风险
Bash 仅只读命令 ls, find, grep, cat, stat, wc, head, tail
Edit / Write 仅限 memory 目录 只能写 memory/*.md
其他所有工具 拒绝 MCP, Agent, rm 等

互斥设计: 主 agent 和提取代理不会同时写入记忆——如果主 agent 已经写了记忆(用户说”记住这个”时 AI 直接保存),提取代理会跳过该轮,推进游标到主 agent 写入之后。这避免了两者竞争。

// 提取代理只在主代理没有写记忆时才运行
if (hasMemoryWritesSince(messages, lastMemoryMessageUuid)) {
  // 跳过,推进游标——下一轮只考虑这之后的新消息
  lastMemoryMessageUuid = lastMessage.uuid
  return
}

2.4 提取策略:两阶段并行写入

提取代理被设计为最少 turn 数的策略:

Turn 1: 并行读取所有可能需要更新的文件 (Read × N)
Turn 2: 并行写入所有需要更新的文件 (Write/Edit × N)

禁止在多个 turn 中交错读写 —— 浪费上下文

提取代理 prompt 中明确指示:

“You have a limited turn budget. Edit requires a prior Read of the same file, so the efficient strategy is:

  • Turn 1 — issue all Read calls in parallel for every file you might update
  • Turn 2 — issue all Write/Edit calls in parallel
    Do not interleave reads and writes across multiple turns.”

同时限制 maxTurns: 5——超过 5 轮强制终止。


三、长期记忆:Team Memory(团队记忆)

Team Memory 是组织级别的知识共享,通过 Git 仓库识别,同一个仓库的所有认证成员共享同一套记忆文件。

3.1 存储位置

~/.claude/team-memory/<owner>/<repo>/
├── MEMORY.md
├── coding_standards.md
├── deployment_process.md
└── ...

3.2 同步协议

Team Memory 在本地文件和服务器 API 之间同步:

本地文件系统                    服务器 API
     │                           │
     │  GET /team_memory?repo=   │  拉取(下载全量)
     │◄─────────────────────────│
     │                          │
     │  PUT /team_memory?repo=   │  推送(增量上传)
     │────────────────────────▶│
     │                          │

同步关键点:

  • 拉取:服务器内容覆盖本地(server wins)
  • 推送:只上传内容哈希不同的文件(delta upload)
  • 删除:本地删除不会传播到服务器,下次拉取会恢复
  • 冲突重试:最多 2 次(基于 ETag lastKnownChecksum

同步状态管理:

type SyncState = {
  lastKnownChecksum: string | null           // ETag,用于条件请求
  serverChecksums: Map<string, string>       // 每个文件内容的 sha256 哈希
  serverMaxEntries: number | null            // 服务器最大条目数(从 413 响应学习)
}

3.3 隐私保护:密钥扫描

推送前会自动扫描密钥,防止敏感信息泄露到团队空间:

// 扫描正则模式
const SECRET_PATTERNS = [
  /(?i)(api[_-]?key|api[_-]?secret)/,
  /(?i)(password|passwd|pwd)/,
  /(?i)(token|auth|credential)/,
  // ...
]

// 超过 250KB 的文件不上传
const MAX_FILE_SIZE_BYTES = 250_000

3.4 权限控制

提取代理可以同时向私有记忆和团队记忆写入,但团队记忆有更严格的约束:

  • 绝不保存 API key、凭证等敏感信息
  • 每个文件类型有 <scope> 指导该写到哪个目录
  • 推送时用 OAuth token 认证

四、系统级配置记忆:CLAUDE.md

CLAUDE.md静态配置文件,不是 AI 自动生成的,而是用户/团队手动维护的:

.claude/CLAUDE.md                 # 项目级
~/.claude/CLAUDE.md              # 用户级

这是每次对话启动时必加载的内容,作为 system prompt 的一部分注入。与动态生成的 Session Memory 和 Auto Memory 不同,CLAUDE.md 是确定性的、版本可控的。


五、记忆如何注入到 AI 上下文

记忆不是全部塞进 prompt 的,而是按需、分层、通过 attachment 机制注入的。

5.1 加载时机

系统启动时扫描所有记忆目录:

1. 加载 CLAUDE.md (静态配置)
2. 加载 memory/MEMORY.md (索引 → 按需加载具体文件)
3. 加载 session-memory.md (当前会话笔记)
4. 如果是团队模式,加载 team memory

5.2 压缩后的记忆再注入

当 Auto Compact 触发时,所有消息被替换为摘要但记忆不会丢失

// compactConversation() 成功后:
// 1. 清除 fileStateCache
// 2. 重新注入关键上下文:
postCompactFileAttachments = [
  ...最近修改的文件快照(最多5个),
  ...异步Agent状态,
  ...plan文件,
  ...已调用的技能内容(25K token预算),
  ...MCP工具增量,
  // 记忆通过 attachment 机制自动重新注入
]

5.3 Session Memory Compact —— 轻量级压缩

当记忆存在且有实质内容时(不只是模板),系统会尝试基于记忆的压缩,这比完整的 AI 摘要压缩更经济:

async function trySessionMemoryCompaction(messages, agentId, threshold) {
  // 1. 等待任何正在进行的记忆提取完成
  await waitForSessionMemoryExtraction()

  // 2. 读取 session memory 内容
  const sessionMemory = await getSessionMemoryContent()
  if (!sessionMemory || isEmpty(sessionMemory)) return null // 降级到 legacy compact

  // 3. 找到 lastSummarizedMessageId 对应的消息索引
  const lastSummarizedIndex = messages.findIndex(
    msg => msg.uuid === lastSummarizedMessageId
  )

  // 4. 调整索引保护 API 不变性:
  //    - 确保保留 tool_use / tool_result 配对
  //    - 确保保留 thinking blocks 与同 message.id 的消息合并
  const startIndex = adjustIndexToPreserveAPIInvariants(
    messages, lastSummarizedIndex + 1
  )

  // 5. 保留 startIndex 之后的新消息 + session memory 摘要
  //    这样新消息有完整上下文,旧信息在 session memory 中有摘要

  // 6. 预算检查:最终 token 数必须在 [minTokens, maxTokens] 范围内
  //    默认:10K - 40K tokens
}

这比完整的 compactConversation(启动 forked agent 做全对话摘要)要便宜得多,因为:

  • 不需要额外的 AI 调用生成摘要(session memory 已经是现成的摘要)
  • Token 从 ~167K 压缩到 ~10-40K(保留最近的对话 + 用 session memory 替代旧对话的摘要)

六、完整记忆生命周期:信息如何流动

用户在对话中提供信息
        │
        ├── 显式指令 ("记住这个") ──→ AI 直接写入 Auto Memory
        │                              同时更新 MEMORY.md 索引
        │
        ├── 隐性经验 (错误修正、偏好) ──→ extractMemories 后台代理
        │                                  每 N 轮自动扫描对话
        │                                  识别值得保存的信息
        │                                  写入对应的 .md 文件
        │
        ├── 会话进行中的状态 ──→ Session Memory (PostSamplingHook)
        │                          定期(每 5K token 或 3 次工具调用)
        │                          由 forked agent 更新笔记
        │
        └── 团队级知识 ──→ Team Memory
                             与 Auto Memory 相同的文件结构
                             但存储到 ~/.claude/team-memory/<repo>/
                             自动同步到服务器共享给团队成员
        │
        ▼
下次会话启动
        │
        ├── CLAUDE.md 必加载 ← 确定性配置
        ├── memory/MEMORY.md 索引加载 ← 持久知识(跨会话)
        ├── team/MEMORY.md 索引加载 ← 团队知识(跨成员)
        ├── session-memory.md 如果存在 ← 上次会话的临时笔记
        │
        ▼
AI 带着所有记忆开始新对话

七、关键设计原则与实现细节

7.1 记忆与压缩的协同

Session Memory 最初是为了替代 Auto Compact 而设计的——如果有了 session memory 摘要,就不需要再浪费一次 AI 调用来做全对话摘要:

// autoCompactIfNeeded() 流程:
// 1. 先尝试 Session Memory Compact(不需要额外 AI 调用)
const sessionMemoryResult = await trySessionMemoryCompaction(...)
if (sessionMemoryResult) {
  return { wasCompacted: true } // 成功!不需要完整压缩
}

// 2. 如果 session memory 不可用或没有内容,才做完整压缩
const compactionResult = await compactConversation(...)

熔断与回退:

  • Session Memory 为空(只有模板)→ 降级到 legacy compact
  • lastSummarizedMessageId 找不到(消息被压缩掉了)→ 降级到 legacy compact
  • 预算检查失败 → 降级到 legacy compact

7.2 索引(MEMORY.md)的设计

- [Title](file.md) — one-line hook

这种设计有几个关键好处:

  1. 低开销加载:索引文件很小(200 行 × 150 字符 ≈ 7.5K 字符),可以快速解析
  2. 按需加载内容:不需要一次加载所有记忆文件,只有相关的时候才 read
  3. 用户可审查:索引文件可读,用户可以删除不需要的条目
  4. 语义组织:按主题分类,不是按时间排序
  5. 自我修复:AI 可以更新或删除过时的条目

7.3 游标与增量提取

提取代理不是每次都扫描整个对话历史:

// 游标机制:lastMemoryMessageUuid
// 每次提取只处理游标之后的新消息
const newMessageCount = countModelVisibleMessagesSince(messages, lastMemoryMessageUuid)

// 如果游标对应的消息被压缩删除了(找不到 UUID),回退到全量扫描
if (!foundStart) {
  return count(messages, isModelVisibleMessage) // 兜底策略
}

这确保提取代理每次只处理增量内容,成本可控。

7.4 并行调用合并(Coalescing)

当多个 trigger 几乎同时到达时,不会启动多个提取代理:

if (inProgress) {
  // 有提取正在进行 → 保存上下文,等当前结束后做一次 trailing extraction
  pendingContext = { context, appendSystemMessage }
  return
}

// 当前提取结束后:
const trailing = pendingContext
pendingContext = undefined
if (trailing) {
  await runExtraction({ context: trailing.context, isTrailingRun: true })
  // trailing run 使用新的游标(上次已推进),只处理间隔期的新消息
}

7.5 敏感信息防护

// Team Memory 推送前扫描密钥
async function scanForSecrets(content: string): Promise<boolean> {
  // 正则匹配 API key、password、token 等模式
  // 发现疑似密钥 → 跳过该文件上传
}

// 超过 250KB 的文件不上传到团队空间
if (fileSize > MAX_FILE_SIZE_BYTES) {
  skippedFiles.push({ path, reason: 'too_large' })
}

八、对低代码开发智能体团队的启示

启示 1:多时间尺度的记忆系统

Claude Code 的记忆不是”一块大脑”,而是多个不同时间尺度的系统:

系统 保留周期 更新频率 触发方式
Session Memory 会话期间 每 5K token / 3 次工具调用 PostSampling Hook
Auto Memory 永久 每 N 轮对话 Stop Hook
Team Memory 永久 + 云端同步 随 Auto Memory 一起管理 同上 + 定时推送
CLAUDE.md 永久 手动 启动时加载

应用: 低代码平台应该有类似的记忆分层——工作流中间状态(短期)、项目经验(中期)、组织最佳实践(长期)应该有不同存储和生命周期。

启示 2:后台提取 + 前台直接保存的双路径

记忆有两条写入路径:

  1. 前台直接保存:用户说”记住这个”时 AI 直接写入
  2. 后台自动提取:对话结束后提取代理分析并提取

两者通过 hasMemoryWritesSince 互斥——避免重复劳动。

应用: 低代码平台中,用户应该能直接保存知识,同时系统也应该有后台自动提取能力(从执行日志、错误模式、流程变更中学习)。

启示 3:索引与内容分离的架构

MEMORY.md(索引)与 .md 文件(内容)分离的设计:

  • 索引永远加载,内容按需加载
  • 索引小(200 行 × 150 字),内容按需读
  • 用户可以审查和编辑索引

应用: 如果低代码平台有大量可学习的”模式”或”规则”,应该用索引+内容的两级结构,而不是一次性加载所有模式。

启示 4:记忆本身可以替代压缩

Session Memory 是 Auto Compact 的廉价替代品——它已经是 AI 生成的结构化摘要,不需要再启动一个 forked agent 生成摘要。

应用: 低代码平台如果积累了工作流执行日志/笔记,可以在需要”精简上下文”时直接使用这些笔记作为摘要,而不需要额外调用 AI。

启示 5:记忆写入的最小化权限模型

提取代理只被授予最小必要权限:

  • 只读工具不受限(Read/Grep/Glob/Bash 只读命令)
  • 写工具严格限定在 memory 目录
  • 其他一切工具拒绝

应用: 低代码平台中自动学习/记忆的智能体应该有类似的受限权限——它可以读取上下文信息、写入记忆文件,但不能执行修改业务数据的操作。

启示 6:增量提取和游标管理

提取代理通过 lastMemoryMessageUuid 游标只处理新消息,如果游标对应消息被压缩删除了就回退到全量。这是增量消费的标准模式。

应用: 任何基于对话历史的持续学习系统都需要游标管理——记住上次处理到哪里了,以及当历史记录被修改/删除时的回退策略。

启示 7:记忆的压缩与截断

Session Memory 有硬性上限(12K tokens,每段 2K tokens),提取时 AI 会自动截断超长段落并优先保留 Current StateErrors & Corrections

应用: 低代码平台的学习模块应该有容量管理——当知识库太大时,AI 应该优先保留哪些信息、淘汰哪些信息。这需要明确的内容策略和截断规则。

启示 8:团队知识的版本控制与安全

Team Memory 用 Git 仓库作为标识、Etag-based 增量同步、推送前自动扫描密钥、单次推送有 200KB 限制、大文件分批上传。

应用: 如果低代码平台有”团队级知识”概念,需要类似的:

  • 基于内容哈希的增量同步(避免全量传输)
  • 冲突检测与重试
  • 敏感信息自动过滤
  • 推送大小限制