Claude Code 的工具延迟加载机制

AI Agent 的工具调用依赖模型理解每个工具的参数 schema。传统做法是在 system prompt 中列出所有工具的完整定义------名称、描述、参数类型、参数说明。当工具数量在 10-20 个以内时,这没有问题。

但当工具数量增长到 50+,schema 的 token 成本就不可忽视了。Claude Code 的内置工具(如 Bash、Read、Edit)每个 schema 约 70-150 token,MCP 工具的 schema 通常更复杂。一个典型的 MCP 场景:用户配置了 Slack、GitHub、Jira、Notion、Linear 五个服务器,每个提供 10-15 个工具,总计 50-75 个。全部内联发送会消耗大量 token------在 200K 上下文窗口中占比可观。

更关键的是,大多数工具在一次对话中不会被用到。用户可能只用了 Slack 的发送消息功能,但 GitHub、Jira、Notion、Linear 的所有工具 schema 都白白占据了上下文空间。

Claude Code 通过 defer_loading(延迟加载)机制解决了这个问题。先看 Claude Code 是怎么把工具传给大模型的,再看延迟加载如何在这个流程中发挥作用。


工具如何传给大模型

每次调用 Claude API 时,工具列表作为 tools 参数传入 anthropic.beta.messages.create()。这个参数是一个数组,每个元素包含工具的名称、描述和参数 schema:

typescript 复制代码
const result = await anthropic.beta.messages.create({
  model: options.model,
  messages: messagesForAPI,
  system,
  tools: allTools, // 工具列表在这里传入
  tool_choice: options.toolChoice,
  max_tokens: maxOutputTokens,
  stream: true,
});

每个工具对象在传入 API 前,会经过转换,取出名称、描述和输入 schema,组装成 API 需要的格式。最终 allTools 中每个元素的结构类似:

json 复制代码
{
  "name": "Bash",
  "description": "Executes a given bash command and returns its output.",
  "input_schema": {
    "type": "object",
    "properties": {
      "command": { "type": "string", "description": "The command to execute" }
    },
    "required": ["command"]
  }
}

这就是工具传入大模型的基本流程。


defer_loading 的核心机制

延迟判定:谁被延迟,谁不被延迟

工具组装时,每个工具都会经过一个判定函数:

typescript 复制代码
function isDeferredTool(tool: Tool): boolean {
  // alwaysLoad 优先------MCP 工具可以通过 _meta 设置此标志
  if (tool.alwaysLoad === true) return false;

  // MCP 工具默认延迟
  if (tool.isMcp === true) return true;

  // ToolSearch 自身不能被延迟------模型需要它来加载其他工具
  if (tool.name === TOOL_SEARCH_TOOL_NAME) return false;

  // 内置工具通过 shouldDefer 显式标记
  return tool.shouldDefer === true;
}

判定规则:

  • MCP 工具默认被延迟------这是延迟加载存在的主要原因
  • alwaysLoad: true 的 MCP 工具跳过延迟------适用于高频使用的工具
  • ToolSearchTool 自身永远不延迟------它是模型发现其他工具的唯一入口
  • 内置工具默认不延迟 ------shouldDefer 字段默认为 false,除非工具显式标记

defer_loading 如何生效

判定结果通过 willDefer() 函数传入 toolToAPISchema(),后者在工具的 API schema 上加上 defer_loading: true 标记:

typescript 复制代码
const toolSchemas = await Promise.all(
  filteredTools.map(tool =>
    toolToAPISchema(tool, {
      deferLoading: willDefer(tool),
    }),
  ),
);

toolToAPISchema() 的核心逻辑:

typescript 复制代码
function toolToAPISchema(tool, options) {
  const schema = {
    name: base.name,
    description: base.description,
    input_schema: base.input_schema,
  };

  if (options.deferLoading) {
    schema.defer_loading = true;
  }

  return schema;
}

defer_loading: true 的工具在 API 侧的行为不同:模型只能看到工具的名称和描述,看不到完整的参数 schema。这正是延迟加载的核心------通过一个 API 字段,控制工具 schema 的可见性。

延迟后的工具在 prompt 中长什么样

被延迟的工具不会从工具池中移除,但发送给 API 时只携带名称,不携带完整的 inputSchema。

模型在 system prompt 中看到的是一个 <available-deferred-tools> 区域,每个工具只有一行:

bash 复制代码
<available-deferred-tools>
mcp__slack__send_message --- send a message to a Slack channel or user
mcp__slack__list_channels --- list available Slack channels
mcp__github__create_issue --- create a new GitHub issue
mcp__jira__search_issues --- search for Jira issues using JQL
</available-deferred-tools>

模型知道这些工具存在,也知道它们大概做什么,但不知道它们需要什么参数。这意味着模型无法直接调用它们------它必须先获取完整的 schema。

ToolSearchTool:按需加载的桥梁

ToolSearchTool 是整个延迟加载机制的关键枢纽。它是唯一一个始终加载的工具,负责在模型需要时加载其他工具的完整 schema。

它支持两种查询方式:

  1. 精确查询"select:Read,Edit,Grep" ------ 按名称直接获取
  2. 关键词搜索"notebook jupyter" ------ 匹配工具描述,返回最相关的工具

搜索结果以 tool_reference 块的形式返回。每个匹配的工具包含完整的 JSONSchema 定义:

json 复制代码
{
  "type": "tool_reference",
  "tool": {
    "name": "mcp__slack__send_message",
    "description": "send a message to a Slack channel or user",
    "input_schema": {
      "type": "object",
      "properties": {
        "channel": { "type": "string", "description": "Channel ID or name" },
        "text": { "type": "string", "description": "Message text" }
      },
      "required": ["channel", "text"]
    }
  }
}

API 层会自动将 tool_reference 展开为完整的工具定义。展开后,工具就像从未被延迟过一样可以正常调用。

完整流程


模型缓存:defer_loading 的隐性问题

defer_loading 解决了上下文窗口膨胀的问题,但它还意外地解决了另一个更隐蔽的问题。

模型的 prompt cache 以 system prompt 的前缀匹配为基础。如果工具列表在两次请求之间发生变化------比如 MCP 服务器重连后工具数量变了------prompt 就会变化,缓存就会失效。缓存失效意味着更多的 token 需要重新计算,API 调用成本和延迟都会大量上升。

针对这个问题,claude 模型 API 支持传入 defer_loading 的工具:它们不参与 KV Cache 的 key 计算。API 侧会将 defer_loading 的工具从 prompt 中剥离,因此它们不影响实际的缓存 key。即使 MCP 工具集合在会话中不断变化,已有的缓存依然有效。

为什么要将工具 Schema 传给模型 API 接口,而不是通过 Prompt 让模型生成工具调用?

这里有一个容易混淆的点:Schema 是给 API 用的,不是给模型看的。

如果只是让模型"知道有哪些工具、怎么调用",prompt 确实够了,但 Schema 的作用不止于此:

  • API 层的结构化输出 :Claude API 的 tools 参数会改变模型的输出格式。传入工具后,模型返回的不是自由文本,而是结构化的 tool_use 块------包含工具名和 JSON 格式的输入参数。这是 API 级别的能力,不是 prompt 能模拟的。
  • 输入校验:API 侧会根据 Schema 校验模型的输入------类型是否正确、必填字段是否缺失、是否符合 enum 约束。校验失败时 API 会自动修正或拒绝,而不是把错误参数传给工具执行层。
  • 可靠性保证:没有 Schema,模型可能幻觉出不存在的参数、用错类型、生成格式错误的 JSON。有了 Schema,这些错误在 API 层就被拦截了。

所以 Schema 和 Prompt 解决的是不同层面的问题:Prompt 告诉模型"有什么、做什么",Schema 告诉 API"怎么验证、怎么格式化"。

如果模型不支持呢

defer_loading 需要模型侧配合------它是一个 API 级别的特性,不是所有模型都能处理。Claude Code 在构建 API 请求前会检查当前模型是否支持:

typescript 复制代码
const toolSearchEnabled = isToolSearchEnabled(options.model, ...)

不支持的模型无法使用 defer_loading,所有工具 schema 只能内联发送。支持的模型还需要在请求头中附加 beta 标志。

这就引出了一个实际问题:延迟行为应该一刀切吗?Claude Code 提供了三种 ToolSearchMode 策略:

模式 行为 适用场景
tst(默认) 始终延迟 MCP 工具和 shouldDefer 工具 MCP 工具较多的场景
tst-auto 仅当延迟工具的 token 超过上下文窗口 10% 时启用 MCP 工具较少时自动内联
standard 不延迟,所有工具 schema 内联发送 模型不支持或工具数量很少

tst-auto 是一个自适应策略:MCP 工具较少时内联发送(省去 ToolSearch 的额外 round trip),数量增长到一定程度自动切换到延迟模式。standard 则是完全不延迟------当模型 不支持 defer_loading 时,这是唯一的选项。

如果模型没有 defer_loading API,要怎么实现工具延迟加载?

可以,思路是用一个通用的代理工具做中转。

注册一个 CallTool,它的 Schema 只有两个字段:tool_nameargs。所有动态工具都不直接注册到 API,而是通过 CallTool 间接调用。模型先通过 ToolSearch 获取目标工具的完整参数定义,然后把参数填进 CallToolargs 字段,由 CallTool 转发给实际工具执行。

bash 复制代码
模型 → ToolSearch("slack send_message") → 获取参数 Schema
模型 → CallTool({ tool_name: "slack_send_message", args: { channel: "#general", text: "hello" } })
     → 内部路由到实际工具执行

这个方案虽然可行,但有一个明显的弊端:失去了 API 层的 Schema 强校验CallToolargs 是一个通用的 JSON 对象,API 无法校验里面的结构是否符合目标工具的定义。模型生成的参数是否正确,完全依赖模型自身的能力 ------ 类型错误、缺少必填字段、幻觉出不存在的参数,都不会在 API 层被拦截。

相比之下,defer_loading 的方案让 API 在工具被调用时拥有完整的 Schema 定义,校验是自动的,当然其问题就在于延迟加载工具需要重复计算缓存了,各有利弊。


MCP 工具:延迟加载的主要受益者

MCP 工具是 defer_loading 存在的核心原因。MCP 工具与内置工具有一个根本区别:数量不可预测

内置工具是 Claude Code 开发团队控制的,数量稳定在 40 个左右,schema 经过优化,token 占用可控,且部分为异步加载。

MCP 工具则来自外部服务器------用户可以配置任意数量的 MCP 服务器,每个服务器可以提供任意数量的工具。

typescript 复制代码
return result.tools.map((tool): Tool => ({
  ...MCPTool,                                // 骨架模板
  name: `mcp__${serverName}__${tool.name}`,  // 命名空间隔离
  isMcp: true,                               // 标记为 MCP → 默认延迟
  alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
  inputJSONSchema: tool.inputSchema,         // 直接使用 JSON Schema

  async call(args, context, ...) {
    const connectedClient = await ensureConnectedClient(client)
    return await callMCPTool(connectedClient, tool.name, args)
  },
}))

isMcp: true 标记让这些工具在 isDeferredTool() 判定中默认返回 true

总结

Claude Code 的 defer_loading 机制解决了工具数量增长时的上下文窗口膨胀问题:

  1. MCP 工具默认被延迟 ------ 只发送名称到 API,不发送完整 schema
  2. 模型通过 ToolSearchTool 按需发现工具 ------ 精确查询或关键词搜索
  3. 搜索结果返回 tool_reference 块 ------ API 自动展开为完整工具定义
  4. 展开后的工具可以正常调用 ------ 对模型来说没有区别
  5. defer_loading 的工具不参与 KV Cache 计算 ------ 工具变化不会导致缓存失效

这套机制让 Claude Code 能够支持几乎无限数量的 MCP 工具,同时保持上下文窗口的高效利用。

如果你觉得这篇文章有帮助,欢迎点赞、收藏,也可以关注我

相关推荐
葫芦和十三1 小时前
执行拓扑|Agent 不只是会什么,还要怎么跑
架构·agent·ai编程
国科安芯2 小时前
国科安芯推出商业航天级抗辐照半双工 RS485 收发器 ASC485S2Y
前端·单片机·嵌入式硬件·架构·安全性测试
小小龙学IT2 小时前
Go 后端开发实战:从单机千QPS到十万级微服务架构的演进之路
微服务·架构·golang
java_cj2 小时前
Caffeine+Redis两级缓存架构实战:从手动实现到自定义注解的完整方案
缓存·架构
kcuwu.4 小时前
Claw Code 项目架构万字解读
人工智能·架构
Rain5094 小时前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·人工智能·react.js·ui·架构·前端框架·ai编程
愚公搬代码4 小时前
【愚公系列】《移动端AI应用开发》014-DeepSeek API开发与集成(处理多轮对话与动态请求)
人工智能·中间件·架构
2603_954708314 小时前
微电网协调控制系统柜的应用场景有哪些?
分布式·安全·架构·能源·需求分析
LONGZETECH4 小时前
汽车仿真教学软件技术实现深度解析:从三维建模到学情数据闭环
c语言·3d·unity·架构·汽车