跳转到内容

事件驱动的工具调度

Gemini CLI — 事件驱动的工具调度

学习目标

  • 理解 Scheduler 的事件驱动状态机架构
  • 掌握并行工具识别和批量执行机制
  • 分析工具生命周期(Validating → Scheduled → Executing → Terminal)

前置知识

本章涉及工具调用的通用原理,建议先阅读:

下文假设你已理解工具调用的声明式定义模式和两阶段(build → execute)流程,直接聚焦 Gemini CLI 的调度实现。


项目实践

Scheduler 的角色定位

Scheduler 是 Gemini CLI 中连接 LLM 响应和工具执行的桥梁。当模型返回 functionCall 后,GeminiClient 将这些调用交给 Scheduler 处理:

模型响应: [functionCall: read_file, functionCall: grep]
GeminiClient.scheduler.schedule(calls, signal)
Scheduler: 验证 → 策略检查 → 执行 → 返回结果
GeminiClient: 将结果回传给模型

工具状态机

每个工具调用经历以下状态流转:

状态说明

状态职责关键操作
Validating参数验证 + Policy 检查 + 用户确认构建 ToolInvocation,检查 Policy,等待用户确认(如需)
Scheduled等待执行策略通过,等待队列调度
Executing正在执行调用工具的 execute() 方法
Success/Error/Cancelled终态记录结果,从活跃集合移除

三阶段处理循环

Scheduler 的 _processQueue() 方法实现核心处理循环:

_processQueue(signal):
while queueLength > 0 || hasActiveCalls:
_processNextItem(signal)
├── Phase 1: 处理 Validating 状态
│ └── Policy 检查 + 用户确认
├── Phase 2: 执行 Scheduled 状态
│ └── 并行执行所有已调度的工具
└── Phase 3: 终结终态调用
└── finalize 并记录结果

关键特征:每次循环迭代处理所有可推进的调用,而非一次只处理一个。这使得:

  • 多个工具可以在同一次迭代中并行执行
  • 状态变化在循环内立即可见,无需等待下一轮

并行工具识别

当队列中第一个工具可并行时,Scheduler 会批量取出所有连续的可并行工具

_isParallelizable(request):
return request.parallelizable && !request.requiresSerialExecution
if _isParallelizable(firstRequest):
while peekQueue() && _isParallelizable(peeked):
dequeue() // 批量取出
// 现在所有取出的工具将并行执行

并行条件

  • 工具的 parallelizable 标记为 true
  • 不要求串行执行(requiresSerialExecution 为 false)

典型并行工具read_fileglobgrepls —— 纯读取操作,无副作用。

典型串行工具write_fileeditshell —— 修改文件系统或执行命令,有副作用。

请求队列机制

当 Scheduler 正在处理一批工具时,新的工具调用会被入队

schedule(requests, signal):
if isProcessing || isActive:
return _enqueueRequest(requests, signal) // 排队等待
else:
return _startBatch(requests, signal) // 立即开始

队内请求在 _processNextInRequestQueue() 中处理:当前批次完成后,自动启动下一批。这保证了工具调用不会交错执行(不会出现批次 A 执行到一半、批次 B 的工具也开始执行的情况)。

Topic 变更优先排序

同一批次中,UPDATE_TOPIC_TOOL_NAME(主题变更工具)会优先处理

sortedRequests.sort((a, b) => {
if (a.name === UPDATE_TOPIC_TOOL_NAME) return -1;
if (b.name === UPDATE_TOPIC_TOOL_NAME) return 1;
return 0;
});

为什么:主题变更可能影响后续工具的上下文或策略,先处理确保后续工具使用正确的主题。

MCP 进度追踪

Scheduler 通过 CoreEvent.McpProgress 事件接收 MCP 工具的进度更新,实时更新执行状态:

handleMcpProgress(payload):
call = state.getToolCall(payload.callId)
if call && call.status === Executing:
state.updateStatus(callId, Executing, {
progressMessage: payload.message,
progressPercent: (payload.progress / payload.total) * 100,
})

这使得长时间运行的 MCP 操作(如远程 API 调用)可以向用户展示进度。


问题与规避

工具状态机的死锁

问题:如果 Validating 状态的工具等待用户确认,但确认请求未能传递到 UI,工具将永久挂起。

对策:MessageBus 发布 TOOL_CONFIRMATION_REQUEST 事件,UI 层监听并展示确认对话框。超时或取消后,Scheduler 的 cancelAll() 将所有等待中的工具标记为 Cancelled。

并行工具的副作用冲突

问题:两个并行工具同时操作同一文件可能导致竞争条件。

对策:有副作用的工具(write、edit、shell)标记为 parallelizable: false,不会被批量取出。但如果模型同时请求两个写操作(理论上不应发生),Scheduler 不会检测文件级冲突——这依赖于模型正确排序工具调用。

取消的延迟传播

问题signal.aborted 后,正在执行的工具不会立即停止。

对策cancelAll() 将所有非终态工具标记为 Cancelled,但实际执行中的工具需要自行检查 AbortSignal。部分工具(如 shell 执行)在收到 abort 后会发送 SIGKILL 终止子进程。


设计取舍

事件驱动状态机 vs 顺序执行

方案优势代价
事件驱动状态机支持并行、可扩展到多 Agent、状态可追踪实现复杂,状态流转需要精细管理
顺序执行简单、易于调试不支持并行、无法利用无依赖工具的并行性

Gemini CLI 的选择:使用状态机是因为工具间存在复杂的依赖关系(确认、策略、并行),顺序执行无法满足性能需求。

批量并行 vs 逐个执行

方案优势代价
批量并行无依赖工具同时执行,减少延迟需要正确识别依赖关系,错误分类会导致竞争
逐个执行永远不会出现竞争延迟累加,多工具调用场景下体验差

Gemini CLI 的选择:连续的可并行工具批量执行,串行工具(有副作用的)逐个执行。这在安全和性能之间取得了平衡。


参考来源