Skip to content
掌握度

CodeAgent CLI 项目深挖 · 单期模块。总览见 需求分析 与 整体设计方案。

当 LLM 在同一轮返回多个工具调用时,我不再一个个串行跑,而是用线程池并行执行,缩短多工具任务的墙钟时间。核心是 ToolRegistry.executeTools,它是 ReAct / Plan / Multi-Agent 三条执行路径共用的统一入口——绝不手写 for 循环。

并行工具执行流程 ​

分五步。

  1. 分叉:只有一个工具调用就直接同步执行,不走线程池,省掉无谓的线程开销;多个才走并行。
  2. 建池:Executors.newFixedThreadPool(min(size, 4)),并发上限 MAX_PARALLEL_TOOLS = 4,线程设成 daemon。
  3. 提交:每个工具包成一个 Callable<ToolExecutionResult>,任务体里先查取消状态,再走 executeToolOutput(也就是 HITL → 策略 → executor 那条链)。
  4. 带超时的批量等待:executor.invokeAll(tasks, 90, TimeUnit.SECONDS),批次超时 90 秒。
  5. 按序收结果:靠 invokeAll 的契约——返回的 Future 列表顺序等于提交任务顺序,所以按索引 future.get,结果顺序天然等于原始 tool_call 顺序。

【主动抛深度点 1:结果为什么要保序】 这是并行工具最容易踩的坑。并行执行时谁先完成是不确定的,但回灌结果时必须严格按原始 tool_call 顺序——因为 tool(toolCallId) 要和 assistant 消息里的 tool_calls[i] 一一对应,顺序一乱,LLM 就会把 A 的结果当成 B 的。所以我的方案是「执行可以乱序完成,收集必须保序」:invokeAll 的 Future 顺序固定,最后按索引取,天然保序。

【主动抛深度点 2:单个超时不拖垮整批】 如果 A 工具卡死了,我不希望它拖累已经跑完的 B、C。所以 invokeAll 超时后,超时的那个 Future 被取消、返回一个 timedOut 结果占位(「工具执行超时,已取消」),已完成工具的结果原样保留。也就是「部分成功、部分超时」是允许的,批次不会因为一个工具失败就整体失败。

结果顺序怎么保证不乱? ​

靠 invokeAll 的契约:它返回的 Future 列表顺序和提交任务的顺序一致,不管谁先完成。我按索引逐个 future.get 收集,所以结果顺序 = 原始 tool_call 顺序。这是必须的,因为回灌时 tool(toolCallId) 要和 assistant 消息里的 tool_calls[i] 一一对应。

并行上限为什么是 4? ​

经验值,平衡三件事:加速收益、线程资源开销、以及防止一次性并发太多工具把文件系统或下游 API 打爆。4 是「够快又不会失控」的折中。

为什么单个工具不也走线程池? ​

为省开销。线程池创建、提交、上下文切换都有成本,单个工具直接同步执行更快,也更好调试。这是「快速路径」优化——大多数情况 LLM 一次只调一个工具,不能为了少数并行场景让每个工具都背上线程池开销。

并行时用户 Ctrl+C 取消怎么办? ​

CancellationContext 检查贯穿两处:提交任务前检查一次(直接返回失败),每个任务体里执行前再检查一次。所以取消时,未执行的工具统一返回「用户取消了此次工具调用」,正在执行的不被打断到一半,结果列表仍保持完整占位、协议不破。

并行执行时终端输出会串吗? ​

工具执行本身不直接写 stdout(结果以字符串返回给 Agent),所以这层不会串。真正要处理输出串扰的是 Multi-Agent 那层——它并行的是多个 Agent,每个 Agent 有自己的流式输出,那里才用「每步独立 ByteArrayOutputStream 缓冲、最后按顺序 flush」来解决。并行工具这一层只关心结果数据的保序,不关心终端渲染。

源码锚点(读码定位用,面试不主动讲) ​

  • ToolRegistry.java:1205
  • ToolRegistry.java:63

本站仅供个人学习使用,请勿外传