Claude Code 的工具按需加载

用 Claude Code 写代码的时候,你大概不会注意到一件事:它注册了超过 40 个工具,但你让它读个文件、改几行代码,它只用到三四个。剩下那三十多个工具的定义,每个大约 500 个 token,全塞进上下文就是一万多 token 的固定开销。你只想改一行 CSS,却要为 WebSearch、NotebookEdit、CronCreate 这些完全用不到的工具买单。

这个问题在传统软件里有现成的解决方案:动态链接。程序启动时不加载所有共享库,等到第一次调用某个函数时再去加载。Claude Code 做了一件类似的事,只不过它管理的不是内存地址空间,而是 token 预算。

问题有多大

一个 LLM Agent 能力越强,注册的工具就越多。读文件、写文件、搜索、执行命令是基础,再加上笔记本编辑、定时任务、计划模式、Web 搜索、各种 MCP 扩展工具,轻松超过 40 个。每次 API 请求,所有工具的名称、描述、参数定义都要作为上下文发给模型。

对于一个 200K 的上下文窗口,光是按需工具的定义就占了将近 7%。这不是一次性成本,是每轮对话都要付的。而且 Anthropic 按 token 计费,即使缓存命中,这些工具定义也在占用缓存空间,影响其他内容的缓存效率。

更关键的是,工具定义占的 token 越多,留给实际对话和代码的空间就越少。在长会话里,这 7% 的固定开销可能就是触发一次额外压缩和不触发之间的差别。

常驻和按需两类工具

Claude Code 的解决方案是把工具分成两类。

第一类是常驻工具,每次请求都带完整定义:

1
2
3
4
Always loaded:
  Bash, Read, Edit, Write    (core file ops)
  Glob, Grep                 (search)
  Agent, ToolSearch, Skill   (infrastructure)

这些是高频工具,几乎每个任务都会用到。注意 ToolSearch 本身也是常驻的,因为它是加载其他工具的入口,不能被按需加载,否则就没人能加载别的工具了。

第二类是按需工具,只发送名字,不发送完整定义。模型知道有这些工具存在,但看不到参数和用法。这类包括 WebSearch、TodoWrite、NotebookEdit、各种 Cron 工具、Plan 模式工具,以及所有 MCP 扩展工具。

判断标准很直觉:第一轮对话就可能需要的工具常驻,可能整个会话都用不到的工具按需。MCP 工具天然适合按需加载,它们是用户按项目配置的,数量不可控,有些项目接了十几个 MCP 服务器,工具定义轻松破万 token。

发现机制

模型怎么加载一个按需工具?通过调用 ToolSearch。

比如用户说"帮我搜一下 Claude Code 的更新日志",模型判断需要 WebSearch,但上下文里只有这个工具的名字,没有参数定义。模型先调用 ToolSearch,指定要加载 WebSearch。ToolSearch 返回一个特殊的引用标记,API 服务端看到这个标记后,把 WebSearch 的完整定义注入模型的上下文。模型随后就能正常调用 WebSearch 了。

整个流程大概是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Turn 1:
  Model sees: Bash, Read, Edit, Write, Glob, Grep,
              Agent, ToolSearch, Skill
  Model sees names only: WebSearch, TodoWrite,
              NotebookEdit, mcp__slack__post, ...

User: "search for Claude Code release notes"

  Model calls ToolSearch("select:WebSearch")
    --> returns reference marker
    --> API injects WebSearch full schema

  Model calls WebSearch("Claude Code release notes")
    --> returns results

Turn 2:
  WebSearch now included as loaded tool
  Other deferred tools still name-only

代价是多一轮 ToolSearch 调用,大约 200 token 的输入。但省下的是 30 多个工具定义的固定开销,一万多 token。只要不是每轮都在搜索新工具,净收益是正的。而且一旦加载过的工具会被记住,后续请求不需要重复加载。

ToolSearch 还支持模糊搜索。模型不确定工具叫什么名字的时候,可以用关键词查询,比如搜索"notebook jupyter"就能找到 NotebookEdit,搜索"slack send"就能找到对应的 MCP 工具。

压缩后不丢工具

上一篇聊过 Claude Code 的上下文压缩机制。压缩会把历史消息压成摘要,但之前加载过的工具引用也会跟着丢失。如果不处理,压缩完模型就忘了自己加载过 WebSearch,下次用的时候又要重新搜索一遍。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Before compaction:
  [msg1] [msg2] ... [msg30] [WebSearch loaded at msg15]
                                      |
                              compaction happens
                                      |
                                      v
After compaction (without recovery):
  [summary] [msg29] [msg30]
  WebSearch? what WebSearch?

After compaction (with recovery):
  [summary + preCompactDiscoveredTools: [WebSearch]]
  [msg29] [msg30]
  WebSearch still loaded

Claude Code 用两层机制解决这个问题。第一层是在压缩边界消息里记录已加载的工具列表,压缩后系统扫描到这个边界就能恢复之前的状态。第二层是增量通知:每轮对话前,系统对比当前可用的按需工具和已通知过的工具,如果有变化(比如某个 MCP 服务器断开了,或者新的服务器连上了),就生成一条消息告诉模型。

两层配合的效果是:API 层面保证工具过滤正确,模型认知层面保证它知道哪些工具可用。压缩不会造成工具状态的断裂。

自适应启用

并不是所有场景都需要按需加载。如果项目只用了两三个 MCP 工具,总共不到 1500 token,多一轮 ToolSearch 调用反而浪费时间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
deferred tool definitions < 10% of context window?
            |
     +------+------+
     |             |
    YES            NO
     |             |
     v             v
  all inline    deferred mode
  (no search    (ToolSearch
   overhead)     on demand)

Claude Code 有一个自动模式:先计算所有可按需加载的工具定义总 token 数,如果超过上下文窗口的 10%,就启用按需加载;不超过就全部内联。小项目工具少,全部常驻更高效;大项目工具多,按需加载省得多。系统自己判断,用户不需要操心。

第三方 API 代理默认不启用这个功能,因为工具引用标记是 Anthropic 的特有能力,代理可能不支持。但如果用户确认代理能处理,可以手动开启。

为什么这个设计有意思

工具按需加载看起来是个很具体的工程优化,但它背后的矛盾其实很普遍:Agent 能力越强,注册的工具越多,上下文开销就越大,留给干活的空间就越小。能力本身成了负担。

这个矛盾在传统软件里早就出现过。程序能调用的函数越多,全部打包进来,体积就越大,启动就越慢。后来的解法是延迟加载,用到哪个再加载哪个,用一次额外的间接调用换来大幅减少的资源占用。Claude Code 对工具做的事情一模一样,只不过省的不是内存,是 token。