跳转至

04. 查询循环

查询循环(query loop)是 Claude Code 的主循环。它不是简单地“请求模型一次并返回”,而是在同一轮用户任务内反复执行四件事:准备本次采样上下文,调用模型并消费流式事件,执行模型请求的工具,把工具结果回填成下一次采样的输入。只要模型继续请求工具,循环就继续;当模型不再请求工具,并且停止钩子、预算和恢复逻辑都允许结束时,这一轮才完成。

开发任务需要连续观察、连续决策和连续验证。查询循环把模型的一次次短采样组织成用户视角的一次长任务:工具输出进入历史,下一次采样读取这些事实,权限拒绝和中断也以结构化结果回到同一条任务链路。

stateDiagram-v2 [*] --> PrepareContext PrepareContext --> SampleModel SampleModel --> StreamAssistant StreamAssistant --> RunTools: 助手消息含工具调用 StreamAssistant --> StopChecks: 无工具调用 RunTools --> AppendToolResults AppendToolResults --> PrepareContext StopChecks --> PrepareContext: 停止钩子 / 续写要求继续 StopChecks --> Recovery: token / 上下文 / API 可恢复 Recovery --> PrepareContext StopChecks --> Completed SampleModel --> Interrupted: abort RunTools --> Interrupted: abort Completed --> [*] Interrupted --> [*]

上下文准备阶段的具体动作

每次循环开始时,本地运行时会先整理即将发送给模型的消息。这里的“整理”不是单纯拼数组。它会从当前历史中找到压缩边界之后仍然有效的消息,处理过大的工具结果,必要时做微压缩(micro-compact,局部压缩过长内容)或上下文折叠,检查是否接近上下文窗口,准备动态工具、技能发现结果和记忆预取。这个阶段还会把系统上下文拼进完整系统提示,并把用户上下文作为特殊用户侧上下文插入请求。模型看到的不是转录记录原样,而是一次重新计算后的采样输入。

“重新计算”意味着这一步不是幂等地读一次缓存。同一个用户任务里,如果前一次工具执行改变了已读文件状态、任务列表或技能可见面,下一次循环的上下文组装会反映这些变化——例如模型编辑过的文件如果被后续工具再次读取,运行时不会重复注入旧版本内容;技能被显式触发后,后续采样才会看到该技能展开出的提示词和允许工具列表。这个阶段决定的是“模型这一步能看到什么”,而不是“历史上发生过什么”,两者并不总是相等。

模型采样与流式事件

随后进入模型采样。本地运行时发出流式请求,携带消息、系统提示、推理配置、工具结构说明、MCP 状态、智能体定义、预算和备用模型信息。模型返回的流式事件会被转换成内部助手消息。文本块可以立即展示;推理内容和签名会按模型协议累积;工具调用的输入 JSON 会随着增量片段拼接,直到内容块结束才形成完整工具调用。流式输出先成为消息,消息中出现工具意图后才进入本地工具系统。

从 Claude API 的公开协议看,一次助手消息由若干内容块(content block)组成,可能包含文本块、思考/推理块和工具调用块(tool_use),消息结束时会带一个停止原因(stop_reason),常见取值包括模型认为已经说完(end_turn)、模型请求了工具(tool_use)、达到最大输出长度(max_tokens)等。查询循环主要靠这个停止原因判断“这次采样之后该做什么”:出现 tool_use 就进入工具执行分支,end_turn 且没有待处理事项就进入停止检查分支。这个协议层面的状态机是查询循环之上的一层薄封装,循环本身不需要理解模型内部如何生成这些块,只需要正确响应块类型和停止原因。

工具批处理:并发安全与串行

当一次采样产出工具调用,循环会把这一批工具调用交给工具运行时。并发安全的工具可以批量并行执行,不安全的工具会串行执行。执行前要做输入校验、钩子、权限检查和沙箱判断;执行后要把输出截断、持久化、渲染为工具结果,并更新已读文件状态、文件历史、任务状态或其他上下文修改。工具结果会被追加为新的用户消息,因为从模型视角看,这是“外部世界对它刚才动作的回应”。

从公开可观察的行为看,这批工具通常按“是否修改状态”划分并发资格:只读检索类工具(例如搜索文件内容、列目录、抓取网页)之间没有写冲突,可以并行跑以缩短墙钟时间;会修改文件或进程状态的工具(编辑、写入、执行 Shell 命令)会被串行化,避免同一批次内出现顺序不确定的写覆盖。这个划分本质上是一致性问题的工程解法,而不是简单的性能优化:如果两个编辑工具并发写同一个文件,谁的结果先落盘会直接影响另一个工具看到的基线内容。

停止条件的判定顺序

下一次循环会带着这些工具结果再采样。模型可以根据命令输出继续读取文件、修改代码、运行测试,或给出最终回答。这个后续采样机制让模型在每次本地动作之后重新观察结果,而不是在第一轮里预判完整任务。

停止条件比“没有工具调用”更复杂。模型不再请求工具时,本地运行时还要检查停止钩子是否要求继续、是否存在 token 预算续写、是否需要恢复最大输出 token、是否要响应式压缩、是否有 API 错误可以通过备用模型或上下文压缩恢复。如果停止钩子返回阻塞结果,循环会把钩子结果作为新的上下文继续采样,而不是直接结束。只有这些检查都通过,查询引擎才把最后的助手输出视为成功结果。

Claude Code 官方 hooks 文档公开了一组生命周期事件,其中与查询循环直接相关的是 Stop(主循环即将结束时触发,可以要求继续)和 SubagentStop(子智能体循环结束时触发)。这两个钩子给了本地策略一个“模型说完了,但我们还要不要再来一轮”的介入点,典型用途包括强制模型在结束前跑一遍测试、检查是否遗漏了用户提出的某个约束。钩子返回阻塞结果时,循环把这个结果当作新的上下文注入,而不是抛异常中断——这保持了“错误也是下一次采样的输入”这一贯穿全循环的设计原则。

中断处理

中断也在循环内处理。用户中断或外部取消会触发中断控制器。若中断发生在模型流式返回期间,本地运行时会给未完成的工具调用补齐合成工具结果,避免下一次请求出现不成对的工具调用。若中断发生在工具执行期间,工具系统会尽量返回取消结果,并让循环以中断状态退出。这样可以保持转录记录可恢复、消息结构合法。

“补齐合成工具结果”这一步值得展开:多数模型提供方的协议要求一条助手消息里的每个 tool_use 块,在后续历史中都必须有一条对应的 tool_result,否则下一次请求会被协议校验拒绝。用户在工具还没执行完就按下中断时,运行时不能简单丢弃这条未完成的工具调用,而要生成一条标记为“已取消”的合成结果占位,历史结构才能继续满足协议要求,会话也才能在恢复后正常续接。

错误与恢复路径

异常路径会沿同一条循环回流。工具参数无效时,工具系统生成错误工具结果;权限被拒时,拒绝原因作为工具结果进入下一次采样;上下文过长时,循环先压缩再继续;模型 API 可恢复失败时,循环尝试备用模型或重试。错误不是单独的旁路日志,而是下一次模型采样可以读取的事实。

据公开资料,Claude Code 在主模型不可用或触发限流等可恢复错误时,具备切换到备用模型继续当前任务的能力,用户可以在会话中观察到模型切换的提示;压缩同样可以在循环中途被动触发,而不只是用户手动执行 /compact。这两类恢复路径共享同一个前提:错误发生后循环不会凭空重试同一份请求,而是先改变会构成下一次请求的状态(换模型、换更短的历史),再继续采样,避免同样的错误在同样的输入上无限重复。

与 Claude Agent SDK 的对应关系

查询循环不是 Claude Code CLI 独有的私有实现,它对应的循环语义在 Claude Agent SDK 的公开文档中也能找到映射:SDK 的 query() 调用同样是“采样 → 出现工具调用 → 执行 → 回填 → 再采样”的循环,canUseTool 回调对应循环里的权限判断点,hooks 配置对应 PreToolUse/PostToolUse/Stop 等生命周期钩子。理解这一点有助于回答“Claude Code 和基于 Claude Agent SDK 自建的 Agent 有什么关系”这类问题:CLI 本身可以理解为 SDK 同一套核心循环之上叠加了终端交互、权限 UI 和转录持久化。

sequenceDiagram participant L as 查询循环 participant M as 模型 participant T as 工具运行时 participant H as hook/权限 L->>L: 构造 messagesForQuery L->>M: callModel(messages, tools, systemPrompt) M-->>L: 文本 / 推理 / 工具调用 alt 无工具调用 L->>H: stop hook 与恢复检查 H-->>L: 完成或继续 else 有工具调用 L->>T: 执行工具调用 T->>H: 校验 + 钩子 + 权限 H-->>T: 允许 / 拒绝 / 询问 T-->>L: 工具结果 L->>L: 消息 = 采样消息 + 助手消息 + 工具结果 L->>M: 下一次采样 end

读查询循环时,不要把它看成一个 while(true) 包住 API 请求。它真正维护的是“下一次模型应该看到什么”和“本地副作用是否已经被合法执行”。同一个用户任务里可能穿插模型流、工具流、权限拒绝、备用模型、压缩和中断。下一章会继续展开这些材料如何进入上下文与记忆系统。