Claude Code 工具调用机制详解

掘金文章对 Claude Code 中 tool_use 的完整执行链路做源码级拆解:从 queryLoop 判断是否需要 follow-up,到工具分批调度、权限检查、Hook、执行、结果映射、后台任务通知、取消机制,再到 Deferred Tools 的规模化加载设计。

基本信息

核心观点

  1. Claude Code 的 agentic turn 由 queryLoop 驱动,是否继续循环只看本轮 assistant 消息里是否出现 tool_usequery() 把控制权交给 queryLoop,后者在每轮执行“调用模型 → 判断 tool_use → 执行工具 → 决定继续或结束”。核心开关是 needsFollowUp:没有工具调用就进入收尾判断,有工具调用就执行工具并把结果喂回下一轮。

  2. 工具调用不是简单 Promise.all,而是先按并发安全性分批,再在批次内并发或串行执行partitionToolCalls 会先解析每个工具 input,再调用工具自己的 isConcurrencySafe(parsedInput.data)。只有“连续出现且都声明并发安全”的工具会合并进同一批;一旦 input 解析失败或并发判定抛错,就保守当作不可并发。批次之间严格串行,批次内并发默认上限为 10,可由 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 调整。

  3. 单个工具调用生命周期由 runToolUse 串起:PreToolUse hooks → 权限决策 → 工具执行 → 结果映射 → PostToolUse hooks → 收尾信号。Hook 可以提前 stop、改写 input、给出权限建议或标记 preventContinuation;权限层会先处理硬 deny/ask,再看工具自身 checkPermissions,最后才考虑 bypass 或 always allow;工具结果会映射成 tool_result,必要时持久化到文件,再追加 Hook 附件消息或 hook_stopped_continuation

  4. Hook allow 不是越权放行,deny/ask 规则和工具自身安全检查仍能覆盖它。文章特别强调 resolveHookPermissionDecision 的语义:Hook 返回 allow 只表示跳过交互确认,但还要跑规则检查;如果命中 deny、ask 或工具自己的安全检查,仍会阻止执行。这解释了 Claude Code 工具调用里“自动化”和“权限护栏”的真实边界。

  5. 工具结果过大时走持久化预览,而不是简单截断。单个结果超过阈值(通常是工具声明上限与 50,000 字符的较小值)会写入 tool-results/{id}.json|txt,返回给模型的是预览片段和文件路径;如果一条消息里多个工具结果合计超过约 200,000 字符,也会挑最大的新鲜结果持久化。这避免了并发工具同时返回大结果时撑爆上下文。

  6. 后台任务不是阻塞等待,而是通过 task-notification 队列旁路回到对话。Bash、PowerShell、AgentTool 支持后台执行:调用方很快拿到 backgroundTaskId,命令或子 agent 继续跑。后台任务完成后塞进进程级队列;如果当时 queryLoop 仍在运行,会在本轮注入通知;如果已经空闲,REPL 组件的队列处理器会主动发起一轮新 query。

  7. 中断与取消依赖 Abort Controller 的父子层级和反向冒泡,默认大多数工具不可被用户中途砍掉。工具调用有 query 级根 controller、批次级 sibling controller、单工具 controller 三层。父级 abort 会级联到子级;子级 abort 也会在非 sibling_error 情况下冒泡到根级,修复权限拒绝类场景无法终止整轮的问题。工具级 interruptBehavior 默认是 block,目前文章核对到唯一声明 cancel 的是 SleepTool。

  8. Deferred Tools 解决 MCP 工具过多导致上下文膨胀的问题,本质是“客户端先过滤,模型按需搜索,再在下一轮加载完整工具 schema”。工具是否被延迟由客户端本地判定和历史扫描决定:未被发现的 deferred 工具不会进入本次请求;模型先调用 SearchExtraToolsTool 搜索,结果以文本形式写入对话;下一次请求构造时,客户端从历史中解析出已发现工具名,再把对应完整定义加入工具列表。

  9. 这篇文章的价值在于把“Claude Code 能调用工具”拆成了一个可工程化理解的运行时系统。它不是把模型能力神秘化,而是把调度、权限、Hook、结果存储、后台任务、取消和工具规模化加载逐层拆开,让开发者能判断一个 Agent harness 是否具备生产级可控性。

实操内容保留

并发分批核心逻辑

原文展示 partitionToolCalls 的关键代码,体现“并发安全由工具显式声明、失败时保守串行”的原则:

return toolUseMessages.reduce((acc: Batch[], toolUse) => {
  const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
  const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
  const isConcurrencySafe = parsedInput?.success
    ? (() => {
        try {
          return Boolean(tool?.isConcurrencySafe(parsedInput.data))
        } catch {
          return false   // 抛错时保守当作不可并发
        }
      })()
    : false
  if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
    acc[acc.length - 1]!.blocks.push(toolUse)   // 连续可并发的合并进同一批
  } else {
    acc.push({ isConcurrencySafe, blocks: [toolUse] })   // 否则单独成批
  }
  return acc
}, [])

批次内并发与串行调度

let currentContext = toolUseContext
for (const { isConcurrencySafe, blocks } of partitionToolCalls(toolUseMessages, currentContext)) {
  if (isConcurrencySafe) {
    // 并发批次:runToolsConcurrently
    for await (const update of runToolsConcurrently(blocks, assistantMessages, canUseTool, currentContext)) {
      yield { message: update.message, newContext: currentContext }
    }
  } else {
    // 串行批次:runToolsSerially
    for await (const update of runToolsSerially(blocks, assistantMessages, canUseTool, currentContext)) {
      yield { message: update.message, newContext: currentContext }
    }
  }
}

权限决策顺序

原文把 hasPermissionsToUseToolInner 简化为三段:先硬边界,再宽松放行,最后兜底 ask。关键点是 bypass 只在硬 deny/ask 和工具自身安全检查之后才生效。

// 1. 硬边界检查:命中即 return,即便 bypass 模式也拦截
const denyRule = getDenyRuleForTool(ctx, tool)
if (denyRule) return { behavior: 'deny', ... }
 
const askRule = getAskRuleForTool(ctx, tool)
if (askRule && !canSandboxAutoAllow) return { behavior: 'ask', ... }
 
const toolPermissionResult = await tool.checkPermissions(input, context)
if (toolPermissionResult.behavior === 'deny') return toolPermissionResult
if (tool.requiresUserInteraction?.() && toolPermissionResult.behavior === 'ask') return toolPermissionResult
if (toolPermissionResult.behavior === 'ask' && toolPermissionResult.decisionReason?.rule?.ruleBehavior === 'ask') return toolPermissionResult
if (toolPermissionResult.behavior === 'ask' && toolPermissionResult.decisionReason?.type === 'safetyCheck') return toolPermissionResult
 
// 2. 宽松放行:只有上面全部没命中,才会执行到这里
if (shouldBypassPermissions) return { behavior: 'allow', decisionReason: { type: 'mode' } }
if (toolAlwaysAllowedRule(ctx, tool)) return { behavior: 'allow', decisionReason: { type: 'rule' } }
 
// 3. 兜底
return { ...toolPermissionResult, behavior: 'ask' }

后台任务触发条件

if (run_in_background === true && !isBackgroundTasksDisabled) {
  const shellId = await spawnBackgroundTask()
  return { stdout: '', stderr: '', code: 0, interrupted: false, backgroundTaskId: shellId }
}

AgentTool 的后台条件不止显式参数:

run_in_background === true || selectedAgent.background === true || isCoordinator || forceAsync

Deferred Tools 模式配置

ENABLE_SEARCH_EXTRA_TOOLS    模式
auto / auto:1-99             tst-auto
true / auto:0                tst
false / auto:100             standard
(未设置)                      tst   ← 默认值

关键概念

  • Claude Code 工具调用机制:Claude Code 内部从 tool_use 出现到工具结果回传的完整运行时机制,包括调度、权限、Hook、执行、结果存储、后台任务和取消。
  • Deferred Tools:当 MCP/外部工具数量过多时,将工具 schema 延迟加载的机制,先让模型搜索工具,再在后续请求中加载完整定义。
  • Function Calling:更通用的模型工具调用机制,Claude Code 的 tool_use 可以看作 Agent harness 中的一种具体实现。
  • MCP 模型上下文协议:Deferred Tools 主要缓解 MCP 工具过多时的上下文占用,MCP 负责标准化外部能力接入。
  • AI Agent 智能体:本文提供了 Agent “观察-行动-再观察”循环中“行动”部分的工程级实现剖面。

与其他素材的关联

原文精彩摘录

queryLoopwhile (true) 驱动一次 agentic turn:每次迭代做“调用模型 → 判断要不要执行工具 → (执行工具)→ 决定继续循环还是结束”。核心判断点只有一个变量:needsFollowUp ——本轮 assistant 消息里是否出现了 tool_use 内容块。

批次之间严格串行——外层是一个普通 for...of 循环,必须等上一批(不管并发还是串行)完全跑完,才会开始下一批。

Hook 返回 allow 不是直接放行,还会再跑一次 checkRuleBasedPermissions——如果规则检查出 deny 或“内容级 ask”,会覆盖 hook 的 allow;只有规则检查也放行,才真正跳过用户确认。

Deferred tools 机制让这些工具先只以“名字”轻量出现,模型需要时再主动检索、展开完整 schema。关键点:deferred 工具“该不该出现在这次请求里”,是客户端本地过滤决定的,不是服务端决定的。

相关页面