主题
13.5 延迟加载:工具与 Skill 的加载经济学
装 30 个 Skill 几乎不花钱,装 5 个 MCP server 可能吃掉你十分之一的窗口。差别在加载时机。
读完你能做什么:算清每一种扩展机制的常驻成本与触发成本,配置 MCP tool search 的加载策略,并知道什么内容该写成脚本而不是文档。
一、常驻成本 vs 触发成本
任何扩展机制都可以拆成两笔账:
text
常驻成本 = 无论用不用,每次请求都要付的 token
触发成本 = 真正用到时,一次性进入上下文的 token常驻成本会被每一轮重新计费,触发成本只在触发那一轮新增(之后按缓存读价重复计费)。所以:
常驻成本的权重远高于触发成本。 一个 100 token 的常驻项,在 50 轮会话里的总量是 5,000 token;一个 5,000 token 的触发项,只被完整计费一次。
对照表:
| 机制 | 常驻成本 | 触发成本 | 备注 |
|---|---|---|---|
| Skill(默认) | 约 100 token/个(name + description) | SKILL.md 正文,建议 < 5k | description 合并 when_to_use 后按 1,536 字符截断 |
Skill(disable-model-invocation: true) | 0 | 同上 | 只能手动 /name 调用 |
Skill 的其他 .md 资源 | 0 | 读多少算多少 | 未被读取时完全免费 |
Skill 的 scripts/ | 0 | 只有输出计入 | 脚本代码从不进上下文 |
| MCP 工具(tool search 开启,默认) | 工具名 + server instructions | 单个工具的完整 schema | server instructions 与工具描述各按 2KB 截断 |
MCP 工具(alwaysLoad: true) | 全部 schema 常驻 | 0 | 每增一个工具就永久占一份 |
CLAUDE.md | 全文常驻 | — | 官方建议控制在 200 行以内 |
带 paths: 的 rules | 0 | 读到匹配文件时加载 | 压缩后丢失,直到再次匹配 |
| Hook | 0 | 只有脚本输出计入 | Hook 是代码,不是上下文 |
| 子代理 | 0(父会话侧) | 只有返回的摘要 | 子代理自己另有一套开销 |
最后三行是「近乎免费」的三种扩展方式,值得优先使用。
二、Skill:为什么可以放心多装
一个真实的启动构成参考(Claude Code 官方给的代表性数值):
| 项目 | token |
|---|---|
| 系统提示词 | 4,200 |
Auto memory(MEMORY.md) | 680 |
| 环境信息 | 280 |
| MCP 工具名(延迟加载) | 120 |
| Skill 描述(若干个) | 450 |
~/.claude/CLAUDE.md | 320 |
项目 CLAUDE.md | 1,800 |
| 合计常驻 | ≈ 7,850 |
Skill 描述只占 450。把 Skill 数量翻到 30 个,常驻成本大约是 3,000 token,占 200k 窗口的 1.5%、占 1M 窗口的 0.3%。
结论:Skill 数量不是问题,Skill 正文的长度和调用频次才是。 一个 8k token 的 Skill 被调用后会常驻整个会话;十个 100 token 的 description 加起来才 1k。
控制 Skill 成本的四个动作
| 动作 | 省下什么 |
|---|---|
正文控制在 500 行以内,细节移到独立 .md | 触发成本从数千降到数百 |
| 重要指令写在文件开头 | 压缩后重新附着时只保留前 5,000 token |
有副作用的流程加 disable-model-invocation: true | 常驻成本归零 |
大量读取型流程加 context: fork | 触发成本完全不进主会话 |
反过来:什么该从 CLAUDE.md 搬进 Skill
CLAUDE.md 在会话启动时全文加载,即使你今天做的事和它八竿子打不着。判断标准:
text
这段内容是「每次会话都用得上的事实」吗?
├─ 是 → 留在 CLAUDE.md(目录约定、命令、硬性规范)
└─ 否 → 它是「某类任务的流程」吗?
├─ 是 → 做成 Skill(PR 评审流程、数据库迁移步骤、发布清单)
└─ 否 → 它只在改某类文件时才相关吗?
├─ 是 → 做成带 paths: 的 rule
└─ 否 → 大概率可以删三、MCP:真正的大头
MCP 工具定义是完整的 JSON Schema。一个中等复杂度的工具,schema 常在 300~800 token。一个提供 25 个工具的 server,全量加载就是一两万 token 常驻。
3.1 tool search 是默认开启的
Claude Code 默认延迟加载 MCP 工具:启动时只放工具名和 server instructions,Claude 需要时用搜索工具把具体 schema 拉进来。
用 ENABLE_TOOL_SEARCH 控制:
| 取值 | 行为 |
|---|---|
| (未设置) | 全部延迟。在 Google Cloud Agent Platform、非第一方 ANTHROPIC_BASE_URL、Azure 托管的 Microsoft Foundry 上回退为全量前置加载 |
true | 强制全部延迟 |
auto | 阈值模式:schema 总量若在窗口的 10% 以内则前置加载,超出部分延迟 |
auto:N | 阈值模式,自定义百分比,例如 auto:5 |
false | 全部前置加载,不延迟 |
bash
# 用 5% 阈值:小工具集前置(省一次往返),大工具集延迟
ENABLE_TOOL_SEARCH=auto:5 claude也可以写进 settings.json 的 env 字段固化。
tool search 需要支持
tool_reference块的模型:Claude Sonnet 4.5、Haiku 4.5、Opus 4.5 及更新版本。设了CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS会让 tool search 失效,且ENABLE_TOOL_SEARCH无法覆盖。
3.2 给高频 server 开豁免
每轮都要用的少量工具,让它一直在场更划算 —— 省掉每次搜索的一轮往返:
json
{
"mcpServers": {
"core-tools": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"alwaysLoad": true
}
}
}代价有两个,都要提前知道:
- 该 server 的全部工具永久常驻,无视
ENABLE_TOOL_SEARCH。 alwaysLoad: true会阻塞启动直到该 server 连上(上限 5 秒),因为构建第一个提示词时必须有这些工具。
alwaysLoad需 Claude Code v2.1.121+。MCP server 也可以在单个工具的_meta里放"anthropic/alwaysLoad": true,只豁免那一个工具。
3.3 优先考虑 CLI,而不是 MCP
官方成本文档里的一条建议:gh、aws、gcloud、sentry-cli 这类命令行工具比对应的 MCP server 更省上下文,因为它们不产生任何按工具计的清单开销 —— Claude 直接跑命令就行。
| 方案 | 常驻成本 | 何时更优 |
|---|---|---|
| CLI 工具 | 0 | 有成熟 CLI、输出可控、你能接受权限规则的管理成本 |
| MCP server(延迟) | 工具名清单 | 需要结构化返回、需要认证托管、没有 CLI |
MCP server(alwaysLoad) | 全量 schema | 每轮都要用的少数几个工具 |
定期用 /mcp 检查有哪些 server 挂着但没在用,用不到就关掉。
四、脚本 > 文档:一条被低估的规则
Skill 里的 scripts/ 目录有一个独特性质:
脚本代码从不进入上下文,只有它的 stdout / stderr 进入。
对比一下同一件事的两种写法:
| 做法 | 上下文成本 | 可靠性 |
|---|---|---|
在 SKILL.md 里写 80 行「如何校验这份表单」的说明,让 Claude 现场生成代码 | 说明常驻 + 生成的代码进上下文 + 可能出错重试 | 每次结果可能不同 |
写一个 scripts/validate_form.py,SKILL.md 里只写一行「跑这个脚本」 | 一行说明 + 脚本输出(如 Validation passed) | 确定性 |
确定性操作写成脚本,判断性操作留给模型。 这是渐进式披露的第三层真正的价值所在。
配套的权限技巧:用 ${CLAUDE_SKILL_DIR} 让 allowed-tools 规则精确匹配脚本路径,跑脚本时就不会弹权限确认:
yaml
---
name: render-chart
description: 从 CSV 渲染图表
allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *)
---
运行 `${CLAUDE_SKILL_DIR}/scripts/render.sh <csv-file>` 生成图表。
allowed-tools里的${CLAUDE_SKILL_DIR}替换需 Claude Code v2.1.129+。更早版本会把它当字面量,永远匹配不上。
五、Hook:零上下文成本的预处理层
Hook 完全不进上下文。它的用途是在信息到达 Claude 之前就把它压小。
最典型的场景:测试输出。一次 npm test 的完整输出可能是 12,000 token,其中你关心的失败信息只有 300 token。
json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/filter-test-output.sh" }
]
}
]
}
}bash
#!/bin/bash
# ~/.claude/hooks/filter-test-output.sh
# 把测试命令改写成只输出失败部分
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command')
if [[ "$cmd" =~ ^(npm test|pytest|go test) ]]; then
filtered_cmd="$cmd 2>&1 | grep -A 5 -E '(FAIL|ERROR|error:)' | head -100"
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"allow\",\"updatedInput\":{\"command\":\"$filtered_cmd\"}}}"
else
echo "{}"
fi验证它生效了:
bash
claude --debug
# 触发一次 npm test,调试日志里应出现:modified tool input keys: [command]同类场景:日志筛选、大 JSON 的字段裁剪、把二进制/图片输出替换成描述。每一个都是把上万 token 压到几百的量级。
六、一张加载策略决策表
| 内容性质 | 推荐载体 | 常驻成本 |
|---|---|---|
| 每次会话都要用的硬事实(构建命令、目录约定) | CLAUDE.md | 全文 |
| 某类任务的流程(发布、评审、迁移) | Skill | ~100 token |
| 只在改某类文件时相关的规范 | 带 paths: 的 rule | 0 |
| 有副作用、要你控制时机的流程 | Skill + disable-model-invocation | 0 |
| 大量读取 / 探索类工作 | Skill + context: fork,或子代理 | 0 |
| 确定性的检查、转换、校验 | scripts/ 下的脚本 | 0 |
| 工具输出的裁剪与过滤 | Hook | 0 |
| 每轮都要用的少数几个外部工具 | MCP + alwaysLoad | 全量 schema |
| 偶尔要用的大量外部工具 | MCP + tool search(默认) | 工具名 |
| 有成熟 CLI 的外部服务 | 直接用 CLI | 0 |
用 /context 核对现状,用这张表决定该往哪一行迁移。