源码位置:
src/query.ts、src/QueryEngine.ts、src/query/目录核心类:QueryEngine(会话管理)、queryLoop(执行循环)设计哲学:LLM 驱动的自治执行,工具调用闭环,多层错误恢复
1. 概述:Agent 自主执行的本质
一个 AI Coding Agent 的核心能力在于「自己干活」-- 自主地思考、行动、观察结果、再思考,形成一个闭环的执行循环。这个循环就是 Claude Code 的灵魂。
1.1 思考-行动-观察循环
Claude Code 的自主执行循环遵循经典的 ReAct(Reasoning + Acting)范式:
用户输入 | v[LLM 思考] --> 生成文本 + 工具调用 | v[执行工具] --> Bash / Read / Edit / Agent / ... | v[获取结果] --> 工具返回值 | v[结果返回 LLM] --> LLM 根据结果决定下一步 | v循环继续,直到 LLM 认为任务完成(不调用任何工具)与简单聊天机器人不同的是,这个循环可以持续数十甚至上百轮,Agent 可以自主地读取文件、编辑代码、运行测试、修复错误,直到任务完成。Claude Code 通过 while(true) 无限循环来实现这一点(src/query.ts:307),退出条件完全由 LLM 的决策决定。
1.2 关键设计原则
从源码中可以提炼出几个关键的设计原则:
- LLM 是决策者
循环的每一次迭代由 LLM 决定是否继续(通过是否输出 tool_use来判断) - 工具是执行者
所有实际操作(读文件、写代码、运行命令)通过工具系统完成 - 结果是反馈
工具执行结果作为下一轮 LLM 决策的输入,形成闭环 - 上下文是记忆
消息数组 messages是 Agent 的完整工作记忆,包含所有历史决策和结果
2. QueryEngine:会话生命周期管理器
2.1 类的职责
QueryEngine(src/QueryEngine.ts)是整个自主执行循环的外层包装,负责:
- 会话状态管理
维护消息历史 mutableMessages、文件读取缓存readFileState、token 用量统计totalUsage - 系统提示词构建
从配置和工具定义中组装系统提示词 - 用户输入处理
将用户输入转化为 API 消息格式 - 查询循环驱动
调用 query()函数并处理返回的流式消息 - 结果提取与上报
从最终消息中提取文本结果,上报给调用者
2.2 核心状态
exportclassQueryEngine {privateconfig: QueryEngineConfigprivatemutableMessages: Message[] // 完整的对话历史privateabortController: AbortController// 中断控制privatepermissionDenials: SDKPermissionDenial[] // 权限拒绝记录privatetotalUsage: NonNullableUsage// 累计 token 用量privatereadFileState: FileStateCache// 文件读取缓存private discoveredSkillNames = newSet<string>() // 已发现的技能}2.3 submitMessage 方法
QueryEngine.submitMessage() 是每次用户发送消息时调用的核心方法(src/QueryEngine.ts:209)。它是一个 AsyncGenerator,通过 yield 逐步产出 SDK 消息给调用者。其主要流程:
- 初始化阶段
设置系统提示词、用户上下文、工具权限 - 处理用户输入
调用 processUserInput()解析斜杠命令、附件等 - 驱动查询循环
通过 for await消费query()生成器 - 消息分发
根据消息类型(assistant/user/system/attachment/stream_event)分别处理 - 终止判定
检查是否达到最大轮数、预算上限等终止条件 - 结果产出
提取最终文本结果,产出 result消息
2.4 ask 便捷函数
ask() 函数(src/QueryEngine.ts:1186)是 QueryEngine 的便捷包装,适用于一次性查询场景(如 SDK 调用、headless 模式)。它创建一个临时的 QueryEngine,执行一轮对话后返回结果。
3. queryLoop:核心执行循环详解
queryLoop(src/query.ts:241)是 Claude Code 自主执行循环的真正核心。它实现了一个 while(true) 无限循环,每次迭代执行一次完整的「LLM 调用 + 工具执行」回合。
3.1 循环结构总览
while (true) { // 1. 上下文预处理 messagesForQuery = getMessagesAfterCompactBoundary(messages) // 微压缩(microcompact) // 上下文折叠(context collapse) // 自动压缩(autocompact) // 2. 调用 LLM(流式) for await (const message of deps.callModel({...})) { // 收集 assistant 消息 // 收集 tool_use 块 // 流式工具执行(StreamingToolExecutor) } // 3. 终止判定 if (!needsFollowUp) { // 没有工具调用 -> 处理 stop hooks,返回 // 有错误 -> 尝试恢复,返回 } // 4. 执行剩余工具 for await (const update of runTools(...)) { // 收集工具结果 } // 5. 附件注入 // 内存附件、技能发现、队列命令 // 6. 继续下一轮 state = { messages: [...messagesForQuery, ...assistantMessages, ...toolResults], ... }}3.2 可变状态(State)
循环维护一个可变的 State 对象(src/query.ts:268),在每次迭代开始时解构:
typeState = {messages: Message[] // 当前消息数组toolUseContext: ToolUseContext// 工具执行上下文autoCompactTracking?: AutoCompactTrackingState// 自动压缩追踪maxOutputTokensRecoveryCount: number// 输出 token 超限恢复次数hasAttemptedReactiveCompact: boolean// 是否已尝试反应式压缩turnCount: number// 当前轮数pendingToolUseSummary?: Promise<...> // 待处理的工具使用摘要stopHookActive?: boolean// 是否正在执行 stop hookmaxOutputTokensOverride?: number// 输出 token 上限覆盖transition?: { reason: string } // 上一次状态转换原因(调试用)}这个设计的核心思想是:每次循环迭代都可能改变整个状态(尤其是 messages),通过 state = { ...next } 实现状态的不可变更新,然后在下一次迭代开始时解构。
3.3 每次迭代的详细流程
第一步:上下文预处理
在发送 API 请求之前,循环对消息进行多层预处理:
- 获取最新消息
getMessagesAfterCompactBoundary(messages)只取最后一次压缩边界之后的消息,避免发送已压缩的旧消息 - 工具结果预算控制
applyToolResultBudget()对过大的工具返回值进行截断 - Snip 压缩
如果启用了 HISTORY_SNIP,进行历史消息裁剪 - 微压缩(Microcompact)
deps.microcompact()对消息进行轻量级压缩 - 上下文折叠(Context Collapse)
如果启用了 CONTEXT_COLLAPSE,对消息进行折叠投影 - 自动压缩(Autocompact)
如果 token 使用量超过阈值,触发完整压缩
这些预处理确保每次 API 调用发送的消息都在上下文窗口限制之内。
第二步:调用 LLM
核心是 deps.callModel()(生产环境为 queryModelWithStreaming),它发送流式请求到 Claude API。循环通过 for await 逐个消费流式响应事件:
stream_event流式内容块(文本、工具调用等) assistant完整的 assistant 消息(含 text + tool_use 块) user工具执行结果(在流式执行模式下)
关键细节:StreamingToolExecutor 在流式阶段就开始执行工具(src/query.ts:562-568)。当 LLM 输出一个完整的 tool_use 块时,StreamingToolExecutor.addTool() 立即启动该工具的执行,而不是等到所有工具调用都输出完毕。这大幅降低了端到端延迟。
第三步:终止判定
needsFollowUp 变量(src/query.ts:558)跟踪是否有工具调用需要继续执行。它在收集到任何 tool_use 块时被设为 true(src/query.ts:834)。
当 needsFollowUp 为 false 时(LLM 没有调用任何工具),循环进入终止路径:
- 检查是否被截断
如果 LLM 输出被 max_output_tokens截断,注入恢复消息让 LLM 继续 - 处理反应式压缩
如果 API 返回 prompt_too_long错误,尝试自动压缩后重试 - 执行 stop hooks
handleStopHooks()执行后处理钩子(如内存提取、模板分类) - 返回终止状态
return { reason: 'completed' }
第四步:执行工具
如果 needsFollowUp 为 true,进入工具执行阶段:
- 流式模式
streamingToolExecutor.getRemainingResults()获取尚未完成的工具执行结果 - 传统模式
runTools(toolUseBlocks, ...)批量执行所有工具
工具执行的并发策略(src/services/tools/toolOrchestration.ts):
- 只读工具
(如 Read、Grep、Glob):可以并行执行,最大并发数为 10 - 写入工具
(如 Edit、Write、Bash):必须串行执行,保证顺序
第五步:附件注入
工具执行完成后,循环注入各种附件消息(src/query.ts:1580-1628):
- 内存附件
startRelevantMemoryPrefetch预取的相关内存文件 - 技能发现
skillPrefetch.collectSkillDiscoveryPrefetch()发现的相关技能 - 队列命令
getQueuedCommandAttachments()获取排队的任务通知
这些附件提供了 LLM 在下一轮决策时需要的额外上下文。
第六步:继续下一轮
循环更新状态并继续(src/query.ts:1715-1728):
constnext: State = {messages: [...messagesForQuery, ...assistantMessages, ...toolResults],toolUseContext: toolUseContextWithQueryTracking,turnCount: nextTurnCount,// ... 其他字段重置}state = next关键:messages 数组在每轮结束时拼接了 assistant 的响应和工具执行结果,确保下一轮 LLM 能看到完整的对话历史。
3.4 终止条件
循环的终止条件(return 语句)包括:
needsFollowUp === false | ||
aborted | ||
max_turns | ||
max_budget_usd | ||
blocking_limit | ||
prompt_too_long | ||
hook_stopped | ||
model_error | ||
stop_hook_prevented | ||
stop_hook_prevented |
4. 工具调用闭环:LLM 决策到执行反馈
4.1 工具执行流水线
从 LLM 输出工具调用到获得执行结果,经历以下阶段:
LLM 输出 tool_use 块 | v工具查找(findToolByName) | v权限检查(canUseTool -> checkPermissions) | v参数验证(tool.inputSchema.safeParse) | v工具执行(tool.call(input, context)) | v结果包装(createUserMessage + tool_result) | v结果注入到 messages 数组 | v下一轮 LLM 调用时作为输入4.2 StreamingToolExecutor:流水线式执行
StreamingToolExecutor(src/services/tools/StreamingToolExecutor.ts)是 Claude Code 的一项重要优化。传统的工具执行模式是「收集所有工具调用 -> 批量执行」,而流式模式是「边收集边执行」:
// 当一个完整的 tool_use 块到达时,立即加入执行队列for (const toolBlock of msgToolUseBlocks) { streamingToolExecutor.addTool(toolBlock, message)}// 立即获取已完成的结果for (const result of streamingToolExecutor.getCompletedResults()) {if (result.message) {yield result.message toolResults.push(...) }}这种设计使得多个工具可以在 LLM 仍在输出时就开始执行,显著降低了总延迟。
4.3 工具并发控制
toolOrchestration.ts 中的 partitionToolCalls() 函数将工具调用分为两类:
- 并发安全
( isConcurrencySafe):如 Read、Grep、Glob 等只读操作,可以并行执行 - 非并发安全
如 Edit、Write、Bash 等写入操作,必须串行执行
// 只读工具并行执行,写入工具串行执行for (const { isConcurrencySafe, blocks } ofpartitionToolCalls(toolUseMessages, ...)) {if (isConcurrencySafe) {yield* runToolsConcurrently(blocks, ...) } else {yield* runToolsSerially(blocks, ...) }}4.4 权限检查与用户交互
每个工具执行前都经过权限检查(canUseTool)。对于需要用户确认的操作(如 Bash 命令、文件编辑),系统会暂停执行并等待用户授权。这确保了 Agent 在自主执行时不会做出危险操作。
5. 上下文压缩:长对话的自动瘦身
5.1 为什么需要压缩
Claude 的上下文窗口有固定上限(如 200K token)。在一个复杂的编码任务中,对话历史、文件内容、工具结果会迅速填满窗口。如果放任不管,Agent 在执行 20-30 轮后就会因为上下文溢出而无法继续工作。
Claude Code 实现了多层压缩策略来解决这个问题。
5.2 自动压缩(Autocompact)
自动压缩是最核心的压缩机制,由 autoCompactIfNeeded()(src/services/compact/autoCompact.ts:241)驱动。
触发条件:当消息的 token 估算值超过阈值时触发:
exportfunctiongetAutoCompactThreshold(model: string): number {const effectiveContextWindow = getEffectiveContextWindowSize(model)return effectiveContextWindow - AUTOCOMPACT_BUFFER_TOKENS// 保留 13K token 缓冲}压缩流程:
调用 compactConversation()生成对话摘要用 LLM(通常使用快速模型)总结历史消息 生成摘要消息替换原始历史 创建 compact_boundary系统消息标记压缩边界恢复最近读取的文件( createPostCompactFileAttachments)恢复计划文件、技能附件等重要上下文
断路器机制:自动压缩有连续失败计数器(最多 3 次),防止在不可恢复的情况下反复尝试压缩浪费资源。
5.3 微压缩(Microcompact)
微压缩是一种轻量级的压缩策略,在每次循环迭代的预处理阶段执行。它对特定类型的工具返回值进行裁剪,减少 token 占用。
5.4 Snip 压缩
Snip 压缩(src/services/compact/snipCompact.ts)通过裁剪历史消息中的冗余部分来减少 token 占用。它在微压缩之前运行,裁剪掉不再需要的旧消息。
5.5 反应式压缩(Reactive Compact)
反应式压缩(src/services/compact/reactiveCompact.ts)是一种被动触发的压缩机制。当 API 返回 prompt_too_long 错误时,它会捕获这个错误,对消息进行压缩,然后自动重试请求。
这是最后一道防线:如果主动压缩(Autocompact)和预处理都没能阻止上下文溢出,反应式压缩会尝试恢复。
5.6 压缩后的上下文恢复
压缩后,Agent 需要恢复一些关键上下文才能继续工作。buildPostCompactMessages() 构建的消息序列:
boundaryMarker -- 压缩边界标记summaryMessages -- 历史摘要messagesToKeep -- 保留的近期消息fileAttachments -- 最近读取的文件(最多 5 个)asyncAgentAttachments -- 后台 Agent 的状态planAttachment -- 计划文件planModeAttachment -- 计划模式指令skillAttachment -- 已调用的技能toolDeltaAttachments -- 工具定义重新公告agentListingDelta -- Agent 列表重新公告mcpInstructionsDelta -- MCP 指令重新公告hookResults -- 钩子执行结果这种精心设计的恢复机制确保了压缩后 Agent 仍然能理解当前任务的状态和可用资源。
6. 错误恢复与重试机制
Claude Code 的自主执行循环内置了多层错误恢复机制,确保 Agent 能在遇到问题时自动恢复而不是崩溃。
6.1 API 错误恢复
Prompt Too Long (413) 恢复
当 API 返回 prompt_too_long 错误时,循环按以下优先级尝试恢复:
- 上下文折叠排空
(Context Collapse drain):如果启用了上下文折叠,先尝试排空已折叠的内容 - 反应式压缩
:如果排空失败,触发完整的反应式压缩 - 冒泡错误
:如果所有恢复都失败,将错误返回给用户
关键细节:错误消息在流式阶段被「扣留」(withheld),不立即返回给用户,而是等恢复机制尝试后决定是否释放(src/query.ts:800-822)。
Max Output Tokens 恢复
当 LLM 输出被 max_output_tokens 截断时:
- 升级输出限制
如果当前使用的是默认 8K 限制,升级到 64K 重试 - 注入恢复消息
向 LLM 注入 "Output token limit hit. Resume directly..." 消息,让它继续 - 多次恢复
最多尝试 3 次恢复
模型 Fallback
当主模型返回高负载错误时,自动切换到备用模型(FallbackTriggeredError):
if (innerError instanceofFallbackTriggeredError && fallbackModel) { currentModel = fallbackModel attemptWithFallback = true// 清除旧消息,用备用模型重新执行continue}6.2 工具执行错误恢复
工具执行中的错误被捕获并包装为 tool_result 消息返回给 LLM:
// 工具未找到yieldcreateUserMessage({content: [{type: 'tool_result',content: `<tool_use_error>Error: No such tool available: ${toolName}</tool_use_error>`,is_error: true,tool_use_id: toolUse.id, }],})// 工具执行异常yieldcreateUserMessage({content: [{type: 'tool_result',content: `<tool_use_error>Error calling tool: ${errorMessage}</tool_use_error>`,is_error: true,tool_use_id: toolUse.id, }],})通过将错误返回给 LLM(而不是让循环崩溃),LLM 可以理解发生了什么错误并尝试其他方法。
6.3 中断恢复
当用户中断(Ctrl+C)时:
- 流式工具清理
streamingToolExecutor.getRemainingResults()为排队中的工具生成合成tool_result - 中断消息
createUserInterruptionMessage()生成中断通知 - Abort 清理
cleanupComputerUseAfterTurn()清理计算机使用会话
6.4 Stop Hooks 机制
handleStopHooks()(src/query/stopHooks.ts)在每轮 LLM 响应后执行,提供:
- 阻止继续
如果 hook 返回 blockingErrors,注入错误消息让 LLM 修正 - 记忆提取
自动提取对话中的重要信息 - 模板分类
对任务进行分类和状态记录 - 提示建议
生成后续问题建议
7. 任务系统:后台任务的生命周期管理
7.1 任务类型
Claude Code 支持多种后台任务类型(src/Task.ts):
local_bash | b | |
local_agent | a | |
remote_agent | r | |
in_process_teammate | t | |
local_workflow | w | |
monitor_mcp | m | |
dream | d |
7.2 任务状态机
每个任务经历以下状态(src/Task.ts:18):
pending -> running -> completed -> failed -> killedisTerminalTaskStatus() 判断任务是否处于终态(completed/failed/killed),终态任务不会被注入消息或清理。
7.3 任务 ID 生成
任务 ID 使用随机 8 字符编码(src/Task.ts:98),前缀标识任务类型:
exportfunctiongenerateTaskId(type: TaskType): string {const prefix = getTaskIdPrefix(type)const bytes = randomBytes(8)let id = prefixfor (let i = 0; i < 8; i++) { id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length] }return id}36^8 约 2.8 万亿种组合,足以抵抗暴力猜测。
7.4 任务终止
stopTask()(src/tasks/stopTask.ts)提供了统一的任务终止接口:
查找任务状态 验证任务正在运行 调用对应类型的 kill()方法标记任务为已通知
7.5 子 Agent 任务
LocalAgentTask(src/tasks/LocalAgentTask/LocalAgentTask.tsx)是最重要的任务类型之一,用于运行后台 Agent。它追踪 Agent 的进度(工具调用次数、token 使用量、最近活动),并通过 <task-notification> XML 格式将结果返回给调用者。
进度追踪器(ProgressTracker)维护:
工具调用计数 最新输入 token 数(API 是累积的) 累计输出 token 数 最近 5 个工具活动记录
8. 协调模式:多 Agent 协作调度
8.1 Coordinator 模式概述
协调模式(src/coordinator/coordinatorMode.ts)将 Claude 分成两个角色:
- Coordinator
(指挥官):只负责理解目标、拆解任务、综合结果。使用 Agent/SendMessage/TaskStop 工具 - Worker
(执行者):执行具体代码操作。拥有完整工具集
8.2 启用条件
exportfunctionisCoordinatorMode(): boolean {if (feature('COORDINATOR_MODE')) {returnisEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) }returnfalse}8.3 系统提示词设计
Coordinator 的系统提示词(getCoordinatorSystemPrompt())精心设计了任务工作流:
Research(并行) -> Synthesis(Coordinator) -> Implementation(Worker) -> Verification(Worker)关键设计原则:
- 并行研究
多个 Worker 同时调查不同方面 - 综合在 Coordinator
Coordinator 必须理解研究结果后再制定实施计划 - Worker 自主执行
Worker 收到完整指令后自主完成任务 - 独立验证
由不同的 Worker 验证实施结果
8.4 Worker 结果处理
Worker 的结果以 <task-notification> XML 格式返回:
<task-notification><task-id>agent-a1b</task-id><status>completed|failed|killed</status><summary>Agent "description" completed</summary><result>agent's final text response</result><usage><total_tokens>N</total_tokens><tool_uses>N</tool_uses><duration_ms>N</duration_ms></usage></task-notification>8.5 继续 vs 重新派发
Coordinator 在收到 Worker 结果后需要决定是继续同一个 Worker(SendMessage)还是派发新 Worker(Agent):
夜雨聆风