Coding Agent 工具调用优化:从全串行到分批并发
在 Coding Agent 中模型通常不会只调用一次工具。为了回答一个问题它可能连续读取多个文件、搜索符号最后再修改代码。如果所有工具都严格串行执行即使前几个工具只是互不影响的读取操作也必须一个接一个等待。我最初采用的就是全串行方案。它实现简单、执行顺序明确但在连续读取多个文件时整体耗时会不断累加。后来我将执行方式改成了批次内并发、批次间串行只让连续出现的只读工具并行遇到可能修改状态的工具就恢复串行。这个方案没有追求最大并发度而是希望在不破坏原有读写顺序的前提下减少无意义的等待。一、全串行执行的问题假设模型一次返回了三个文件读取调用每次分别耗时 300 ms、500 ms 和 400 ms。全串行执行时总耗时大约为300 500 400 1200 ms但三个调用都只读取数据彼此之间没有依赖。如果并发执行理论耗时会接近最慢的那个工具max(300, 500, 400) 500 ms因此真正需要解决的问题不是“怎样让所有工具同时运行”而是怎样识别互不影响的工具并在保持原始执行语义的情况下让它们并行。二、把工具调用拆成多个执行批次我的做法是按照模型返回的工具调用顺序遍历将连续的并发安全工具合并到同一个批次中。遇到非并发安全工具时先结束当前并发批次再把该工具单独划分为一个串行批次。例如模型返回下面这组调用ReadFile → Glob → Grep → EditFile → ReadFile调度器会将它们拆成三个批次批次 1ReadFile、Glob、Grep 并发执行 批次 2EditFile 串行执行 批次 3ReadFile 等待批次 2 完成后执行简化后的分批逻辑如下。这里是为了说明设计思路而写的伪代码并非具体项目源码batches[]parallel_batch[]fortool_callintool_calls:iftool_call.tool.parallel_safe:parallel_batch.append(tool_call)continueifparallel_batch:batches.append(parallel_batch)parallel_batch[]batches.append([tool_call])ifparallel_batch:batches.append(parallel_batch)执行阶段按照批次顺序推进。并发批次内部可以通过异步任务同时运行串行批次则等待当前工具结束后再进入下一批。这里最重要的一点是只能合并连续出现的安全工具不能跨过写工具重新排列调用。例如下面的调用顺序Read A → Write B → Read CRead C不能提前和Read A并发因为它原本位于Write B之后可能需要读取写入后的最新内容。正确做法是保留三个批次让写操作成为前后两个读取阶段之间的顺序屏障。三、怎么判断工具能不能并发我没有让调度器根据工具名称临时猜测而是让每个工具通过属性显式声明自己是否支持并发。调度器只读取这个属性并完成分批。判断标准主要看一点工具是否会修改文件、进程或其他共享的外部状态。工具类型例子执行方式只读工具ReadFile、Glob、Grep支持并发写入工具WriteFile、EditFile串行执行通用命令工具Bash保守地串行执行Bash比较特殊。它既可以执行只读命令也可以修改文件、启动进程甚至访问外部服务。调度器很难只根据一段命令可靠判断它的副作用所以当前实现将它视为非并发安全工具。这会损失一部分并发机会但执行边界更加明确。Spawn 子 Agent 的特殊处理Spawn 也没有进入普通并发批次。它在创建子 Agent 时通常需要创建或分配工作目录、注册任务、加入 Team 消息通道并更新相关状态。多个 Spawn 同时执行可能在这些共享操作上产生竞争。因此我将Spawn 调用本身串行化。不过Spawn 完成后已经创建的子 Agent 可以在后台并行工作主 Agent 不需要一直阻塞等待。换句话说并发发生在两个阶段之间创建阶段多个 Spawn 调用按顺序完成 运行阶段多个子 Agent 在后台并行工作这和“同时并发执行多个 Spawn 工具”并不是一回事。四、并发批次如何保证正确性1. 工具会看到彼此的中间状态吗不会。一个并发批次中只允许放入并发安全的只读工具它们不会修改共享状态也就没有需要被同批其他工具观察的中间写入结果。并发工具的完成顺序可能不同但调度器可以按照原始工具调用顺序整理结果再统一返回给模型。因此执行完成顺序可以变化模型接收到的结果顺序仍然保持稳定。当调用序列中出现写工具时它会形成单独的串行批次。写之前的批次必须先全部完成写之后的读取也必须等待写操作结束。这样能够保证后续读取看到更新后的状态。这里保证的是调度器内部工具调用之间的顺序关系。如果还有不受调度器控制的外部进程同时修改同一资源则需要额外的文件锁或事务机制处理不属于当前批处理方案的保证范围。2. 一个工具很慢会拖住整个批次吗会。当前批次需要等待所有工具执行结束才能把完整结果交给模型并进入下一批因此批次耗时由最慢的工具决定。这不是调度错误而是批次之间可能存在依赖。例如下一批是写工具时系统必须确认前面的读取任务已经全部结束不能在结果还不完整时继续执行。针对慢工具我保留了两个优化方向为不同类型的工具设置独立超时读取类工具使用较短超时命令执行类工具使用较长超时。超时后返回结构化错误避免单个任务无限阻塞整个批次。让耗时较长的工具流式上报进度使 UI 能展示当前状态。流式输出主要改善等待体验不会取消批次结束前的同步屏障。更激进的方案是分析每个工具访问的具体资源再构建依赖图动态调度。例如两个写工具修改不同文件时也允许并行。但这需要准确描述工具副作用还要处理资源锁、异常传播和任务取消复杂度会明显增加。对于当前场景收益还不足以覆盖实现与维护成本。五、总结这次优化的核心不是简单地把串行改成并行而是先确定并发的安全边界连续的只读工具组成并发批次减少多个读取操作的累计耗时。写工具和通用命令工具单独串行执行避免状态竞争。不跨越写操作重新排列调用保证写后的读取能看到最新状态。Spawn 创建过程保持串行创建完成后的子 Agent 可以后台并行运行。每个批次等待全部任务结束并通过独立超时和流式进度处理慢工具问题。工程层只优化执行效率不改变模型给出的读写顺序。这种分批机制虽然没有实现理论上的最大并发度但规则简单、行为可预测也更适合逐步扩展新的工具类型。