职责概述

本文档追踪一次查询从用户输入到最终响应渲染的完整生命周期。它跨越 REPL 的 ask() 函数、QueryEngine.submitMessage()query() 生成器、API 流式传输、工具调度、状态管理、转录持久化等多个子系统。与 Query 循环 页面关注内部机制不同,本页面聚焦于所有子系统的端到端协作,包括错误路径和恢复机制。

架构设计

用户界面层
screens/REPL.js
hooks/useLogMessages.ts
components/App.js
查询编排层
ask() (REPL)
QueryEngine.submitMessage()
核心循环层
query.ts queryLoop()
processUserInput()
API 与工具层
services/api/claude.ts
StreamingToolExecutor
runTools()
基础设施层
compact/autocompact
permissions/canUseTool
hooks (SessionStart/Stop)
transcript 持久化

核心数据流

Stage 1: 用户输入捕获
用户在 REPL 输入框中键入提示词并按下 Enter。REPL 组件捕获输入文本,构建 UserMessage,并传递给 ask() 函数。在非交互模式(-p)中,输入通过 stdin 读取(支持 textstream-json 格式)。
// main.tsx:857-868
async function getInputPrompt(prompt, inputFormat) {
  if (!process.stdin.isTTY && !process.argv.includes('mcp')) {
    if (inputFormat === 'stream-json') return process.stdin;
    process.stdin.setEncoding('utf8');
    // ... 从 stdin 读取完整输入
  }
}
Stage 2: processUserInput — 输入预处理
QueryEngine.ts:410-428 调用 processUserInput() 处理用户输入。此阶段:
  • 检测并执行斜杠命令(如 /compact/clear
  • 加载附件消息(CLAUDE.md、memory 文件、图片等)
  • 更新 allowedTools(来自 --allowedTools 标志)
  • 返回 messagesFromUserInputshouldQueryresultText
如果 shouldQuery 为 false(斜杠命令已处理完毕),直接 yield 本地结果并返回。
Stage 3: 消息持久化(转录写入)
QueryEngine.ts:450-463 在进入查询循环之前将用户消息写入转录文件。这确保即使在 API 请求之前进程被杀死,会话仍然可以通过 --resume 恢复:
// QueryEngine.ts:450-463
if (persistSession && messagesFromUserInput.length > 0) {
  const transcriptPromise = recordTranscript(messages);
  if (isBareMode()) { void transcriptPromise; }
  else { await transcriptPromise; }
}
--bare 模式下异步写入(非阻塞),正常模式下同步等待。
Stage 4: 系统提示与技能加载
QueryEngine.ts:288-551 并行执行系统提示构建和技能/插件加载:
// QueryEngine.ts:534-537
const [skills, { enabled: enabledPlugins }] = await Promise.all([
  getSlashCommandToolSkills(getCwd()),
  loadAllPluginsCacheOnly(),
]);
然后构建 SystemInitMessage(包含工具列表、MCP 客户端、模型、权限模式等信息)并 yield。
Stage 5: 进入 query() 循环
QueryEngine.ts:675-686 开始消费 query() 生成器:
// QueryEngine.ts:675-686
for await (const message of query({
  messages, systemPrompt, userContext, systemContext,
  canUseTool: wrappedCanUseTool,
  toolUseContext: processUserInputContext,
  fallbackModel, querySource: 'sdk', maxTurns, taskBudget,
})) { ... }
query()while(true) 循环开始第一次迭代。
Stage 6: 上下文压缩管道
query.ts:365-549 按序执行四层压缩:
工具结果预算
applyToolResultBudget()
限制大型工具结果大小
Snip
snipCompactIfNeeded()
裁剪历史尾部,释放 token
Microcompact
deps.microcompact()
缓存编辑级压缩
Autocompact
deps.autocompact()
全文摘要(达到阈值时)
Context Collapse 在 Microcompact 和 Autocompact 之间执行。如果折叠成功将上下文降到 autocompact 阈值以下,autocompact 跳过——保留粒度上下文。
Stage 7: 阻塞限制检查
query.ts:628-648 检查是否达到硬阻塞限制。当自动压缩关闭且上下文超过模型限制时,阻止 API 调用并返回错误。跳过条件:刚完成压缩、compact/session_memory 查询源、reactive compact 启用、context collapse 启用。
Stage 8: API 流式请求
query.ts:654-659 发起流式 API 请求。模型逐块返回响应,每块可能是文本、thinking、或 tool_use。系统:
  • 收集 assistantMessages 用于后续工具执行
  • 检测 tool_use 块并设置 needsFollowUp = true
  • 对每个 tool_use 块,StreamingToolExecutor 立即开始执行
  • withhold 可恢复错误(prompt-too-long、max-output-tokens)
// query.ts:799-825 withhold 逻辑
let withheld = false;
if (contextCollapse?.isWithheldPromptTooLong(message, ...)) withheld = true;
if (reactiveCompact?.isWithheldPromptTooLong(message)) withheld = true;
if (isWithheldMaxOutputTokens(message)) withheld = true;
if (!withheld) yield yieldMessage;
Stage 9: 恢复决策(无工具调用时)
当模型不请求工具时(!needsFollowUp),进入恢复决策树:
// 决策优先级
if (isWithheld413) {
  // 1. 尝试 Context Collapse drain → continue
  // 2. 尝试 Reactive Compact → continue
  // 3. 都失败 → yield 错误,终止
}
if (isWithheldMaxOutputTokens) {
  // 1. 首次:提升 max_output_tokens 到 64k → continue
  // 2. 后续:注入恢复消息 → continue
  // 3. 达到限制 → yield 错误
}
if (lastMessage?.isApiErrorMessage) {
  // 其他 API 错误 → 执行失败 hooks → 终止
}
// Stop Hooks 检查 → 可能 continue
// Token Budget 检查 → 可能 continue
// 正常完成 → return { reason: 'completed' }
Stage 10: 工具执行与结果收集
query.ts:1366-1408 当模型请求工具时:
  1. 权限检查canUseTool() 根据权限模式决定允许/拒绝/询问用户
  2. 工具执行:每个工具实现 execute() 方法,返回结构化结果
  3. 结果规范化normalizeMessagesForAPI() 将工具结果转换为 API 可消费的格式
  4. 上下文更新:工具可能返回新的 toolUseContext(如新的文件状态)
// query.ts:1380-1408
const toolUpdates = streamingToolExecutor
  ? streamingToolExecutor.getRemainingResults()
  : runTools(toolUseBlocks, assistantMessages, canUseTool, toolUseContext);
for await (const update of toolUpdates) {
  if (update.message) {
    yield update.message;
    toolResults.push(...normalizeMessagesForAPI([update.message], tools));
  }
  if (update.newContext) updatedToolUseContext = { ...update.newContext, queryTracking };
}
Stage 11: 状态更新与循环继续
工具执行完成后,构建新的 State 对象并 continue 回到 Stage 6:
// query.ts 简化的 continue 模式
state = {
  messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
  toolUseContext: updatedToolUseContext,
  autoCompactTracking: tracking,
  maxOutputTokensRecoveryCount: 0,
  // ...
  transition: { reason: 'tool_execution' },
};
continue; // → 回到 Stage 6 的压缩管道
循环继续,带着工具结果再次调用 API,模型基于工具输出决定是否继续使用工具或完成回复。
Stage 12: 终止与结果交付
当循环终止时(模型不请求工具且无恢复条件),query() 返回 Terminal 对象。QueryEngine.submitMessage() 处理终止:
  1. yield SDKResultMessage(包含成本、持续时间、使用量统计)
  2. 更新 this.mutableMessages(会话消息历史)
  3. 记录最终转录
REPL 端的 useLogMessages hook 消费 yield 的消息并更新 React 状态,触发终端 UI 重渲染,显示最终响应。

关键类型与接口

query() 的 yield 类型联合

// query.ts:222-228
AsyncGenerator<
  | StreamEvent           // 流式内容块(text、tool_use、thinking 等)
  | RequestStartEvent     // 请求开始信号
  | Message               // 完整消息(assistant、user、system)
  | TombstoneMessage      // 消息移除控制信号
  | ToolUseSummaryMessage, // 工具使用摘要(Haiku 生成)
  Terminal                // 返回值:终止原因
>

Terminal 返回类型

// query.ts 中的终止原因
{ reason: 'completed' }
{ reason: 'blocking_limit' }
{ reason: 'aborted_streaming' }
{ reason: 'prompt_too_long' }
{ reason: 'image_error' }
{ reason: 'model_error', error }
{ reason: 'stop_hook_prevented' }

Continue(transition 原因)

// query.ts State.transition 的可能值
type Continue =
  | { reason: 'reactive_compact_retry' }
  | { reason: 'collapse_drain_retry', committed: number }
  | { reason: 'max_output_tokens_recovery', attempt: number }
  | { reason: 'max_output_tokens_escalate' }
  | { reason: 'stop_hook_blocking' }
  | { reason: 'token_budget_continuation' }
  | { reason: 'model_fallback' }

QueryEngine 消息消费模式

// QueryEngine.ts:675-780 消费 query() yield 的消息
for await (const message of query({ messages, systemPrompt, ... })) {
  if (message.type === 'assistant' || message.type === 'user' ||
      (message.type === 'system' && message.subtype === 'compact_boundary')) {
    messages.push(message);
    if (persistSession) void recordTranscript(messages);
  }
  switch (message.type) {
    case 'assistant': yield* normalizeMessage(message); break;
    case 'progress':  // 记录工具执行进度
    case 'system':    // 处理 compact_boundary、local_command
    case 'result':    // SDK 结果消息
    case 'tombstone': // 跳过
  }
}

设计模式与亮点

1. Withhold-Recover 模式

query 循环对可恢复错误使用"抑制-恢复"模式:API 返回 prompt-too-long 或 max-output-tokens 错误时,不立即 yield 给消费者,而是 withheld 并尝试恢复(压缩、提升限制)。只有当所有恢复路径都失败时才 yield 错误。这避免了消费者看到瞬态错误,提供更流畅的用户体验。

2. Fire-and-Forget 转录写入

Assistant 消息的转录写入使用 fire-and-forget(void recordTranscript(messages)),因为写入队列的 100ms 延迟字符串化确保了不阻塞生成器。User 消息和 compact_boundary 则同步等待,确保转录的完整性。

这种设计的关键是 enqueueWrite 的顺序保证——即使 fire-and-forget,消息仍然按序写入磁盘。

3. 权限拒绝追踪

QueryEnginewrappedCanUseTool 中包装了原始的 canUseTool,记录所有权限拒绝事件。这些拒绝在终止时作为 permission_denials 包含在结果消息中,SDK 消费者可以据此决定是否重试。

4. 命令生命周期通知

query() 跟踪所有消费的斜杠命令 UUID,在正常终止时通过 notifyCommandLifecycle(uuid, 'completed') 通知。这确保了命令的状态机正确转换。

5. Post-Sampling Hooks

模型响应完成后,query.ts:1000-1009 异步执行 post-sampling hooks,用于分析和日志记录。这些 hooks 不阻塞查询循环,在后台运行。

开发者实践指南

query.ts 中有多个 queryCheckpoint() 调用点。在开发环境中设置 CLAUDE_CODE_DEBUG=1 可以看到每个检查点的执行时间和顺序。

调试查询循环中的问题

常见调试场景:

添加新的消息处理阶段

在 QueryEngine.submitMessage() 的 for await 循环中添加新的 switch case。确保:

  1. 新消息类型添加到 query() 的 yield 联合类型中
  2. 如果需要持久化,在 messages.push(message) 条件中包含
  3. 考虑对转录写入的影响(是否需要同步等待)

扩展恢复机制

新的恢复机制应该:

  1. !needsFollowUp 分支中添加检测逻辑
  2. 设置 State.transition 记录原因
  3. 使用递增计数器或标志防止无限循环
  4. withheld 检查中添加对应的抑制逻辑
  5. 添加 logEvent 用于遥测

架构师决策指南

端到端延迟的关键路径

从用户按下 Enter 到看到第一个字符,关键路径是:

  1. processUserInput (~5ms) — 输入解析和附件加载
  2. 系统提示构建 (~50ms) — fetchSystemPromptParts 并行加载技能和插件
  3. 转录写入 (~4-30ms) — 磁盘 I/O
  4. 压缩管道 (0-5000ms) — 取决于上下文大小和压缩触发
  5. API 首字节延迟 (~200-500ms) — 网络 + 模型推理
最影响用户感知延迟的是压缩管道和 API 首字节。对于长会话,autocompact 可能需要 2-5 秒(额外的 API 调用)。这是上下文长度和延迟之间的根本权衡。

错误处理的分层策略

系统采用三层错误处理:

转录持久化的一致性保证

系统提供"至少一次"的转录持久化保证:用户消息在 API 调用前同步写入;assistant 消息异步写入但有顺序保证;compact_boundary 前的 preservedSegment 尾部同步刷新。在进程崩溃时,可能丢失最后一个 assistant 消息,但用户消息和之前的对话历史是安全的。

流式 vs 批量工具执行的权衡

StreamingToolExecutor 减少了多工具调用的延迟(工具在模型流式输出时就开始执行),但引入了复杂性:需要 discard() 在 fallback 时清理、需要 getRemainingResults() 在循环末尾收集未完成的结果、需要处理部分工具结果与后续消息的交错。对于工具数量少(1-2 个)的场景,收益有限;对于大量并行工具调用的场景,收益显著。

代码索引

文件行数说明
QueryEngine.ts~1296会话管理、submitMessage 入口、SDK 消息 yield 与转录
query.ts~1730query()/queryLoop() 核心循环、压缩管道、API 调用、工具调度
main.tsx~4684getInputPrompt()、print 模式查询消费、REPL ask() 调用
entrypoints/init.ts~341全局初始化(配置、网络、遥测)
setup.ts~478环境设置(权限、worktree、后台预取)
state/AppStateStore.ts~1200+AppState 类型、工具权限上下文、MCP 状态
state/store.ts~35不可变状态容器
state/onChangeAppState.ts~120状态变更副作用(权限同步、遥测通知)
coordinator/coordinatorMode.ts~370协调器模式提示词生成(多 Agent 场景)
interactiveHelpers.tsx~366设置对话框序列、renderAndRun
replLauncher.tsx~23REPL 启动(App + REPL 组件动态加载)