Skip to main content

什么是 Agentic Loop

传统聊天机器人:你问一句,它答一句。
Claude Code 不一样:你说一个需求,它可能连续执行十几步操作才给你最终结果。
这背后的机制叫做 Agentic Loop(智能体循环),核心实现在 src/query.tsqueryLoop() 异步生成器函数(第 241 行)。它是一个 while(true) 无限循环,每次迭代代表一次”思考→行动→观察”周期。
Agentic Loop 循环图

循环的完整结构

queryLoop() 的每次迭代(src/query.ts:307 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() 发起流式请求(第 659 行),返回一个 AsyncGenerator。在流式过程中:
  • AssistantMessage 被收集到 assistantMessages[] 数组
  • tool_use 块 被提取到 toolUseBlocks[],设置 needsFollowUp = true
  • StreamingToolExecutor 在流式过程中就开始并行执行工具(不等流结束)
  • 可恢复的错误(prompt-too-long、max-output-tokens)被暂扣(withheld),先尝试恢复
流式回调中的关键守卫:
  • backfillObservableInput()(第 763 行)—— 为 tool_use 块回填可观察字段(如文件路径展开),但只在添加了新字段时才克隆消息,避免破坏 prompt cache 的字节一致性
  • 流式降级检测——如果 streamingFallbackOccured,已收集的消息被标记为 tombstone(第 717 行),清空后重试

阶段 3:工具执行(Tool Execution)

如果 needsFollowUp 为 true,循环不会终止,而是执行工具:
// 两种工具执行器(互斥)
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()  // 流式:获取已完成的+等待中的
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)
工具结果通过 normalizeMessagesForAPI() 标准化后,与原始消息合并,进入下一轮循环迭代

阶段 4:终止或继续

每次迭代结束时,根据条件决定 return(终止)或 continue(继续):

7 种终止条件(源码级)

终止原因触发位置机制
completed第 1360 行AI 未发出 tool_use → needsFollowUp = false → 经过 stop hooks → 返回
blocking_limit第 646 行Token 计数超过硬限制(非 autocompact 模式)→ 生成 PTL 错误消息 → 返回
aborted_streaming第 1054 行abortController.signal.aborted → 为未完成的 tool_use 生成合成 tool_result → 返回
model_error第 999 行callModel() 抛出异常 → 生成错误消息 → 返回
prompt_too_long第 1178 行413 错误且 reactive compact 无法恢复 → 暂扣的错误消息被释放 → 返回
image_error第 980/1178 行图片尺寸/大小错误 → 直接返回
stop_hook_prevented第 1282 行Stop hook 返回 preventContinuation: true → 返回

4 种继续条件(恢复路径)

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

1. 正常工具循环

needsFollowUp = true → 执行工具 → 新消息追加到 messagesForQuerycontinue

2. max_output_tokens 恢复(第 1191-1255 行)

当 AI 输出被截断时(apiError === 'max_output_tokens'):
  • 首次:尝试将 maxOutputTokens 从默认值提升到 ESCALATED_MAX_TOKENS(64K),无 meta 消息,静默重试
  • 后续:注入恢复消息”Output token limit hit. Resume directly…”,最多重试 MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3
  • 恢复耗尽后,暂扣的错误消息被释放

3. Prompt-Too-Long 恢复(第 1088-1186 行)

当遇到 413 错误时,有两个恢复阶段:
  • Context Collapse Drain(第 1097 行):提交所有已暂存的折叠,释放空间后重试。如果上一轮已经是 collapse_drain_retry 则跳过
  • Reactive Compact(第 1123 行):触发即时压缩,生成摘要后重试。hasAttemptedReactiveCompact 防止无限循环

4. Stop Hook 阻塞重试(第 1285-1308 行)

Stop hook 可以注入阻塞错误消息,强制 AI 重新思考。新的消息(包含阻塞错误)被追加到对话中,stopHookActive = true,进入下一轮迭代。

模型降级(Fallback)

当主模型不可用时(FallbackTriggeredError,第 897 行):
  1. 已收集的 assistantMessages 被清空,tool_use 块收到合成 tool_result:“Model fallback triggered”
  2. 思维签名块被移除(stripSignatureBlocks)—— 因为思维签名与模型绑定,跨模型回放会 400
  3. 切换到 fallbackModel,更新 toolUseContext.options.mainLoopModel
  4. 生成系统消息:“Switched to due to high demand for
  5. 重新发起流式请求

状态机:State 对象

每次迭代的状态通过 State 类型(第 204 行)传递:
type State = {
  messages: Message[]                        // 当前对话消息
  toolUseContext: ToolUseContext              // 工具上下文(含权限)
  autoCompactTracking: AutoCompactTrackingState  // 压缩跟踪
  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 启用时(第 1311 行),循环在终止前会检查 token 消耗:
  • continuation:未达到预算但超过阈值 → 注入 nudge 消息,让 AI 加速收尾
  • diminishing_returns:检测到收益递减 → 提前终止
  • 预算数据来自 createBudgetTracker(),跨迭代累计

为什么不是”一次规划,批量执行”

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

一个完整的迭代示例

用户:“帮我找到项目里所有未使用的导入语句,然后删掉它们”
迭代 1: 思考→行动
  预处理: 无需压缩(上下文很短)
  API 调用: 返回 tool_use(Glob, "**/*.ts")
  工具执行: 返回 42 个文件路径
  → needsFollowUp = true, continue

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

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

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