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 bugsfeedback_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 工作流时,第一步永远是问:这条信息属于哪一层?