Claude Code 源码解析 Part 3:QueryEngine 与主循环,为什么它不是聊天机器人,而是一个 runtime
前两篇我们分别讨论了 Claude Code 的多 Agent 编排,以及权限系统、Swarm 审批与 Sandbox。接下来进入它真正的内核:一次用户输入,到底是怎么在源码里被推进成一个长期可控的 agent loop 的。
如果只从表面看,Claude Code 很像“命令行里的聊天机器人”。但只要读过 src/QueryEngine.ts 和 src/query.ts,你会发现它的设计重心根本不在“对话”,而在“运行时”。
我想先给出一个判断:
Claude Code 的核心不是 prompt,也不是一次模型调用,而是一个显式的 runtime。
这句话听上去像概念,但源码里是有很强支撑的。它把一次请求拆成了两层:
QueryEngine负责会话级编排queryLoop负责 turn 级执行
这两个层次拆开之后,整个系统才具备了下面这些能力:
- 会话能恢复,而不是进程一断就丢状态
- 工具、权限、系统提示词、memory 能在统一入口里注入
- 上下文能被持续治理,而不是靠一次 summarize 硬救
- token / cost 有边界,主循环不是无止境采样
- 模型选择可以按运行时条件动态变化
一、QueryEngine 不是“回答问题”的类,而是“建立运行时条件”的类
很多人看到 QueryEngine 这个名字,会自然把它理解成“调用模型并返回回答的地方”。但源码里的实际职责更像是一个 coordinator。
它关心的不是“这次该怎么回答”,而是“这次回答开始前,运行时条件有没有准备好”。
从 src/QueryEngine.ts 里能看到几个关键动作:
- 包装
canUseTool,把工具权限拒绝纳入 SDK 状态 - 拉取 system prompt parts
- 注入 coordinator user context
- 在 custom prompt 存在时,额外处理 memory 相关的运行时约束
- 如果要求结构化输出,则注册 synthetic output tool
- 调
processUserInput - 把用户消息写入 mutable history
- 在正式 query 前先记录 transcript,确保中途异常也能恢复
这里最值得注意的一点是:
Claude Code 在模型真正回答之前,就先把“用户已经说了什么”落到会话记录里。
这不是一个普通聊天程序会优先考虑的事情。它更像数据库里的 write-ahead 思路:先把事实记录下来,再推进后续执行。这样即使主循环中途被打断,系统也能知道这轮交互已经开始过,恢复逻辑才有依据。
这就是 runtime 视角和 demo 视角的差别。
二、真正的内核在 query.ts:它不是一次调用,而是一个循环状态机
如果说 QueryEngine 是会话壳,那么 src/query.ts 才是 Claude Code 的执行内核。
这里最重要的理解是:
queryLoop 不是一个“模型 API 包装器”,而是一个持续推进状态的状态机。
在每一轮迭代里,它都会重新评估:
- 当前消息历史应该以什么形式进入下一轮
- 当前上下文是否已经接近失控,需要压缩或截断
- 当前工具调用结果如何并入历史
- 当前 token budget 和输出 budget 是否还允许继续
- 当前模型是否应该切换
- 当前是否要自动续跑,还是应该停住
这意味着 Claude Code 并不把“调用模型”当成一个孤立动作,而是把它放在一个更大的执行环里。模型只是 runtime 里的一个阶段,前后还有大量控制逻辑。
三、Claude Code 的上下文治理,不是一次 summarize,而是持续治理
这是这套设计里我认为最值得研究的一点。
很多 agent 系统在长对话里会逐渐失控,然后在某个时刻突然做一次总结,试图靠摘要把历史压回窗口里。Claude Code 不是这么做的。
从 src/query.ts 的执行流程可以看到,它对上下文的处理是分层的,而且是在每轮循环里不断发生:
- compact boundary 之后的消息投影
- history snip
- microcompact
- context collapse
- autocompact
这背后反映的是一种很成熟的工程观:
上下文不是“越长越好”,而是“必须持续治理”。
真正稳定的 agent,不是单纯依赖大窗口模型,而是主动管理哪些信息该保留,哪些信息该降级,哪些信息该摘要化,哪些信息根本不值得继续携带。
这也是为什么 Claude Code 更像 runtime,而不像聊天壳。聊天壳默认历史只会堆积;runtime 则会主动重写和整理自己的执行上下文。
四、QueryConfig 的 snapshot 设计,说明它非常在意执行边界
在 src/query/config.ts 里,Claude Code 会在 query 开始前生成一份 QueryConfig snapshot。
这个动作看起来很普通,但工程含义很强。
为什么要 snapshot?
因为一旦主循环开始运行,它就不应该在执行中途被外部漂移状态不断污染。运行时边界如果不冻结,系统会出现很多很难排查的问题:
- 某些 feature gate 在第 1 轮和第 3 轮看到的值不一样
- 某些模型配置在半途中改变
- 某些执行路径和日志无法复现
所以 Claude Code 的处理方式是:先冻结这轮 query 的执行条件,再让 loop 开始跑。
这个细节看起来不起眼,但非常体现工程成熟度。很多 agent 系统的问题,不是出在推理能力不够,而是出在运行时边界模糊。
五、Budget 系统说明 Claude Code 关心的不是“答出来”,而是“答得可控”
src/query/tokenBudget.ts 这类文件,特别容易被忽略。因为它们既不直接产出 UI,也不直接体现模型能力。
但恰恰是这些代码,说明 Claude Code 已经不是“采样器”,而是在做系统。
Budget tracking 的真正意义不是节省 token,而是把“能不能继续推理”从模型偏好变成 runtime 决策。一个真正可运行的 agent,必须接受这件事:
- 有些问题不能无限做
- 有些过程需要提前止损
- 有些自动续跑必须在边界内发生
换句话说,Claude Code 不是让模型自己决定什么时候停,而是让 runtime 决定这轮执行还有没有继续的资格。
这就是“系统约束高于单次采样”的设计哲学。
六、为什么说 Claude Code 不是聊天机器人,而是 runtime
如果把前面的点串起来,我们会得到一个清晰结论:
Claude Code 的主循环已经具备了 runtime 的几个典型特征:
- 有显式状态
- 有执行边界
- 有恢复意识
- 有资源预算
- 有上下文治理
- 有工具与权限注入
而一个普通聊天机器人,通常只具备其中最表层的一部分:接输入、调模型、吐输出。
这也是我认为 Claude Code 值得研究的原因。它真正给行业的启发,不是“prompt 怎么写”,而是:
当我们想把 LLM 变成长期工作单元时,必须先设计 runtime。
七、这一篇最值得学的 4 个点
最后做个收束。
如果你也在做 agent 系统,我认为 Claude Code 的 QueryEngine / queryLoop 至少有四个地方非常值得学:
- 把“会话级编排”和“turn 级执行”拆开,不要把所有逻辑塞进一次调用里。
- 让 transcript 在主执行前落盘或落状态,优先保证恢复一致性。
- 把上下文压缩做成持续机制,而不是对话快炸了才做一次总结。
- 让 budget、permission、tooling 成为 runtime 的一部分,而不是外围 patch。
在很多 agent demo 里,模型调用是中心,其他东西只是配件。Claude Code 刚好相反:runtime 才是中心,模型调用只是其中一个环节。
下一篇我们就继续看这个 runtime 的另一半:既然前台主循环能跑起来,那么它是怎么把 agent 托管为后台任务、怎么恢复、怎么停止、怎么统一管理本地与远程执行的?
这正是 Task Runtime 要解决的问题。
相关阅读:
深求社区(DeepSeek.club)出品,国内领先的AI大模型及应用开源社区!