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–L648 | microcompact → context collapse → autocompact → 阻塞检查 |
| API 调用 + 流式接收 | L653–L997 | deps.callModel() 生成器,逐 chunk 处理 tool_use 和文本 |
| 错误恢复 | L1062–L1256 | prompt-too-long → collapse drain → reactive compact → max_output_tokens |
| 工具执行 + 继续决策 | L1360–L1728 | runTools / 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_blocking | Stop hook 产生了 blocking error,注入模型后重试 | L1302 |
reactive_compact_retry | Reactive compact 成功,重试 API 调用 | L1162 |
collapse_drain_retry | Context collapse 排空成功,重试 API 调用 | L1110 |
max_output_tokens_escalate | 8k → 64k token 上限升级 | L1217 |
max_output_tokens_recovery | 注入恢复消息,让模型继续 | L1246 |
token_budget_continuation | Token 预算 nudge,注入 meta 消息 | L1338 |
Terminal 返回值(循环结束):
| reason 值 | 含义 | 行号 |
|---|---|---|
completed | 模型未请求工具调用,对话自然结束 | L1264, L1357 |
stop_hook_prevented | Stop hook 设置了 preventContinuation | L1279 |
aborted_streaming | 用户在流式接收时中止 (Ctrl+C) | L1051 |
aborted_tools | 用户在工具执行时中止 | L1515 |
max_turns | 达到 maxTurns 限制 | L1711 |
hook_stopped | 工具 hook 设置了 hook_stopped_continuation | L1520 |
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)
两种中止场景:
流式接收期间(
src/query.ts:1015):toolUseContext.abortController.signal.aborted- StreamingToolExecutor 消费
getRemainingResults()为已排队/执行中的工具生成合成错误 - 返回
{ reason: 'aborted_streaming' }
- StreamingToolExecutor 消费
工具执行期间(
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.ts | L307 | while (true) 主循环入口 |
| State 类型 | src/query.ts | L204–L217 | 跨迭代的可变状态 |
| 流式接收 | src/query.ts | L659 | for await (const message of deps.callModel(...)) |
| 工具检测 | src/query.ts | L829–L835 | tool_use block 过滤 + needsFollowUp 标记 |
| 工具注册 | src/query.ts | L841–L843 | streamingToolExecutor.addTool(toolBlock, message) |
| 结果消费 | src/query.ts | L851–L862 | streamingToolExecutor.getCompletedResults() |
| 恢复入口 | src/query.ts | L1062 | if (!needsFollowUp) 分支 |
| Stop Hooks | src/query.ts | L1267–L1306 | handleStopHooks 调用 + blocking error 处理 |
| Token Budget | src/query.ts | L1308–L1355 | TOKEN_BUDGET feature gate 内的检查 |
| 工具执行 | src/query.ts | L1380–L1408 | runTools / getRemainingResults 消费循环 |
| 下一轮状态 | src/query.ts | L1715–L1727 | state = { ... } continue 赋值 |
| maxTurns | src/query.ts | L1705–L1712 | turn 上限检查 |
| 流式工具执行器 | src/services/tools/StreamingToolExecutor.ts | L40–L519 | 全类 |
| 并发控制 | StreamingToolExecutor.ts | L129–L135 | canExecuteTool() |
| 兄弟中止 | StreamingToolExecutor.ts | L357–L363 | Bash 错误 → sibling abort |
| 工具编排 | src/services/tools/toolOrchestration.ts | L19–L82 | runTools() 入口 |
| 分区逻辑 | toolOrchestration.ts | L91–L116 | partitionToolCalls() |
| 并发执行 | toolOrchestration.ts | L152–L177 | runToolsConcurrently() |
| 串行执行 | toolOrchestration.ts | L118–L150 | runToolsSerially() |
| Stop Hooks 处理 | src/query/stopHooks.ts | L65–L473 | handleStopHooks() 完整实现 |
| Token Budget | src/query/tokenBudget.ts | L45–L93 | checkTokenBudget() 决策逻辑 |
| 工具执行 | src/services/tools/toolExecution.ts | L1–L1745 | runToolUse() 单工具执行 |
8. Architecture Summary(架构总结)
Claude Code 的 agentic loop 本质上是一个递归状态机,但用 while(true) + state = next; continue 实现而非递归调用。每次迭代的逻辑链路是:
[预处理上下文] → [调用模型] → [流式接收 + 实时执行工具] → [执行剩余工具] → [评估终止条件] → [构建下一轮 State] → continue
设计要点:
- 流式工具执行:模型还在输出时就开始执行工具,大幅降低总延迟
- 渐进式恢复:prompt-too-long 和 max_output_tokens 都有分级恢复,尽量不中断用户工作流
- 状态不可变传递:
state = { ...state, ...changes }模式,避免直接修改导致状态不一致 - 分区并发:只对
isConcurrencySafe的工具并行执行,写操作保证串行 - 双层终止:既有自然终止(模型不再调用工具),又有强制终止(maxTurns / abort / budget / hooks)