#
你见过那些"一问一答"的 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 直接把用户消息塞给模型,那就太天真了。真实的流程像一条生产线:
- 预取 Memory(第 297 行)—— 加载项目级的协作规则、用户的长期记忆、这一轮的会话状态
- 预取 Skill Discovery(第 323 行)—— 检索当前可用的工具和 Agent 能力
- 截取 Compact Boundary(第 365 行)—— 找到上次压缩的切割线,只保留有效的消息部分
- 应用 Tool Result Budget(第 369 行)—— 限制工具执行结果的体积,防止爆炸性增长
- History Snip(第 396 行)—— 去掉太久远的无效历史
- Microcompact(第 412 行)—— 微粒度的上下文压缩
- Context Collapse(第 428 行)—— 结构化坍塌冗余信息
- 最后才 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 区分了至少七种停止情况:
- Streaming 完成但有未处理的 tool_use
- 被用户中断(Ctrl+C)
- 触发 prompt-too-long 恢复流程
- 触发 max-output-tokens 恢复流程
- Stop hook 阻塞(某个钩子认为现在不该继续)
- API 错误直接返回
- 正常 stop_reason(end_turn)
每一种停止情况对应不同的恢复策略。不能统一处理,因为它们代表了不同的系统状态。这就是为什么状态机在 Agent 系统里不是可选项,而是必须项。
QueryEngine 拥有会话的整个生命周期
代码里有一句话很能说明问题:"QueryEngine owns the query lifecycle and session state for a conversation"。
翻译一下:QueryEngine 不只是一个"问答处理器",它是整个会话生命周期的所有者。从用户输入的第一秒,到系统决定说"我完成了",QueryEngine 都在那儿,维护着状态、管理着恢复、见证着每一个转移。
核心观点
一个 Agent 系统的成熟度,先看它有没有一个真正的执行循环。
这个循环的心跳是什么?不是"调用了模型",而是"从上一个已知状态出发,经过一系列可控的中间步骤,抵达下一个一致的新状态"。每一个心跳都能被记录、被理解、被恢复。
模型只是这个心跳过程中的一个跳跃点。真正的智能,在于系统知道怎么从跳跃中活下来。
<!-- 改动说明:核心选题为 queryLoop 在 Claude Code 中的设计,重点突出状态维护、上下文治理优先级、流式处理和故障恢复的理念。用"心跳"比喻来类比系统的循环特性,避免过度技术化表述。 -->