乐于分享
好东西不私藏

源码分析(1):Claude Code 真正的发动机,不是终端界面,而是那条不会停的 query loop

源码分析(1):Claude Code 真正的发动机,不是终端界面,而是那条不会停的 query loop

以下内容都是基于泄露的claudecode源码项目,改造为本地运行起来的版本展开分析,项目地址:

https://github.com/claude-code-best/claude-code

很多人第一次看 Claude Code,盯住的都是它最显眼的部分:终端里的交互体验、会读文件会改代码的工具、还有那种“像人在命令行里结对编程”的流畅感。仓库 README 里也确实把 REPL、FileRead、FileEdit、BashTool 这些能力摆在前排,给人的第一印象很容易是:这是一个把模型、工具和终端包起来的 AI CLI。(GitHub[1])

但如果真往工程结构里看,最值得拆的并不是 UI。仓库自己写得很直白:src/query.ts 是“主 API query 函数”,负责流式响应、工具调用和整轮对话循环;QueryEngine.ts 是它的上层编排;REPL 只是交互外壳。这说明 Claude Code 的核心不是“把模型接进终端”,而是把 Agent 做成了一条持续运转的查询回路。(GitHub[2])

这也是我对这篇文章的中心判断:Claude Code 最值得看的,不是它会不会写代码,而是它把 AI Coding 写成了一条可持续推进的 query loop;只有当模型、工具、上下文、恢复机制都被塞进同一条循环里,Agent 才从一次调用变成一个系统。 所以下面不列功能,我只拆这条 loop 为什么是整个项目的“发动机”,以及它到底暴露了什么工程现实。(GitHub[2])

UI 很显眼,但它只是入口

先把坐标系立住。README 里把 query.ts 单独列成“流式对话与工具调用循环”,QueryEngine.ts 单独列成“会话引擎”,而 REPL.tsx 在 CLAUDE.md 里被定义成交互屏幕。这不是目录命名的习惯问题,而是系统边界的划分:界面负责展示和输入,真正驱动会话推进的是 loop。(GitHub[1])

这点很关键。很多 AI Coding 工具看起来都是“用户发一句话,模型回一句话,必要时调个工具”。Claude Code 不是这个心智模型。它更接近一个常驻控制器:收用户输入,整理上下文,发请求,接流式块,发现 tool_use,执行工具,把结果折回消息流,再决定是否继续下一轮。也就是说,它的单位不是一次 completion,而是一整段能自己往前拱的工作回路。 这也是为什么 query.ts 会做到 1700 多行——长的不是功能清单,长的是系统闭环。(GitHub[1])

while (true) 才是 Agent 的真实形态

先看 src/query.ts 里最核心的一段:

// src/query.tsasyncfunctionqueryLoop(params, consumedCommandUuids) {const { systemPrompt, userContext, systemContext, canUseTool, querySource, maxTurns } = paramsletstateState = {messages: params.messages,toolUseContext: params.toolUseContext,maxOutputTokensOverride: params.maxOutputTokensOverride,autoCompactTrackingundefined,stopHookActiveundefined,maxOutputTokensRecoveryCount0,hasAttemptedReactiveCompactfalse,turnCount1,pendingToolUseSummaryundefined,transitionundefined,  }while (true) {let { toolUseContext } = stateconst {      messages,      autoCompactTracking,      maxOutputTokensRecoveryCount,      hasAttemptedReactiveCompact,      maxOutputTokensOverride,      pendingToolUseSummary,      stopHookActive,      turnCount,    } = state// ...  }}

这段代码最有价值的地方,不是 while (true) 本身,而是 State 里装的东西。它不只存消息,还存 autoCompactTrackingmaxOutputTokensRecoveryCounthasAttemptedReactiveCompactpendingToolUseSummarystopHookActivetransition。换句话说,Claude Code 把“这轮对话发生过什么、失败过什么、压缩到哪一步、要不要继续”都显式放进了状态。(GitHub[3])

这说明一个很现实的判断:Agent 系统绕不开状态管理。你可以把 demo 做成一次模型调用加几个函数,但只要任务会跨多轮推进,只要工具会失败、上下文会膨胀、输出会截断、用户会中断,会话就一定不是“消息数组 + 工具调用”这么简单。Claude Code 在第一层就承认了这件事,所以它不是把状态藏在 UI 里,也不是散落在工具回调里,而是集中放进 loop。(GitHub[3])

从这个角度看,query.ts 其实更像一个状态机,只不过它长得像生成器。它每轮都要判断:这次继续,是因为工具结果回来了,还是因为输出被截断,还是因为 stop hook 拦住了,还是因为 token budget 允许再推一步。真正可用的 Agent,从来不是“会调用工具”,而是“知道为什么要继续下一轮”。 这才是 query loop 的分量。(GitHub[3])

在调用模型之前,它先处理成本、压缩和失败

再看模型请求之前的那一段,顺序非常耐人寻味:

// src/query.tsmessagesForQuery = awaitapplyToolResultBudget(...)if (feature('HISTORY_SNIP')) {const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)  messagesForQuery = snipResult.messages}const microcompactResult = await deps.microcompact(messagesForQuery, toolUseContext, querySource)messagesForQuery = microcompactResult.messagesif (feature('CONTEXT_COLLAPSE') && contextCollapse) {const collapseResult = await contextCollapse.applyCollapsesIfNeeded(...)  messagesForQuery = collapseResult.messages}const { compactionResult } = await deps.autocompact(  messagesForQuery,  toolUseContext,  { systemPrompt, userContext, systemContext, toolUseContext, forkContextMessages: messagesForQuery },  querySource,  tracking,  snipTokensFreed,)

这段最值得看的,是它把“压缩”放在了请求前,而且不是一个压缩,而是一串分层处理:工具结果预算、snipmicrocompactcontext collapseautocompact。这基本等于直接承认:上下文管理不是补丁,是主流程。 只要 Agent 真在写代码,它就一定会遇到上下文膨胀,而系统如果不在主循环里处理这个问题,后面所有能力都会被拖垮。(GitHub[3])

更重要的是,这里处理的不只是“窗口太长”。代码里还专门处理 task_budget.remainingprompt too longmax_output_tokens、媒体尺寸错误,以及 compact 失败后的恢复路径。也就是说,Claude Code 不是把失败当异常抛出去,而是把失败当 loop 的组成部分,先尝试吞掉、修复、改写,再决定是否继续。(GitHub[3])

这背后反映的是 AI Coding 的真实成本:贵的不只是 token,贵的是连续工作时的系统维护。上下文越长,失败越多;失败越多,就越需要压缩、回放、恢复、重试;而这些都不能靠模型“自己聪明一点”解决,只能靠工程层把循环管住。很多人高估了模型生成能力,低估了这种运行时治理的复杂度。Claude Code 恰好把这层复杂度直接写在了主干上。(GitHub[1])

工具不是“调用一下”,而是要能接回下一轮

如果说前半段在解决“怎么把请求发出去”,那后半段真正解决的是“怎么把工作继续下去”。

// src/query.tsconst toolUpdates = streamingToolExecutor  ? streamingToolExecutor.getRemainingResults()  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext)forawait (const update of toolUpdates) {if (update.message) {yield update.message    toolResults.push(...normalizeMessagesForAPI([update.message], toolUseContext.options.tools).filter(_ => _.type === 'user'))  }if (update.newContext) {    updatedToolUseContext = { ...update.newContext, queryTracking }  }}// ...constnextState = {messages: [...messagesForQuery, ...assistantMessages, ...toolResults],toolUseContext: toolUseContextWithQueryTracking,autoCompactTracking: tracking,turnCount: nextTurnCount,pendingToolUseSummary: nextPendingToolUseSummary,transition: { reason'next_turn' },}state = next

这里最关键的不是 runTools,而是“工具结果如何回流”。工具执行完,并不会就地结束,而是被 normalizeMessagesForAPI 折回成下一轮可消费的消息;与此同时,memory prefetch、skill discovery、queued commands、附件消息也会在这一轮尾部被补进 toolResults,最后一起进入下一轮 state.messages。这说明 Claude Code 的工具层不是外挂,而是 loop 的延伸部分。(GitHub[3])

这套设计解决的是一个很容易被忽略的问题:AI Coding 真正难的不是“会看、会改、会执行”,而是能不能把“看过什么、改了什么、执行出了什么结果”稳定地串回模型的下一次判断里。如果工具调用只是 side effect,没有被重新组织进对话状态,Agent 很快就会失忆、跑偏,或者在失败时留下不完整的 tool_resultquery.ts 里专门有 yieldMissingToolResultBlocks、fallback 后 tombstone orphan messages、abort 后补齐 tool result,这些都在防止 loop 断裂。(GitHub[3])

说白了,Claude Code 把工具调用当成“会话推进器”,不是“能力插件”。这也是它和很多简单 agent 框架的分水岭:后者能接很多工具,但未必能把工具后的状态持续维护好;前者工具数量未必最多,但它知道工具结果必须回到主循环,才能形成真正的 agentic 行为。(GitHub[2])

这条 loop 暴露了 AI Coding 的真正门槛

拆到这里,其实能看清一个更大的判断:Claude Code 的难点从来不是“调一次模型再调用几个函数”。真正的门槛,是把一段长任务做成可恢复、可压缩、可中断、可继续、可控成本的运行回路。仓库里把 query.tsQueryEngine.tscontext.ts、权限系统、Hook、compaction 全都抬成核心模块,本身就在说明这一点。(GitHub[1])

所以第一篇看 query loop,真正该得出的结论不是“Claude Code 写得很复杂”,而是:AI Coding 一旦从演示走向真实工作,系统的重心就会从模型能力转移到循环治理。 UI 可以换,模型可以换,工具甚至也可以继续加,但那条 loop 不能塌。它决定了 Agent 到底只是一个会说会调工具的助手,还是一个能在真实工程里连续干活的系统。(GitHub[2])

而这也是 Claude Code 第一层最值得拆的地方。终端只是你看到的表面,真正让它“像个能干活的东西”的,是那条不会轻易停下来的 query loop。下一篇再往下拆,你就会发现:这条 loop 之所以能跑起来,背后绕不开另一个更硬的问题——状态管理。(GitHub[2])

参考链接

  1. GitHub: https://github.com/claude-code-best/claude-code/blob/main/README.md
  2. GitHub: https://github.com/claude-code-best/claude-code/blob/main/CLAUDE.md
  3. GitHub: https://raw.githubusercontent.com/claude-code-best/claude-code/main/src/query.ts

claudecode无疑仍是当前最强cli agent

部署、改造、优化、定制

欢迎进群交流

群定位是跟进和研究最新的ai agent技术