深入 Agentic 循环:Gemini CLI 如何处理一条提示词
前置知识
- ›第 1 篇:架构与导航指南
- ›TypeScript 异步生成器与迭代器
- ›对 LLM 工具调用与流式传输有基本了解
深入 Agentic 循环:Gemini CLI 如何处理一条提示词
当你在 Gemini CLI 中输入一条提示词时,它会进入一个可能跨越数十轮的循环——向模型发送请求、接收流式响应、分发工具调用、将结果回传,如此往复,直到任务完成或模型发出结束信号。这并非简单的请求-响应模式,而是一套三层架构,在每个环节都涉及流式事件、重试逻辑、压缩机制、循环检测以及 hook 集成。
本文将完整追踪一条用户提示词在 GeminiClient、Turn 和 GeminiChat 中的生命周期。
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() 的执行流程如下:
- Hook 状态管理 — 触发
BeforeAgenthook,并检查是否需要停止或拦截执行 - 轮次处理 — 委托给
processTurn(),由其创建Turn并管理模型路由 - 循环检测 — 对轮次产生的事件进行
LoopDetectionService检查 - 轮次后 hook —
AfterAgenthook 可以停止执行、清空上下文,或触发继续轮次 - 状态清理 — 在
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 驱动的控制事件形式 yieldchunk事件 → 解析其中的思考内容、正文文本、工具调用、引用以及完成原因
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至关重要——sendMessageStreamyield 完一个轮次的所有事件后,调用方(通常是 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 点——BeforeModel 和 AfterModel——在 GeminiChat 层面运作,拦截单次 API 调用。BeforeModel 可以修改请求配置或返回一个合成响应;AfterModel 可以对响应进行转换或触发附加操作。完整的 hook 系统将在第 5 篇中详细探讨。
提示: GeminiClient 中的
hookStateMap(第 143 行)是理解 hook 去重机制的关键所在。如果你在调试为什么一个 hook 在多个轮次中只触发了一次,不妨检查hasFiredBeforeAgent和activeCalls的值。
下一篇文章将跟踪模型请求工具调用后发生的一切——深入解析工具系统的 builder 模式,以及 Scheduler 如何通过事件驱动的编排来验证、确认和执行工具调用。