工具并行执行 Deep-Dive

May 22, 2026 · View on GitHub

当模型一次返回多个工具调用时,Agent 如何执行它们?串行逐个执行还是智能并行?本文基于 Claude Code(v2.1.89 反编译)和 Qwen Code(v0.16.0 开源)的源码分析,对比两者在工具执行并发模型、依赖分析和流式处理方面的架构差异。


1. 问题定义

现代 LLM 可在一次响应中返回多个 tool_use block。例如模型可能同时请求:

tool_use: Read("src/main.ts")
tool_use: Read("src/config.ts")  
tool_use: Grep("TODO", "src/")
tool_use: Bash("npm test")

前三个是只读操作,可安全并行;第四个可能依赖前三个结果。Agent 如何处理这种混合场景?


2. Claude Code:智能分批 + 流式执行

2.1 并发配置

// 源码: services/tools/toolOrchestration.ts#L8-L12
function getMaxToolUseConcurrency(): number {
  return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10
}
// 默认最大 10 个工具并发,可通过环境变量调整

2.2 分批算法

Claude Code 将工具调用分为连续的批次,每批要么全部并行,要么单独串行:

// 源码: services/tools/toolOrchestration.ts#L91-L116
function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {
  return toolUseMessages.reduce((acc, toolUse) => {
    const isConcurrencySafe = tool?.isConcurrencySafe(parsedInput.data) ?? false
    // fail-closed: 解析失败时视为不安全
    
    if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
      acc[acc.length - 1].blocks.push(toolUse)  // 追加到当前并行批次
    } else {
      acc.push({ isConcurrencySafe, blocks: [toolUse] })  // 新批次
    }
    return acc
  }, [])
}

分批规则

  • 连续的并发安全工具 → 合并为一个并行批次
  • 遇到非并发安全工具 → 独立为一个串行批次
  • 非并发安全工具后的并发安全工具 → 新的并行批次

示例

输入: [Read, Read, Grep, Edit, Read, Read]
分批: [Read, Read, Grep]  →  [Edit]  →  [Read, Read]
       ↑ 并行批次(3个)    ↑ 串行      ↑ 并行批次(2个)

2.3 isConcurrencySafe() 分类

每个工具定义自己是否并发安全:

// 源码: Tool.ts#L402, L759
// 默认实现(fail-closed)
isConcurrencySafe: (_input?: unknown) => false
工具并发安全原因
FileReadTool纯读取
GlobTool纯读取
GrepTool纯读取
WebFetchTool无副作用
WebSearchTool无副作用
BashTool⚠️ 条件仅当命令被判定为只读时
FileEditTool文件修改
FileWriteTool文件写入
AgentTool子进程副作用
其他默认不安全

2.4 并行执行路径

// 源码: services/tools/toolOrchestration.ts#L152-L177
async function* runToolsConcurrently(toolUseMessages, ...): AsyncGenerator<MessageUpdate> {
  yield* all(
    toolUseMessages.map(async function* (toolUse) {
      // 标记为执行中
      toolUseContext.setInProgressToolUseIDs(prev => new Set(prev).add(toolUse.id))
      // 执行工具
      yield* runToolUse(toolUse, ...)
      // 标记完成
      markToolUseAsComplete(toolUseContext, toolUse.id)
    }),
    getMaxToolUseConcurrency(),  // 并发上限: 10
  )
}

all() 是自定义的并发 AsyncGenerator 合并器,限制最大并发数。

2.5 上下文修改队列(防竞态)

并行执行的工具可能修改共享上下文(如文件状态缓存)。Claude Code 使用队列化策略:

// 源码: services/tools/toolOrchestration.ts#L31-L62
// 并行批次:上下文修改先队列化,批次结束后按工具顺序串行应用
const queuedContextModifiers: Record<string, Function[]> = {}
for await (const update of runToolsConcurrently(...)) {
  if (update.contextModifier) {
    queuedContextModifiers[toolUseID].push(modifyContext)
  }
}
// 批次完成后:
for (const block of blocks) {
  for (const modifier of queuedContextModifiers[block.id] ?? []) {
    currentContext = modifier(currentContext)  // 按工具顺序串行应用
  }
}
// 源码: services/tools/toolOrchestration.ts#L118-L150
// 串行批次:上下文修改立即应用
for (const toolUse of toolUseMessages) {
  for await (const update of runToolUse(toolUse, ..., currentContext)) {
    if (update.contextModifier) {
      currentContext = update.contextModifier.modifyContext(currentContext)  // 立即
    }
  }
}

2.6 StreamingToolExecutor(流式路径)

config.gates.streamingToolExecution 开启时,工具在 API 响应流式到达时就开始执行,无需等待完整响应:

// 源码: services/tools/StreamingToolExecutor.ts#L129-L150
private canExecuteTool(isConcurrencySafe: boolean): boolean {
  const executingTools = this.tools.filter(t => t.status === 'executing')
  return (
    executingTools.length === 0 ||                              // 无工具执行中
    (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))  // 都是安全的
  )
}

工具状态机

queued → executing → completed → yielded

Bash 错误级联

// 源码: StreamingToolExecutor.ts#L359-L363
if (tool.block.name === BASH_TOOL_NAME) {
  this.hasErrored = true
  this.siblingAbortController.abort('sibling_error')
  // Bash 失败时取消同批次其他工具(隐式依赖假设)
}

仅 Bash 工具的错误会级联取消兄弟工具——因为 Bash 命令常有隐式依赖(如 mkdir 失败后续命令无意义)。

2.7 查询循环集成

// 源码: query.ts#L561-L568, L1366-L1382
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()  // 流式路径
  : runTools(toolUseBlocks, ...)                  // 非流式路径(partitionToolCalls)

for await (const update of toolUpdates) {
  yield update.message       // 逐条 yield 结果
  toolResults.push(...)      // 收集
}

3. Qwen Code:智能分批并行(v0.16.0 架构升级)

重要变更:v0.16.0 的 coreToolScheduler.ts 彻底重构了并发模型,从 v0.15.0 的"类型分流(Agent 并发 / 其他顺序)"升级为 Claude Code 同款的 isConcurrencySafe + partitionToolCalls 分批并行。以下分析基于 v0.16.0 源码。

3.1 执行模型(v0.16.0)

// 源码: qwen-code/packages/core/src/core/coreToolScheduler.ts(3,284 行)
// 分批算法与 Claude Code 高度一致:连续并发安全工具合并为并行批次,
// 每个不安全工具独立为串行批次

const batches = partitionToolCalls(callsToExecute);
for (const batch of batches) {
  if (batch.concurrent && batch.calls.length > 1) {
    await this.runConcurrently(batch.calls, signal);  // 并行执行
  } else {
    for (const call of batch.calls) {
      await this.executeSingleToolCall(call, signal);  // 串行执行
    }
  }
}

3.2 并发安全判定(isConcurrencySafe

// 源码: coreToolScheduler.ts#L747-L764
function isConcurrencySafe(call: ScheduledToolCall): boolean {
  // Agent 工具(独立子代理,无共享状态)→ 并发安全
  if (canonicalToolName(call.request.name) === ToolNames.AGENT) return true;
  // Shell 命令:使用同步 regex checker(isShellCommandReadOnly)判断只读 → 安全
  // 注意:这里用的是同步 shellReadOnlyChecker,而非异步 AST 版本,
  // 因为 partitionToolCalls 同步执行。AST 版本仅用于权限决策。
  if (call.tool.kind === Kind.Execute) {
    const command = (call.request.args as { command?: string }).command;
    try { return isShellCommandReadOnly(stripShellWrapper(command)) }
    catch { return false; }  // fail-closed
  }
  // Read、Search、Fetch 类型工具 → 并发安全
  return CONCURRENCY_SAFE_KINDS.has(call.tool.kind);
}

CONCURRENCY_SAFE_KINDS(源码: tools.ts):

export const CONCURRENCY_SAFE_KINDS: ReadonlySet<Kind> = new Set([
  Kind.Read,    // 读文件、ReadFile
  Kind.Search,  // Grep、Glob、LS
  Kind.Fetch,   // WebFetch
]);

3.3 分批算法(partitionToolCalls

// 源码: coreToolScheduler.ts#L775-L784
function partitionToolCalls(calls: ScheduledToolCall[]): ToolBatch[] {
  return calls.reduce<ToolBatch[]>((batches, call) => {
    const safe = isConcurrencySafe(call);
    const lastBatch = batches[batches.length - 1];
    if (safe && lastBatch?.concurrent) {
      lastBatch.calls.push(call);   // 追加到当前并行批次
    } else {
      batches.push({ concurrent: safe, calls: [call] });  // 新批次
    }
    return batches;
  }, []);
}

示例(与 Claude Code 分批逻辑相同):

输入: [Read, Read, Grep, Edit, Read, Read]
分批: [Read, Read, Grep]  →  [Edit]  →  [Read, Read]
       ↑ 并行批次(3个)    ↑ 串行      ↑ 并行批次(2个)

3.4 并发执行(runConcurrently

// 源码: coreToolScheduler.ts#L473-L493
private async runConcurrently(calls: ScheduledToolCall[], signal: AbortSignal): Promise<void> {
  const parsed = parseInt(process.env['QWEN_CODE_MAX_TOOL_CONCURRENCY'] || '', 10);
  const maxConcurrency = Number.isFinite(parsed) && parsed >= 1 ? parsed : 10;
  // 信号量限流:最大 10 个并发(可通过 QWEN_CODE_MAX_TOOL_CONCURRENCY 调整)
  const executing = new Set<Promise<void>>();
  for (const call of calls) {
    const p = this.executeSingleToolCall(call, signal).finally(() => executing.delete(p));
    executing.add(p);
    if (executing.size >= maxConcurrency) await Promise.race(executing);
  }
  await Promise.all(executing);
}

3.5 工具状态机(7 状态)

validating → scheduled → awaiting_approval → executing → success / error / cancelled
// 源码: coreToolScheduler.ts
type Status = 'validating' | 'scheduled' | 'awaiting_approval'
             | 'executing' | 'success' | 'error' | 'cancelled'

3.6 权限流程

5 阶段权限评估(源码: coreToolScheduler.ts):

L3(Tool 默认) → L4(PermissionManager 策略) → L5(ApprovalMode/Auto mode) → Hooks → 非交互处理

v0.16.0 新增 Auto 模式(#4151):LLM 分类器自动评估工具调用风险,低风险自动批准,减少打断。

3.7 无流式工具执行

Qwen Code 等待完整 API 响应后才开始工具执行,不支持流式到达时即执行(Claude Code 的 StreamingToolExecutor 特性)。


4. 逐维度对比

维度Claude CodeQwen Code v0.16.0
并发模型isConcurrencySafe() 智能分批isConcurrencySafe() 智能分批(v0.16.0 升级,与 Claude Code 同款)
默认并发上限10(CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY10(QWEN_CODE_MAX_TOOL_CONCURRENCY
安全判定每个工具实现 isConcurrencySafe(input)(含动态参数判断)isConcurrencySafe() 基于 Kind(Read/Search/Fetch)+ Agent + Shell 只读检测
分批策略连续并发安全工具合并为一批同左(partitionToolCalls 逻辑相同)
Shell 并发✅ 只读 Bash 可并行(AST 检测)✅ 只读 Shell 可并行(同步 regex checker)
流式执行✅ 工具在 API 响应流到达时开始执行❌ 等待完整响应
上下文修改并行时队列化,批次后串行应用顺序执行(各 call 独立,无全局队列化机制)
错误级联Bash 错误取消同批次兄弟无级联,各工具独立
工具状态机4 状态(queued/executing/completed/yielded)7 状态(含 validating/awaiting_approval)
进度显示并行工具各自独立显示进度并行工具同时显示

5. 性能影响

5.1 典型场景对比

场景Claude CodeQwen Code v0.16.0
模型返回 5 个 Read 调用并行执行,~1× 延迟并行执行,~1× 延迟
模型返回 3 个 Read + 1 个 Edit + 2 个 Read批次 1: 3×Read 并行 → 批次 2: Edit 串行 → 批次 3: 2×Read 并行同左(分批逻辑相同)
模型返回 2 个 Agent 调用并行(Agent 工具 isConcurrencySafe 返回 true)并行(Agent 工具并发安全)
模型返回 1 个 Bash(非只读)+ 1 个 ReadBash 串行 → Read 串行Bash 串行 → Read 串行
模型返回 1 个 Bash(只读,如 git log)+ 1 个 Read并行(Bash 只读安全)并行(Shell 只读安全)

5.2 大规模代码探索

v0.16.0 后,Qwen Code 与 Claude Code 在并行读取场景性能对等:

Claude Code: [Glob₁ + Glob₂ + Grep₁ + Grep₂ + Read₁ + Read₂ + Read₃]
             → 一个并行批次,~1× 延迟

Qwen Code v0.16.0: 同左(partitionToolCalls 合并为单个并行批次)
             → ~1× 延迟

仍存在的差距:流式工具执行(Claude Code 在 API 响应流到达时即执行,Qwen Code 需等完整响应)。


6. 关键源码文件

Claude Code

文件行数职责
services/tools/toolOrchestration.ts~189分批算法 + 并行/串行执行路径
services/tools/StreamingToolExecutor.ts~531流式工具执行 + 状态机 + Bash 错误级联
Tool.tsL402, L759isConcurrencySafe() 接口定义 + 默认实现
utils/generators.tsL32-L72all() 并发 AsyncGenerator 合并器
query.tsL561-L568流式 vs 非流式路径选择

Qwen Code

文件行数职责
packages/core/src/core/coreToolScheduler.ts3,284工具调度器(v0.16.0 全面重构:智能分批并行,与 Claude Code 同款)
packages/core/src/tools/tools.tsCONCURRENCY_SAFE_KINDS(Read/Search/Fetch)、isConcurrencySafe 辅助
packages/core/src/agents/runtime/agent-core.tsprocessFunctionCalls 调用调度器
packages/core/src/agents/runtime/agent-events.ts工具事件类型定义

7. 设计启示

  1. v0.16.0 Qwen Code 并发模型全面对齐 Claude Code:从 v0.15.0 的"Agent 并发 / 其他顺序"升级为基于 isConcurrencySafe + partitionToolCalls 的智能分批并行,并发上限同为 10,可通过 QWEN_CODE_MAX_TOOL_CONCURRENCY 调整
  2. 流式执行仍是 Claude Code 独有优势:在 API 响应流到达时即执行工具,不等完整响应,进一步重叠 I/O;Qwen Code 仍需等待完整响应
  3. 动态参数判断是 Claude Code 的额外精细度:Claude Code 的 isConcurrencySafe(input) 允许根据具体命令参数动态判断(如只读 Bash 命令),Qwen Code 的 Shell 判定依赖同步 regex checker,覆盖相同语义
  4. Bash 错误级联(Claude Code StreamingToolExecutor)是 Qwen Code 暂未移植的设计:Bash 失败后取消同批次兄弟工具,避免无意义执行

免责声明: 以上分析基于 2026 年 Q1 初稿,2026-05-22 对照 v0.16.0 复核。Claude Code v2.1.89;Qwen Code v0.16.0。实质变更:Qwen Code v0.16.0 的 coreToolScheduler.ts 从 1,710 行(v0.15.0 类型分流模型)重构为 3,284 行(v0.16.0 智能分批并行),并发模型已与 Claude Code 对齐,原文档关于"Qwen Code 顺序执行"的性能分析已过时。