---
title: "Agentic Loop：AI 自主循环的核心机制"
description: "深入解析 Claude Code 的 query() 异步生成器循环——从流式 API 调用、工具并行执行、上下文压缩、错误恢复到终止条件的完整状态机，基于 src/query.ts 的源码级分析。"
keywords: ["Agentic Loop", "query loop", "tool_use", "状态机", "auto-compact", "streaming", "recovery"]
sourceRef: "3ec5675 (2026-04-08)"
---

{/* 本章目标：基于 src/query.ts 揭示 Agentic Loop 的完整状态机 */}

## 什么是 Agentic Loop

传统聊天机器人：你问一句，它答一句。  
Claude Code 不一样：你说一个需求，它可能连续执行十几步操作才给你最终结果。

这背后的机制叫做 **Agentic Loop**（智能体循环），核心实现在 `src/query.ts` 的 `queryLoop()` 异步生成器函数。它是一个 `while(true)` 无限循环，每次迭代代表一次"思考→行动→观察"周期。

<Frame caption="Agentic Loop 循环示意">
  <img src="/docs/images/agentic-loop.png" alt="Agentic Loop 循环图" />
</Frame>

## 循环的完整结构

`queryLoop()` 的每次迭代（`src/query.ts` 中 `while(true)` 主循环）包含以下阶段：

### 阶段 1：上下文预处理（Pre-Processing Pipeline）

在调用 API 之前，依次执行 5 个压缩/优化步骤：

```
messagesForQuery（原始消息）
  ↓ applyToolResultBudget()    — 工具结果预算截断（按 maxResultSizeChars）
  ↓ snipCompactIfNeeded()      — 历史 Snip 压缩（HISTORY_SNIP feature）
  ↓ microcompact()             — 微压缩（工具结果摘要）
  ↓ applyCollapsesIfNeeded()   — 上下文折叠（CONTEXT_COLLAPSE feature）
  ↓ autocompact()              — 自动压缩（超出阈值时触发）
messagesForQuery（处理后的消息）→ 发往 API
```

每个步骤的输出是下一步的输入，形成串行管道。Snip 和 Microcompact 的释放 token 数会传递给 autocompact 的阈值计算（`snipTokensFreed`），避免重复压缩。

### 阶段 2：流式 API 调用（Streaming Loop）

`deps.callModel()` 发起流式请求（`src/query.ts` 中 `attemptWithFallback` 循环内），返回一个 AsyncGenerator。在流式过程中：

- **AssistantMessage** 被收集到 `assistantMessages[]` 数组
- **tool_use 块** 被提取到 `toolUseBlocks[]`，设置 `needsFollowUp = true`
- **StreamingToolExecutor** 在流式过程中就开始并行执行工具（不等流结束）
- 可恢复的错误（prompt-too-long、max-output-tokens）被**暂扣**（withheld），先尝试恢复

流式回调中的关键守卫：
- `backfillObservableInput()` —— 为 tool_use 块回填可观察字段（如文件路径展开），但只在添加了新字段时才克隆消息，避免破坏 prompt cache 的字节一致性
- 流式降级检测——如果 `streamingFallbackOccured`，已收集的消息被标记为 tombstone，清空后重试

### 阶段 3：工具执行（Tool Execution）

如果 `needsFollowUp` 为 true，循环不会终止，而是执行工具：

```typescript
// 两种工具执行器（互斥）
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()  // 流式：获取已完成的+等待中的
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)
```

工具结果通过 `normalizeMessagesForAPI()` 标准化后，与原始消息合并，进入**下一轮循环迭代**。

### 阶段 4：终止或继续

每次迭代结束时，根据条件决定 `return`（终止）或 `continue`（继续）：

## 终止条件（源码级）

循环有多种终止路径，按触发时机排列：

| 终止原因 | 触发位置 | 机制 |
|----------|---------|------|
| **blocking_limit** | 第 686 行 | Token 计数超过硬限制（非 autocompact 模式）→ 生成 PTL 错误消息 → 返回 |
| **image_error** | 第 1021 行 | `ImageSizeError` / `ImageResizeError` 异常 → 直接返回 |
| **model_error** | 第 1040 行 | `callModel()` 抛出不可恢复异常 → 生成错误消息 → 返回 |
| **aborted_streaming** | 第 1095 行 | `abortController.signal.aborted`（流式阶段）→ 为未完成的 tool_use 生成合成 tool_result → 返回 |
| **prompt_too_long** | 第 1219/1226 行 | 413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回 |
| **completed** | 第 1308 行 | API 错误（限流、认证失败等）导致无法继续 → 返回 |
| **stop_hook_prevented** | 第 1323 行 | Stop hook 返回 `preventContinuation: true` → 返回 |
| **completed** | 第 1401 行 | 正常完成：AI 未发出 tool_use → `needsFollowUp = false` → 经过 stop hooks → 返回 |
| **aborted_tools** | 第 1559 行 | `abortController.signal.aborted`（工具执行阶段）→ 返回 |
| **hook_stopped** | 第 1564 行 | 工具执行期间 hook 返回 `shouldPreventContinuation` → 返回 |
| **max_turns** | 第 1755 行 | 轮次计数超过 `maxTurns` 限制 → 返回 |

## 继续条件（恢复路径）

循环不仅是一个简单的"有 tool_use 就继续"，它还包含多种恢复/重试路径：

### 1. 正常工具循环（`next_turn`）
`needsFollowUp = true` → 执行工具 → 新消息追加到 `messagesForQuery` → state 重新赋值 → `continue`

### 2. max_output_tokens 恢复（`max_output_tokens_escalate` / `max_output_tokens_recovery`）
当 AI 输出被截断时（`apiError === 'max_output_tokens'`），分两阶段恢复：
- **提升阶段**（`max_output_tokens_escalate`）：首次截断时，将 `maxOutputTokens` 从默认值提升到 `ESCALATED_MAX_TOKENS`（64K）。静默重试，不注入 meta 消息。
- **恢复阶段**（`max_output_tokens_recovery`）：提升后仍然截断时，注入恢复消息"Output token limit hit. Resume directly..."，最多重试 `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3` 次。恢复耗尽后，暂扣的错误消息被释放。

### 3. Prompt-Too-Long 恢复（`collapse_drain_retry` / `reactive_compact_retry`）
当遇到 413 错误时，按优先级尝试两种压缩策略：
- **Context Collapse Drain**（`collapse_drain_retry`）：提交所有已暂存的折叠（collapse），释放空间后重试。如果上一轮已经是 `collapse_drain_retry` 则跳过，避免无限循环。
- **Reactive Compact**（`reactive_compact_retry`）：如果 collapse drain 无法恢复，触发即时压缩（reactive compact），生成摘要后重试。`hasAttemptedReactiveCompact` 标志防止无限循环。

### 4. Stop Hook 阻塞重试（`stop_hook_blocking`）
Stop hook 可以注入阻塞错误消息，强制 AI 重新思考。新的消息（包含阻塞错误）被追加到对话中，`stopHookActive = true`，进入下一轮迭代。

### 5. Token Budget 继续提示（`token_budget_continuation`）
当 `TOKEN_BUDGET` feature 启用时，如果 token 消耗达到阈值但未超出预算，注入 nudge 消息让 AI 加速收尾，然后继续。

## 模型降级（Fallback）

当主模型不可用时（`FallbackTriggeredError`，`src/query.ts` 中 `attemptWithFallback` 循环的 catch 分支）：

1. 已收集的 `assistantMessages` 被清空，tool_use 块收到合成 tool_result："Model fallback triggered"
2. 思维签名块被移除（`stripSignatureBlocks`）—— 因为思维签名与模型绑定，跨模型回放会 400
3. 切换到 `fallbackModel`，更新 `toolUseContext.options.mainLoopModel`
4. 生成系统消息："Switched to {fallback} due to high demand for {original}"
5. 重新发起流式请求

## 状态机：State 对象

每次迭代的状态通过 `State` 类型（`src/query.ts`，类型定义）传递：

```typescript
// src/query.ts — State 类型定义
type State = {
  messages: Message[]                        // 当前对话消息
  toolUseContext: ToolUseContext              // 工具上下文（含权限）
  autoCompactTracking: AutoCompactTrackingState | undefined  // 压缩跟踪
  maxOutputTokensRecoveryCount: number       // 输出截断恢复计数
  hasAttemptedReactiveCompact: boolean       // 是否已尝试即时压缩
  maxOutputTokensOverride: number | undefined // 输出 token 上限覆盖
  pendingToolUseSummary: Promise<...> | undefined  // 异步工具摘要
  stopHookActive: boolean | undefined        // Stop hook 是否激活
  turnCount: number                          // 轮次计数
  transition: Continue | undefined           // 上一次继续的原因
}
```

每次 `continue` 都创建新的 State 对象（不可变更新），而非就地修改。`transition` 字段记录了为什么继续——让后续迭代能检测特定恢复路径（如 `collapse_drain_retry`）避免循环。

## Token Budget（实验性）

当 `TOKEN_BUDGET` feature 启用时（`src/query.ts` 中 `!needsFollowUp` 分支内的预算检查逻辑），循环在终止前会检查 token 消耗：

- **continuation**：未达到预算但超过阈值 → 注入 nudge 消息，让 AI 加速收尾
- **diminishing_returns**：检测到收益递减 → 提前终止
- 预算数据来自 `createBudgetTracker()`，跨迭代累计

## 为什么不是"一次规划，批量执行"

<Note>
源码揭示了为什么 Claude Code 选择逐步循环：
</Note>

- **每一步都产生真实信息**：`runTools()` 返回的 `toolResults` 是 API 不可能预知的——命令输出、文件内容、错误信息
- **动态上下文管理**：每轮迭代前都重新评估压缩需求（autocompact → microcompact → snip），基于最新的 token 计数
- **错误即时恢复**：工具失败不需要推倒重来——stop hook 可以注入阻塞错误让 AI 修正策略
- **用户可控**：`abortController.signal` 在循环的多个检查点被检测（第 1059、1095、1529 行），用户按 ESC 可以优雅中断
- **成本控制**：Token Budget 在每轮终止前检查，防止 AI 无效循环

## 一个完整的迭代示例

> 用户："帮我找到项目里所有未使用的导入语句，然后删掉它们"

```
迭代 1: 思考→行动
  预处理管道: applyToolResultBudget → snipCompact(HISTORY_SNIP feature) → microcompact → applyCollapses(CONTEXT_COLLAPSE feature) → autocompact
    → 上下文很短，无需压缩
  API 调用: 返回 tool_use(Glob, "**/*.ts")
  工具执行: 返回 42 个文件路径
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 2: 思考→行动
  预处理管道: 42 个文件结果仍在预算内
  API 调用: 返回 tool_use(Grep, "import.*from")
  工具执行: 在 15 个文件中找到 120 条 import
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 3: 思考→行动（多轮）
  预处理管道: 120 条 Grep 结果触发 microcompact → 摘要化
  API 调用: 返回 3 个 tool_use(FileEdit, ...)
  工具执行: 删除 5 条未使用导入
  → needsFollowUp = true
  → transition: { reason: 'next_turn' }, continue

迭代 4: 总结
  API 调用: 返回纯文本"已清理 3 个文件中的 5 条未使用导入"
  → needsFollowUp = false
  → Stop hooks 通过
  → Token Budget 检查通过（如果启用）
  → return { reason: 'completed' }
```
