claude-code-learn_
07MEMORY & CONTEXT

07 - 记忆与上下文

Agent 的"记忆"分成三个层次:当轮上下文跨轮会话跨会话持久化。三者生命周期不同,解决的问题也不同。搞混层级是 Agent 使用的头号坑。

读完你能回答

问题
"这次会话结束就没了"和"下次还能用",分别对应哪一层?
为什么 prompt cache 对静态前缀那么敏感?
CLAUDE.md 和自动记忆有什么差别?哪个适合写"一次性决定"?
/compact 做了什么?哪些消息会被压缩,哪些保留?

三层记忆全貌

层次 生命周期 写入方 丢失后果
当轮上下文 一次 API 调用 运行时组装 无,每轮重建
会话内上下文 会话结束前 messages[] 累加 丢失即失忆
跨会话持久化 长期 CLAUDE.md + 自动记忆 需重新学习

第一层:当轮上下文

一次 API 调用中模型实际能读到的全部文本是 system[] + messages[] + tools[]。受模型 context window 物理上限约束(Claude 模型典型 200K-1M token)。

成分 典型占比
系统提示词 数千 token,固定
工具定义 数千 token,随工具数量增加
历史 messages 随会话增长
当前 user turn 视输入

当累计 token 接近上限时,压缩机制介入。

上下文压缩(/compact)

压缩的目标:在不丢失关键信息的前提下,把历史 messages 折叠成更短的摘要。

触发条件:

  • 累计 token 超过预设阈值
  • 用户显式请求 /compact
  • 进入新任务阶段(自动边界检测)

压缩后的结构:

保留首尾、压缩中间——不丢任务初始意图,不丢当前进展。

展开摘要消息的真实形态

摘要消息在 messages[] 里的位置就是被替换掉的那几轮:

{
  "role": "user",
  "content": [
    {
      "type": "text",
      "text": "<compacted-summary turns=3..45>\n用户在中段要求实现一个 Skill 调用追踪,已完成:\n- components/SkillTracer.tsx(监听 Skill 工具调用事件)\n- lib/skill-log.ts(本地 JSON 持久化)\n关键决策:\n- 选 localStorage 而非 IndexedDB\n- 字段固定为 {ts, name, args, result}\n未完成:前端展示面板\n</compacted-summary>"
    }
  ]
}
  • 摘要由模型自己生成,压缩前把要压的 turns 作为输入问一句"请总结成结构化摘要"
  • 结果以 role:user 注入(和工具结果一个通道)
  • 下一次调用时,模型看到这段摘要会把它当作"之前发生了这些事"的事实

Prompt Cache:为什么前缀顺序这么重要

Prompt cache 是 Anthropic API 层的前缀缓存。命中前缀的 token 按折扣价计费(典型 1/10),且延迟更低。但它的规则很严格——纯字节级前缀匹配

下面是一个交互演示。切换三种场景,观察命中与重算的区别。

场景 后果
相同前缀 100% 命中,下一次请求几乎免费
末尾差异 前缀命中,末尾重算;这是 Claude Code 追求的常态
开头就不同 第一个字节就分叉,后面全 miss,和没缓存一样

把"当前日期""cwd"这类每次都变的信息放在 system[] 最前面是致命错误——整条 prompt 每次都 miss。

条件 影响
前缀字节完全相等 命中,按缓存价计费
前缀第 K byte 不同 只命中前 K-1 byte,之后全量处理
距上次命中超过 TTL(典型 5 分钟) 未命中,重建缓存

Claude Code 的系统提示词布局(见 03-system-prompt.md)把静态内容放前、动态内容放后,正是为了最大化命中率。

第三层:CLAUDE.md

CLAUDE.md 是持久化的"项目规则"机制。Claude Code 在启动会话时自动合并多层 CLAUDE.md 进系统提示词的项目规则区块。

层级

路径 范围 典型内容
~/.claude/CLAUDE.md 用户级,跨项目 通用偏好、沟通语言、命名约定
<repo>/CLAUDE.md 项目级 架构约定、测试要求、分支规范
<subdir>/CLAUDE.md 子模块级 特定目录的细化规则

合并顺序:从外到内,具体覆盖泛。

编写原则

CLAUDE.md 会进入每次 API 调用,token 成本直接体现在延迟与费用上,因此:

  • 只写长期成立的规则,不写一次性上下文
  • 用命令式语句,不写解释
  • 必要时用表格替代段落,信息密度更高
展开一份真实的项目 CLAUDE.md(截取)
# CLAUDE.md — claude-code-learn

## 设计系统
- 字体:仅 Fraunces / Noto Serif SC / Instrument Sans / PingFang SC / JetBrains Mono
- 配色:近单色 + 单一赤陶 accent
- 圆角:3 / 6 / 10px,不使用 16px+

## 内容原则
- 技术陈述基于公开行为
- 不写"隐藏特性""未公开命令"

## 技术栈
- Next.js 15 App Router,output: "export" 静态导出
- 字体通过 @fontsource 自托管,禁用 Google Fonts

## 分支与提交
- 始终在功能分支工作
- commit message 英文

不该写的东西

  • ❌ "这次任务先加个 console.log 看看" — 一次性,不属于持久规则
  • ❌ "A 文件负责 X,B 文件负责 Y" — 从代码能读出来的事实,别占 token
  • ❌ 长段解释 — 命令式一句话足矣

自动记忆

除了手写的 CLAUDE.md,Claude Code 可维护一套自动记忆:在会话中观察到"值得跨会话保留"的事实,写入文件系统,下次会话加载时读回。

存储结构

文件 作用
MEMORY.md 索引,每行一条指向具体记忆文件
<topic>.md 单条记忆,带 frontmatter 标注类型

记忆类型

类型 典型内容
user 用户角色、偏好、知识背景
feedback 用户对工作方式的明确指示
project 当前工作的背景、截止日期、干系人
reference 外部系统指针(Linear 项目、Grafana 面板)
展开一份真实的 MEMORY.md 索引与单条记忆

MEMORY.md(索引):

- [User role](user_role.md) — backend engineer, 10 yrs Go
- [Testing preference](feedback_testing.md) — integration tests must hit real DB
- [Release freeze](project_freeze_2026-03.md) — no merges after 2026-03-05
- [Linear project](ref_linear.md) — INGEST tracks pipeline bugs

feedback_testing.md(单条记忆):

---
name: Integration tests must hit real DB
description: Reject mocked DB in integration tests
type: feedback
---

Integration tests must hit a real database, not mocks.

**Why:** 上季度一次 prod migration 失败就是因为 mocked 测试通过了,
mock 和真实 schema drift 被掩盖。

**How to apply:** 写或审 `tests/integration/` 目录下的用例时,
如果看到 `jest.mock('pg')` 或类似 pattern,立即指出。

写入与读取

  • 写入:会话中观察到新的、非易失信息时,追加记忆文件并更新索引
  • 读取:下次会话启动时加载索引,相关时按需读入完整内容
  • 更新:发现旧记忆与当前事实冲突时,以当前为准更新或删除

CLAUDE.md vs 自动记忆

维度 CLAUDE.md 自动记忆
写入者 用户手动 Agent 自动
内容性质 规则 事实、偏好、上下文
稳定性 长期 可能随时间失效
进入 prompt 的时机 每次会话自动进入 system[] 按需读入(索引常驻,单条按需)
适合写 "commit message 用英文" "用户叫小明,偏好中文回复"

两者互补:CLAUDE.md 固化规则,自动记忆补充动态事实

放错层级的常见失误

失误 后果 正确层级
把一次性指令写进 CLAUDE.md 永久占用每次 prompt 的 token 当前会话即可
把重要规则只留在会话中 会话结束即丢 CLAUDE.md
让自动记忆记录"今天要改 A 文件" 下次会话已过期 TODO / 任务管理
把项目规则写到全局 CLAUDE.md 污染其他项目 项目级 CLAUDE.md

设计 Agent 工作流时,第一步永远是问:这条信息属于哪一层?