QueryLoop 才是 Agent 的心跳

内容纲要

#

你见过那些"一问一答"的 AI 对话应用吗?用户发一条消息,模型思考两秒,返回一个答案,然后什么都结束了。感觉很聪明,但问题来了——一旦你让这个系统去调用工具、重试失败的操作、保存工作进度,整个架构就会像纸糊的一样崩掉。

说白了,大多数人对 Agent 的理解还停留在"调用模型"这个动作上。但真正决定一个 Agent 系统能不能活到生产环境的,不是那一次模型调用有多聪明,而是系统能不能维持一个有状态、可恢复的执行循环

模型调用只是表象,循环才是本质

在 Claude Code 里,有两个函数看起来很像,其实差别大得离谱:<code>query()</code> 和 <code>queryLoop()</code>。

<code>query()</code> 是外壳——暴露给用户界面的那个入口。但真正干活的是 <code>queryLoop()</code>。这里面维护着一套完整的跨迭代状态对象:

  • <code>messages</code> —— 对话历史
  • <code>toolUseContext</code> —— 工具执行的上下文
  • <code>autoCompactTracking</code> —— 自动压缩追踪
  • <code>maxOutputTokensRecoveryCount</code> —— 超长输出的恢复计数
  • <code>hasAttemptedReactiveCompact</code> —— 是否已经尝试过被动压缩
  • <code>pendingToolUseSummary</code> —— 待处理的工具摘要
  • <code>stopHookActive</code> —— 停止钩子是否激活
  • <code>turnCount</code> —— 轮数计数
  • <code>transition</code> —— 状态转移

这些状态不是散落在函数各个角落的局部变量。它们被集中存放在 State 对象里,每次 continue 分支都整体更新。这意味着什么?意味着系统的行为谁负责、谁说了算,一眼就能看清楚。这就是一个状态机——一旦进入某个状态,系统的下一步动作就是确定的,可追踪的,能回溯的。

调用模型之前,系统先做了什么

如果你以为 queryLoop 直接把用户消息塞给模型,那就太天真了。真实的流程像一条生产线:

  1. 预取 Memory(第 297 行)—— 加载项目级的协作规则、用户的长期记忆、这一轮的会话状态
  2. 预取 Skill Discovery(第 323 行)—— 检索当前可用的工具和 Agent 能力
  3. 截取 Compact Boundary(第 365 行)—— 找到上次压缩的切割线,只保留有效的消息部分
  4. 应用 Tool Result Budget(第 369 行)—— 限制工具执行结果的体积,防止爆炸性增长
  5. History Snip(第 396 行)—— 去掉太久远的无效历史
  6. Microcompact(第 412 行)—— 微粒度的上下文压缩
  7. Context Collapse(第 428 行)—— 结构化坍塌冗余信息
  8. 最后才 Autocompact(第 453 行)—— 如果还是超长,触发自动压缩恢复流程

看到了吗?模型推理排在这八道关卡之后。这说明 Claude Code 把"上下文治理"的优先级放在了"模型推理"之前。因为再聪明的模型也救不了烂上下文。

流式输出意味着循环从不停止

模型的输出在 queryLoop 里不是"最终答案",而是一串事件流

  • Assistant 文本段落
  • Tool_use block(工具调用请求)
  • Usage 更新(token 计数更新)
  • Stop reason(停止原因)
  • API 错误

系统能在模型还没完全结束输出的时候,就开始安排下一步。这不像传统的"请求-响应"架构,而更像"驱动-调度-反馈"的并发过程。模型在吐字的时候,系统已经开始准备调用工具了。

中断和恢复,才是心跳的真正考验

一个只能顺利执行的循环,不值得叫"心跳"。真正的心跳必须能处理故障。

中断的情况:用户按了 Ctrl+C,或者网络中断了。系统会先消费 StreamingToolExecutor 的剩余缓冲结果,然后生成合成的 tool_result,补齐已经发出去的 tool_use 请求的因果账本。换句话说,系统不会留下"发了个指令但没有结果"这样的悬念。

恢复的情况更复杂:

  • 如果触发了 <code>prompt-too-long</code> 错误,系统先走 context collapse 的排水管,再进入 reactive compact(被动压缩)流程
  • 如果触发了 <code>max-output-tokens</code> 错误,系统不会让模型去"总结前面说了什么",而是直接提上 token 上限,然后让模型继续写(这叫 cap-and-continue)

看这两个细节,你就能看出系统设计者的执念:尽量保留工作的连续性。它不相信"重新开始",只相信"改正进度"。

七种停止条件,说明循环有多复杂

Claude Code 区分了至少七种停止情况:

  1. Streaming 完成但有未处理的 tool_use
  2. 被用户中断(Ctrl+C)
  3. 触发 prompt-too-long 恢复流程
  4. 触发 max-output-tokens 恢复流程
  5. Stop hook 阻塞(某个钩子认为现在不该继续)
  6. API 错误直接返回
  7. 正常 stop_reason(end_turn)

每一种停止情况对应不同的恢复策略。不能统一处理,因为它们代表了不同的系统状态。这就是为什么状态机在 Agent 系统里不是可选项,而是必须项。

QueryEngine 拥有会话的整个生命周期

代码里有一句话很能说明问题:"QueryEngine owns the query lifecycle and session state for a conversation"。

翻译一下:QueryEngine 不只是一个"问答处理器",它是整个会话生命周期的所有者。从用户输入的第一秒,到系统决定说"我完成了",QueryEngine 都在那儿,维护着状态、管理着恢复、见证着每一个转移。

核心观点

一个 Agent 系统的成熟度,先看它有没有一个真正的执行循环。

这个循环的心跳是什么?不是"调用了模型",而是"从上一个已知状态出发,经过一系列可控的中间步骤,抵达下一个一致的新状态"。每一个心跳都能被记录、被理解、被恢复。

模型只是这个心跳过程中的一个跳跃点。真正的智能,在于系统知道怎么从跳跃中活下来。

<!-- 改动说明:核心选题为 queryLoop 在 Claude Code 中的设计,重点突出状态维护、上下文治理优先级、流式处理和故障恢复的理念。用"心跳"比喻来类比系统的循环特性,避免过度技术化表述。 -->

滚动至顶部