Claude Code 是怎么为每次 API 调用组装 Context 的

一篇关于 Claude Code 上下文工程的系统性梳理:从会话启动期"底色"的拼装,到每一轮 API 调用的增量累加,到按需注入的旁路机制,再到会话结束后什么被持久化、什么被丢弃。

0. 为什么要搞清楚这个

Anthropic 的 LLM API 本质上是无状态的:每一次 HTTP 请求都必须把完整的 system prompt 和 messages 历史发过去,服务端不会替你记任何东西。所谓"Claude 记得我们之前聊过什么",全是客户端在每次调用前重新把 context 拼起来的结果。

Claude Code 这个客户端做的事情比想象中复杂得多——它需要在一次 API 调用之前完成至少这些工作:

理解这个组装过程,对三件事很有用:(1) 写出 Claude 实际能遵守的 CLAUDE.md;(2) 在 context 爆掉之前知道哪些内容能拆出去;(3) 排查 Claude 为什么没按你期望的行为执行。

1. 心智模型:三层时间维度

把整个生命周期切成三段,会清晰很多:

┌──────────────────────────────────────────────────────────────────┐
│  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 里写的, 现在留下了      │
└──────────────────────────────────────────────────────────────────┘

两种状态:

2. 会话启动期:底色 context 怎么拼

启动期的 context 是一份"模板",每次 API 调用都带着它。组成部分如下:

2.1 内置 system prompt

Claude Code 自带的 system prompt(描述 Claude 的角色、工具用法、安全约束等)。这部分用户不能直接编辑,但可以通过 --append-system-prompt 在末尾追加。

2.2 CLAUDE.md 层级

按"广 → 窄"顺序加载,全部拼接,不是覆盖:

层级 路径 作用域
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 节细讲。

2.3 Auto Memory(MEMORY.md)

这是整个体系里唯一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 不是每个会话都会写。它根据"这条信息对未来对话是否有用"来决定。

2.4 Skill descriptions(渐进式加载第一层)

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 显式调用。

2.5 MCP 工具元信息

启动期只加载工具名清单和简短描述,详细 schema 默认懒加载。这跟 skill 是同一种压缩思路。

2.6 环境与 git 状态

工作目录、平台、当前 git branch、最近 commit、是否有未提交改动等。这部分默认进启动 context,但内置的 Explore 和 Plan subagent 会跳过以保持轻量。

3. 每一轮 API 调用:context 在动什么

3.1 完整重传 + Prompt Caching

每一轮调用,客户端都把"启动期 context + 全部历史 messages + 本轮新内容"打包成一个完整请求 POST 给 API。没有"只发增量"这回事。

之所以这样还能保持成本和延迟可控,靠的是 Anthropic 的 prompt caching:稳定不变的前缀(system prompt、CLAUDE.md、MEMORY.md、靠前的 messages)在服务端有缓存,命中后大幅降低 token 计费和首字延迟。所以"传输上看是完整 context,计费上看是增量"。

这也解释了为什么应该把 CLAUDE.md 写得稳定——经常变动会让 cache 频繁失效。

3.2 每轮累加的内容

一轮典型的 tool use 循环:

user prompt
  ↓
assistant message(可能含 tool_use)
  ↓
[客户端执行工具]
  ↓
tool_result
  ↓
assistant message(继续 reasoning 或 tool_use)
  ↓
... 反复直到 assistant 给出最终答复

这一整串都进 messages 历史,下一轮全部带着。一个长会话的 context 增长几乎全部来自这里——尤其是 tool_result(文件内容、bash 输出)通常占大头。

4. 按需注入:context 怎么动态长大

除了 messages 自然累加,Claude Code 还有四种"旁路注入"机制,会在特定触发条件下把额外内容塞进 context:

4.1 Path-scoped rules

.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

加载逻辑:

这是"细则该写但不该一直占 context"场景的标准解法。

4.2 子目录里的 CLAUDE.md

跟 path-scoped rules 类似——启动期工作目录上方的 CLAUDE.md 全加载,工作目录下方子目录里的 CLAUDE.md 不加载,等 Claude 真正读子目录下的文件时再注入。

monorepo 场景特别有用:每个 package 可以放自己的 CLAUDE.md,互不污染。

4.3 Skill body(渐进式加载第二、第三层)

如前面所讲,skill 命中后才注入完整 SKILL.md。SKILL.md 里 @reference.md 这类引用是第三层,进一步按需读。

4.4 Hooks 注入

PostToolUse 这类 hook 可以在工具执行后返回一段 JSON,其中 hookSpecificOutput.additionalContext 字段会作为额外文本注入下一轮调用的 context。常见用法:自动 lint 后把错误注入回 Claude;commit 前注入 diff 摘要等。

5. Subagent:另起一个 context

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 适合"自包含、输入输出明确"的子任务,不适合"需要理解整个对话脉络"的工作。

6. 会话结束:什么留下、什么丢

关闭 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 自己会写的、内容会变的。

7. /compact 之后呢

会话变长后,用户或 Claude 会触发 /compact 把历史 messages 压缩成摘要。这之后 context 会怎么变?

重新从磁盘注入:

不自动重新注入,要等再次触发:

永久丢失:

这就是为什么"要让 Claude 长期记住"的内容必须落到磁盘——不写进 CLAUDE.md 或让 Claude 写进 MEMORY.md,一次 compact 就没了。

8. 完整时序图

用户         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 / 配置文件 留磁盘     │
 │                  │                                               │

9. 实战启示

把组装机制梳理清楚之后,几个实践原则就有了来由:

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 各部分占比。哪一层吃得太多就重点优化哪一层。

参考文档

JOIN US

加入 AI 超级群

和西门口超级波的朋友们一起
聊 AI · 聊赚钱

联系二维码

长按识别二维码 · 添加好友

添加时请备注 AI 入群


— 全文完 —