Claude Code Agent Loop 详细设计文档
本文基于
claude-code/src/QueryEngine.ts与Claude-code-open-explain/02-agentic-loop/README.md,从软件架构视图、运行时流程视图、状态视图和关键逻辑视图分析 Claude Code 的 Agent Loop 设计。
1. 设计定位
Claude Code 的 Agent Loop 不是一个简单的 while 循环,也不是单个类完成所有事情。它采用了两层职责拆分:
QueryEngine.ts:会话级控制器,负责一轮请求前后的状态管理、参数组装、上下文准备、事件归一化、持久化与结果封装。query.ts:请求级执行引擎,负责模型调用、工具执行、继续或终止判断、上下文压缩、错误恢复等真正的循环推进。
可以把 QueryEngine 理解成"会话控制面",把 query() / queryLoop() 理解成"执行数据面"。前者决定本轮怎么开始、如何记账、如何把内部事件转成 SDK 消息;后者决定本轮如何一跳一跳地推进,直到模型不再需要工具或触发退出条件。
2. 总体架构视图
User / SDK Caller
ask convenience wrapper
QueryEngine
processUserInput
fetchSystemPromptParts
sessionStorage / transcript
AppState
query
queryLoop
Claude API streaming
Tool orchestration / StreamingToolExecutor
snip / microcompact / autocompact / context collapse
post-sampling hooks / stop hooks
Built-in tools / MCP tools / Agent tools
tool_result messages
assistant content blocks
SDKMessage stream
2.1 分层说明
| 层次 | 主要代码 | 核心职责 |
|---|---|---|
| 对外入口层 | ask()、QueryEngine.submitMessage() |
暴露 AsyncGenerator,支持流式输出、中断、SDK 结果封装 |
| 会话状态层 | QueryEngine 字段与 submitMessage() |
保存消息历史、权限拒绝、token 用量、文件状态、技能发现记录 |
| 输入处理层 | processUserInput() |
解析用户 prompt、slash command、附件、模型切换、允许工具规则 |
| 上下文构建层 | fetchSystemPromptParts()、asSystemPrompt() |
生成 system prompt、userContext、systemContext,并拼接自定义 prompt |
| 循环执行层 | query()、queryLoop() |
模型流式调用、工具执行、上下文整理、终止条件判断 |
| 工具执行层 | runTools()、StreamingToolExecutor |
识别 tool_use、执行工具、生成 tool_result |
| 记账与持久化层 | recordTranscript()、flushSessionStorage()、usage tracker |
会话恢复、token/cost 统计、错误诊断 |
| 输出适配层 | normalizeMessage()、SDK result messages |
将内部 message / stream event 转换成 SDK 可消费消息 |
3. 关键对象视图
3.1 QueryEngineConfig
QueryEngineConfig 是会话控制器的依赖注入边界。它把环境、工具、命令、模型配置、状态读写函数、权限函数以及 SDK 选项集中传入。
QueryEngineConfig
cwd
tools
commands
mcpClients
agents
canUseTool
initialMessages
readFileCache
customSystemPrompt
appendSystemPrompt
userSpecifiedModel
fallbackModel
thinkingConfig
maxTurns
maxBudgetUsd
taskBudget
jsonSchema
replayUserMessages
includePartialMessages
abortController
orphanedPermission
snipReplay
getAppState()
setAppState()
QueryEngine
config
mutableMessages
abortController
permissionDenials
totalUsage
readFileState
discoveredSkillNames
loadedNestedMemoryPaths
submitMessage()
interrupt()
getMessages()
getReadFileState()
getSessionId()
setModel()
3.2 QueryEngine 持有的长期状态
| 状态 | 生命周期 | 作用 |
|---|---|---|
mutableMessages |
跨多轮 submitMessage() |
保存会话消息历史,是后续上下文、持久化和恢复的基础 |
abortController |
引擎生命周期 | 支持用户中断或外部取消 |
permissionDenials |
引擎生命周期 | 汇总被拒绝的工具调用,最终通过 SDK result 返回 |
totalUsage |
引擎生命周期 | 汇总 token usage,用于成本和结果统计 |
readFileState |
引擎生命周期,ask() finally 写回 |
缓存文件读取状态,减少重复读取并保持文件状态一致 |
discoveredSkillNames |
每轮清空 | 记录本轮发现的 skill,供工具调用标记 was_discovered |
loadedNestedMemoryPaths |
跨轮保留 | 避免重复加载嵌套 memory |
这些状态说明 QueryEngine 不是无状态函数,而是一个会话对象。它每次处理一条用户消息时,都会基于已有状态构建本轮请求。
4. 运行时流程视图
4.1 一轮 submitMessage() 的主流程
是且未处理
否
否
是
是
否
submitMessage(prompt)
清空本轮 skill 发现记录
设置 cwd 与 session 持久化策略
包装 canUseTool 以记录 permissionDenials
确定初始模型和 thinkingConfig
fetchSystemPromptParts 获取 prompt/context
组合 systemPrompt
构造 ProcessUserInputContext
是否有 orphanedPermission
handleOrphanedPermission 并 yield 消息
processUserInput
追加用户消息到 mutableMessages
提前 recordTranscript 支持 resume
更新 AppState 中的 alwaysAllowRules
重新构造 ProcessUserInputContext
加载 skills / plugins
yield system init message
shouldQuery?
输出本地命令结果和 success result
进入 query()
消费 query 产生的内部消息和流事件
更新 mutableMessages / transcript / usage / stop_reason
预算、turn、structured output 等是否触发错误出口
yield error result 并 return
query 正常结束
判断最终 result 是否成功
yield success 或 error_during_execution
4.2 时序图
Transcript Storage Tool Executor Claude API query/queryLoop queryContext processUserInput QueryEngine SDK Caller Transcript Storage Tool Executor Claude API query/queryLoop queryContext processUserInput QueryEngine SDK Caller alt [assistant contains tool_use] [no tool_use] loop [until no tool_use or terminal condition] alt [local command only] [requires model query] submitMessage(prompt) fetchSystemPromptParts() defaultSystemPrompt, userContext, systemContext processUserInput(prompt) messages, shouldQuery, allowedTools, model recordTranscript(user messages) system init SDKMessage replay command output result(success) query(messages, prompt, context, canUseTool) compact / normalize / prepare messagesForQuery streaming request stream events + assistant content blocks stream / assistant messages normalized SDK messages permission check + execute tool tool_result user message with tool_result recordTranscript(updated messages) terminal assistant message result(success/error)
5. query.ts 循环执行视图
介绍文档里提到的关键点是:query() 本身只是生成器入口,真正的持续推进发生在 queryLoop() 的 while (true) 中。
5.1 query() 与 queryLoop() 的关系
query(params)
创建 consumedCommandUuids
yield* queryLoop(params, consumedCommandUuids)
queryLoop 正常返回 terminal
notifyCommandLifecycle completed
return terminal
query() 的职责很薄,主要是包住 queryLoop(),并在循环正常结束后通知被消费的命令完成。异常或 generator 被 .return() 关闭时,不会走完成通知,从而保留"已开始但未完成"的语义。
5.2 queryLoop() 单次迭代逻辑
否
是
是
否
while true iteration
从 State 解构 messages / toolUseContext / turnCount 等
启动 skill discovery prefetch
yield stream_request_start
建立 queryTracking chainId/depth
getMessagesAfterCompactBoundary
applyToolResultBudget
snip compact 可选
microcompact
context collapse 可选
append systemContext 到 systemPrompt
autocompact
prepend userContext / normalizeMessagesForAPI
queryModelWithStreaming
收集 assistantMessages 与 toolUseBlocks
是否有 tool_use
执行 stop hooks / 判断最终结束
return terminal
执行工具并生成 tool_result
追加 assistant + tool_result 到 messages
maxTurns 是否超限
yield max_turns_reached attachment; return
更新 State, transition=next_turn
5.3 循环状态对象
queryLoop() 内部维护一个跨迭代 State,用不可变参数 + 可变状态的组合降低复杂度。
State
messages
toolUseContext
autoCompactTracking
maxOutputTokensRecoveryCount
hasAttemptedReactiveCompact
maxOutputTokensOverride
pendingToolUseSummary
stopHookActive
turnCount
transition
设计要点:
messages是每轮传给模型的基础,但进入模型前会经过 compact boundary、tool result budget、microcompact、context collapse、autocompact 等多层整理。toolUseContext会在迭代中加入queryTracking,用于区分当前 loop chain 的深度。turnCount用于maxTurns控制,工具回流后会进入下一 turn。transition记录上一轮继续的原因,便于测试和诊断恢复路径。
6. 消息与数据流视图
6.1 消息闭环
否
是
否
是
User prompt
user message
queryLoop prepares messagesForQuery
Claude API
assistant message
扫描 content blocks
包含 tool_use?
assistant text / end_turn
tool_use block
canUseTool permission check
allow?
记录 permissionDenials
生成错误 tool_result
执行工具
生成 tool_result
user message with tool_result
6.2 内部消息到 SDK 消息的转换
query() 产生的是内部 Message、StreamEvent、RequestStartEvent、AttachmentMessage 等。QueryEngine.submitMessage() 会按类型处理,再转换为 SDK 语义:
| 内部消息 | QueryEngine 行为 |
对外结果 |
|---|---|---|
assistant |
追加到 mutableMessages,归一化输出 |
assistant SDK message |
user |
通常代表 tool_result 回流,追加并输出 |
user replay / tool result related message |
stream_event |
累计 usage、捕获 stop_reason;可选透出 partial | stream event SDK message 或仅内部记账 |
progress |
追加并记录 transcript | normalized progress |
attachment |
处理 structured output、max turns、queued command | result error / user replay / internal state |
system compact_boundary |
裁剪本地历史,输出 compact boundary SDK message | system compact boundary |
system api_error |
转成 SDK api_retry |
system api_retry |
tool_use_summary |
直接转成 SDK tool use summary | tool_use_summary |
tombstone |
作为控制信号,不对外输出 | 无 |
7. 上下文构建与整形视图
7.1 QueryEngine 层的上下文准备
submitMessage
确定 initialMainLoopModel
确定 initialThinkingConfig
fetchSystemPromptParts
defaultSystemPrompt
baseUserContext
systemContext
合并 coordinator userContext
选择 customSystemPrompt 或 defaultSystemPrompt
可选注入 memoryMechanicsPrompt
可选 appendSystemPrompt
asSystemPrompt
传入 query()
QueryEngine 这一层的上下文关注"来源":
defaultSystemPrompt:默认系统规则。customSystemPrompt:SDK 调用方显式覆盖系统提示词时使用。appendSystemPrompt:在基础提示词后追加策略。userContext:用户侧上下文,例如环境、工作区信息、coordinator 信息。systemContext:系统侧上下文,在queryLoop()内被追加到 system prompt。
7.2 queryLoop() 层的上下文整形
messages
getMessagesAfterCompactBoundary
applyToolResultBudget
HISTORY_SNIP snipCompactIfNeeded
microcompact
CONTEXT_COLLAPSE applyCollapsesIfNeeded
autocompact
appendSystemContext
prependUserContext
normalizeMessagesForAPI
Claude API request
这说明 Claude Code 不会把内存里的所有消息原样丢给模型。每次 API 调用前都会构造一个"本轮可见视图",它可能已经丢弃 compact boundary 之前的消息、替换超长工具结果、应用 snip/microcompact/autocompact 或 context collapse。
8. 工具调用逻辑视图
8.1 为什么不只看 stop_reason
query.ts 明确认为 stop_reason === 'tool_use' 不可靠,因此本地会直接扫描 assistant content block 中的 tool_use。这比依赖模型响应的 stop reason 更具体,因为真正决定是否继续循环的是:是否存在尚未回流的工具调用。
是
否
是
否
assistant message
遍历 content blocks
block.type == tool_use?
加入 toolUseBlocks
继续扫描
needsFollowUp = true
扫描结束
toolUseBlocks.length > 0
执行工具并回流 tool_result
本轮可能结束
8.2 权限包装逻辑
QueryEngine 不直接决定工具权限,而是包装外部传入的 canUseTool。包装层只增加 SDK 需要的审计信息:
是
否
wrappedCanUseTool
调用原始 canUseTool
result.behavior == allow?
返回 allow 结果
记录 SDKPermissionDenial
tool_name
tool_use_id
tool_input
返回拒绝结果
这个设计让权限策略与审计记录解耦:权限系统仍然由调用方或 AppState 决定,QueryEngine 只负责把拒绝事件纳入会话结果。
9. 状态机视图
9.1 QueryEngine 会话生命周期
constructor 初始化状态
submitMessage
shouldQuery=false
yield local result
shouldQuery=true
query emits stream/message
tool_result 回流后继续
final assistant/user successful
maxTurns / budget / structured output / execution error
yield success result
yield error result
interrupt()
后续调用可复用或由上层释放
Constructed
Ready
PreparingTurn
LocalOnly
Querying
Streaming
Succeeded
Failed
Interrupted
9.2 queryLoop() 迭代状态
tool_use detected
next_turn
no tool_use
hook requests retry
prompt too long / max output tokens / fallback
PrepareVisibleContext
CallModel
StreamAssistant
ExecuteTools
AppendToolResults
CheckLimits
MaxTurnsReached
StopHooks
TerminalSuccess
Recovery
TerminalError
10. 持久化与恢复视图
QueryEngine 很重视 transcript 写入时机。它会在用户消息被接受后、进入模型请求前提前写 transcript。这样即使进程在 API 返回前被杀掉,--resume 仍然能找到这次用户输入,而不是得到"没有对话"的状态。
否
是
是
否
是
否
processUserInput 返回 messagesFromUserInput
push 到 mutableMessages
persistSession?
继续执行
recordTranscript(messages)
bare mode?
fire-and-forget
await 写入完成
EAGER_FLUSH 或 COWORK?
flushSessionStorage
后续在 query() 输出 assistant、user、compact boundary、progress、attachment 时,QueryEngine 还会继续维护 transcript。对 assistant 消息,它倾向于 fire-and-forget,避免阻塞流式输出;对需要保证顺序和恢复正确性的消息,则会 await。
11. 退出条件与错误出口
11.1 QueryEngine 层的结果封装
否
是
query for-await 结束
找到最后一个 assistant 或 user
flush transcript if needed
isResultSuccessful?
yield error_during_execution
提取 assistant 最后一段 text
yield result success
11.2 主要错误出口
| 出口 | 触发条件 | 返回 subtype | 说明 |
|---|---|---|---|
| 本地命令正常结束 | shouldQuery=false |
success |
不进入模型循环 |
| 最大 turn 数 | query.ts 产生 max_turns_reached attachment |
error_max_turns |
防止工具循环无限推进 |
| 预算上限 | getTotalCost() >= maxBudgetUsd |
error_max_budget_usd |
SDK/CLI 成本保护 |
| 结构化输出重试过多 | jsonSchema 下 SyntheticOutput tool 调用超过限制 |
error_max_structured_output_retries |
防止模型一直无法满足 schema |
| 执行期间异常终态 | 最终消息不满足成功谓词 | error_during_execution |
附带 turn-scoped error diagnostics |
| API retry | system 消息 subtype 为 api_error |
api_retry 系统事件 |
不是最终 result,而是中间重试事件 |
12. 并发与顺序性的设计
单个 queryLoop() 对工具调用采取偏顺序、偏保守的推进方式。即使模型一次给出多个 tool_use,系统也会围绕"assistant tool_use -> 本地 tool_result -> 下一轮 messages"的闭环推进。这样做有几个工程收益:
- 前一个工具结果可以影响下一步模型判断,避免过早并发造成无效工作。
- 消息链与 transcript 更容易保持一致,便于恢复。
- UI 可以按顺序展示模型决策和工具结果,用户更容易理解。
- 权限拒绝、工具失败和中断更容易定位到具体
tool_use_id。
Claude Code 并不是没有并发,而是把并发放在更高层:例如 AgentTool 或团队/多 Agent 能力可以启动多个独立 loop。单个 loop 保持可推理,多 Agent 层负责并行。
13. 设计权衡
13.1 会话控制器与执行循环拆分
优点:
QueryEngine可以专注 SDK/会话语义,包括 transcript、usage、权限审计、消息归一化。queryLoop()可以专注模型和工具之间的执行闭环。- 测试边界更清晰:状态管理与循环推进可以分开验证。
代价:
- 一条用户请求会跨多个抽象层,初学者容易误以为
QueryEngine.ts就是全部 Agent Loop。 - 消息在内部格式、API 格式、SDK 格式之间多次转换,需要严格维护配对关系。
13.2 显式 tool_result 回流
优点:
- 模型不会"自动知道"工具结果,所有结果都在本地显式写回 messages。
- transcript、resume、debug 都能看到完整因果链。
- 可以在中断或异常时补齐缺失的
tool_result,避免 API 看到不成对的工具消息。
代价:
- 工具执行、错误恢复和消息修复逻辑必须非常谨慎,尤其是流式 fallback 或 abort 场景。
13.3 每轮主动整理上下文
优点:
- 长会话可持续运行,不会无限膨胀。
- tool result budget、microcompact、autocompact、context collapse 可以分别处理不同类型的上下文压力。
代价:
- "内存中的完整历史"和"发给模型的可见视图"并不总是一致,调试时需要区分这两个概念。
14. 软件视图总结
Claude Code Agent Loop
会话控制面 QueryEngine
状态持有
mutableMessages
readFileState
totalUsage
permissionDenials
输入准备
processUserInput
fetchSystemPromptParts
thinking/model selection
输出适配
normalizeMessage
SDK result
api_retry
持久化
recordTranscript
flushSessionStorage
执行数据面 queryLoop
上下文整形
compact boundary
tool result budget
microcompact
autocompact
context collapse
模型调用
streaming
usage
stop_reason capture
工具闭环
scan tool_use
canUseTool
execute tool
emit tool_result
退出控制
no tool_use
maxTurns
budget
structured output retry
execution error
产品化能力
流式体验
中断恢复
权限审计
多 Agent 并发
会话 resume
从软件设计角度看,Claude Code 的 Agent Loop 成熟之处不在于 while (true) 本身,而在于它围绕这个循环建立了完整的工程外壳:上下文构建、工具闭环、权限审计、流式输出、持久化恢复、token/成本记账和多种压缩策略。QueryEngine 让一轮请求具备"产品会话"的完整语义,queryLoop() 则让模型和工具能在一个可控、可恢复、可观测的闭环中持续协作。