Claude Code 源码拆解第五篇:一次提问背后到底跑了几轮?query loop 才是 agent 真正的发动机
一次提问背后到底跑了几轮?query loop 才是 agent 真正的发动机
你以为它是在“自动思考”,其实它是在一轮一轮把任务继续推进。
很多人第一次用 Claude Code,都会有一种很强的错觉。
你发出去一句话,它不会像普通聊天机器人那样答一下就停,而是会自己继续往下干:读文件、调工具、拿结果、再继续分析,直到它觉得这轮任务真的结束了才停下来。
它好像真的会自己思考。
这句话不能说全错,但如果你想看懂 Claude Code 源码,就得把它拆开。
因为在代码层面,所谓“agent 感”并不神秘。它最核心的东西,不是某句 prompt,也不是某个工具,而是一条会持续推进的 query loop。
第四篇我们刚讲完:上下文快炸时,Claude Code 会靠 compact 机制续命。这一篇要接着回答另一个更底层的问题:
上下文续命,是续给谁用的?
答案就是:续给这条 query loop 用的。

这篇你会看到什么
-
为什么 agent 不是“一次请求 + 几个工具调用” QueryEngine
和 query()分别负责什么-
一次提问内部如何经历 assistant -> tool_use -> tool_result -> continue -
为什么你会感觉它在“自己接着干” stopHooks
、 tokenBudget、恢复逻辑为什么会插进主循环
一、先打掉一个误解:agent 不是“一次请求”
很多文章讲 agent,都会默认一种简单模型:用户发消息,模型回答;如果要调工具,就调一下;拿到结果再答一次。
这个理解不完全错,但它太平了,也太像聊天机器人。Claude Code 源码里的真实情况是:一次提问,不一定只对应一次模型调用。
只要 assistant 消息里出现了 tool_use、stop hook 给了阻塞反馈、命中了 output token 上限、或者 token budget 认为“这轮还没到该收的时候”,它就不会停。
所以从工程视角看,agent 不是“一次调用”,而是:一次用户意图,驱动一条循环,直到它满足停止条件。
二、别在入口处迷路:QueryEngine 管开局,query() 管推进
如果你顺着源码找入口,最先会看到的是 src/QueryEngine.ts 里的 submitMessage()。
但往下读会发现,QueryEngine 更像是会话总管,它负责接住用户输入、处理 slash command、本地输入预处理、组装 system prompt、user context、system context,准备 tools、MCP、skills、permission context 和 transcript 持久化。
真正的主循环在 src/query.ts 里:query() 只是薄封装,真正推进整轮任务的是 queryLoop()。
这层分工为什么重要
QueryEngine 负责“这一轮怎么开局”,queryLoop() 负责“这一轮怎么推进到底”。如果所有逻辑都塞进入口函数里,后面 compact、fallback、tool execution、stop hooks 很快就会缠成一团。
三、先别急着看循环,先看它手里攥着什么状态
src/query.ts 里有两个很关键的类型:QueryParams 和 State。
QueryParams 像开局条件,放的是进入这轮 query 时已经确定下来的东西,比如 messages、systemPrompt、userContext、systemContext、canUseTool、toolUseContext、fallbackModel、querySource、taskBudget。
State 则是循环运行态,放的是这条循环跑着跑着会变化的东西,比如 messages、autoCompactTracking、maxOutputTokensRecoveryCount、hasAttemptedReactiveCompact、maxOutputTokensOverride、stopHookActive、turnCount、transition。
翻成人话就是:QueryParams 决定“你带着什么出发”,State 决定“你现在跑到哪了”。

四、真正让它像 agent 的,是这个 while (true)
queryLoop() 里最关键的一句代码,其实非常朴素:
while (true) {
// ...
}
Claude Code 的 agent 感,基本就是从这里长出来的。
每次迭代,它都会做几件事:取出当前状态,对消息做预处理,判断是否需要 compact / microcompact / collapse,发起模型调用,收集 assistant 消息和 tool_use,执行工具拿到 tool_result,再判断是否继续下一轮或者结束。
它不是“跑一次”,而是“跑完一轮以后,再决定还要不要再跑一轮”。
这就是 agent 和普通问答的根本区别。
五、拆开看,一次提问在内部到底经历了什么
第一步:先把当前要发给模型的上下文准备好
在每一轮循环开始时,queryLoop() 会先从当前 state 里拿出 messages、toolUseContext、autoCompactTracking 等运行态,然后对 messagesForQuery 做几层处理:
getMessagesAfterCompactBoundary(messages)applyToolResultBudget(...)snipCompactIfNeeded(...)microcompact(...)contextCollapse.applyCollapsesIfNeeded(...)
这说明 Claude Code 的主循环不是纯模型循环,而是上下文管理 + 模型采样 + 工具执行的混合循环。
翻成人话就是:模型每次开口之前,系统都先帮它把桌面收拾一遍,避免把一堆又脏又重的历史原样塞进去。
第二步:在真正发请求前,先看要不要自动压缩
在正式调用模型前,queryLoop() 会先执行一层自动压缩判断:
const { compactionResult, consecutiveFailures } = await deps.autocompact(...)
如果 compact 成功,它会记录压缩前后的 token 变化,重置 compact tracking,调用 buildPostCompactMessages(compactionResult) 重建压后的消息视图,把这些 post-compact messages 先 yield 出去,然后继续当前 query。
如果你把这一段看成真实工作流,其实很好懂:项目快聊崩了,系统先拉一个人出去整理纪要,再把整理后的结果塞回现场,然后原任务继续,不是整场会直接散掉。
第三步:发起一次模型流式采样
上下文准备好以后,主循环才真正进入模型调用:
for await (const message of deps.callModel({ ... })) {
// ...
}
这里不是一次性拿完整结果,而是流式消费消息。好处很直接:UI 能更早看到 assistant 输出,一旦流里出现 tool_use,系统也能更早准备工具执行。
在这段逻辑里,Claude Code 会持续收集 assistantMessages、toolUseBlocks、toolResults 和 needsFollowUp。其中 needsFollowUp 本质上就是这条循环的一个信号灯。
第四步:assistant 一旦请求工具,系统就把这轮变成多段式
当流式输出里出现 tool_use block 时,Claude Code 会把这些 block 收集进 toolUseBlocks。如果开启了 streaming tool execution,还会一边流一边往 StreamingToolExecutor 里塞。
这说明在 Claude Code 里,“assistant 说要调工具”和“工具真正跑完”不是同一个时刻。主循环会先收模型产出的工具请求,再执行工具,把结果转成 tool_result 风格的消息回填,然后继续下一轮模型调用。

第五步:如果这轮没有 tool_use,才有资格讨论“结束”
源码里有个特别朴素但非常重要的判断点:if (!needsFollowUp)。
背后的意思很直接:如果这一轮 assistant 没请求工具,那这轮才有可能是真的结束;如果请求了工具,那就别急着收尾,后面还要继续推进。
在系统内部,判断一轮是否结束的标准根本不是“assistant 有没有说完一句话”,而是这轮任务链条是不是已经闭环了。
六、为什么你会感觉它“自己接着干”
Claude Code 之所以看起来像“自己会继续工作”,不是因为它有什么神秘人格,而是因为它的主循环天然支持 continuation。
1. 工具调用 continuation
assistant 请求工具,工具跑完,结果回填,再继续问模型“接下来怎么办”。
2. max output tokens continuation
如果模型输出被截断,query.ts 不会立刻认输。它会先尝试把默认上限抬高重试;如果还不行,就注入一条 meta user message:
Output token limit hit. Resume directly — no apology, no recap...
翻成人话就是:别复述,别道歉,直接从刚才断掉的地方接着干。
3. stop hook continuation
handleStopHooks() 不是边角逻辑。它在 turn-end 阶段会跑 stop hooks,存 cache-safe params,触发 prompt suggestion,触发 extractMemories、autoDream,还会处理 task completed、阻塞错误和 prevent continuation。
如果 stop hook 给了 blocking error,主循环不会简单退出,而是会把这些错误作为新的 meta 信息塞回消息列表,再继续下一轮。
4. token budget continuation
src/query/tokenBudget.ts 里也很有意思。它不是只会说“预算没了,停”,而是会根据当前 turn token 消耗判断:还没到 COMPLETION_THRESHOLD = 0.9,那就继续;如果已经连续推进了几次,而且新增 token 很少,说明开始边际递减了,就停。
也就是说,Claude Code 不只是看“还能不能继续”,还会看:继续到底值不值。

七、还有个容易被忽略的点:别让主循环烂成 if/else 大泥球
src/query/config.ts 只有几十行,但非常值得讲。它做的是一件很工程化、但很重要的事:把 query 级别的不可变配置在入口处一次性快照。
比如 sessionId、streamingToolExecution、emitToolUseSummaries、isAnt、fastModeEnabled 这些东西,都收进了 QueryConfig。
这样主循环里会随着迭代变化的东西放进 State,相对稳定的配置放进 QueryConfig,feature gate 该内联的继续内联。否则整条 loop 很快就会烂成条件分支大泥球。
八、QueryEngine 这层不是绕,它是在替主循环挡杂事
很多人看到 QueryEngine -> query() 这种两层结构,会下意识觉得有点绕。但它其实很有必要。
QueryEngine 负责的是一次用户输入进入系统之前和之后的外围事务:处理用户输入和 slash command,记录 transcript,保证 /resume 不会断档,初始化 system prompt、memory prompt、skills、plugins,构建 ProcessUserInputContext,以及决定这次输入到底要不要真的进入主循环。
你可以把它理解成:QueryEngine 是 orchestration,queryLoop() 是 runtime。
九、它什么时候才算“真结束”,其实不是一个按钮说了算
如果你把整段源码看完,会发现 Claude Code 并没有一个单一的“stop”按钮。它的停止,是很多条件共同决定的:
-
没有新的 tool_use了 -
stop hooks 没有阻塞 -
没有 max output tokens recovery 要继续 -
没有 reactive compact 要重试 -
token budget 认为可以收工了 -
用户也没有中途打断
这很像真实工作流。因为一个长任务到底什么时候算“完成”,本来就不应该只由模型一句话决定。
十、这篇最值得带走的,不是某个函数名,而是一个视角
Claude Code 真正的内核,不是 REPL,不是 SDK,不是单个工具,而是 query loop。
工具只是这条循环里会被调用的能力。compact 是这条循环快撑不住时的续命机制。memory 是这条循环跨回合后的延伸。而让它表现出“会自己接着干”的那个根,恰恰是这条循环本身。
你把这层看懂了,再回头看很多 agent 产品,就会少掉很多幻觉。所谓 agent 的差距,很多时候不在“模型会不会说”,而在会不会把一次任务拆成多轮推进、会不会在工具和模型之间稳定来回切换、会不会在中途出错时继续把任务接下去、会不会在该停的时候真的停住。
十一、对普通用户和开发者,有什么直接启发
-
不要把 agent 理解成“更长的 prompt”,真正的 agent 更像一条带状态的循环。 -
工具调用不是重点,循环调度才是重点。 -
“会继续干”背后一定有 continuation 机制。 -
设计自己的 agent 时,先想停止条件,再想提示词。
结语
这次我们讲的是:上下文快炸时,Claude Code 怎么把 agent 救回来。第五篇更底层的答案是:救回来以后,到底是谁继续往下跑。
答案不是某个工具,不是某个 prompt,而是这条 query loop。它决定了一次提问不会只停在“一次响应”,而会在 assistant、tool_use、tool_result、恢复逻辑和停止条件之间,一轮一轮把任务推下去。
第三篇讲上下文为什么会脏,第四篇讲上下文快炸时怎么续命,第五篇讲续命之后系统到底靠什么继续跑。这三篇合起来,正说明一件事:agent 不是更长的 prompt,而是一套能持续推进任务的循环系统。
关键源码位置
src/QueryEngine.ts:209-551src/query.ts:181-217src/query.ts:241-468src/query.ts:551-1062src/query.ts:1185-1349src/query.ts:1368-1725src/query/config.ts:8-46src/query/stopHooks.ts:65-284src/query/tokenBudget.ts:3-93
夜雨聆风