Claude Code 的核心不是“一次回答”,而是一轮又一轮的模型与工具协作
很多人会把 QueryEngine.ts 当成完整循环,但这样理解会错过一些关键细节。
更准确的说法是:
QueryEngine.ts负责组织一轮请求——它是会话级的状态管理器query.ts负责执行真正的循环——它是请求级的执行引擎
打个比方:如果把一次对话比作一场演出,QueryEngine 就像是导演和舞台监督,负责准备道具、安排演员、记录进度;而 query() 则是真正上台表演的演员,负责一幕一幕地推进剧情。
职责分离
QueryEngine 管理的是"这次对话的整体状态":消息历史、token 用量、权限拒绝记录、文件状态缓存。这些状态可能跨越多轮请求,需要长期持有。
query() 管理的是"这一轮请求的执行过程":调模型、执行工具、回传结果、判断继续还是停止。这些逻辑是请求级的,每次都是一个完整的生命周期。
可测试性 如果把所有逻辑都塞进一个巨大的类里,测试会变得非常困难。拆分后,你可以单独测试"状态管理是否正确"和"循环执行是否正确"。
可维护性
当你需要修改"如何判断是否继续循环"时,只需要看 query.ts;当你需要修改"如何记录权限拒绝"时,只需要看 QueryEngine.ts。职责清晰,改动范围可控。
这种拆分不是过度设计,而是让一个复杂系统保持可理解的必要手段。
| 文件 | 作用 |
|---|---|
src/QueryEngine.ts |
一轮请求的状态管理、参数组装、对外接口 |
src/query.ts |
模型调用、工具执行、继续/终止判断的核心循环 |
src/utils/queryContext.ts |
获取 System Prompt、userContext、systemContext |
Claude Code 的一次回答,通常不是:
用户提问 -> 模型一次性回答 -> 结束
而更像:
用户提问
↓
模型先判断要不要调工具
↓
如果要,就请求某个工具
↓
Claude Code 执行工具
↓
把工具结果再发回模型
↓
模型基于新结果继续决定下一步
↓
直到模型认为任务完成
这就是 Agent Loop。
从源码看,一个 QueryEngine 实例大体对应一段对话会话。
它会长期持有这些状态:
- 消息历史
- 中断控制器
- 权限拒绝记录
- token 用量
- 文件读取状态缓存
- 本轮或本会话发现的技能/工具信息
所以它不是单纯的函数,而是一个带状态的会话控制器。
当用户发来一条消息时,submitMessage() 会先做准备工作,再把任务交给 query()。
例如:
- 用哪个模型
- thinking 是用户强制指定,还是默认 adaptive
- 本轮有哪些工具可用
- 这轮请求的 prompt 和上下文是什么
源码里有一个非常值得注意的小细节:canUseTool 会被包一层,变成 wrappedCanUseTool。
这层包装的作用不是改权限逻辑,而是补充记录:
- 哪些工具被拒绝了
- 对应的
tool_use_id是什么 - 输入参数是什么
这说明 Claude Code 不只是“问一句允许不允许”,它还在认真记录整轮工具交互的状态。
如果要用一句话概括 query.ts,可以说:
它负责把“调用模型”和“执行工具”拼成一个可以持续推进的循环。
query() 本身不是那条长期运转的核心循环。
它更像一层外部包装,真正反复推进的是 queryLoop() 里的 while (true)。
这不是命名细节,而是理解整套系统的关键:
query()负责暴露生成器接口queryLoop()负责维护循环状态- 很多 gate 会在进入
queryLoop()前先被快照
准备好的 messages + prompt + context
↓
调用模型
↓
流式收到 assistant 输出
↓
如果包含 tool_use:
- 找到对应工具
- 检查权限
- 执行工具
- 生成 tool_result
- 把结果追加回消息历史
- 再次调用模型
↓
如果不再包含 tool_use:
- 输出最终文本
- 本轮结束
但真实代码比这个简图多了几层重要细节:
- 发给模型的消息不是原始 transcript,而是先做上下文裁切、压缩视图整理,再做 API 协议归一化
tool_use的识别不是靠stop_reason === 'tool_use',而是直接扫描 assistant content blocktool_result回流不是隐式发生,而是本地显式重建成下一轮的 user-side message- “继续 / 停止 / 压缩”也不是一个二叉判断,而是多层关卡串起来的结果
因为 Claude Code 的绝大多数聪明行为,都不是 CLI 自己规划出来的,而是模型在每一轮拿到新上下文后临时做的决定。
所以这套 loop 的首要任务不是“替模型思考”,而是:
- 准确传递上下文
- 正确执行工具
- 稳定记录状态
- 在每一轮之间保持一致性
这里还有一个很重要但新手容易漏掉的事实:
每一轮发给模型的内容,已经被运行时主动整理过一次,而不是把内存里的所有消息原封不动扔进去。
如果只是普通的 async function,那调用方只能“等最终结果”。
但 AsyncGenerator 的好处是:
- 模型吐出一点文本,就能立刻显示一点
- 工具开始执行时,UI 可以实时显示
- 某一步失败时,不需要等整轮结束才知道
- 用户可以在中途打断
这和终端产品的体验直接相关。对 CLI 来说,“能流出来”比“最后一次性返回”重要得多。
在 QueryEngine.ts 里,本轮请求并不是只拿用户输入去调用模型。
它会先通过 fetchSystemPromptParts() 拿到三类上下文:
defaultSystemPromptuserContextsystemContext
这一步很关键,因为 Claude Code 从一开始就不把“上下文”简单理解为聊天记录。它知道不同来源的信息要走不同通道,这样模型才能既有规则,又有环境感知,还能兼顾缓存。
从 query.ts 和 claude.ts 的调用链看,进入模型 API 前至少有两层整形:
-
循环级整形
- 裁掉 compact 边界之前不需要直接参与本轮的历史
- 处理过长的 tool result
- 结合 snip / microcompact / context collapse 等机制
- 把
systemContext和userContext拼进本轮可见输入
-
协议级整形
normalizeMessagesForAPI()- 修复 tool/result pairing
- 清理某些模型相关的冗余字段
- 控制媒体与附加内容的发送形状
submitMessage() 还会处理两个基础问题:
大体是:
- 如果用户显式指定模型,就优先用显式值
- 否则回退到会话或默认模型
源码里默认偏向 adaptive,意思是:
让系统按情况决定是否进入更重的思考模式,而不是每轮都强开。
这本质上是在质量、速度和 token 成本之间做平衡。
这是很多人读到这里会问的问题:既然模型可能一次返回多个工具调用,为什么不并发跑?
Claude Code 选择了更保守也更稳定的策略:默认按顺序推进。
原因很现实:
- 前一个工具结果可能决定后一个工具是否还需要执行
- 顺序执行更容易让模型保持一致的推理链
- 出错时更容易定位责任点
- UI 展示和用户理解成本更低
在开启流式工具执行时,assistant 流一到达带 tool_use 的 content block,系统就能立刻把它结构化捕获并交给执行器,而不是必须等整条 assistant 响应结束。
Claude Code 不是完全不能并发,而是把并发放在更高层:
- 通过
AgentTool生成子 Agent - 通过
TeamCreateTool管理多 Agent 协作 - 由每个子 Agent 各自运行独立 loop
也就是说,单个 loop 保持简单,真正需要并行时,用多个 loop,而不是把一个 loop 改成复杂并发引擎。
一旦某个工具执行完成,Claude Code 会把工具产物整理成下一轮输入的一部分,而不是让模型“自动知道结果回来了”。
真正的闭环更像这样:
assistant content
-> 识别 tool_use
-> 本地执行工具
-> 生成 tool_result
-> 重建 messages
-> 再次调用模型
如果你以后要做自己的 Agent CLI,这一章最值得借鉴的是:
QueryEngine.ts 与 query.ts 的拆分很重要,它让代码职责更清楚。
从模型输出、工具执行到 UI 渲染,都能自然串起来。
真正复杂的是 prompt、上下文、权限和工具,不是 while 循环本身。
不准确。真正难的是每轮之间怎么维护消息、权限、上下文、压缩和错误恢复。
不对。它是组织者,真正执行循环的核心仍在 query.ts。
不对。AsyncGenerator 是这类交互式 Agent 工具体验的重要基础。
不对。Claude Code 在本地会直接扫描 assistant content block 里的 tool_use,这比只看 stop_reason 更具体,也更稳定。
Claude Code 的 Agent Loop 体现出一个很成熟的产品化思路:
- 主循环保持直接
- 会话状态独立维护
- 工具调用串行推进
- 结果以流式方式暴露给 UI
- 并发能力通过多 Agent 层引入,而不是污染单 loop
- 发给模型的消息每轮都会被主动整理,而不是原样透传
tool_use -> tool_result -> 下一轮 messages是本地显式闭环
- 03 — 工具系统架构:继续看模型请求的工具到底是怎么定义和执行的
- 05 — 上下文管理与压缩:继续看这套循环跑久了以后上下文是怎么管理的