跳转到内容

Agent 核心循环

Gemini CLI — Agent 核心循环

学习目标

  • 理解 GeminiClient 的 sendMessageStream 如何构建 Agent 循环
  • 掌握 AsyncGenerator 事件流驱动的 turn 执行模型
  • 分析模型粘滞性和防循环检测机制

前置知识

本章涉及 Agent 核心循环的通用原理,建议先阅读:

下文假设你已理解 Session/Turn 两层循环模型和 SQ/EQ 事件驱动架构,直接聚焦 Gemini CLI 的具体实现。


项目实践

核心循环入口

Gemini CLI 的 Agent 循环入口是 GeminiClient.sendMessageStream()packages/core/src/core/client.ts),它返回一个 AsyncGenerator<ServerGeminiStreamEvent, Turn>。调用方通过 for await...of 消费事件流,而非传统的回调或 Promise 链。

sendMessageStream(request, signal, prompt_id, turns=MAX_TURNS)
├── 1. BeforeAgent Hook(可选拦截/注入上下文)
├── 2. processTurn(request, signal, prompt_id, boundedTurns)
│ ├── history 管理 / 压缩检查
│ ├── 模型路由 / 模型粘滞性
│ ├── turn.run() → 调用 Gemini API,流式返回
│ ├── 工具调用 → Scheduler 调度执行
│ └── 循环判断:有工具调用则继续,否则返回
└── 3. AfterAgent Hook(可选停止/继续)

MAX_TURNS 硬限值为 100,防止无限循环。

AsyncGenerator 事件流模型

与传统的 while-loop 不同,Gemini CLI 使用 AsyncGenerator 作为事件流载体:

async *processTurn(...) {
const turn = new Turn(this.getChat(), prompt_id);
// 调用模型,获取流式结果
const resultStream = turn.run(modelConfigKey, request, signal, {...});
for await (const event of resultStream) {
// 检测循环
const loopResult = this.loopDetector.addAndCheck(event);
if (loopResult.detected) { ... }
// 检测工具调用
if (event.hasToolCalls) {
yield event;
// 通过 Scheduler 执行工具
const results = await scheduler.schedule(toolCalls, signal);
// 将工具结果回传给模型,继续循环
turn.addToolResults(results);
continue; // 模型会再次响应
}
yield event;
}
return turn;
}

关键设计

  • 事件流中同时包含内容块和工具调用请求,调用方无需区分处理
  • continue 驱动循环:当有工具调用时,工具结果作为新消息回传,模型再次响应
  • 当模型不再调用工具时,循环自然结束

模型粘滞性(Sequence Stickiness)

Gemini CLI 维护一个 currentSequenceModel 字段:

if (this.currentSequenceModel) {
// 保持使用已选中的模型
modelToUse = this.currentSequenceModel;
} else {
// 首次 turn,通过 Router 选择模型
const decision = await router.route(routingContext);
modelToUse = decision.model;
}
this.currentSequenceModel = modelToUse;

为什么需要粘滞性

  • 避免同一对话中模型频繁切换导致的行为不一致
  • 保持提供商缓存温暖(同一模型连续调用命中率更高)
  • 减少路由开销(每次路由需要运行分类器)

粘滞性何时解除

  • 用户显式切换模型(-m 参数或 /model 命令)
  • 会话重置(/new/reset
  • 模型不可用时(fallback 触发)

防循环检测

LoopDetectionService 在两个阶段检测循环:

  1. turnStarted:Turn 开始前检查当前 prompt 的历史是否已有相似状态
  2. addAndCheck:Turn 执行中,逐个事件检查是否进入重复模式

检测结果分级处理:

检测级别处理方式
count > 1直接终止当前 turn,报告 LoopDetected 事件
count == 1 且 boundedTurns <= 1报告 MaxSessionTurns,无剩余 turn 可用
count == 1 且 boundedTurns > 1调用 _recoverFromLoop,注入引导文本让模型改变行为

_recoverFromLoop 的具体做法是压缩最近的历史(保留关键工具调用和结果),然后添加引导文本(如 “你似乎陷入了循环。请尝试不同的方法。”),让模型在新上下文中重新思考。

BeforeAgent / AfterAgent 钩子生命周期

sendMessageStream 前后分别触发 BeforeAgent 和 AfterAgent 钩子:

BeforeAgent Hook

  • 可以阻止执行(返回 AgentExecutionStopped
  • 可以注入额外上下文(返回 additionalContext,包装在 <hook_context> 标签中)

AfterAgent Hook

  • 可以停止执行shouldStopExecution() → 返回 AgentExecutionStopped
  • 可以请求继续isBlockingDecision() → 自动递归调用 sendMessageStream,触发新一轮 BeforeAgent)
  • 请求继续时,会重置 hasFiredBeforeAgent 状态,确保钩子不会重复触发

hookStateMap 管理每个 prompt_id 的钩子状态:

  • hasFiredBeforeAgent:防止 BeforeAgent 钩子重复触发
  • activeCalls:追踪当前活跃的钩子调用数

问题与规避

AsyncGenerator 的取消处理

问题:AsyncGenerator 本身不支持取消,AbortSignal 需要在循环内部显式检查。

对策:Gemini CLI 在所有关键点检查 signal.aborted

  • turn 开始前
  • 模型调用中(通过 abortSignal 传给 SDK)
  • 工具调度中(通过 signal 传给 Scheduler)
  • 循环判断后

模型粘滞性与回退冲突

问题:当粘滞的模型不可用时,fallback 会切换模型,但 currentSequenceModel 仍指向旧模型。

对策handleModelChanged 事件监听器在模型切换时重置 currentSequenceModel = null,强制下一次 turn 重新路由。

Hook 递归深度

问题:AfterAgent 的 isBlockingDecision() 会递归调用 sendMessageStream,理论上可形成深度嵌套。

对策boundedTurns 逐层递减(boundedTurns - 1),防止无限递归。同时 stopHookActive 标志位告知 AfterAgent 钩子这是重试调用,避免重复决策。


设计取舍

AsyncGenerator vs SQ/EQ 队列

方案优势代价
AsyncGenerator消费方代码简洁(for await...of),天然支持流式 UI生成方需要手动 yield 事件,取消和错误传播需要小心处理
SQ/EQ 队列双向通信更灵活,支持并发操作消费方需要管理队列状态,代码复杂度高

Gemini CLI 的选择:使用 AsyncGenerator 作为事件输出通道,但内部通过 MessageBus 实现双向通信(确认、取消等),结合了两种模式的优点。

100 Turn 硬限值的合理性

方案优势代价
固定上限(如 100)简单、可预测可能在复杂任务中过早终止
动态上限根据任务复杂度调整难以估计复杂度,可能设置过高
无上限理论上可以完成任意复杂任务实际中几乎总是导致无限循环或 token 耗尽

Gemini CLI 的选择:100 是一个经验值——足够完成大多数编程任务,又不会让失控的 Agent 消耗过多资源。用户可通过 --max-session-turns 自定义。


参考来源