查询的完整生命周期
从用户按下 Enter 键到终端渲染最终响应的端到端流程,集成所有子系统
职责概述
本文档追踪一次查询从用户输入到最终响应渲染的完整生命周期。它跨越 REPL 的 ask() 函数、QueryEngine.submitMessage()、query() 生成器、API 流式传输、工具调度、状态管理、转录持久化等多个子系统。与 Query 循环 页面关注内部机制不同,本页面聚焦于所有子系统的端到端协作,包括错误路径和恢复机制。
架构设计
核心数据流
UserMessage,并传递给 ask() 函数。在非交互模式(-p)中,输入通过 stdin 读取(支持 text 和 stream-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 读取完整输入
}
}
QueryEngine.ts:410-428 调用 processUserInput() 处理用户输入。此阶段:
- 检测并执行斜杠命令(如
/compact、/clear) - 加载附件消息(CLAUDE.md、memory 文件、图片等)
- 更新
allowedTools(来自--allowedTools标志) - 返回
messagesFromUserInput、shouldQuery、resultText
shouldQuery 为 false(斜杠命令已处理完毕),直接 yield 本地结果并返回。
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 模式下异步写入(非阻塞),正常模式下同步等待。
QueryEngine.ts:288-551 并行执行系统提示构建和技能/插件加载:
// QueryEngine.ts:534-537
const [skills, { enabled: enabledPlugins }] = await Promise.all([
getSlashCommandToolSkills(getCwd()),
loadAllPluginsCacheOnly(),
]);
然后构建 SystemInitMessage(包含工具列表、MCP 客户端、模型、权限模式等信息)并 yield。
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) 循环开始第一次迭代。
query.ts:365-549 按序执行四层压缩:
applyToolResultBudget()限制大型工具结果大小
snipCompactIfNeeded()裁剪历史尾部,释放 token
deps.microcompact()缓存编辑级压缩
deps.autocompact()全文摘要(达到阈值时)
query.ts:628-648 检查是否达到硬阻塞限制。当自动压缩关闭且上下文超过模型限制时,阻止 API 调用并返回错误。跳过条件:刚完成压缩、compact/session_memory 查询源、reactive compact 启用、context collapse 启用。
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;
!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' }
query.ts:1366-1408 当模型请求工具时:
- 权限检查:
canUseTool()根据权限模式决定允许/拒绝/询问用户 - 工具执行:每个工具实现
execute()方法,返回结构化结果 - 结果规范化:
normalizeMessagesForAPI()将工具结果转换为 API 可消费的格式 - 上下文更新:工具可能返回新的
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 };
}
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,模型基于工具输出决定是否继续使用工具或完成回复。
query() 返回 Terminal 对象。QueryEngine.submitMessage() 处理终止:
- yield
SDKResultMessage(包含成本、持续时间、使用量统计) - 更新
this.mutableMessages(会话消息历史) - 记录最终转录
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. 权限拒绝追踪
QueryEngine 在 wrappedCanUseTool 中包装了原始的 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 可以看到每个检查点的执行时间和顺序。调试查询循环中的问题
常见调试场景:
- 查询卡住:检查
AbortController.signal是否被触发,查看transition历史确认是否进入了恢复循环 - 上下文丢失:检查 autocompact 是否触发(
tengu_auto_compact_succeeded事件),确认 compact 后的消息是否正确传递 - 工具不执行:确认
streamingToolExecutor是否启用(config.gates.streamingToolExecution),检查toolUseBlocks是否被正确收集 - 权限被意外拒绝:查看
wrappedCanUseTool中的permissionDenials记录
添加新的消息处理阶段
在 QueryEngine.submitMessage() 的 for await 循环中添加新的 switch case。确保:
- 新消息类型添加到 query() 的 yield 联合类型中
- 如果需要持久化,在
messages.push(message)条件中包含 - 考虑对转录写入的影响(是否需要同步等待)
扩展恢复机制
新的恢复机制应该:
- 在
!needsFollowUp分支中添加检测逻辑 - 设置
State.transition记录原因 - 使用递增计数器或标志防止无限循环
- 在
withheld检查中添加对应的抑制逻辑 - 添加
logEvent用于遥测
架构师决策指南
端到端延迟的关键路径
从用户按下 Enter 到看到第一个字符,关键路径是:
- processUserInput (~5ms) — 输入解析和附件加载
- 系统提示构建 (~50ms) — fetchSystemPromptParts 并行加载技能和插件
- 转录写入 (~4-30ms) — 磁盘 I/O
- 压缩管道 (0-5000ms) — 取决于上下文大小和压缩触发
- API 首字节延迟 (~200-500ms) — 网络 + 模型推理
错误处理的分层策略
系统采用三层错误处理:
- 可恢复层(query 循环内部):prompt-too-long → 压缩重试;max-output-tokens → 提升限制;model overload → fallback 模型。这些对用户透明。
- 可报告层(yield 错误消息):不可恢复的 API 错误、图片大小错误。这些作为 Assistant 消息中的错误内容呈现。
- 致命层(throw/catch):运行时异常。这些触发
yieldMissingToolResultBlocks补齐未匹配的 tool_use,然后 yield 错误消息。
转录持久化的一致性保证
系统提供"至少一次"的转录持久化保证:用户消息在 API 调用前同步写入;assistant 消息异步写入但有顺序保证;compact_boundary 前的 preservedSegment 尾部同步刷新。在进程崩溃时,可能丢失最后一个 assistant 消息,但用户消息和之前的对话历史是安全的。
流式 vs 批量工具执行的权衡
StreamingToolExecutor 减少了多工具调用的延迟(工具在模型流式输出时就开始执行),但引入了复杂性:需要 discard() 在 fallback 时清理、需要 getRemainingResults() 在循环末尾收集未完成的结果、需要处理部分工具结果与后续消息的交错。对于工具数量少(1-2 个)的场景,收益有限;对于大量并行工具调用的场景,收益显著。
代码索引
| 文件 | 行数 | 说明 |
|---|---|---|
QueryEngine.ts | ~1296 | 会话管理、submitMessage 入口、SDK 消息 yield 与转录 |
query.ts | ~1730 | query()/queryLoop() 核心循环、压缩管道、API 调用、工具调度 |
main.tsx | ~4684 | getInputPrompt()、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 | ~23 | REPL 启动(App + REPL 组件动态加载) |