在上篇文章中,我们完整梳理了 QueryEngine 请求生命周期的上半程——从用户输入到请求组装、上下文压缩、Token 预算检查,直到最终调用 callModel() 将请求发往 LLM API。本文将承接这一脉络,深入解析下半程:LLM 返回的流式响应如何处理?Tool Call 如何从 SSE 片段中被提取并组装?当网络抖动、模型超时或 Prompt 过长时,系统如何优雅地降级与恢复?以及当一轮对话结束后,Stop Hooks 如何决定对话是否继续推进?
这两个阶段合起来,才构成 QueryEngine 完整的请求生命周期闭环。
一、流式响应处理:SSE 到终端的实时链路
1.1 SSE 接收与事件类型
Claude Code 通过 Anthropic SDK 的流式接口与 LLM 通信,底层传输协议是 SSE(Server-Sent Events)。在 query.ts 中,流式响应的入口位于第 700 行附近的 callModel() 调用:
// src/query.ts:700-720
for await (const message of deps.callModel({
messages: prependUserContext(messagesForQuery, userContext),
systemPrompt: fullSystemPrompt,
thinkingConfig: toolUseContext.options.thinkingConfig,
tools: toolUseContext.options.tools,
signal: toolUseContext.abortController.signal,
options: { /* ... */ },
})) {
// 处理每一条流式消息
}SSE 流中的每一条事件都会被解析为特定的消息类型。QueryEngine 内部定义了统一的 StreamEvent 类型来封装这些原始事件,主要包括以下几种:
| 事件类型 | 含义 | 处理位置 |
|---|---|---|
message_start | 新消息开始,重置当前消息的 usage 统计 | QueryEngine.ts:800-805 |
content_block_start | 新内容块开始(text / tool_use / thinking) | 流式底层处理 |
content_block_delta | 内容块增量更新 | 流式底层处理 |
content_block_stop | 内容块结束 | 流式底层处理 |
message_delta | 消息级增量(如 stop_reason 更新) | QueryEngine.ts:806-812 |
message_stop | 消息结束,累加 usage 到总计 | QueryEngine.ts:813-818 |
1.2 增量内容解析与实时渲染
流式响应的核心价值在于低延迟的实时反馈。Claude Code 并不等待完整响应返回后再渲染,而是在每一个 content_block_delta 到达时就立即处理。
在 QueryEngine.ts 中,流式事件通过 switch-case 分发到不同的处理分支。对于 stream_event 类型的消息,系统会执行以下操作:
// src/QueryEngine.ts:793-825(简化示意)
case 'stream_event':
if (message.event.type === 'message_start') {
currentMessageUsage = EMPTY_USAGE
currentMessageUsage = updateUsage(
currentMessageUsage,
message.event.message.usage,
)
}
if (message.event.type === 'message_delta') {
currentMessageUsage = updateUsage(
currentMessageUsage,
message.event.usage,
)
// 关键:stop_reason 只在 message_delta 中才能拿到真实值
if (message.event.delta.stop_reason != null) {
lastStopReason = message.event.delta.stop_reason
}
}
if (message.event.type === 'message_stop') {
this.totalUsage = accumulateUsage(
this.totalUsage,
currentMessageUsage,
)
}
break这段代码有几个值得注意的设计细节:
Usage 的层级累加:
currentMessageUsage跟踪单条消息的 Token 消耗,totalUsage跟踪整个会话的累计消耗。这种分层设计使得预算检查可以在任意粒度上进行。stop_reason的延迟捕获:在content_block_stop时,stop_reason可能为null,真实的停止原因要到message_delta才会送达。QueryEngine 特意在这一步捕获并保存,否则result.stop_reason将永远是null。includePartialMessages选项:当 SDK 调用方需要原始流事件时,QueryEngine 会将每个stream_event原样 yield 出去,供上层消费。
1.3 内容类型判断:文本 vs Tool Call
在流式接收过程中,系统需要实时判断当前正在接收的内容类型。Anthropic 的响应格式中,每个 content_block 都有一个明确的 type 字段:
text:普通文本内容,直接渲染到终端tool_use:Tool Call 的开始,需要进入工具解析流程thinking/redacted_thinking:思考块,按规则保留或隐藏
在 query.ts 的流式循环中,当检测到 assistant 类型的消息时,系统会提取其中所有的 tool_use 块:
// src/query.ts:750-760
if (message.type === 'assistant') {
assistantMessages.push(message)
const msgToolUseBlocks = message.message.content.filter(
content => content.type === 'tool_use',
) as ToolUseBlock[]
if (msgToolUseBlocks.length > 0) {
toolUseBlocks.push(...msgToolUseBlocks)
needsFollowUp = true // 标记需要后续工具执行
}
}这里的关键变量是 needsFollowUp。一旦有任何 tool_use 块出现,系统就知道这轮对话不会以纯文本结束,而是需要进入工具执行阶段,执行完成后再将结果送回 LLM 进行下一轮推理。
flowchart TD
A[SSE 事件到达] --> B{事件类型?}
B -->|message_start| C[重置 usage 计数]
B -->|content_block_delta| D{内容块类型?}
D -->|text| E[实时渲染到终端]
D -->|tool_use| F[提取 Tool Call 片段]
D -->|thinking| G[按规则处理思考块]
B -->|message_delta| H[捕获 stop_reason]
B -->|message_stop| I[累加 usage 到总计]
F --> J[设置 needsFollowUp = true]
J --> K[流结束 → 进入 Tool 执行]
E --> L[流结束 → 直接进入 Stop Hooks]二、Tool Call 解析:从流式片段到完整调用
2.1 Tool Call 的流式组装
与一次性返回完整 JSON 不同,Anthropic 的 tool_use 块在 SSE 流中是分段到达的。一个完整的 Tool Call 可能由以下事件序列组成:
content_block_start: { type: "tool_use", name: "Read", id: "toolu_01xxx" }
content_block_delta: { partial_json: "{\"file_path\": \"/" }
content_block_delta: { partial_json: "tmp/test.txt" }
content_block_delta: { partial_json: "\"}" }
content_block_stopClaude Code 的底层 SDK 负责将这些片段拼接成完整的 JSON 对象。当 content_block_stop 到达时,block.input 已经被解析为一个完整的 JavaScript 对象。在 query.ts 中,系统此时会执行 input backfill——即工具定义的 backfillObservableInput 钩子:
// src/query.ts:720-745
if (message.type === 'assistant') {
let clonedContent: typeof message.message.content | undefined
for (let i = 0; i < message.message.content.length; i++) {
const block = message.message.content[i]!
if (
block.type === 'tool_use' &&
typeof block.input === 'object' &&
block.input !== null
) {
const tool = findToolByName(
toolUseContext.options.tools,
block.name,
)
if (tool?.backfillObservableInput) {
const originalInput = block.input as Record<string, unknown>
const inputCopy = { ...originalInput }
tool.backfillObservableInput(inputCopy)
// 只有当新增了字段时才 yield clone
const addedFields = Object.keys(inputCopy).some(
k => !(k in originalInput),
)
if (addedFields) {
clonedContent ??= [...message.message.content]
clonedContent[i] = { ...block, input: inputCopy }
}
}
}
}
if (clonedContent) {
yieldMessage = {
...message,
message: { ...message.message, content: clonedContent },
}
}
}这段代码的设计非常精妙:
- Immutable 原则:原始
message对象不直接修改,因为这条消息会流回 API 作为下一轮的上下文。任何字节级的变化都会破坏 Prompt Caching。 - 按需克隆:只有当
backfillObservableInput真正添加了新字段时,才创建克隆消息用于 yield 和转录。 - Observable Input:某些工具(如文件操作工具)需要在输入被确认后展开相对路径、解析符号链接等,这些派生字段对后续执行有用,但不应被 API 看到。
2.2 参数 JSON 解析与校验
Tool Call 的参数是一个 JSON 对象。Claude Code 使用 Zod 进行运行时类型校验。当参数不符合工具的 Schema 时,会触发 formatZodValidationError 函数生成友好的错误提示:
// src/utils/toolErrors.ts:65-115
export function formatZodValidationError(
toolName: string,
error: ZodError,
): string {
const missingParams = error.issues
.filter(
err =>
err.code === 'invalid_type' &&
err.message.includes('received undefined'),
)
.map(err => formatValidationPath(err.path))
const unexpectedParams = error.issues
.filter(err => err.code === 'unrecognized_keys')
.flatMap(err => err.keys)
const typeMismatchParams = error.issues
.filter(
err =>
err.code === 'invalid_type' &&
!err.message.includes('received undefined'),
)
.map(err => {
const typeErr = err as { expected: string }
const receivedMatch = err.message.match(/received (\w+)/)
const received = receivedMatch ? receivedMatch[1] : 'unknown'
return {
param: formatValidationPath(err.path),
expected: typeErr.expected,
received,
}
})
// 构建人类可读的错误消息
const errorParts = []
if (missingParams.length > 0) {
errorParts.push(...missingParams.map(
param => `The required parameter \`${param}\` is missing`,
))
}
if (unexpectedParams.length > 0) {
errorParts.push(...unexpectedParams.map(
param => `An unexpected parameter \`${param}\` was provided`,
))
}
if (typeMismatchParams.length > 0) {
errorParts.push(...typeMismatchParams.map(
({ param, expected, received }) =>
`The parameter \`${param}\` type is expected as \`${expected}\` but provided as \`${received}\``,
))
}
return errorParts.length > 0
? `${toolName} failed due to the following ${errorParts.length > 1 ? 'issues' : 'issue'}:\n${errorParts.join('\n')}`
: error.message
}这个错误格式化器将 Zod 的校验失败转换为 LLM 能够理解的结构化反馈,使得模型在下一轮可以尝试修正参数。
2.3 StreamingToolExecutor:工具执行的并行化
Claude Code 引入了一个重要的优化机制——StreamingToolExecutor。传统的工具执行需要等待完整的 assistant 消息到达后才能开始,而 StreamingToolExecutor 允许在流式接收过程中就提前启动工具:
// src/query.ts:600-610
const useStreamingToolExecution = config.gates.streamingToolExecution
let streamingToolExecutor = useStreamingToolExecution
? new StreamingToolExecutor(
toolUseContext.options.tools,
canUseTool,
toolUseContext,
)
: null当流中出现 tool_use 块时,StreamingToolExecutor 立即将其加入执行队列:
// src/query.ts:755-760
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
for (const toolBlock of msgToolUseBlocks) {
streamingToolExecutor.addTool(toolBlock, message)
}
}与此同时,执行器会在后台消费已完成的工具结果:
// src/query.ts:763-772
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
for (const result of streamingToolExecutor.getCompletedResults()) {
if (result.message) {
yield result.message
toolResults.push(
...normalizeMessagesForAPI(
[result.message],
toolUseContext.options.tools,
).filter(_ => _.type === 'user'),
)
}
}
}这种设计将网络等待时间(LLM 生成剩余内容)与工具执行时间(读取文件、执行命令等)重叠,显著降低了端到端延迟。
三、错误处理与恢复:韧性设计的艺术
3.1 网络错误重试策略
LLM API 调用不可避免地会遇到网络波动。Claude Code 在 withRetry.ts 中实现了一套精细的重试机制:
// src/services/api/withRetry.ts:15-25
const DEFAULT_MAX_RETRIES = 10
const FLOOR_OUTPUT_TOKENS = 3000
const MAX_529_RETRIES = 3
export const BASE_DELAY_MS = 500
// 前景查询源(用户在等待结果)才会重试 529
const FOREGROUND_529_RETRY_SOURCES = new Set<QuerySource>([
'repl_main_thread',
'sdk',
'agent:custom',
'compact',
// ... 其他前景源
])重试策略按错误类型分层处理:
| 错误类型 | 重试策略 | 说明 |
|---|---|---|
529 (Overloaded) | 前景源最多重试 3 次,指数退避 | 后台任务(如摘要生成)立即失败,避免加重网关压力 |
429 (Rate Limit) | 按 Retry-After 头等待后重试 | 清缓存后重新获取凭据 |
Connection Error | 最多 10 次,指数退避至 5 分钟 | 持久化重试模式(CLAUDE_CODE_UNATTENDED_RETRY)下可无限重试 |
401 (Auth) | 触发 OAuth 刷新,然后重试 | 支持 Claude AI 和企业订阅的自动续期 |
在 QueryEngine.ts 中,API 错误以 system 消息的 api_error 子类型形式 yield 出去,包含重试计数和延迟信息:
// src/QueryEngine.ts:880-890
if (message.subtype === 'api_error') {
yield {
type: 'system',
subtype: 'api_retry' as const,
attempt: message.retryAttempt,
max_retries: message.maxRetries,
retry_delay_ms: message.retryInMs,
error_status: message.error.status ?? null,
error: categorizeRetryableAPIError(message.error),
// ...
}
}3.2 模型降级:Fallback 策略
当主模型(如 claude-sonnet-4-20250514)因高负载不可用时,Claude Code 支持自动降级到备用模型(Fallback Model):
// src/query.ts:920-960
catch (innerError) {
if (innerError instanceof FallbackTriggeredError && fallbackModel) {
currentModel = fallbackModel
attemptWithFallback = true
// 清除已产生的 assistant 消息和 tool 结果
yield* yieldMissingToolResultBlocks(
assistantMessages,
'Model fallback triggered',
)
assistantMessages.length = 0
toolResults.length = 0
toolUseBlocks.length = 0
needsFollowUp = false
// 丢弃 StreamingToolExecutor 的 pending 结果
if (streamingToolExecutor) {
streamingToolExecutor.discard()
streamingToolExecutor = new StreamingToolExecutor(/* ... */)
}
// 处理 Thinking 签名:不同模型的签名不兼容,需要 strip
if (process.env.USER_TYPE === 'ant') {
messagesForQuery = stripSignatureBlocks(messagesForQuery)
}
yield createSystemMessage(
`Switched to ${renderModelName(innerError.fallbackModel)} due to high demand for ${renderModelName(innerError.originalModel)}`,
'warning',
)
continue // 重试循环
}
throw innerError
}降级策略的清理工作非常彻底:
- Tombstone 已产生的消息:为部分生成的消息(尤其是 thinking 块)发送 tombstone,防止它们以无效状态进入后续轮次。
- 重置执行状态:清空所有累积的 assistant 消息、tool 结果和执行器状态。
- 签名剥离:某些模型(如 Capybara 实验模型)的 thinking 签名与其他模型不兼容,降级时需要剥离,否则会触发 API 400 错误。
3.3 Prompt 过长与最大输出 Token 的恢复
当遇到 prompt_too_long 或 max_output_tokens 错误时,QueryEngine 不会立即失败,而是尝试自动恢复:
Prompt Too Long 恢复链:
// src/query.ts:1000-1050
if (isWithheld413) {
// 第一步:尝试 Context Collapse 的 drain 恢复
if (contextCollapse && state.transition?.reason !== 'collapse_drain_retry') {
const drained = contextCollapse.recoverFromOverflow(
messagesForQuery,
querySource,
)
if (drained.committed > 0) {
state = { /* ... */ transition: { reason: 'collapse_drain_retry' } }
continue
}
}
}
// 第二步:尝试 Reactive Compact
if ((isWithheld413 || isWithheldMedia) && reactiveCompact) {
const compacted = await reactiveCompact.tryReactiveCompact({ /* ... */ })
if (compacted) {
state = { /* ... */ transition: { reason: 'reactive_compact_retry' } }
continue
}
}Max Output Tokens 恢复链:
// src/query.ts:1070-1100
if (isWithheldMaxOutputTokens(lastMessage)) {
// 第一步:尝试从 8k 升级到 64k
if (capEnabled && maxOutputTokensOverride === undefined) {
state = { /* ... */ maxOutputTokensOverride: ESCALATED_MAX_TOKENS }
continue
}
// 第二步:多轮恢复提示,最多 3 次
if (maxOutputTokensRecoveryCount < MAX_OUTPUT_TOKENS_RECOVERY_LIMIT) {
const recoveryMessage = createUserMessage({
content: `Output token limit hit. Resume directly — no apology...`,
isMeta: true,
})
state = {
messages: [...messagesForQuery, ...assistantMessages, recoveryMessage],
maxOutputTokensRecoveryCount: maxOutputTokensRecoveryCount + 1,
// ...
}
continue
}
}这种分层恢复设计确保了系统在各种边缘情况下都能尽量自我愈合,而不是把错误抛给用户。
3.4 无效响应的处理
如果 callModel() 抛出了未被捕获的异常(这通常意味着 SDK 内部的 Bug),QueryEngine 会执行兜底处理:
// src/query.ts:970-995
catch (error) {
logError(error)
const errorMessage = error instanceof Error ? error.message : String(error)
// 为已 emit 的 tool_use 块生成 tool_result,防止 API 状态不一致
yield* yieldMissingToolResultBlocks(assistantMessages, errorMessage)
// 生成一个 synthetic assistant 消息来 surface 错误
yield createAssistantAPIErrorMessage({
content: errorMessage,
})
return { reason: 'model_error', error }
}关键点在于 yieldMissingToolResultBlocks:如果流已经 emit 了 tool_use 块但随后发生了错误,API 状态要求每个 tool_use 必须有对应的 tool_result。这个函数会为所有未完成的 tool use 生成错误类型的 tool result,保持对话状态的一致性。
四、停止钩子(Stop Hooks):对话继续的裁判
4.1 Stop Hooks 的设计与触发时机
Stop Hooks 是 Claude Code 中一个独特而强大的机制。它们位于每轮对话的末尾,在 assistant 响应完成(且没有 tool_use 需要执行)之后执行。其核心逻辑在 query/stopHooks.ts 中实现:
// src/query/stopHooks.ts:35-50
export async function* handleStopHooks(
messagesForQuery: Message[],
assistantMessages: AssistantMessage[],
systemPrompt: SystemPrompt,
userContext: { [k: string]: string },
systemContext: { [k: string]: string },
toolUseContext: ToolUseContext,
querySource: QuerySource,
stopHookActive?: boolean,
): AsyncGenerator<StreamEvent | RequestStartEvent | Message | /* ... */,
StopHookResult
> {
const hookStartTime = Date.now()
const stopHookContext: REPLHookContext = {
messages: [...messagesForQuery, ...assistantMessages],
systemPrompt,
userContext,
systemContext,
toolUseContext,
querySource,
}
// ...
}Stop Hooks 的触发条件是:
- 当前轮次的 assistant 消息已完整接收
- 该消息不包含
tool_use块(即needsFollowUp === false) - 该消息不是 API 错误消息
sequenceDiagram
participant QE as QueryEngine
participant LLM as LLM API
participant SH as StopHooks
participant TE as ToolExecution
QE->>LLM: 发送请求
LLM-->>QE: 流式响应
alt 包含 tool_use
QE->>TE: 执行工具
TE-->>QE: 工具结果
QE->>LLM: 下一轮请求
else 纯文本响应
QE->>SH: 触发 Stop Hooks
SH->>SH: 执行用户配置的 hooks
alt preventContinuation
SH-->>QE: 阻止继续
QE->>QE: 返回 completed
else blockingErrors
SH-->>QE: 阻塞错误
QE->>LLM: 将错误送入下一轮
else 通过
SH-->>QE: 允许继续
QE->>QE: 返回 completed
end
end4.2 优雅停止 vs 强制停止
Stop Hooks 支持两种停止语义:
优雅停止(Prevent Continuation):
当 Stop Hook 判定对话应该终止时(例如,检测到任务已完成或用户明确要求停止),它会设置 preventContinuation = true:
// src/query/stopHooks.ts:175-185
if (result.preventContinuation) {
preventedContinuation = true
stopReason = result.stopReason || 'Stop hook prevented continuation'
yield createAttachmentMessage({
type: 'hook_stopped_continuation',
message: stopReason,
hookName: 'Stop',
toolUseID: stopHookToolUseID,
hookEvent: 'Stop',
})
}这种停止是不可逆的。一旦触发,handleStopHooks 立即返回 { preventContinuation: true },QueryEngine 随后以 reason: 'stop_hook_prevented' 结束当前 turn。
阻塞错误(Blocking Errors):
当 Stop Hook 检测到需要修正的问题(例如,代码风格检查失败、测试未通过),但不会阻止对话继续时,它会生成阻塞错误:
// src/query/stopHooks.ts:165-175
if (result.blockingError) {
const userMessage = createUserMessage({
content: getStopHookMessage(result.blockingError),
isMeta: true, // 对模型隐藏,仅在 summary 中显示
})
blockingErrors.push(userMessage)
yield userMessage
hasOutput = true
hookErrors.push(result.blockingError.blockingError)
}阻塞错误会以 user 消息的形式注入对话上下文,让 LLM 在下一轮有机会修正问题。这与优雅停止的关键区别在于——对话会继续推进。
4.3 Stop Hooks 的并行执行与结果聚合
Stop Hooks 实际上是并行执行的。executeStopHooks 返回一个 AsyncGenerator,每个 hook 的结果被逐个消费:
// src/query/stopHooks.ts:135-200
for await (const result of generator) {
if (result.message) {
yield result.message
if (result.message.type === 'progress' && result.message.toolUseID) {
stopHookToolUseID = result.message.toolUseID
hookCount++
}
if (result.message.type === 'attachment') {
const attachment = result.message.attachment
// 分类处理 hook 的各种结果类型
if (attachment.type === 'hook_non_blocking_error') {
hookErrors.push(attachment.stderr || `Exit code ${attachment.exitCode}`)
} else if (attachment.type === 'hook_error_during_execution') {
hookErrors.push(attachment.content)
} else if (attachment.type === 'hook_success') {
if (attachment.stdout?.trim() || attachment.stderr?.trim()) {
hasOutput = true
}
}
}
}
// ...
}执行完成后,如果 hooks 产生了任何输出,系统会生成一个 Stop Hook Summary Message:
// src/query/stopHooks.ts:240-255
if (hookCount > 0) {
yield createStopHookSummaryMessage(
hookCount,
hookInfos,
hookErrors,
preventedContinuation,
stopReason,
hasOutput,
'suggestion',
stopHookToolUseID,
)
if (hookErrors.length > 0) {
const expandShortcut = getShortcutDisplay(
'app:toggleTranscript',
'Global',
'ctrl+o',
)
toolUseContext.addNotification?.({
key: 'stop-hook-error',
text: `Stop hook error occurred · ${expandShortcut} to see`,
priority: 'immediate',
})
}
}这个 summary 对用户可见,提供了 hooks 执行结果的概览,包括每个 hook 的命令、执行时长、错误信息等。
4.4 Teammate 模式下的扩展 Hooks
在 Teammate(多 Agent 协作)模式下,Stop Hooks 之后还会执行 TaskCompleted Hooks 和 TeammateIdle Hooks:
// src/query/stopHooks.ts:280-350
if (isTeammate()) {
const teammateName = getAgentName() ?? ''
const teamName = getTeamName() ?? ''
// 为所有进行中的任务运行 TaskCompleted hooks
const tasks = await listTasks(taskListId)
const inProgressTasks = tasks.filter(
t => t.status === 'in_progress' && t.owner === teammateName
)
for (const task of inProgressTasks) {
const taskCompletedGenerator = executeTaskCompletedHooks(
task.id, task.subject, task.description,
teammateName, teamName, /* ... */
)
// ...
}
// 运行 TeammateIdle hooks
const teammateIdleGenerator = executeTeammateIdleHooks(
teammateName, teamName, /* ... */
)
// ...
}这些扩展 hooks 使得多 Agent 协作时的状态同步和任务交接能够自动化完成。
五、完整链路:从响应到下一轮请求
现在,让我们将上半程和下半程连接起来,看看一个完整的请求生命周期是如何闭环的。
5.1 无 Tool Call 的纯文本路径
用户输入
→ processUserInput 处理(第11篇)
→ 上下文压缩 / Snip / Microcompact(第11篇)
→ callModel() 发送请求(第11篇)
→ SSE 流式接收(本文 §1)
→ 内容类型判断:纯文本(本文 §1.3)
→ needsFollowUp = false
→ 检查 maxBudgetUsd / maxTurns(本文 §3)
→ handleStopHooks()(本文 §4)
→ 通过 / prevent / blocking
→ yield result 消息
→ 本轮结束5.2 含 Tool Call 的循环路径
用户输入
→ processUserInput 处理
→ 上下文压缩
→ callModel() 发送请求
→ SSE 流式接收
→ 检测到 tool_use 块(本文 §2)
→ needsFollowUp = true
→ StreamingToolExecutor 并行执行工具(本文 §2.3)
→ 收集 toolResults
→ 将 assistantMessages + toolResults 拼接为新的 messages
→ state = { messages: [...], ... }(本文 §3)
→ continue(进入下一轮 queryLoop)
→ callModel() 再次发送请求
→ ...(循环直到 needsFollowUp = false)5.3 关键状态机转换
query.ts 中的 queryLoop 本质上是一个状态机,每次 continue 都代表一次状态转换:
| 触发条件 | 下一状态 | 说明 |
|---|---|---|
needsFollowUp === true | Tool 执行 → 继续循环 | 最常见的 Agent 推理循环 |
stopHookResult.preventContinuation | 终止,reason: 'stop_hook_prevented' | Hook 要求停止 |
stopHookResult.blockingErrors.length > 0 | 注入错误消息 → 继续循环 | Hook 发现问题,让 LLM 修正 |
isWithheld413 && collapse drain 成功 | transition: 'collapse_drain_retry' | Prompt 过长恢复 |
isWithheld413 && reactive compact 成功 | transition: 'reactive_compact_retry' | 上下文压缩恢复 |
isWithheldMaxOutputTokens && 可恢复 | transition: 'max_output_tokens_recovery' | 输出超限恢复 |
maxBudgetUsd 超出 | 终止,subtype: 'error_max_budget_usd' | 预算耗尽 |
maxTurns 达到 | 终止,subtype: 'error_max_turns' | 轮次上限 |
六、总结
QueryEngine 的下半程展示了 Claude Code 作为生产级 AI Agent 框架的深厚工程功底:
流式响应处理:通过 SSE 实时消费、增量解析、即时渲染,将用户等待时间最小化。
Tool Call 解析:在流中完成 Tool Call 的组装、Backfill 和校验,配合 StreamingToolExecutor 实现工具执行的零等待并行化。
错误恢复的多层防线:从网络重试、模型降级,到 Prompt 过长和输出超限的自动恢复,系统在各种异常路径上都保留了继续推进的可能性。
Stop Hooks 的灵活控制:通过优雅停止和阻塞错误两种语义,让用户和系统都能参与到对话何时结束、何时继续的决策中。
与上半程的无缝衔接:
queryLoop的while (true)循环将请求组装(上半程)和响应处理(下半程)编织成一个完整的 Agent 推理循环。
理解了这个完整生命周期,你就能把握 Claude Code 的核心调度逻辑——这也是构建任何复杂 AI Agent 系统都必须面对的课题:如何在流式不确定性中保持状态一致,如何在错误边缘优雅恢复,以及如何让系统知道"何时该停下来"。
本系列文章导航:
- 第 11 篇:《QueryEngine 请求生命周期(上)》——请求组装、上下文压缩、Token 预算
- 第 12 篇:《QueryEngine 请求生命周期(下)》——流式响应、Tool Call、错误恢复、Stop Hooks(本文)