queryLoop 核心机制

本质:一个 while(true) 的 agent 主循环

queryLoopquery.ts:241-1729)是 Claude Code 的心脏。它把“用户一句话”变成“模型 + 工具反复协作直到任务完成”的循环。骨架极简:

while (true) {
  // 拆出本轮状态
  let { toolUseContext } = state

  // ① 调模型、流式接收回复
  // ② 根据回复里有没有 tool_use 块分叉
  if (!needsFollowUp) {
    // 收尾路径:恢复尝试 -> stop hook -> token budget -> return completed
  }

  // ③ 执行工具、收集结果 -> continue 进下一轮
}

它只能靠 return 退出(while(true) 没有条件),靠 continue 重启迭代自救或推进。理解它的关键是三样东西:

  • State — 跨轮状态
  • toolUseContext — 共享总线
  • needsFollowUp — 继续/结束的开关

一、核心骨架与三块基石

1. State:跨轮携带的可变状态(:204-217

type State = {
  messages: Message[]                       // 对话历史(含 tool_use/tool_result 对)
  toolUseContext: ToolUseContext             // 共享运行时上下文
  autoCompactTracking                       // 压缩状态(跨轮延续)
  maxOutputTokensRecoveryCount              // 截断恢复计数
  hasAttemptedReactiveCompact                // compact 是否已试过
  maxOutputTokensOverride                   // 输出上限覆盖
  pendingToolUseSummary                     // 上轮启动的摘要 promise
  stopHookActive                            // stop hook 阻塞中?
  turnCount                                 // 轮次
  transition                                // 上轮为何 continue(测试+守卫用)
}

每个 continue 都构造一个新 State改写哪些字段、保留哪些字段是理解每个 continue 的钥匙——改写的是本轮新行为,保留的是防死循环守卫。


2. toolUseContext:贯穿一切的共享总线

它是调用工具时传入的运行时对象,把以下内容打包:

  • 配置(options.tools / model
  • 共享状态(readFileState / toolDecisions
  • 控制信号(abortController
  • UI 回调
  • 身份(agentId
  • 追踪(queryTracking

工具从它取所需、往它写副作用;执行器和循环给它追加追踪、合并改写。它的演化(newContext + 可变 mutation)就是工具间状态流转的载体,简单来说,作为唯一的"可变外部环境",它记录了每一轮次发生的变化和一些关键信息,为后续轮次大模型的决策提供依据。
举个例子:

  toolUseContext = {
    // ── 配置(options):本轮可用的"工具箱"和模型设置 ──
    options: {
      tools: [Read, Edit, Write, Bash, Glob, Grep, AgentTool, ...], // 所有可用工具
      mainLoopModel: 'claude-sonnet-4-6',
      thinkingConfig: { type: 'enabled', budget_tokens: 8000 },
      commands: [...],
      mcpClients: [<已连的 MCP server>],
      isNonInteractiveSession: false,
      agentDefinitions: {...},
      refreshTools: f,   // 中途连上 MCP 时刷新工具列表
      ...
    },

    // ── 控制信号 ──
    abortController: AbortController { signal: <未 abort> },  // Ctrl+C 时 abort

    // ── 共享可变状态(跨工具、跨轮累积)──
    readFileState: Map {  // FileStateCache:哪些文件读过、内容/时间戳
      'E:\\...\\src\\query.ts' => { content: '...', timestamp: 1690000000, offset: undefined, limit: undefined }
    },
    toolDecisions: Map { 'toolu_01ABC' => { source: 'config', decision: 'accept', ... } },

    // ── UI / 状态回调 ──
    getAppState: () => ({
      toolPermissionContext: { mode: 'default', alwaysAllowRules: {...}, ... },
      mcp: { tools: [...], clients: [...] },
      fastMode: false,
      effortValue: 'medium',
      ...
    }),
    setAppState: f,
    setInProgressToolUseIDs: f,    // 告诉 UI 哪些工具在跑
    setResponseLength: f,
    updateFileHistoryState: f,     // 改文件历史
    updateAttributionState: f,
    addNotification: f,            // 推通知
    appendSystemMessage: f,
    setToolJSX: f,                 // 渲染工具的自定义 UI

    // ── 身份 ──
    agentId: undefined,            // undefined = 主线程;子代理才有值
    agentType: undefined,

    // ── 当前消息与追踪 ──
    messages: [...],               // 当前 messages 历史
    queryTracking: { chainId: 'abc-123', depth: 2 },  // query 链追踪

    // ── 工具调用级(toolExecution 调 call 前临时加上)──
    toolUseId: 'toolu_01ABCxyz',   // 这次调用的 id
    userModified: false,
  }

3. needsFollowUp:继续/结束的开关

  • 初始化 false:558
  • 流式收到任何 tool_use 块就置 true:834

代码不用不可靠的 stop_reason,改用直接检测 tool_use

  • true → 走工具执行 → next_turn
  • false → 走收尾 → completed

简单来说,当大模型决定还需要使用tool,他就会生成一个tool_use_block放到ToolUseBlock[],needsFollowUp=True,循环继续。当大模型决定只说话不用tool了,ToolUseBlock[]为空,needsFollowUp=False,循环结束
继续的最终决策权在模型手里(它决定吐不吐 tool_use)。


二、一次迭代的完整轨迹

阶段 A:准备(:307-580

每轮开头:

  1. State 拆出 toolUseContext
  2. 启动 memory 预取skill 发现预取(与模型流式并发的后台任务)
  3. toolUseContext 追加 queryTracking、更新 messages
  4. 依次跑上下文管理流水线(把过长的历史压短,避免 prompt too long):
    • snipmicrocompactcontext-collapseautocompact
  5. 算出本轮要发的系统提示词模型工具清单

阶段 B:调模型、流式接收(:653-997

for await (const message of deps.callModel({
  messages: prependUserContext(messagesForQuery, userContext),
  systemPrompt: fullSystemPrompt,
  tools: toolUseContext.options.tools,        // 工具清单(核心工具全量、延迟工具 defer_loading)
  signal: toolUseContext.abortController.signal,
  ...,
})) { ... }

这一段把系统提示词 + 历史 messages + 工具清单发给模型,流式收回复。每收到一条 assistant 消息:

  • 扣留检查:799-822):如果是可恢复的错误(prompt-too-longmax-output-tokens、媒体超限),先扣下不 yield,等后面看能不能恢复。避免把中间错误漏给 SDK 调用方(它们见到 error 就终止会话)。
  • 挑出 tool_use:829-835):toolUseBlocks.push(...) + needsFollowUp = true
  • 流式开跑工具:841-844):开了流式执行的话,streamingToolExecutor.addTool(block, message) 让工具在模型还没说完时就开始跑。
  • 顺手 yield 已完成结果:851):getCompletedResults() 非阻塞地把已跑完的工具结果推给 UI。
  • fallback 处理:894-953):模型降级重试时,清掉半成品、发 tombstone、换 fallback model 重来。

流式期间被 abortreturn aborted_streaming:1051)。


阶段 C:分叉(:1062

if (!needsFollowUp) { ... }   // 模型没要工具 -> 收尾
// 否则 -> 工具执行 -> next_turn

分叉一:收尾路径(!needsFollowUp:1062-1357

模型这轮没要工具(纯文本回复),本该结束。但插了一堆“恢复尝试”(都是 continue,不退出)和“退出点”。按顺序:

恢复尝试(continue,自救重试)

  1. collapse_drain_retry:1115
    解决上下文过长,会自动尝试把一部分历史消息归档掉,缩小上下文体积

    • 被扣留的 413?先提交暂存的 context-collapse 缩小上下文重试。
    • 守卫:transition !== 自身(单发)。
  2. reactive_compact_retry:1164
    collapse drain 没解决(或不适用),跑一次全量摘要,把整段对话压成 summary 替换掉 messages

    • 413 或媒体超限?跑全量摘要压缩重试。
    • 守卫:hasAttemptedReactiveCompact=true(防 compact 螺旋)。
  3. max_output_tokens_escalate:1219
    模型输出被 8k 默认上限截断 ,用同一个请求、更大的输出上限(8k -> 64k ESCALATED_MAX_TOKENS)重试,不加任何消息

    • 输出被 8k 截断?升到 64k 重试。
    • 守卫:maxOutputTokensOverride===undefined(每轮一次)。
  4. max_output_tokens_recovery:1250
    max_output_tokens 截断,且升级(第 3 个)已试过或未启用,且恢复次数 < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT,升级到 64k 仍不够时的多轮恢复。靠"从断点续写 + 拆小任务"逐步完成,而非无限抬高上限。

    • 还截断?注入“从断点续写”消息重试。
    • 守卫:count < 3
  5. stop_hook_blocking:1305
    Stop hook 是用户配置的、在"模型结束一轮、不再调用工具时"触发的 shell 命令。
    典型用途:模型说"我做完了"时,跑个钩子检查"真的做完了吗"–比如"有没有跑测试",“提交信息格式对吗”

    • stop hook 说没干完(如没跑测试)?塞反馈回去让模型返工。
    • 守卫:保留 hasAttemptedReactiveCompact(防跨路径螺旋)。

这五个都是“卡住了 → 自救重试”,每个带防死循环守卫。

退出点(return)

恢复都不适用/失败时:

  • API 错误:1264)→ completed(跳过 stop hook,防 error→hook→retry 死循环)
  • prompt_too_long / image_error:1175/1182)→ 恢复失败,暴露错误
  • stop_hook_prevented:1279)→ stop hook 硬停

推进(continue)

  1. token_budget_continuation:1340
    • 用户给了 +500k 预算、还没花到 90%、且没收益递减?注入 nudge 让模型继续自主干。
    • 这是唯一“模型本可停、系统推它继续”的 continue
    • 守卫:checkTokenBudget 的收益递减检测(续≥3 次且最近两次产出都 <500 token 就停)。

都没触发 → return completed:1357),循环正常结束。


分叉二:工具执行路径(needsFollowUp:1360-1727

模型要了工具,执行它们然后 next_turn 推进。共 8 个阶段

Phase 1:执行工具(:1366-1409

const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()                              // 流式:收尾 drain
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)   // 批量:现跑

二选一拿 async generator。批量路径 runTools 分批:

  • 并发安全工具(读类)并行 ≤ 10
  • 非并发安全工具(写类 / Bash)串行

每个工具走 runToolUse 的完整链:

查表找实现 → Zod 校验类型 → validateInput 校验语义 → PreToolUse 钩子 → 权限(allow/deny/ask)→ tool.call() 真正执行 → mapToolResult 转结果 → PostToolUse 钩子

每条结果 yield 给 UI + push 进 toolResults + 合并工具返回的 newContext

Phase 2:异步启动工具摘要(:1411-1482

用 Haiku 给这批工具生成一句话摘要(如 “Searched in auth/”),给移动端用户看进度,不喂回模型

  • fire-and-forget 存进 nextPendingToolUseSummary
  • 下一轮早期 yield — 把 ~1s Haiku 延迟藏进下一轮 5-30s 模型流式里,零新增延迟
  • 子代理跳过

Phase 3:中止/钩子停拦截(:1484-1521

  • 工具执行中被 abortreturn aborted_tools
  • 某工具钩子要求停 → return hook_stopped

这是 next_turn 路径上的两个 return 闸门。

Phase 4:post-compact 轮次追踪(:1523-1533

若处于压缩后上下文,递增 turnCounter(分析用,与 maxTurnsturnCount 不同)。

Phase 5:收集附件(:1535-1657

必须在工具执行后(API 不允许 tool_resultuser 消息交错)。收集 4 类“非工具结果上下文”喂模型下轮:

  • 排队命令:用户中途输入 / 任务通知(按 agentId 作用域过滤)
  • getAttachmentMessages:最关键的是 edited_text_file(文件被改的 diff,防模型基于过期内容干活)+ nested_memory、各种 delta、todo 提醒、模式状态等(1s 超时尽力而为)
  • memory 预取:零等待消费,用 readFileState 去重
  • skill 发现预取:注入发现的技能

全部 yield + push 进 toolResults(追加在工具结果之后,不交错)。

Phase 6:刷新工具列表(:1659-1671

中途有新 MCP server 连上 → 刷新 tools,下轮可用新工具。

Phase 7:maxTurns 拦截(:1704-1712

turnCount+1 > maxTurnsreturn max_turns。第三个 return 闸门。

Phase 8:构造 next State 并 continue(:1714-1727

const next: State = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],  // ← 工具结果进历史
  toolUseContext: toolUseContextWithQueryTracking,
  turnCount: nextTurnCount,                       // +1
  maxOutputTokensRecoveryCount: 0,               // 重置
  hasAttemptedReactiveCompact: false,             // 重置
  pendingToolUseSummary: nextPendingToolUseSummary,
  transition: { reason: 'next_turn' },
  ...
}
state = next  // continue 进下一轮
  • 恢复计数重置(新轮清零)
  • 跨轮状态保留autoCompactTracking

next_turn 是 agent loop 的心跳:模型调工具 → 执行 → 喂结果回去 → 模型继续。


三、全局机制与总结

1. tool_use 的完整生命周期

  1. 模型吐 tool_use{id, name, input}
    → 流式收集进 toolUseBlocks + needsFollowUp=true(流式执行器可能提前开跑)
  2. → Phase 1 执行:查表→校验→权限→tool.call()
  3. → 生成 tool_result{tool_use_id:同id} 配对回执
  4. → Phase 8 拼进 messages 历史(assistant 消息含 tool_use + user 消息含 tool_result
  5. → 下一轮发给模型,它看结果决定再开单子(next_turn)或收工(结束)
  6. → 对话太长时 compact 摘要掉原始块,但副作用(readFileState/文件改动)留存 → 退休

每个 tool_use 必须有配对tool_result(API 协议要求),所以中断/出错时系统要合成回执兜底。


2. continue / return 的完整分类

7 个 continue(重启迭代)

类型 具体入口
恢复型(5个) collapse_drain_retryreactive_compact_retrymax_output_tokens_escalatemax_output_tokens_recoverystop_hook_blocking
推进型(2个) token_budget_continuation(系统推模型继续)、next_turn(模型要继续)

12 个 return(退出循环),归三类

类别 退出点
正常完成 completed(主出口)、stop_hook_preventedhook_stopped
中断/错误 aborted_streamingaborted_toolsmodel_errorimage_errorprompt_too_longblocking_limit
上限 max_turns

3. 防死循环守卫

while(true) + continue 恢复 = 潜在死循环。每个恢复 continue 都配守卫,把“无限重试”变“有界恢复阶梯”:

  • 计数器封顶count < 3
  • 单次幂等override === undefined
  • 转移记录去重transition !== 自身
  • 黏性跨路径保留hasAttemptedReactiveCompactstop_hook_blocking 里不清零,防 compact↔stop_hook 螺旋 — 曾烧掉几千次 API 调用)
  • 外部预算耗尽checkTokenBudget 递减检测、maxTurns

守卫触发时停止恢复、return 暴露错误或降级到更重手段。


4. 工具清单每轮发但不全量

每轮 API 带 tools 参数,但:

  • 核心工具发完整 schema
  • 延迟工具(MCP/LSP)标 defer_loading 轻量登记(模型要先 ToolSearch 加载)
  • 新增工具只发 delta 增量
  • schema 会话级缓存 + prompt cache 跨轮几乎零成本

发完清单后用哪个工具由模型按 description 自行判断


5. 一个端到端例子

用户“找出 src 下所有 TODO 并修第一个”

迭代 行为
迭代1 系统发 [系统提示词+历史+工具清单]
模型 → tool_use Glob("src/**/*.ts")
needsFollowUp=true → Phase1 执行 Glob → 结果 [文件列表] 进历史 → next_turn continue
迭代2 模型看到文件列表 → tool_use Grep("TODO", 多个文件)
→ 执行 Grep → 结果 [含TODO的行] 进历史 → next_turn
迭代3 模型 → tool_use Read("src/foo.ts")(读含TODO的文件)
→ 执行 Read(readFileState 记录)→ next_turn
迭代4 模型 → tool_use Edit("src/foo.ts", ...)(改TODO)
→ Edit 校验 readFileState(读过✓)+ 新鲜度(没被改✓)→ 执行 → next_turn
迭代5 模型 → tool_use Bash("npm test")(验证)
→ 执行 → next_turn
迭代6 模型 → text "已修复并测试通过"(纯文本,无 tool_use
needsFollowUp=false → 收尾路径
→ stop hook 检查(若配了“必须跑测试”且没跑 → stop_hook_blocking 返工)
→ token budget 检查(若有 +500k 且没花完 → 续写,否则)
return completed 循环结束

任一环节出岔子都有对应机制:

  • Read 读目录报错 → 模型下轮换 Bash(错误反馈自愈)
  • Edit 发现文件被改过 → 抛 FILE_UNEXPECTEDLY_MODIFIED_ERROR 让模型重读
  • 上下文太长 → reactive compact 压缩重试
  • 输出被截断 → 升 64k 或注入续写消息
  • 用户 Ctrl+C → aborted_tools 配合成回执干净退出

6. 一句话总结

queryLoop 是一个 while(true) 的 agent 主循环:每轮把系统提示词 + 历史 messages + 工具清单发给模型,流式收回复;据回复里有没有 tool_use 块分叉——有则执行工具、收集附件、把结果喂回去、next_turn 推进下一轮;没有则走收尾路径,经五道恢复尝试(context collapse / reactive compact / max_output 升级+续写 / stop hook 返工)和 token budget 续写后 return completed 结束。它以 toolUseContext 为共享总线串联工具间状态,以 tool_use/tool_result 配对维护 API 协议,以每个 continue 的防死循环守卫保证“能自救又不会无限自救”,以 12 个 return 覆盖正常完成、中断、错误、上限四类退出。整个设计的精神是:模型负责语义决策(选什么工具、要不要继续),系统负责机械执行与容错(校验、权限、恢复、配对、状态流转),循环既“能持续协作推进任务”又“能容忍各种故障并最终收敛退出”。

更多推荐