Read OSS

深入 Agentic 循环:Gemini CLI 如何处理一条提示词

高级

前置知识

  • 第 1 篇:架构与导航指南
  • TypeScript 异步生成器与迭代器
  • 对 LLM 工具调用与流式传输有基本了解

深入 Agentic 循环:Gemini CLI 如何处理一条提示词

当你在 Gemini CLI 中输入一条提示词时,它会进入一个可能跨越数十轮的循环——向模型发送请求、接收流式响应、分发工具调用、将结果回传,如此往复,直到任务完成或模型发出结束信号。这并非简单的请求-响应模式,而是一套三层架构,在每个环节都涉及流式事件、重试逻辑、压缩机制、循环检测以及 hook 集成。

本文将完整追踪一条用户提示词在 GeminiClientTurnGeminiChat 中的生命周期。

GeminiClient:外层循环的编排者

GeminiClient 是所有 LLM 交互的入口。它接收一个 AgentLoopContext(详见第 1 篇),并负责管理整个会话生命周期,包括循环检测、对话压缩、工具输出脱敏以及 hook 触发。

核心方法是位于 第 881 行sendMessageStream()。它的类型是 AsyncGenerator<ServerGeminiStreamEvent, Turn>——一边向调用方持续 yield 流式事件,一边在内部管理多个模型轮次。

sequenceDiagram
    participant UI as UI Layer
    participant GC as GeminiClient
    participant T as Turn
    participant Chat as GeminiChat
    participant API as Gemini API
    
    UI->>GC: sendMessageStream(request, signal, prompt_id)
    GC->>GC: fireBeforeAgentHook
    GC->>GC: processTurn()
    GC->>T: new Turn(chat, prompt_id)
    T->>Chat: sendMessageStream()
    Chat->>API: generateContentStream()
    API-->>Chat: streaming chunks
    Chat-->>T: StreamEvent yields
    T-->>GC: ServerGeminiStreamEvent yields
    GC-->>UI: ServerGeminiStreamEvent yields
    GC->>GC: fireAfterAgentHook

sendMessageStream() 的执行流程如下:

  1. Hook 状态管理 — 触发 BeforeAgent hook,并检查是否需要停止或拦截执行
  2. 轮次处理 — 委托给 processTurn(),由其创建 Turn 并管理模型路由
  3. 循环检测 — 对轮次产生的事件进行 LoopDetectionService 检查
  4. 轮次后 hookAfterAgent hook 可以停止执行、清空上下文,或触发继续轮次
  5. 状态清理 — 在 finally 块中清理 hook 状态

该方法具有递归性——如果 AfterAgent hook 携带原因拦截了执行,sendMessageStream 会将该原因作为新提示词、并以 stopHookActive: true 再次调用自身。

Turn:AsyncGenerator 的核心

Agentic 循环中每一次对模型的调用,都被封装在一个 Turn 实例中。Turn 负责将原始 API 流式响应转换为带类型的 ServerGeminiStreamEvent

位于 第 253 行run() 方法本身就是一个 AsyncGenerator

async *run(
    modelConfigKey: ModelConfigKey,
    req: PartListUnion,
    signal: AbortSignal,
    displayContent?: PartListUnion,
    role: LlmRole = LlmRole.MAIN,
): AsyncGenerator<ServerGeminiStreamEvent>

它遍历 GeminiChat.sendMessageStream() 的响应,并对流事件类型进行模式匹配:

  • retry 事件 → 以 GeminiEventType.Retry 的形式 yield,便于 UI 丢弃已接收的部分内容
  • agent_execution_stopped/blocked → 以 hook 驱动的控制事件形式 yield
  • chunk 事件 → 解析其中的思考内容、正文文本、工具调用、引用以及完成原因

Turn 会累积 pendingToolCalls——这些是模型在流式响应中通过 functionCall 部分请求、需要由 Scheduler 分发执行的函数调用。

stateDiagram-v2
    [*] --> Created: new Turn()
    Created --> Running: run() called
    Running --> YieldingContent: Content parts received
    Running --> YieldingThought: Thought parts received
    Running --> CollectingToolCalls: FunctionCall parts received
    YieldingContent --> Running: continue iteration
    YieldingThought --> Running: continue iteration
    CollectingToolCalls --> Running: continue iteration
    Running --> Finished: FinishReason received
    Running --> Cancelled: signal.aborted
    Running --> Error: API error
    Finished --> [*]
    Cancelled --> [*]
    Error --> [*]

提示: Turn 上的 pendingToolCalls 至关重要——sendMessageStream yield 完一个轮次的所有事件后,调用方(通常是 UI 或 SDK)会检查 turn.pendingToolCalls,并通过 Scheduler 分发工具执行。只有将工具结果回传之后,agentic 循环才会继续推进。

ServerGeminiStreamEvent 联合类型

ServerGeminiStreamEvent 是一个包含 18 种事件类型的可辨识联合类型,由 第 52–71 行GeminiEventType 枚举定义:

事件类型 用途
Content 模型输出的流式文本
Thought 模型的思考/推理过程文本
ToolCallRequest 模型请求执行某个工具
ToolCallResponse 工具执行后的返回结果
ToolCallConfirmation 等待用户确认的详情
ChatCompressed 上下文已压缩以适配窗口大小
Finished 轮次完成,附带原因与用量元数据
Retry 流式重试进行中,需丢弃部分内容
Error 发生错误
UserCancelled 用户中止了操作
LoopDetected 检测到无限循环
MaxSessionTurns 达到会话轮次上限
Citation 模型返回的引用/溯源元数据
ContextWindowWillOverflow 剩余 token 数量不足
InvalidStream 流式响应无效
ModelInfo 报告当前使用的模型
AgentExecutionStopped Hook 停止了执行
AgentExecutionBlocked Hook 拦截了执行

这个联合类型是后端与所有前端之间的契约。CLI 的 React 组件、非交互式处理器以及 SDK,都消费同一条事件流,并通过对 event.type 的模式匹配来驱动各自的行为。

GeminiChat:底层会话管理

GeminiChat 是最底层的组件——它封装了 @google/genai SDK,负责维护对话历史并处理流中断后的重试。该文件本身是上游 chats.ts 的修改版,之所以单独维护,是为了绕过一个函数响应无法被识别为有效响应的 bug。

其核心亮点是流式重试逻辑,配置见 第 89–93 行

const MID_STREAM_RETRY_OPTIONS: MidStreamRetryOptions = {
  maxAttempts: 4, // 1 initial call + 3 retries mid-stream
  initialDelayMs: 1000,
  useExponentialBackoff: true,
};

当流式传输在响应过程中失败(网络断开、内容无效等),GeminiChat 会 yield 一个 RETRY 事件,丢弃已接收的部分结果,并以指数退避的方式重新发起请求。它还支持模型降级——当主模型多次失败后,可通过 handleFallback 工具函数切换到备用模型。

sequenceDiagram
    participant GChat as GeminiChat
    participant CG as ContentGenerator
    participant API as Gemini API
    
    GChat->>CG: generateContentStream(request)
    CG->>API: HTTP stream
    API-->>CG: chunk 1
    CG-->>GChat: yield chunk 1
    API--x CG: connection error
    GChat->>GChat: yield RETRY event
    GChat->>GChat: backoff (1000ms)
    GChat->>CG: generateContentStream(request) [attempt 2]
    CG->>API: HTTP stream
    API-->>CG: full response
    CG-->>GChat: yield chunks

ContentGenerator 接口 对实际 API 调用进行了抽象。其实现包括:基于 GoogleGenAI 的标准生成器、用于调试追踪的 LoggingContentGenerator、用于会话回放的 RecordingContentGenerator,以及用于测试的 FakeContentGenerator

对话压缩与循环检测

两套安全机制共同防止 agentic 循环失控。

对话压缩

当对话历史接近模型的上下文窗口限制时,GeminiClient.processTurn() 会触发压缩。ChatCompressionService 会对早期轮次进行摘要,以释放 token 预算。位于 第 167–185 行CompressionStatus 枚举记录了以下几种结果:

  • COMPRESSED — 摘要已成功替换旧历史
  • COMPRESSION_FAILED_INFLATED_TOKEN_COUNT — 摘要反而比原内容更长
  • CONTENT_TRUNCATED — 前次压缩失败,因此将内容直接截断至预算上限
flowchart TD
    A[processTurn starts] --> B{Context management enabled?}
    B -- Yes --> C[AgentHistoryProvider.manageHistory]
    B -- No --> D[tryCompressChat]
    D --> E{Compression succeeded?}
    E -- Yes --> F[Yield ChatCompressed event]
    E -- No --> G[Track failure for future truncation]
    C --> H[Check remaining token count]
    F --> H
    G --> H
    H --> I{Request fits in window?}
    I -- Yes --> J[Continue with model call]
    I -- No --> K[Yield ContextWindowWillOverflow]

有一个值得注意的细节:一旦压缩失败,客户端会将 hasFailedCompressionAttempt 置为 true,后续轮次将直接退化为内容截断,而不会再次尝试(并再次失败)压缩。

循环检测

LoopDetectionService 跨轮次持续监控事件模式。在 第 688 行,每轮开始前都会检查是否存在重复模式。若检测到循环计数为 1(早期预警),客户端会尝试通过 _recoverFromLoop() 恢复;若计数超过 1,则 yield 一个 LoopDetected 事件并中止执行。

此外,在轮次内部(第 757 行),每个流式事件也会实时送入循环检测器。这能捕获在单轮内部涌现的循环,例如模型反复请求同一个工具调用的情况。

Hook 集成点

正如我们在 sendMessageStream() 中所见,hook 在两个关键位置拦截 agentic 循环:

BeforeAgent — 在一次提示词序列的第一次模型调用前触发。可以:

  • 完全停止执行(yield AgentExecutionStopped
  • 携带原因拦截执行(yield AgentExecutionBlocked
  • 向请求中注入额外上下文

AfterAgent — 在模型响应且没有待处理工具调用时触发。可以:

  • 停止执行并可选地清空上下文
  • 拦截并触发携带新提示词的继续轮次
  • 让执行正常继续

Hook 状态以 prompt_id 为键存储在一个 Map 中,以应对 sendMessageStream 的递归调用场景。activeCalls 计数器确保 BeforeAgent 在每次提示词中只触发一次,即便该方法在继续轮次中被递归调用也不例外。

另外两个 hook 点——BeforeModelAfterModel——在 GeminiChat 层面运作,拦截单次 API 调用。BeforeModel 可以修改请求配置或返回一个合成响应;AfterModel 可以对响应进行转换或触发附加操作。完整的 hook 系统将在第 5 篇中详细探讨。

提示: GeminiClient 中的 hookStateMap(第 143 行)是理解 hook 去重机制的关键所在。如果你在调试为什么一个 hook 在多个轮次中只触发了一次,不妨检查 hasFiredBeforeAgentactiveCalls 的值。

下一篇文章将跟踪模型请求工具调用后发生的一切——深入解析工具系统的 builder 模式,以及 Scheduler 如何通过事件驱动的编排来验证、确认和执行工具调用。