Claude Code 的工具延迟加载机制
AI Agent 的工具调用依赖模型理解每个工具的参数 schema。传统做法是在 system prompt 中列出所有工具的完整定义——名称、描述、参数类型、参数说明。当工具数量在 10-20 个以内时这没有问题。但当工具数量增长到 50schema 的 token 成本就不可忽视了。Claude Code 的内置工具如 Bash、Read、Edit每个 schema 约 70-150 tokenMCP 工具的 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()。这个参数是一个数组每个元素包含工具的名称、描述和参数 schemaconstresultawaitanthropic.beta.messages.create({model:options.model,messages:messagesForAPI,system,tools:allTools,// 工具列表在这里传入tool_choice:options.toolChoice,max_tokens:maxOutputTokens,stream:true,});每个工具对象在传入 API 前会经过转换取出名称、描述和输入 schema组装成 API 需要的格式。最终allTools中每个元素的结构类似{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 的核心机制延迟判定谁被延迟谁不被延迟工具组装时每个工具都会经过一个判定函数functionisDeferredTool(tool:Tool):boolean{// alwaysLoad 优先——MCP 工具可以通过 _meta 设置此标志if(tool.alwaysLoadtrue)returnfalse;// MCP 工具默认延迟if(tool.isMcptrue)returntrue;// ToolSearch 自身不能被延迟——模型需要它来加载其他工具if(tool.nameTOOL_SEARCH_TOOL_NAME)returnfalse;// 内置工具通过 shouldDefer 显式标记returntool.shouldDefertrue;}判定规则MCP 工具默认被延迟——这是延迟加载存在的主要原因alwaysLoad: true的 MCP 工具跳过延迟——适用于高频使用的工具ToolSearchTool自身永远不延迟——它是模型发现其他工具的唯一入口内置工具默认不延迟——shouldDefer字段默认为false除非工具显式标记defer_loading 如何生效判定结果通过willDefer()函数传入toolToAPISchema()后者在工具的 API schema 上加上defer_loading: true标记consttoolSchemasawaitPromise.all(filteredTools.map(tooltoolToAPISchema(tool,{deferLoading:willDefer(tool),}),),);toolToAPISchema()的核心逻辑functiontoolToAPISchema(tool,options){constschema{name:base.name,description:base.description,input_schema:base.input_schema,};if(options.deferLoading){schema.defer_loadingtrue;}returnschema;}带defer_loading: true的工具在 API 侧的行为不同模型只能看到工具的名称和描述看不到完整的参数 schema。这正是延迟加载的核心——通过一个 API 字段控制工具 schema 的可见性。延迟后的工具在 prompt 中长什么样被延迟的工具不会从工具池中移除但发送给 API 时只携带名称不携带完整的 inputSchema。模型在 system prompt 中看到的是一个available-deferred-tools区域每个工具只有一行available-deferred-toolsmcp__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 — searchforJira issues using JQL/available-deferred-tools模型知道这些工具存在也知道它们大概做什么但不知道它们需要什么参数。这意味着模型无法直接调用它们——它必须先获取完整的 schema。ToolSearchTool按需加载的桥梁ToolSearchTool是整个延迟加载机制的关键枢纽。它是唯一一个始终加载的工具负责在模型需要时加载其他工具的完整 schema。它支持两种查询方式精确查询select:Read,Edit,Grep—— 按名称直接获取关键词搜索notebook jupyter—— 匹配工具描述返回最相关的工具搜索结果以tool_reference块的形式返回。每个匹配的工具包含完整的 JSONSchema 定义{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 请求前会检查当前模型是否支持consttoolSearchEnabledisToolSearchEnabled(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_name和args。所有动态工具都不直接注册到 API而是通过CallTool间接调用。模型先通过ToolSearch获取目标工具的完整参数定义然后把参数填进CallTool的args字段由CallTool转发给实际工具执行。模型 → ToolSearch(slack send_message)→ 获取参数 Schema 模型 → CallTool({tool_name:slack_send_message, args:{channel:#general, text:hello}})→ 内部路由到实际工具执行这个方案虽然可行但有一个明显的弊端失去了 API 层的 Schema 强校验。CallTool的args是一个通用的 JSON 对象API 无法校验里面的结构是否符合目标工具的定义。模型生成的参数是否正确完全依赖模型自身的能力 —— 类型错误、缺少必填字段、幻觉出不存在的参数都不会在 API 层被拦截。相比之下defer_loading的方案让 API 在工具被调用时拥有完整的 Schema 定义校验是自动的当然其问题就在于延迟加载工具需要重复计算缓存了各有利弊。MCP 工具延迟加载的主要受益者MCP 工具是 defer_loading 存在的核心原因。MCP 工具与内置工具有一个根本区别数量不可预测。内置工具是 Claude Code 开发团队控制的数量稳定在 40 个左右schema 经过优化token 占用可控且部分为异步加载。MCP 工具则来自外部服务器——用户可以配置任意数量的 MCP 服务器每个服务器可以提供任意数量的工具。returnresult.tools.map((tool):Tool({...MCPTool,// 骨架模板name:mcp__${serverName}__${tool.name},// 命名空间隔离isMcp:true,// 标记为 MCP → 默认延迟alwaysLoad:tool._meta?.[anthropic/alwaysLoad]true,inputJSONSchema:tool.inputSchema,// 直接使用 JSON Schemaasynccall(args,context,...){constconnectedClientawaitensureConnectedClient(client)returnawaitcallMCPTool(connectedClient,tool.name,args)},}))isMcp: true标记让这些工具在isDeferredTool()判定中默认返回true总结Claude Code 的 defer_loading 机制解决了工具数量增长时的上下文窗口膨胀问题MCP 工具默认被延迟 —— 只发送名称到 API不发送完整 schema模型通过 ToolSearchTool 按需发现工具 —— 精确查询或关键词搜索搜索结果返回tool_reference块 —— API 自动展开为完整工具定义展开后的工具可以正常调用 —— 对模型来说没有区别defer_loading 的工具不参与 KV Cache 计算 —— 工具变化不会导致缓存失效这套机制让 Claude Code 能够支持几乎无限数量的 MCP 工具同时保持上下文窗口的高效利用。如果你觉得这篇文章有帮助欢迎点赞、收藏也可以关注我。