【Claude code 源码启示录】QueryLoop 才是 Agent 的心跳
你见过那些”一问一答”的 AI 对话应用吗?用户发一条消息,模型思考两秒,返回一个答案,然后什么都结束了。感觉很聪明,但问题来了——一旦你让这个系统去调用工具、重试失败的操作、保存工作进度,整个架构就会像纸糊的一样崩掉。
说白了,大多数人对 Agent 的理解还停留在”调用模型”这个动作上。但真正决定一个 Agent 系统能不能活到生产环境的,不是那一次模型调用有多聪明,而是系统能不能维持一个「有状态、可恢复的执行循环」。
模型调用只是表象,循环才是本质
在 Claude Code 里,有两个函数看起来很像,其实差别大得离谱:query() 和 queryLoop()。
query() 是外壳——暴露给用户界面的那个入口。但真正干活的是 queryLoop()。这里面维护着一套完整的跨迭代状态对象:
-
messages—— 对话历史 -
toolUseContext—— 工具执行的上下文 -
autoCompactTracking—— 自动压缩追踪 -
maxOutputTokensRecoveryCount—— 超长输出的恢复计数 -
hasAttemptedReactiveCompact—— 是否已经尝试过被动压缩 -
pendingToolUseSummary—— 待处理的工具摘要 -
stopHookActive—— 停止钩子是否激活 -
turnCount—— 轮数计数 -
transition—— 状态转移
这些状态不是散落在函数各个角落的局部变量。它们被「集中存放在 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 请求的因果账本。换句话说,系统不会留下”发了个指令但没有结果”这样的悬念。
「恢复」的情况更复杂:
-
如果触发了 prompt-too-long错误,系统先走 context collapse 的排水管,再进入 reactive compact(被动压缩)流程 -
如果触发了 max-output-tokens错误,系统不会让模型去”总结前面说了什么”,而是直接提上 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 系统的成熟度,先看它有没有一个真正的执行循环。
这个循环的心跳是什么?不是”调用了模型”,而是”从上一个已知状态出发,经过一系列可控的中间步骤,抵达下一个一致的新状态”。每一个心跳都能被记录、被理解、被恢复。
模型只是这个心跳过程中的一个跳跃点。真正的智能,在于系统知道怎么从跳跃中活下来。
夜雨聆风
