主题
13.3 上下文生命周期与释放机制
进入上下文的东西,不会因为「用完了」而自动消失。
读完你能做什么:说清每一类内容何时进入上下文、是否常驻、唯一的退出路径是什么;理解 Skill 被读取后为什么不会被释放,以及有哪些手段可以真正把它请出去。
一、一条铁律
凡是进入对话历史的内容,在同一会话内不会被释放。没有 unload 这个动作。
这是很多人的第一个认知缺口。直觉上会觉得:Claude 读了一个 Skill,用完了,下一步不需要了,那它应该被回收。
实际不是。对话历史是一个只追加的日志。模型每一轮都收到完整的日志。「不再需要」不是日志里能表达的状态 —— 客户端没有把某条消息从中间挖掉的机制,因为那会让后面所有内容的缓存前缀全部失效(见 13.4)。
所以准确的说法是:
- 加载是渐进式的(不用就不进来)
- 卸载是批量式的(要么整段丢弃,要么整段摘要,没有单点回收)
二、内容生命周期全表
以 Claude Code 会话为例:
| 内容 | 何时进入上下文 | 会话内是否常驻 | 唯一退出方式 |
|---|---|---|---|
| 系统提示词、工具定义 | 启动 | 是 | 重启(不受压缩影响) |
| 输出风格(output style) | 启动 | 是 | 重启或 /clear |
项目根 CLAUDE.md、无作用域 rules | 启动 | 是 | 压缩后从磁盘重新注入 |
Auto memory(MEMORY.md) | 启动 | 是 | 压缩后从磁盘重新注入 |
Skill 的 name + description | 启动 | 是 | 从磁盘移除该 Skill,或设 disable-model-invocation: true |
| MCP 工具名(延迟加载时) | 启动 | 是 | 断开该 server |
| MCP 工具完整 schema | Claude 首次搜索到该工具时 | 是 | /clear、/compact |
| Skill 正文(SKILL.md body) | 被调用时 | 是 | /clear、/compact(受配额截断)、/rewind |
| Skill 目录下的其他文件 | 被显式读取时 | 是 | 同上 |
| 文件读取结果 | Read / Grep / Glob 返回时 | 是 | 同上 |
| 工具与 Bash 输出 | 工具返回时 | 是 | 同上;API 侧可用 tool result clearing |
带 paths: 的 rules | 首次读到匹配文件时 | 是 | 压缩后丢失,直到再次读到匹配文件 |
子目录里的嵌套 CLAUDE.md | 首次读到该目录下文件时 | 是 | 同上 |
| 思考块 | 生成时 | 取决于模型 | Opus 4.5+ / Sonnet 4.6+ 默认保留;更早模型与 Haiku 自动剥离 |
| Hooks | 从不进入上下文 | — | Hook 是代码,只有它的输出会进上下文 |
| 脚本文件本身 | 从不进入上下文 | — | 只有 stdout / stderr 进上下文 |
最后两行是本章最重要的杠杆,13.5 会展开。
三、Skill 的完整生命周期
这是提问最集中的地方,单独拆开讲。
3.1 三级加载模型
| 层级 | 何时加载 | Token 成本 | 内容 |
|---|---|---|---|
| L1 元数据 | 启动时,全部 Skill | 每个约 100 token | frontmatter 的 name 与 description |
| L2 正文 | 被调用时 | 建议控制在 5k 以内 | SKILL.md 的 markdown 正文 |
| L3 资源 | 被显式读取时 | 未读取时为 0 | 其他 .md、模板、数据文件 |
| L3 脚本 | 被执行时 | 只有输出计入 | scripts/ 下的可执行文件 |
description 与 when_to_use 合并后,在 Skill 清单里按 1,536 字符截断,以控制常驻成本。所以关键用途要写在最前面。
3.2 调用之后发生了什么
官方对这一段的描述是精确的:
当你或 Claude 调用一个 Skill 时,渲染后的
SKILL.md内容作为一条消息进入对话,并在本次会话的剩余时间里一直留在那里。Claude Code 不会在后续轮次重新读取该 Skill 文件。
三条直接推论:
- 用完不释放。 下一个流程不需要它了,它依然占着上下文,并在之后每一轮被重新计费(缓存命中时按 0.1× 价)。
- 正文里的每一行都是重复成本。 所以写 Skill 时要用写
CLAUDE.md的标准:说「做什么」,不说「为什么」和「怎么想的」。 - 要写成常驻指令,不要写成一次性步骤。 因为它会一直在场,措辞应该是「本任务中始终遵守 X」,而不是「现在请做 X」。
3.3 重复调用会不会叠加
会去重,但有条件:
| 情形 | 行为 |
|---|---|
| 再次调用,渲染结果与已在上下文中的副本完全相同 | 只追加一句「该 Skill 已加载」的提示,不重复正文 |
渲染结果不同(参数变了,或 !`command` 动态注入的输出变了) | 追加完整正文,形成第二份副本 |
需 Claude Code v2.1.202+。更早版本每次重新调用都会追加一份完整正文。
3.4 压缩之后会发生什么
/compact 是唯一能「部分释放」Skill 的机制,规则很具体:
text
压缩时:
1. 对话历史被摘要替换
2. 摘要之后,重新附着每个 Skill 的最近一次调用内容
3. 每个 Skill 只保留前 5,000 token(从文件开头截断)
4. 所有重新附着的 Skill 共享 25,000 token 总预算
5. 从最近调用的 Skill 开始填预算,超出后老的 Skill 被整个丢弃三条实操结论:
- 最重要的指令写在
SKILL.md靠前位置,因为截断保留的是开头。 - 一次会话里别调用太多 Skill。调用了 8 个各 4k token 的 Skill,压缩后只有最近 6 个能进 25k 预算,最早的 2 个消失。
- 如果某个 Skill 在压缩后「失效」了,先确认是被丢弃还是模型只是没在用它 —— 前者重新调用一次即可恢复。
3.5 让 Skill 用完即走的两个办法
办法一:context: fork。 Skill 内容成为子代理的提示词,在独立上下文里执行,只有结果回到主会话。
yaml
---
name: deep-research
description: 深入调研一个主题并返回结论
context: fork
agent: Explore
---
调研 $ARGUMENTS:
1. 用 Glob / Grep 定位相关文件
2. 阅读并分析
3. 输出带文件引用的结论主会话为此付出的上下文成本,只有最后那份摘要。
办法二:disable-model-invocation: true。 连 description 都不进常驻上下文,只有你手动敲 /skill-name 时才加载。适合有副作用、需要你控制时机的流程(部署、提交、发消息)。
| frontmatter | 你能调用 | Claude 能自动调用 | 常驻成本 |
|---|---|---|---|
| (默认) | 是 | 是 | description 常驻 |
disable-model-invocation: true | 是 | 否 | 零常驻 |
user-invocable: false | 否 | 是 | description 常驻 |
四、四种真正的释放手段
| 手段 | 释放范围 | 自身成本 | 缓存影响 | 适用时机 |
|---|---|---|---|---|
/clear | 全部对话历史 | 零(不发请求) | 从头重建 | 切换到无关任务 |
/compact | 历史 → 摘要 | 一次完整请求 | 对话层击穿,系统层保留 | 需要保留连续性的长任务 |
/rewind | 截断回到某一轮 | 零 | 命中已有缓存 | 走错路,想退回分叉点 |
子代理 / context: fork | 内容从不进入主上下文 | 子代理自身的启动开销 | 主会话缓存不受影响 | 大量读取、探索、验证 |
4.1 /clear 与 /compact 的选择
text
需要延续之前的工作状态吗?
├─ 不需要 → /clear(零成本,且新会话上下文最干净)
└─ 需要 → 走错了路吗?
├─ 是 → /rewind(缓存友好,比 compact 便宜得多)
└─ 否 → /compact,并且带上指令:
/compact 重点保留鉴权模块的改动与未解决的测试失败/compact 的时机同样重要:在任务边界主动执行,而不是等它在任务中途自动触发。自动压缩会按它的判断取舍,而你在任务边界执行时可以指定保留重点。
也可以把压缩偏好固化到 CLAUDE.md:
markdown
# Compact instructions
压缩时优先保留:未解决的失败用例、已确定的架构决策、正在修改的文件路径。
可以丢弃:已通过的测试输出、成功的构建日志、已完成步骤的复述。4.2 API 侧的精细释放
在自己写 Agent 时,有比 /compact 更外科手术式的选项(需 beta 头 context-management-2025-06-27):
python
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=4096,
messages=messages,
tools=tools,
betas=["context-management-2025-06-27"],
context_management={
"edits": [
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30000}, # 超过此阈值才动手
"keep": {"type": "tool_uses", "value": 3}, # 保留最近 3 组工具调用
"clear_at_least": {"type": "input_tokens", "value": 5000}, # 清不够这么多就不清
"exclude_tools": ["web_search"], # 这些工具的结果永不清除
}
]
},
)| 策略 | 清什么 | 关键权衡 |
|---|---|---|
clear_tool_uses_20250919 | 旧的工具结果(可选连工具调用一起清) | 会击穿缓存前缀,所以要用 clear_at_least 保证清得够本 |
clear_thinking_20251015 | 历史思考块,可指定保留最近 N 轮 | 保留思考块可维持缓存命中,清除则在清除点击穿 |
compact_20260112 | 整段历史 → 摘要(服务端) | 额外一次采样,账单要遍历 usage.iterations |
关键设计点:context editing 在服务端执行,客户端不需要同步。 你本地照常维护完整历史,服务端在请求到达模型之前做裁剪。
五、「我这句话去了哪里」的时序
一个具体例子,帮助建立直觉:
text
T0 启动
→ 系统提示词 4.2k / auto memory 0.7k / 环境信息 0.3k
→ MCP 工具名 0.12k / Skill 描述 0.45k
→ 用户 CLAUDE.md 0.32k / 项目 CLAUDE.md 1.8k
常驻小计 ≈ 7.9k,这部分每一轮都要重新计费
T1 你:"给鉴权加上 refresh token" +0.045k
T2 Read src/api/auth.ts +2.4k ← 永久留下
T3 Read src/lib/tokens.ts +1.1k ← 永久留下
T4 自动加载 rules/api-conventions.md +0.38k ← 压缩后会丢失
T5 Edit auth.ts +0.4k
T6 PostToolUse hook 跑 prettier +0.12k ← 只有输出进来,脚本本身没有
T7 npm test 输出 +1.2k ← 未过滤的话可能是 12k
T8 Claude 总结 +0.4k
此刻窗口占用 ≈ 13.9k
第 8 轮的计费输入 ≈ 13.9k(其中约 12k 走缓存读)
T9 你派一个子代理去调研 +0.08k
子代理内部读了 3 个文件共 8k token → 全部留在子代理的上下文里
子代理返回摘要 +0.42k ← 主会话只付这 0.42k
T10 /compact
→ T1~T9 被摘要替换
→ 项目 CLAUDE.md、auto memory 从磁盘重新注入
→ rules/api-conventions.md 丢失,直到下次读到匹配文件
→ 已调用的 Skill 按 5k/个、25k 总额重新附着T9 那一步是整段流程里性价比最高的动作:8k token 的读取,主会话只承担 0.5k。
六、常见误解清单
| 误解 | 实际情况 |
|---|---|
| Skill 用完会被卸载 | 不会。会话内一直在,直到 /clear 或 /compact |
| 只要不再提它,它就不占钱了 | 每一轮都重新计费,缓存命中时按 0.1× |
| 装了 30 个 Skill,上下文会爆 | 未触发时每个约 100 token,30 个约 3k,可接受 |
中途改 CLAUDE.md 会立刻生效 | 不会。项目根与用户级 CLAUDE.md 在会话启动时读入并驻留内存,改动要等 /clear、/compact 或重启 |
中途改 CLAUDE.md 会击穿缓存 | 不会,因为它压根没重新读 |
/compact 比 /clear 省钱 | 相反。/clear 零成本,/compact 是一次完整请求 |
| Hook 脚本会占上下文 | 不会。Hook 是代码,只有它的输出进上下文 |
| 子代理读的文件会进主上下文 | 不会。只有它返回的摘要会进来 |