Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Deep Dive: Claude Code Agentic Loop

Deep Dive: Claude Code Agentic Loop

核心定位

Claude Code 的自主性来自 src/query.ts 中的一个 while(true) 循环。这个循环驱动了整个 agentic 工作流:发送 prompt → 接收流式响应 → 检测工具调用 → 执行工具 → 将结果追加回消息历史 → 再次调用模型。循环一直持续到模型不再请求任何工具调用,或触发了某个终止条件。


1. Loop Architecture(循环架构)

1.1 入口与循环封装

query()                    ← 公开的 async generator,外层包装
  └── queryLoop()          ← 实际 while(true) 循环所在

query()(src/query.ts:219)是一个 AsyncGenerator,它调用内部的 queryLoop()(src/query.ts:241),后者包含 while(true) 主循环。外层的 query() 负责在循环正常结束时发送 completed 生命周期通知(notifyCommandLifecycle)。

1.2 Mermaid 流程图

1.3 循环体的四大阶段

每次循环迭代(src/query.ts:307)经历以下阶段:

阶段行号范围说明
预处理L365–L648microcompact → context collapse → autocompact → 阻塞检查
API 调用 + 流式接收L653–L997deps.callModel() 生成器,逐 chunk 处理 tool_use 和文本
错误恢复L1062–L1256prompt-too-long → collapse drain → reactive compact → max_output_tokens
工具执行 + 继续决策L1360–L1728runTools / getRemainingResults → stop hooks → token budget → 下一轮

2. State Machine(状态机)

2.1 State 类型定义

// src/query.ts:204
type State = {
  messages: Message[]                        // 跨迭代的消息历史
  toolUseContext: ToolUseContext              // 工具上下文(可变)
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number       // max_output_tokens 重试计数
  hasAttemptedReactiveCompact: boolean       // 是否已尝试过 reactive compact
  maxOutputTokensOverride: number | undefined
  pendingToolUseSummary: Promise<...> | undefined
  stopHookActive: boolean | undefined
  turnCount: number                          // 当前 turn 计数
  transition: Continue | undefined           // 上一轮为何继续
}

2.2 Continue / Terminal 类型

transition.reason 枚举了所有循环继续的原因:

reason 值含义行号
next_turn正常:有工具结果,进入下一轮L1725
stop_hook_blockingStop hook 产生了 blocking error,注入模型后重试L1302
reactive_compact_retryReactive compact 成功,重试 API 调用L1162
collapse_drain_retryContext collapse 排空成功,重试 API 调用L1110
max_output_tokens_escalate8k → 64k token 上限升级L1217
max_output_tokens_recovery注入恢复消息,让模型继续L1246
token_budget_continuationToken 预算 nudge,注入 meta 消息L1338

Terminal 返回值(循环结束):

reason 值含义行号
completed模型未请求工具调用,对话自然结束L1264, L1357
stop_hook_preventedStop hook 设置了 preventContinuationL1279
aborted_streaming用户在流式接收时中止 (Ctrl+C)L1051
aborted_tools用户在工具执行时中止L1515
max_turns达到 maxTurns 限制L1711
hook_stopped工具 hook 设置了 hook_stopped_continuationL1520
blocking_limit硬性阻塞限制(未压缩时 token 超限)L646
prompt_too_long所有恢复手段耗尽,提示过长L1175, L1182
model_error模型调用抛出异常L996
image_error图片尺寸/缩放错误L977

2.3 Mermaid 状态转移图


3. Tool Call Detection & Dispatch(工具调用检测与分发)

3.1 流式检测

在 API 流式接收循环中(src/query.ts:659),每个到达的 assistant chunk 都被检查:

// src/query.ts:829-835
const msgToolUseBlocks = message.message.content.filter(
  content => content.type === 'tool_use',
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
  toolUseBlocks.push(...msgToolUseBlocks)
  needsFollowUp = true   // ← 这是循环是否继续的唯一标志
}

needsFollowUp 是决定循环走向的核心变量:

  • needsFollowUp = true:进入工具执行阶段,然后 continue 到下一轮
  • needsFollowUp = false:进入"无工具调用"路径,检查恢复或终止

3.2 StreamingToolExecutor(流式工具执行器)

当 config.gates.streamingToolExecution 开启时(src/query.ts:561),使用 StreamingToolExecutor。它的核心优势是:在模型还在流式输出时,就可以开始执行已经到达的工具调用。

// src/services/tools/StreamingToolExecutor.ts:40
export class StreamingToolExecutor {
  private tools: TrackedTool[] = []
  private siblingAbortController: AbortController  // 兄弟工具错误时联动中止
  private discarded = false

  addTool(block, assistantMessage): void        // 注册工具,立即尝试执行
  *getCompletedResults(): Generator<...>        // 取已完成的结果(非阻塞)
  async *getRemainingResults(): AsyncGenerator<...>  // 等待所有剩余工具完成
}

工具状态机:'queued' → 'executing' → 'completed' → 'yielded'

每次 assistant chunk 到达后(src/query.ts:848-862):

// 在流式循环内,每收到一个 assistant chunk 就:
for (const toolBlock of msgToolUseBlocks) {
  streamingToolExecutor.addTool(toolBlock, message)  // 注册并可能立即开始执行
}
// 然后取已经完成的结果
for (const result of streamingToolExecutor.getCompletedResults()) {
  yield result.message  // 立即输出工具结果
  toolResults.push(...)
}

这意味着一个 Read 工具可能在模型还在输出后续的 Grep 工具调用时就已经完成了。

3.3 非流式路径(runTools)

当 StreamingToolExecutor 未启用时,使用 runTools()(src/services/tools/toolOrchestration.ts:19)。它在流结束后一次性执行所有工具。


4. Concurrent vs Sequential Tool Execution(并发 vs 串行)

4.1 分区逻辑

partitionToolCalls()(src/services/tools/toolOrchestration.ts:91)将工具调用列表分为若干批次(batches):

工具列表: [Read(a), Read(b), Bash(rm), Read(c), Write(d)]
分区结果:
  Batch 1: { concurrencySafe: true,  blocks: [Read(a), Read(b)] }
  Batch 2: { concurrencySafe: false, blocks: [Bash(rm)] }
  Batch 3: { concurrencySafe: true,  blocks: [Read(c)] }
  Batch 4: { concurrencySafe: false, blocks: [Write(d)] }

规则:

  • 连续的并发安全工具合并为一个批次,并行执行
  • 非并发安全工具单独成批,串行执行
  • 并发安全判断由每个工具的 isConcurrencySafe(input) 方法决定(src/services/tools/toolOrchestration.ts:101)

4.2 并发执行机制

// src/services/tools/toolOrchestration.ts:152
async function* runToolsConcurrently(toolUseMessages, ...) {
  yield* all(
    toolUseMessages.map(async function* (toolUse) {
      yield* runToolUse(toolUse, ...)
    }),
    getMaxToolUseConcurrency(),  // 默认 10,可通过 CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY 调整
  )
}

all()(来自 src/utils/generators.ts)是一个并发生成器,同时运行 N 个工具但将结果交错产出。

4.3 串行执行机制

// src/services/tools/toolOrchestration.ts:118
async function* runToolsSerially(toolUseMessages, ...) {
  let currentContext = toolUseContext
  for (const toolUse of toolUseMessages) {
    for await (const update of runToolUse(toolUse, ...)) {
      if (update.contextModifier) {
        currentContext = update.contextModifier.modifyContext(currentContext)
      }
      yield { message: update.message, newContext: currentContext }
    }
    markToolUseAsComplete(toolUseContext, toolUse.id)
  }
}

串行工具可以修改 context(contextModifier),后续工具可以看到修改后的上下文。

4.4 StreamingToolExecutor 的并发控制

StreamingToolExecutor(src/services/tools/StreamingToolExecutor.ts:129)也遵循相同规则:

private canExecuteTool(isConcurrencySafe: boolean): boolean {
  const executingTools = this.tools.filter(t => t.status === 'executing')
  return (
    executingTools.length === 0 ||
    (isConcurrencySafe && executingTools.every(t => t.isConcurrencySafe))
  )
}
  • 没有正在执行的工具 → 可以启动
  • 当前工具是并发安全的,且所有正在执行的也是并发安全的 → 可以并发
  • 否则等待(对于非并发工具,还会阻止后续工具启动)

4.5 Bash 错误的兄弟中止

// src/services/tools/StreamingToolExecutor.ts:357-363
if (tool.block.name === BASH_TOOL_NAME) {
  this.hasErrored = true
  this.erroredToolDescription = this.getToolDescription(tool)
  this.siblingAbortController.abort('sibling_error')  // 杀死所有并发 Bash
}

只有 Bash 工具的错误会触发兄弟中止。Read/WebFetch 等独立工具的失败不会影响其他并发工具。


5. Stop Conditions(终止条件)

5.1 自然终止:无工具调用

最常见的情况。模型在最终 assistant message 中只产出 text/thinking 块,没有 tool_use 块:

// src/query.ts:1062
if (!needsFollowUp) {
  // ... 恢复检查 ...
  // 进入 stop hooks
  // 然后检查 token budget
  return { reason: 'completed' }
}

5.2 maxTurns 限制

// src/query.ts:1705
if (maxTurns && nextTurnCount > maxTurns) {
  yield createAttachmentMessage({
    type: 'max_turns_reached',
    maxTurns,
    turnCount: nextTurnCount,
  })
  return { reason: 'max_turns', turnCount: nextTurnCount }
}

maxTurns 在 QueryParams 中传入,每次迭代后 turnCount + 1。

5.3 用户中止(Abort)

两种中止场景:

  1. 流式接收期间(src/query.ts:1015):toolUseContext.abortController.signal.aborted

    • StreamingToolExecutor 消费 getRemainingResults() 为已排队/执行中的工具生成合成错误
    • 返回 { reason: 'aborted_streaming' }
  2. 工具执行期间(src/query.ts:1485):同样的 abort 检查

    • 返回 { reason: 'aborted_tools' }

Abort signal 的 reason 区分 'interrupt'(用户输入新消息打断)和普通取消。

5.4 Stop Hooks

Stop hooks 在模型完成响应且无工具调用后执行(src/query/stopHooks.ts:65):

const stopHookResult = yield* handleStopHooks(...)

if (stopHookResult.preventContinuation) {
  return { reason: 'stop_hook_prevented' }
}

if (stopHookResult.blockingErrors.length > 0) {
  // 注入 blocking error 作为 meta 消息,continue 到下一轮
  state = { ..., transition: { reason: 'stop_hook_blocking' }, stopHookActive: true }
  continue
}

Stop hook 的 JSON 输出支持 stopReason 字段来解释为什么阻止继续。

5.5 Token Budget

// src/query.ts:1308
if (feature('TOKEN_BUDGET')) {
  const decision = checkTokenBudget(budgetTracker!, ...)
  if (decision.action === 'continue') {
    // 注入 nudge message,继续循环
    state = { ..., transition: { reason: 'token_budget_continuation' } }
    continue
  }
  // action === 'stop' → fall through to return { reason: 'completed' }
}

Token budget 逻辑(src/query/tokenBudget.ts:45):

  • 阈值:90%(COMPLETION_THRESHOLD)
  • 递减收益检测:连续 3 次 continuation 后,如果增量 < 500 tokens 则停止
  • 用于 Agent 模式,让 Claude Code 在预算内持续工作

5.6 Blocking Limit(硬性阻塞)

// src/query.ts:628-648
if (!compactionResult && ...) {
  const { isAtBlockingLimit } = calculateTokenWarningState(...)
  if (isAtBlockingLimit) {
    yield createAssistantAPIErrorMessage({ content: PROMPT_TOO_LONG_ERROR_MESSAGE })
    return { reason: 'blocking_limit' }
  }
}

仅在自动压缩关闭时生效,为 /compact 命令保留空间。

5.7 Hook Stopped Continuation

// src/query.ts:1519
if (shouldPreventContinuation) {
  return { reason: 'hook_stopped' }
}

工具执行的 hook(如 PreToolUse / PostToolUse)可以通过 hook_stopped_continuation attachment 阻止循环继续。


6. Error Recovery(错误恢复)

6.1 Prompt Too Long(413)三级恢复

API 413 错误
  ├── Level 1: Context Collapse 排空 (collapse_drain_retry)
  │   └── 将 staged collapses 全部 commit,减少 token 数
  │       └── 仅在 transition !== 'collapse_drain_retry' 时尝试(防重入)
  ├── Level 2: Reactive Compact (reactive_compact_retry)
  │   └── 将整个消息历史压缩为摘要
  │       └── 仅在 hasAttemptedReactiveCompact === false 时尝试
  └── Level 3: 无法恢复 → 返回 prompt_too_long

关键实现:错误消息在流式接收时被"暂扣"(withheld),不立即 yield 给调用者,给恢复路径留出机会。

// src/query.ts:799-822
let withheld = false
if (contextCollapse?.isWithheldPromptTooLong(message, ...)) { withheld = true }
if (reactiveCompact?.isWithheldPromptTooLong(message)) { withheld = true }
if (isWithheldMaxOutputTokens(message)) { withheld = true }
if (!withheld) { yield yieldMessage }  // 只有非暂扣消息才输出

6.2 Max Output Tokens 二级恢复

模型输出截断 (max_output_tokens)
  ├── Level 1: Cap 升级 (8k → 64k)
  │   └── 使用 ESCALATED_MAX_TOKENS 重试同一请求
  │       └── 仅在 maxOutputTokensOverride === undefined 时触发
  ├── Level 2: 多轮恢复 (最多 3 次)
  │   └── 注入 meta 消息: "Output token limit hit. Resume directly..."
  │       └── 消息拼接: [...messagesForQuery, ...assistantMessages, recoveryMessage]
  └── Level 3: 恢复耗尽 → 输出暂扣的错误消息

6.3 Model Fallback

// src/query.ts:893-953
} catch (innerError) {
  if (innerError instanceof FallbackTriggeredError && fallbackModel) {
    currentModel = fallbackModel
    attemptWithFallback = true
    // 生成合成 tool_result 错误,清空 assistantMessages
    // 创建新的 StreamingToolExecutor
    continue  // 重试 while(attemptWithFallback) 循环
  }
}

6.4 工具执行错误

  • 未知工具:StreamingToolExecutor.addTool() 在 tool 不存在时直接生成 is_error: true 的 tool_result(src/services/tools/StreamingToolExecutor.ts:79)
  • Bash 错误:触发 siblingAbortController.abort() 杀死所有并发 Bash 进程
  • 中断:生成 REJECT_MESSAGE 或 'User rejected tool use' 的合成结果

6.5 流式 Fallback

// src/query.ts:712-740
if (streamingFallbackOccured) {
  // 为已产生的 assistant 消息生成 tombstone(避免签名不一致)
  for (const msg of assistantMessages) {
    yield { type: 'tombstone', message: msg }
  }
  // 清空所有状态
  assistantMessages.length = 0
  toolResults.length = 0
  toolUseBlocks.length = 0
  // 丢弃旧 executor,创建新 executor
  streamingToolExecutor.discard()
  streamingToolExecutor = new StreamingToolExecutor(...)
}

7. Key Code References(关键代码索引)

模块文件关键行内容
主循环src/query.tsL307while (true) 主循环入口
State 类型src/query.tsL204–L217跨迭代的可变状态
流式接收src/query.tsL659for await (const message of deps.callModel(...))
工具检测src/query.tsL829–L835tool_use block 过滤 + needsFollowUp 标记
工具注册src/query.tsL841–L843streamingToolExecutor.addTool(toolBlock, message)
结果消费src/query.tsL851–L862streamingToolExecutor.getCompletedResults()
恢复入口src/query.tsL1062if (!needsFollowUp) 分支
Stop Hookssrc/query.tsL1267–L1306handleStopHooks 调用 + blocking error 处理
Token Budgetsrc/query.tsL1308–L1355TOKEN_BUDGET feature gate 内的检查
工具执行src/query.tsL1380–L1408runTools / getRemainingResults 消费循环
下一轮状态src/query.tsL1715–L1727state = { ... } continue 赋值
maxTurnssrc/query.tsL1705–L1712turn 上限检查
流式工具执行器src/services/tools/StreamingToolExecutor.tsL40–L519全类
并发控制StreamingToolExecutor.tsL129–L135canExecuteTool()
兄弟中止StreamingToolExecutor.tsL357–L363Bash 错误 → sibling abort
工具编排src/services/tools/toolOrchestration.tsL19–L82runTools() 入口
分区逻辑toolOrchestration.tsL91–L116partitionToolCalls()
并发执行toolOrchestration.tsL152–L177runToolsConcurrently()
串行执行toolOrchestration.tsL118–L150runToolsSerially()
Stop Hooks 处理src/query/stopHooks.tsL65–L473handleStopHooks() 完整实现
Token Budgetsrc/query/tokenBudget.tsL45–L93checkTokenBudget() 决策逻辑
工具执行src/services/tools/toolExecution.tsL1–L1745runToolUse() 单工具执行

8. Architecture Summary(架构总结)

Claude Code 的 agentic loop 本质上是一个递归状态机,但用 while(true) + state = next; continue 实现而非递归调用。每次迭代的逻辑链路是:

[预处理上下文] → [调用模型] → [流式接收 + 实时执行工具] → [执行剩余工具] → [评估终止条件] → [构建下一轮 State] → continue

设计要点:

  1. 流式工具执行:模型还在输出时就开始执行工具,大幅降低总延迟
  2. 渐进式恢复:prompt-too-long 和 max_output_tokens 都有分级恢复,尽量不中断用户工作流
  3. 状态不可变传递:state = { ...state, ...changes } 模式,避免直接修改导致状态不一致
  4. 分区并发:只对 isConcurrencySafe 的工具并行执行,写操作保证串行
  5. 双层终止:既有自然终止(模型不再调用工具),又有强制终止(maxTurns / abort / budget / hooks)