Claude Code(CC)批量工具调用:并发安全分组 + Batch 执行
用 Claude Code 的时候,你可能会注意到一个现象:让它一次性读十几个文件、改几处代码,整个过程并没有明显卡顿,进度条几乎是一起往前推的。
但如果换成最朴素的"一个工具调完、把结果丢回模型、模型再决定下一个"的循环,十几个工具就意味着十几轮大模型往返,每一轮都要承担一次完整的 RTT 和推理延迟。
Claude Code 之所以快,核心就在于它的批量工具调用(Batch Tool Calling)机制。
一句话结论:CC 收到模型一次返回的多个 tool_use 之后,编排调度器会按照「工具是否并发安全」分组,同组内并行跑;有状态修改 / 依赖的放到串行分组,顺序执行。以此减少多轮 LLM 往返,降低整体延迟。
一、怎么划分 Batch 分组(核心规则)
判断依据:工具注解标记 readOnlyHint(MCP / CC SDK)。
注意,这不是模型自己猜的,而是工具在注册时就定义好的属性。
1. ✅ 并发安全(只读工具,可并行 Batch)
内置工具 :Read 读文件、Glob 遍历目录、Grep 搜索、ListDir。
特点:只读、不修改磁盘 / 共享状态、互相之间没有依赖。
所有连续、无依赖的只读工具,打包成同一个并行 Batch,一起并发执行。
例:一次性读取
src/a.py、src/b.py、src/c.py→ 打包成一个 batch,3 个任务同时跑。
2. ❌ 非并发安全(带副作用,串行 Batch)
内置工具 :Write 写文件、Edit 修改文件、Bash 终端命令。
特点:修改文件系统 / 共享状态,有执行顺序依赖;不能并行,否则会出现文件覆盖、竞争冲突。
每一个或者一组有依赖的修改操作,单独放到串行队列,按顺序执行,前一个成功才跑下一个;一旦失败,后面同 batch 的任务直接标记不执行。
自定义 MCP 工具默认不支持并行 ;想要开启并发安全,必须在 tool annotations 里显式设置
readOnlyHint: true。
分组算法简化流程
- LLM 一轮输出多条 tool call 列表;
- CC 调度器遍历 tool 列表,按工具类型切分:
-
连续多个只读工具 → 合并成并行 Batch
-
碰到 Write / Edit / Bash → 终止当前并行 batch;这个以及后面有依赖的工具,进入串行 Batch
- 多个 Batch 之间:先执行完前面整个 Batch,再跑下一个 Batch。
举例:
Read、Read、Write、Read
- Batch1:2 个
Read,并发执行- Batch2:
Write,串行执行- Batch3:1 个
Read,并发执行
二、执行流程怎么跑
- 模型侧 :Claude 模型识别多个独立任务,一次性输出多个
tool_use(stop_reason=tool_use),不再一次只返回单个工具调用; - CC 调度器分组:按上面的规则切并行 / 串行 batch;
- 执行
-
并行 Batch:
asyncio.gather/Promise.all,全部并发拉起,等待全部完成; -
串行 Batch:循环逐个
await,顺序执行;前面报错,后续任务不再执行;
- 结果收集 :不管并行还是串行,每个 tool 都保留独立
tool_use_id;收集全部tool_result打包成一条消息一次性回传给大模型。
关键点:所有 batch 的结果一次性返回,不需要每调用一次工具就轮询一次 LLM,这就是提速的根源,省去大量 LLM 往返 RTT。
三、容易混淆的两个点
1. 是 CC 本地调度器做分组,不是模型内部做并发
模型只负责输出一批 tool_use;并行 / 串行的决策是 CC 本地 runtime 做的,不是 Anthropic API。
API 本身只返回 tool_use 数组,不定义执行策略。换句话说,同一批 tool_use 换一个 runtime,完全可能被串行走完。
2. 并行不是无上限
CC 有并发上限(默认最大并发数),防止瞬间打开成千上万个读文件句柄,触发系统资源限制(比如 ulimit 下的 fd 耗尽)。
四、举个完整例子
需求:读取 3 个源码文件,修改其中 1 个,再读取配置文件。
模型一轮返回 tool list:
Read(file=a.py), Read(file=b.py), Write(file=c.py), Read(config.yaml)
CC 分组:
- Batch1(并行安全) :
Read(a)、Read(b)→ 并发同时读取;全部拿到结果 - Batch2(串行) :
Write(c.py)→ 等 Batch1 结束,执行写入;写入失败,则后面Read不再跑 - Batch3(并行安全) :
Read(config.yaml)→Write成功之后,执行读取
全部 4 个工具执行完,一次性把 4 份 tool_result 打包发给 Claude。
如果 Write(c.py) 失败了,Batch3 的 Read(config.yaml) 会被直接标记不执行 ------ 这是有意为之:既然前一步写入没成功,后续基于"写入已完成"这一假设的读取就没有意义了。
五、收益与代价
✅ 收益
减少 LLM 来回轮次。原本 4 次工具调用要 4 轮 LLM 交互;现在 1 轮 LLM 输出,本地分批执行,一次回传结果,总耗时大幅下降。
对于"读一堆文件再改"这类典型 Agent 任务,这个优化带来的体感差异非常明显。
⚠️ 限制
- 带副作用的操作不能打包并行,防止竞态;
- 如果并行 batch 中个别工具报错,不影响同 batch 其他只读任务(只读操作互相独立);
Bash/ComputerUse工具强制串行,就算放同一个 tool 列表,也会顺序跑。
最后一点尤其值得注意:Bash 能执行的动作范围太大(可能动数据库、动网络、动别的进程),保守起见强制串行是合理的默认策略。
补充:和普通单轮工具调用对比
传统方式:LLM 返回 tool1 → 执行 tool1 → 返回结果 → LLM 再返回 tool2......多轮往返。
CC Batch:LLM 一次性输出 tool1 / tool2 / tool3 → CC 本地分组并行 / 串行跑所有工具 → 一次性返回全部结果。
传统: [LLM] → t1 → [LLM] → t2 → [LLM] → t3 → [LLM]
└─RTT──┘ └─RTT──┘ └─RTT──┘
Batch: [LLM] → ┌─ t1 ─┐
├─ t2 ─┤ → [LLM]
└─ t3 ─┘
└─ 一次 RTT ─┘
省掉的正是中间那些 [LLM] → ... → [LLM] 的往返。
小结
- 能不能并行,由工具注册时的
readOnlyHint决定,不是模型现场判断; - 只读连续段 → 并行 Batch;遇到写操作 → 切串行;写完若还有连续只读 → 再开一个并行 Batch;
- 串行段失败会短路后续任务,这是保护而非 bug;
- 自定义 MCP 工具默认串行,记得显式声明
readOnlyHint: true才能享受并行加速 ------ 这是大多数自建 MCP 最容易漏掉的一步。