一篇关于 Claude Code 上下文工程的系统性梳理:从会话启动期"底色"的拼装,到每一轮 API 调用的增量累加,到按需注入的旁路机制,再到会话结束后什么被持久化、什么被丢弃。
Anthropic 的 LLM API 本质上是无状态的:每一次 HTTP 请求都必须把完整的 system prompt 和 messages 历史发过去,服务端不会替你记任何东西。所谓"Claude 记得我们之前聊过什么",全是客户端在每次调用前重新把 context 拼起来的结果。
Claude Code 这个客户端做的事情比想象中复杂得多——它需要在一次 API 调用之前完成至少这些工作:
CLAUDE.md 找出来并按层级合并理解这个组装过程,对三件事很有用:(1) 写出 Claude 实际能遵守的 CLAUDE.md;(2) 在 context 爆掉之前知道哪些内容能拆出去;(3) 排查 Claude 为什么没按你期望的行为执行。
把整个生命周期切成三段,会清晰很多:
┌──────────────────────────────────────────────────────────────────┐ │ T0 会话启动期 │ │ → 拼"底色 context":内置 system prompt + 各层 CLAUDE.md │ │ + MEMORY.md + skill descriptions + MCP 工具名 + env │ ├──────────────────────────────────────────────────────────────────┤ │ T1..Tn 每一轮交互 │ │ → 在底色基础上追加:user prompt / assistant message / │ │ tool_use / tool_result / 按需注入的 rule、skill body、 │ │ hook output │ ├──────────────────────────────────────────────────────────────────┤ │ Tend 会话结束 │ │ → messages 历史丢弃 │ │ → 磁盘上的 .md/.json 保留 │ │ → MEMORY.md 因为是 Claude 在 T1..Tn 里写的, 现在留下了 │ └──────────────────────────────────────────────────────────────────┘
两种状态:
MEMORY.md。前者随会话结束消失,后者是 Claude 自己写的、跨会话持久。启动期的 context 是一份"模板",每次 API 调用都带着它。组成部分如下:
Claude Code 自带的 system prompt(描述 Claude 的角色、工具用法、安全约束等)。这部分用户不能直接编辑,但可以通过 --append-system-prompt 在末尾追加。
按"广 → 窄"顺序加载,全部拼接,不是覆盖:
| 层级 | 路径 | 作用域 |
|---|---|---|
| Managed policy | /Library/Application Support/ClaudeCode/CLAUDE.md (macOS) /etc/claude-code/CLAUDE.md (Linux/WSL) C:\Program Files\ClaudeCode\CLAUDE.md (Windows) |
全机器、全项目,IT 部署,用户无法覆盖 |
| User | ~/.claude/CLAUDE.md | 当前用户全部项目 |
| Project | ./CLAUDE.md 或 ./.claude/CLAUDE.md | 当前项目,跟随 git 共享给团队 |
| Local | ./CLAUDE.local.md | 当前项目,gitignore,仅本机 |
一个容易踩的坑:CLAUDE.md 的内容是作为 system prompt 之后的 user message 投递的,不是 system prompt 本身的一部分。这意味着 Claude 把它当作"用户给的强烈建议",而不是"硬约束"。所以 vague 的指令很容易被忽略,写得越具体、可验证,遵守率越高。
加载顺序也有讲究:从工作目录向上沿目录树走,从文件系统根目录到工作目录依次拼接。同一目录内 CLAUDE.local.md 接在 CLAUDE.md 之后。这意味着越靠近工作目录的指令越靠后出现,相对更受重视。
子目录里的 CLAUDE.md 启动时不加载,等 Claude 真正读到那个目录下的文件时再注入——属于按需机制,放在第 4 节细讲。
这是整个体系里唯一Claude 自己写、跨会话持久且会自动变化的一层。
位置:~/.claude/projects/<project>/memory/MEMORY.md,外加同目录的主题文件(debugging.md、api-conventions.md 等)。<project> 由 git 仓库推导,所以同一 repo 的所有 worktree 和子目录共享同一份 memory。
加载规则:只加载 MEMORY.md 的前 200 行或 25KB(先到者为准)。主题文件不会启动时加载,Claude 用 Read 工具按需读取。这就是为什么应该把 MEMORY.md 当索引用、细节拆到主题文件。
写入逻辑:
Claude 不是每个会话都会写。它根据"这条信息对未来对话是否有用"来决定。
Claude Code 的 skill 走的是 progressive disclosure(渐进式披露)——三层加载:
| 时机 | 加载内容 | 大小 |
|---|---|---|
| 启动期 | 每个 SKILL.md 的 YAML frontmatter(name + description) | ~100 token/skill |
| 命中后 | 完整的 SKILL.md body | 数 KB |
| 进一步需要时 | SKILL.md 引用的脚本、reference 文件 | 任意 |
启动期只有 description 进 context。Claude 看到用户 prompt 后判断哪个 skill 描述匹配,才决定加载 body。这种设计让一个仓库可以挂几十个 skill 而不撑爆 context。
可以在 SKILL.md frontmatter 里写 disable-model-invocation: true,那连 description 都不进启动 context,只能用 /name 显式调用。
启动期只加载工具名清单和简短描述,详细 schema 默认懒加载。这跟 skill 是同一种压缩思路。
工作目录、平台、当前 git branch、最近 commit、是否有未提交改动等。这部分默认进启动 context,但内置的 Explore 和 Plan subagent 会跳过以保持轻量。
每一轮调用,客户端都把"启动期 context + 全部历史 messages + 本轮新内容"打包成一个完整请求 POST 给 API。没有"只发增量"这回事。
之所以这样还能保持成本和延迟可控,靠的是 Anthropic 的 prompt caching:稳定不变的前缀(system prompt、CLAUDE.md、MEMORY.md、靠前的 messages)在服务端有缓存,命中后大幅降低 token 计费和首字延迟。所以"传输上看是完整 context,计费上看是增量"。
这也解释了为什么应该把 CLAUDE.md 写得稳定——经常变动会让 cache 频繁失效。
一轮典型的 tool use 循环:
user prompt ↓ assistant message(可能含 tool_use) ↓ [客户端执行工具] ↓ tool_result ↓ assistant message(继续 reasoning 或 tool_use) ↓ ... 反复直到 assistant 给出最终答复
这一整串都进 messages 历史,下一轮全部带着。一个长会话的 context 增长几乎全部来自这里——尤其是 tool_result(文件内容、bash 输出)通常占大头。
除了 messages 自然累加,Claude Code 还有四种"旁路注入"机制,会在特定触发条件下把额外内容塞进 context:
.claude/rules/*.md 是项目级的模块化规则文件,支持 YAML frontmatter 指定路径模式:
---
paths:
- "src/api/**/*.ts"
---
# API Development Rules
- All endpoints must validate input with zod
- Return shape is always { data, error, meta }
- Include OpenAPI comments
加载逻辑:
paths 字段的 rule → 启动期全部加载,地位等同 .claude/CLAUDE.mdpaths 字段的 rule → Claude 读到匹配该 glob 的文件时才注入这是"细则该写但不该一直占 context"场景的标准解法。
跟 path-scoped rules 类似——启动期工作目录上方的 CLAUDE.md 全加载,工作目录下方子目录里的 CLAUDE.md 不加载,等 Claude 真正读子目录下的文件时再注入。
monorepo 场景特别有用:每个 package 可以放自己的 CLAUDE.md,互不污染。
如前面所讲,skill 命中后才注入完整 SKILL.md。SKILL.md 里 @reference.md 这类引用是第三层,进一步按需读。
PostToolUse 这类 hook 可以在工具执行后返回一段 JSON,其中 hookSpecificOutput.additionalContext 字段会作为额外文本注入下一轮调用的 context。常见用法:自动 lint 后把错误注入回 Claude;commit 前注入 diff 摘要等。
Subagent 不是"在主 context 里增加一段",而是独立的一份完整 context。
| 维度 | 主会话 | Subagent |
|---|---|---|
| Context window | 主 | 独立、全新 |
| System prompt | Claude Code 内置 | .claude/agents/<name>.md 的 markdown body |
| CLAUDE.md | 全部层级加载 | 同样加载(少数内置 agent 如 Explore/Plan 会跳过) |
| 主会话历史 | — | 看不到 |
| 主会话已读文件 | — | 看不到 |
| 主会话调用过的 skill | — | 看不到 |
| 任务输入 | 用户 prompt | 主 Claude 写给它的 task prompt |
| Auto memory | 共享项目级 MEMORY.md | 仅当 memory: 字段声明时启用独立 memory |
Subagent 的隔离设计意味着:把一个长任务委派给 subagent 能让主 context 保持干净,但代价是 subagent 不知道你们之前聊过什么。所以 subagent 适合"自包含、输入输出明确"的子任务,不适合"需要理解整个对话脉络"的工作。
关闭 Claude Code │ ├─ messages 历史 → 丢弃(除非有 transcript 持久化) ├─ scratchpad / 工具调用的中间状态 → 丢弃 │ ├─ CLAUDE.md(所有层级) → 留(磁盘) ├─ .claude/rules/、agents/、skills/、commands/ → 留(磁盘) ├─ settings.json → 留(磁盘) ├─ MEMORY.md + 主题文件 → 留(Claude 在会话中可能改过) └─ ~/.claude/projects/<p>/<session>.jsonl → 留(默认 30 天后清理)
注意 transcript(<session>.jsonl)默认 30 天清理,由 cleanupPeriodDays 控制。它是用来支持 --continue 和 --resume 的,不会默认加载进下次新会话的 context。
下次同一项目启动时:
所以"什么是真正跨 session 持久"这个问题的精确答案是:所有磁盘上的 markdown 和 JSON 都持久,但只有 MEMORY.md 是 Claude 自己会写的、内容会变的。
会话变长后,用户或 Claude 会触发 /compact 把历史 messages 压缩成摘要。这之后 context 会怎么变?
重新从磁盘注入:
.claude/rules/*.md(无 paths 字段的)不自动重新注入,要等再次触发:
永久丢失:
这就是为什么"要让 Claude 长期记住"的内容必须落到磁盘——不写进 CLAUDE.md 或让 Claude 写进 MEMORY.md,一次 compact 就没了。
用户 Claude Code 客户端 本地磁盘 Anthropic API
│ │ │ │
│ │ [T0 会话启动期] │ │
│ │──── 读内置 system prompt ────────────────────►│
│ │──── 沿目录树读 CLAUDE.md (managed→user→ │
│ │ project→local) │
│ │──── 读 MEMORY.md 前 200 行/25KB ──────────────│
│ │──── 扫 skills/ 取 YAML frontmatter ───────────│
│ │──── 扫 MCP 工具名清单 ─────────────────────────│
│ │──── 取 git status / env / cwd ───────────────│
│ │ │
│ │ 拼成 system prompt + 启动期 user message │
│ │ │
│ │ [T1 第一轮] │
│── 输入 prompt ──►│ │
│ │── POST {system, messages=[startup_ctx, ───►│
│ │ user_prompt]} │
│ │◄─── assistant message (含 tool_use) ─────────│
│ │ │
│ │ ┌─ tool use 循环 ─────────────────┐ │
│ │ │ 执行 Read/Edit/Bash │ │
│ │ │ 命中 skill → 加载 SKILL.md │ │
│ │ │ 读到匹配路径 → 注入 path rule │ │
│ │ │ 读到子目录 → 注入子目录 CLAUDE.md │ │
│ │ │ PostToolUse hook → addCtx │ │
│ │ │── POST {..., tool_result} ─────►│ │
│ │ │◄── 下一段 assistant message ────│ │
│ │ └─────────────────────────────────┘ │
│ │ │
│ │ [Tn 后续轮次] │
│── 新 prompt ────►│ │
│ │── POST 全部历史 + 启动期 ctx + 新 prompt ───►│
│ │ (prompt caching 命中稳定前缀) │
│ │◄─── 响应 ────────────────────────────────────│
│ │ │
│ │ [Tend 关闭会话] │
│ │ messages 历史丢弃 │
│ │ CLAUDE.md / MEMORY.md / 配置文件 留磁盘 │
│ │ │
把组装机制梳理清楚之后,几个实践原则就有了来由:
1. CLAUDE.md 写具体、可验证的指令。
"Use 2-space indentation" 比 "Format code properly" 有用得多,因为 CLAUDE.md 是作为 user message 投递的强烈建议,不是硬约束。模糊的指令容易被忽略。
2. 控制 CLAUDE.md 大小,超过 200 行就要拆。
CLAUDE.md 整文件加载、每次 API 调用都带,膨胀代价直接体现在 token 上。大项目用 .claude/rules/ 加 paths 字段做路径门控,让规则按需进 context。
3. MEMORY.md 别手动塞太多。
它只加载前 200 行/25KB。让 Claude 自己管理索引、细节拆主题文件是正确用法。
4. Skill 适合"重复性、有明确触发条件的工作流"。
它的渐进式加载是 context 友好的——一百个 skill 启动期也就一万 token 的 descriptions。
5. 长会话定期 /compact,但记得先把要持久的东西落盘。
对话里说过的话 compact 后会丢,除非进了 CLAUDE.md、MEMORY.md 或主题 memory 文件。
6. 子任务考虑用 subagent 而不是继续在主对话里堆。
独立 context 意味着主会话能保持清爽。但 subagent 看不到主对话历史,所以任务描述要写清楚。
7. 监控你的 context 占用。
用 /context 命令能看到当前 context window 各部分占比。哪一层吃得太多就重点优化哪一层。
和西门口超级波的朋友们一起
聊 AI · 聊赚钱
长按识别二维码 · 添加好友
添加时请备注 AI 入群
— 全文完 —