# 工具为什么能在模型还没停止说话时就开始执行？

在传统的 AI 工具调用中，"等模型说完再动手"是默认假设。但 Claude Code 打破了这个假设——它让工具在模型还在输出的过程中就开始并行执行，将等待时间变成了工作时间。本章揭示 `StreamingToolExecutor` 和 `runTools` 批次调度背后的并发设计，以及它如何将多工具调用的延迟从串行累加变成并行重叠。

> 💡 **通俗理解**：就像餐厅厨师听到"先来一个汤"后，不等客人说完全部菜就开始烧汤了。

### 🌍 行业背景：AI 工具调用的并发执行现状

"流式解析 + 提前执行"并非全新概念，但在 AI 编程助手中的实现程度各不相同：

- **OpenAI Function Calling**：API 层面支持 `parallel_tool_calls` 参数（2023 年底引入），允许模型在一次响应中输出多个工具调用。但执行侧的并行化由客户端自行实现——OpenAI SDK 本身不提供流式提前执行能力。
- **Cursor**：工具执行采用"完整响应后批量执行"模式。由于 Cursor 的工具调用（代码编辑、终端命令）通常需要用户确认，提前执行的收益有限。
- **Aider**：工具调用是同步串行的——模型生成完整响应后，解析出编辑指令，逐个应用。没有流式并行机制。
- **LangChain**：`AgentExecutor` 默认串行执行工具调用；`LangGraph` 支持通过图结构定义并行节点，但需要开发者手动设计并行拓扑，不是自动的。
- **Codex（OpenAI）**：v0.118.0 版本引入并行 Agent 工作流和邮箱通信机制（Mailbox），支持多后台进程异步交互。底层已用 Rust 彻底重写（具体比例以官方为准），在并发性能上有先天优势，但其并行化更侧重于多 Agent 间的异步通信，而非单个 Agent 内的流式提前执行。

Claude Code 的 `StreamingToolExecutor` 做了两层优化：(1) 流式解析阶段即执行——不等完整响应；(2) 按并发安全性自动分批——只读工具并行、写操作串行。这种**流式 + 安全分批**的组合在 AI 编程工具中属于较精细的实现。OpenAI 的 `parallel_tool_calls` 解决了模型侧的并行输出，Claude Code 则在客户端侧进一步压缩了执行延迟。

---

## 问题

通常我们理解的 AI 工具调用顺序是：模型生成完整响应 → 发现里面有工具调用 → 执行工具 → 把结果发回给模型。但 Claude Code 有个设计叫 `StreamingToolExecutor`，它让工具在模型还在输出的过程中就开始执行了。这是怎么做到的，有什么意义？

---

## 你可能以为……

你可能以为这需要某种复杂的流式协议，或者需要修改 Anthropic API 的调用方式。其实原理很直接，而且它的应用比你想象的更有价值。

---

## 实际上是这样的

### 关键洞察：tool_use block 可以提前出现

当 Claude 要使用工具时，它会在响应里生成一个 `tool_use` block，格式大概是：

```json
{
  "type": "tool_use",
  "id": "tool_abc123",
  "name": "Read",
  "input": {"file_path": "src/main.tsx"}
}
```

在 streaming 模式下，这个 block 的内容是增量到来的——先是 `type`，然后 `name`，然后 `input` 的各个字段。**当 `input` 完整到来时，我们就拥有了执行这个工具所需的全部信息。**

而此时，模型可能还在继续输出——也许它还在解释自己为什么要读这个文件，或者还有另一个工具调用在后面。

### StreamingToolExecutor 的设计

`StreamingToolExecutor` 做的事很简单：

1. 模型每产出一个完整的 `tool_use` block，立即调用 `addTool(block)`
2. 工具立即进入执行队列，如果是并发安全的工具（只读操作），立即开始运行
3. 模型继续输出……
4. 当模型完全停止输出时，大多数工具可能已经完成执行了
5. 调用 `getCompletedResults()` 收集所有结果

**关键细节：** 非并发安全工具按原始调用顺序 yield（即使下游已完成也需等待前序写操作完成）；并发安全工具则按完成顺序可提前 yield，不会阻塞已完成的兄弟。这样既保证了写操作的因果顺序，又不浪费已完成的只读工具的时效。

### 有多大收益？

> **[图表预留 2.4-A]**：对比时序图——左侧"传统串行"vs 右侧"StreamingToolExecutor 并行"，直观展示三个 Read 工具从 150ms → 50ms 的收益

举个典型场景：模型在一次响应中调用了 3 个 `Read` 工具，分别读取三个文件：

**传统路径：**
```
模型输出 Read(a) → 模型输出 Read(b) → 模型输出 Read(c) → 模型停止
→ 读 a（50ms）→ 读 b（50ms）→ 读 c（50ms）
总工具等待时间：150ms（串行）
```

**StreamingToolExecutor：**
```
模型输出 Read(a) → 立即开始读 a（50ms）
模型输出 Read(b) → 立即开始读 b（50ms，与 a 并行）
模型输出 Read(c) → 立即开始读 c（50ms，与 a/b 并行）
模型停止
→ 三个文件基本同时读完（50ms）
总工具等待时间：50ms（并行）
```

当工具都是 I/O 操作（文件读取、网络请求、Bash 命令）时，这种并行化效果显著。

> 📚 **课程关联 · 计算机体系结构**：这正是体系结构课程中 **CPU 流水线**（instruction pipeline）思想的软件实现。经典 5 级流水线（IF→ID→EX→MEM→WB）让多条指令在不同阶段重叠执行，把吞吐量提升数倍。StreamingToolExecutor 借用了同一种思路：模型输出近似"取指"（IF），JSON 解析近似"译码"（ID），工具执行近似"执行"（EX）——多个工具调用在不同阶段同时推进。`siblingAbortController`（一个出错则终止兄弟）的**效果**与流水线 **冲刷**（pipeline flush）相似——当前置步骤失败时，清空已入流水线的后续步骤；但注意这是一种比喻：CPU 流水线 flush 针对的是分支预测误判，siblingAbort 针对的是工具错误传播，触发条件并不相同。

---

## 更广泛的并发设计：runTools 的批次调度

即使不用 StreamingToolExecutor，`runTools()` 也会对工具做并发优化：

**按并发安全性分批：**
```
假设 AI 调用了：Read, Grep, Read, Edit, Bash(写)
→ 批次 1（并发）：Read, Grep, Read   ← 只读，可以一起跑
→ 批次 2（串行）：Edit               ← 写操作，单独跑
→ 批次 3（串行）：Bash(写)           ← 写操作，单独跑
```

每个工具通过 `isConcurrencySafe(input)` 方法逐次声明对当前输入是否支持并发——**Bash 并非一刀切串行**：`BashTool.isConcurrencySafe(input)` 会基于命令本身的只读性（`isReadOnly(input)`）返回 true/false，因此"只读 Bash 命令"也可以进入并发批次。只读工具通常返回 true，写操作工具返回 false。系统最多同时运行的并发工具数由 `getMaxToolUseConcurrency()` 决定（默认值可通过环境变量 `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` 调整；具体变量名以源码为准）。

> 📚 **课程关联 · 数据库 / 操作系统**：这种"只读可并发、写操作串行"的分批策略，与数据库课程中的**读写锁**（readers-writer lock）原理一致——多个读者可以同时访问共享资源，但写者需要独占访问。`isConcurrencySafe()` 本质上就是每个工具声明自己是"读者"还是"写者"。这也与操作系统中的信号量（semaphore）机制呼应——最大并发数 10 就是一个计数信号量的初始值。

### 一个意外的细节：Bash 出错时会杀掉兄弟进程

当多个**只读** Bash 工具被并发执行（比如 AI 同时运行了三个 `ls` / `cat` / `grep` 等只读命令，触发 `BashTool.isConcurrencySafe(input) === true`），如果其中一个出错，`StreamingToolExecutor` 会立即通过 `siblingAbortController` 中止其他正在运行的工具。

逻辑是：如果一步出错了，后续步骤大概率也是错的——与其让它们继续跑完、浪费资源，不如立即停下来，让 AI 看到错误信息后决定如何处理。

---

## 这个设计背后的取舍

**优势很明显：** 在工具多为只读 I/O、且最长单工具耗时 ≤ 模型输出耗时的理想情况下，整体延迟接近"模型输出时间"；一旦有工具耗时超过模型输出时长（例如慢网络请求、大文件扫描），或出现写工具被迫串行，总延迟仍会由最长的那条路径决定——所以"延迟 = 模型输出时间"是上界近似而非恒等式。

**但也有代价：**

系统需要在工具还在执行时继续消费模型的输出流。如果模型的后续输出依赖这些工具的结果（这在一次响应中调用多个相互依赖的工具时可能发生），就需要额外的协调。好在模型通常会在一次响应中提前规划好所有独立的并行工具调用，真正有依赖关系的工具会被拆到不同的对话轮次里。

另一个代价是：当流式回退（fallback）发生时（主模型失败，切换到备用模型），所有已经开始执行的工具结果都要被丢弃（`discard()` 方法），防止孤儿 `tool_result` 干扰下一次请求。这需要在重试逻辑里小心处理。

---

## 从这里能学到什么

**在任何有"等待时间"的流水线里，寻找可以在这段等待时间内完成的工作，然后把它们并行化。**

Claude Code 的工具执行并行化利用了两个"隐性等待窗口"：
1. 模型正在输出但工具参数已完整：在这个窗口内启动工具
2. 模型输出多个独立工具调用：所有**并发安全**的工具在最大并发上限内同时执行，非并发安全工具串行

这和 CPU 的流水线执行、Web 应用的请求预加载、数据库的批量 I/O 是同一个优化思路：**找到等待，在等待里藏工作。**

---

## 代码落点

- `src/services/tools/StreamingToolExecutor.ts`：完整实现，关注 `addTool()`、`getCompletedResults()`、`siblingAbortController`
- `src/services/tools/toolOrchestration.ts`：工具调用分组与批次执行逻辑（无独立 `processToolCallGroup.ts` 文件）
- `src/services/tools/toolOrchestration.ts`，`partitionToolCalls()` 函数（第 90 行起）：批次划分逻辑
- `src/services/tools/toolOrchestration.ts`，`getMaxToolUseConcurrency()`（第 8 行）：最大并发数配置，默认 10，可由 `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` 环境变量覆写
- `src/query.ts`，第 561-568 行：StreamingToolExecutor 的初始化条件（feature gate `streamingToolExecution`）
- `src/query.ts`，第 1380-1408 行：工具结果收集的汇合点

---

## 还可以追问的方向

- 工具的 `isConcurrencySafe(input)` 是如何实现的？FileRead 为什么是安全的，Edit 为什么不是？
- 权限检查（canUseTool）在并发工具执行时是怎么协调的？多个工具同时弹权限确认对话框会发生什么？
- 用户在工具执行过程中提交新消息，`interruptBehavior` 是怎么影响工具命运的？

---

