Skip to content

工具执行 sequential/parallel

源码版本v0.73.1

executeToolCallsrunLoop 内层处理 assistant 消息里 tool calls 的子流程。它把工具调用的生命周期切成五段:prepareToolCall(查表+校验+beforeToolCall 钩子)→ executePreparedToolCall(真跑 tool.execute)→ finalizeExecutedToolCall(afterToolCall 钩子覆盖)→ emitToolExecutionEndcreateToolResultMessage。sequential 和 parallel 两种模式共用这五段,区别只在调度顺序:sequential 一条走完再下一条,parallel 先串行 prepare,再 Promise.all 并发执行+finalize。

职责

  1. 模式分发:executeToolCalls 检查 config.toolExecution 和是否有任一工具的 executionMode === "sequential",任一为真就走 sequential。见 packages/agent/src/agent-loop.ts:350-365
  2. 准备阶段:prepareToolCall 找工具、prepareToolCallArguments 兼容旧参数、validateToolArguments 校验、beforeToolCall 钩子可 block。见 packages/agent/src/agent-loop.ts:529-579
  3. 执行阶段:executePreparedToolCalltool.execute(id, args, signal, onUpdate),partial result 通过 tool_execution_update 事件流给 UI,异常转成 error 结果。见 packages/agent/src/agent-loop.ts:581-616
  4. 收尾阶段:finalizeExecutedToolCallafterToolCall 钩子,按字段覆盖 content/details/isError/terminate,钩子自己抛错也转成 error result。见 packages/agent/src/agent-loop.ts:618-661
  5. 构造结果消息:createToolResultMessage 把 finalized 结果包成 ToolResultMessage(role/toolCallId/content/details/isError/timestamp),由调用方推进 context。见 packages/agent/src/agent-loop.ts:680-690

设计动机

为什么 prepare 串行、execute 并发?因为 beforeToolCall 钩子通常要做鉴权、日志、限流这类有副作用的事,并发触发容易竞态(比如同时刷 OAuth token)。prepare 串行能保证钩子按顺序跑。execute 并发是因为工具本身(读文件、跑 bash、HTTP 请求)互相独立,串行浪费 wall time。

为什么 tool_execution_end 按「完成顺序」发、toolResult message 按「源顺序」发?parallel 模式里 Promise.all 后再循环构造 toolResultMessage,但 emitToolExecutionEnd 在每个工具 finalize 完就立刻调——UI 能在工具完成的瞬间拿到结果,而不必等整批都完。toolResult message 是要喂回 LLM 的,顺序错乱会让模型混乱,所以按 assistant 消息里的 toolCall 顺序发。这种「事件流按完成顺序、消息流按源顺序」的拆分,让 UI 响应快且 LLM 上下文稳定。

为什么有 immediateprepared 两种 outcome?工具找不到、参数校验失败、beforeToolCall block 这些情况,根本没到 execute 那步,直接返回一个 error result。把它和正常 execute 的结果用同一个 FinalizedToolCallOutcome 形状封装,下游的 emitToolExecutionEnd/createToolResultMessage 不用区分两种路径。

关键文件

sequential 模式的核心循环,一条工具走完五段才下一条:

typescript
// packages/agent/src/agent-loop.ts:383-416
for (const toolCall of toolCalls) {
  await emit({ type: "tool_execution_start", toolCallId: toolCall.id, toolName: toolCall.name, args: toolCall.arguments });
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
  let finalized: FinalizedToolCallOutcome;
  if (preparation.kind === "immediate") {
    finalized = { toolCall, result: preparation.result, isError: preparation.isError };
  } else {
    const executed = await executePreparedToolCall(preparation, signal, emit);
    finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
  }
  await emitToolExecutionEnd(finalized, emit);
  const toolResultMessage = createToolResultMessage(finalized);
  await emitToolResultMessage(toolResultMessage, emit);
  finalizedCalls.push(finalized);
  messages.push(toolResultMessage);
}

parallel 模式里 prepare 串行、execute 并发,事件顺序是关键差异:

typescript
// packages/agent/src/agent-loop.ts:434-477
for (const toolCall of toolCalls) {
  await emit({ type: "tool_execution_start", ... });
  const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
  if (preparation.kind === "immediate") {
    const finalized = { toolCall, result: preparation.result, isError: preparation.isError } satisfies FinalizedToolCallOutcome;
    await emitToolExecutionEnd(finalized, emit);   // immediate 立刻发 end
    finalizedCalls.push(finalized);
    continue;
  }
  finalizedCalls.push(async () => {                 // 延迟到 Promise.all
    const executed = await executePreparedToolCall(preparation, signal, emit);
    const finalized = await finalizeExecutedToolCall(currentContext, assistantMessage, preparation, executed, config, signal);
    await emitToolExecutionEnd(finalized, emit);    // finalize 完就发 end
    return finalized;
  });
}
const orderedFinalizedCalls = await Promise.all(finalizedCalls.map((entry) => typeof entry === "function" ? entry() : Promise.resolve(entry)));
// 再按源顺序构造 toolResultMessage ...

数据流

一个 assistant 消息带 3 个 tool calls,parallel 模式下的时序:

边界与失败

小结

工具执行被切成五段:prepare/execute/finalize/emitEnd/createToolResultMessage。sequential 整批串行,parallel prepare 串行、execute 并发,事件按完成顺序、toolResult 按源顺序。beforeToolCall/afterToolCall 钩子在 prepare 和 finalize 阶段调用,任何异常都被转成 error result 而不是中断整批。钩子形状和 AgentTool 字段,看 类型契约;五段函数被 runLoop 内层调用的位置,看 双层 while 主循环