第八篇:QueryEngine查询引擎,Claude Code的核心对话循环

源码位置:src/QueryEngine.ts(1295行)|难度:高级

一、引言:为什么QueryEngine是心脏

在前七篇中,我们依次拆解了Claude Code的架构全景、CLI入口、Handler处理器链。但有一个核心问题始终没有回答:用户输入一段提示词后,Claude Code究竟做了什么?

答案就在 QueryEngine.ts 里。这个1295行的TypeScript文件,是整个系统的"心脏"——它掌管着从用户输入到AI回复、从工具调用到结果回传的完整生命周期。每一次你按下回车,都是一次 submitMessage() 的调用。

💡 设计亮点:QueryEngine 被刻意设计成一个无头(headless)友好的独立类,既可以驱动终端REPL,也可以支撑SDK远程调用。这种解耦使得Claude Code能同时服务多种前端形态。


二、QueryEngine架构总览

核心职责

  • 对话状态管理:维护 mutableMessages 数组,所有对话历史都在这里
  • 上下文构建:动态组装系统提示词、用户上下文(CLAUDE.md、git状态等)
  • API调用编排:调用Anthropic Messages API,处理流式响应
  • 工具执行循环:解析AI返回的工具调用,执行,将结果注入下一轮对话
  • 权限与取消:通过 AbortController 支持中断,通过 canUseTool 控制权限
  • 用量追踪:实时累积token用量和成本

QueryEngineConfig:配置接口核心字段

字段 说明
cwd 当前工作目录,决定CLAUDE.md搜索范围
tools 可用工具集合(Bash、Read、Write、Edit等)
commands 斜杠命令列表(/help、/clear等)
mcpClients 已连接的MCP服务器列表
canUseTool 权限检查函数,决定是否允许某工具执行
maxTurns 最大对话轮次(防止无限循环)
maxBudgetUsd 预算上限(美元),超支自动停止
thinkingConfig 思维链(Thinking)配置
abortController 取消控制器,用户按Ctrl+C时触发

核心私有状态

private mutableMessages: Message[]      // 可变对话历史
private abortController: AbortController // 取消控制
private permissionDenials: SDKPermissionDenial[] // 权限拒绝记录
private totalUsage: NonNullableUsage    // 累积用量
private readFileState: FileStateCache   // 文件状态缓存(优化重复读取)
private discoveredSkillNames: Set<string> // 本turn发现的技能名

三、submitMessage:一次对话请求的完整生命周期

submitMessage() 是一个 AsyncGenerator<SDKMessage>,通过 yield 逐步向外推送事件(文本、工具调用、用量等),而不是等整个处理完成后一次性返回。

处理管道(8步)

  1. 清理与初始化 — 清除discoveredSkillNames,重置cwd,记录开始时间
  2. 处理用户输入(processUserInput) — 解析prompt字符串或ContentBlockParam[],展开斜杠命令、技能调用、@文件引用
  3. 构建上下文(fetchSystemPromptParts) — 加载系统提示词片段、用户上下文(CLAUDE.md)、git状态
  4. 调用Anthropic API(query) — 将messages + system + tools发送给Anthropic Messages API,开启流式响应
  5. 处理流式响应 — 逐块解析SSE事件:文本内容→输出;tool_use→收集输入;thinking→存储思维链
  6. 执行工具调用 — 对AI返回的每一个tool_use,调用对应Tool的execute方法
  7. 注入工具结果,继续下一轮 — 将tool_result消息追加到mutableMessages,如果未达到maxTurns则回到步骤4
  8. 收尾:记录用量、保存session — 调用flushSessionStorage()持久化对话

四、上下文管理系统

Claude Code的"上下文"不仅仅是对话历史,还包括环境状态、用户偏好、项目配置。这些由 context.ts 统一管理。

SystemContext:环境快照

通过 getSystemContext()(memoize缓存)获取,包含:

{
  "gitStatus": "Current branch: main\nStatus:\n (clean)\nRecent commits:...",
  "cacheBreaker": "[CACHE_BREAKER: ...]"
}

性能优化:getSystemContext和getUserContext都用了 lodash-es/memoize,在一次对话会话内只计算一次,后续直接返回缓存值。git status这种耗时操作不会每次都执行。

UserContext:项目与个人偏好

通过 getUserContext() 获取,核心内容是 CLAUDE.md 的聚合:

{
  "claudeMd": "# 项目规范\n- 使用TypeScript...\n- 测试框架:vitest...",
  "currentDate": "Today's date is 2026-06-30."
}

CLAUDE.md的加载逻辑:支持项目级(./CLAUDE.md)、用户级(~/.claude/CLAUDE.md)、插件级三个层级,通过 getMemoryFiles() 递归搜索所有父目录。


五、流式响应处理

QueryEngine通过导入的 query() 函数(来自 src/query.ts)与Anthropic API通信。query() 返回的是一个异步生成器,逐块产出SSE事件。

核心处理循环(伪代码)

for await (const event of query({ messages, system, tools, ... })) {
  if (event.type === 'content_block_delta') {
    // 文本增量 → 累积到输出缓冲区
    textBuffer += event.delta.text;
  }
  if (event.type === 'content_block_start' && event.content_block.type === 'tool_use') {
    // 工具调用开始 → 初始化工具输入收集器
    currentToolUse = { name: event.content_block.name, input: '' };
  }
  if (event.type === 'content_block_delta' && event.delta.type === 'input_json_delta') {
    // 工具输入JSON流式传输 → 逐步拼接
    currentToolUse.input += event.delta.partial_json;
  }
  if (event.type === 'message_stop') {
    // 消息结束 → 处理收集到的所有tool_uses
    break;
  }
}

Thinking 思维链支持

thinkingConfig 启用时,API会返回 thinking 类型的content block。QueryEngine将其原样保留在消息历史中,供下一轮对话使用(Anthropic API支持thinking块作为上下文传入)。


六、工具执行与权限控制

AI返回tool_use后,QueryEngine需要执行这些工具。

权限包装器(wrappedCanUseTool)

canUseTool 是注入的权限检查函数,QueryEngine对其进行了包装:

const wrappedCanUseTool: CanUseToolFn = async (
  tool, input, toolUseContext, assistantMessage, toolUseID
) => {
  const result = await canUseTool(tool, input, ...);
  if (result.type === 'deny') {
    this.permissionDenials.push({ toolName: tool.name, ... });
  }
  return result;
};

工具执行结果注入

每个工具的执行结果被封装成 tool_result 消息,追加到 mutableMessages

mutableMessages.push({
  role: 'user',
  content: [{
    type: 'tool_result',
    tool_use_id: toolUseID,
    content: toolOutput
  }]
});

七、错误处理与重试

API调用可能失败(速率限制、网络抖动、服务不可用)。QueryEngine通过 categorizeRetryableAPIError() 对错误分类:

  • 可重试错误:429(速率限制)、500/502/503(服务端临时错误)、网络超时
  • 不可重试错误:401(认证失败)、400(请求格式错误)、内容策略拦截

可重试错误会触发指数退避重试,最多重试3次。AbortController 可在任意时刻中断等待中的重试。


八、用量追踪与成本计算

每次API调用返回Usage信息(input_tokens、output_tokens),QueryEngine通过 accumulateUsage() 实时累积:

this.totalUsage = accumulateUsage(this.totalUsage, usage);

同时,CostTracker 根据模型定价将token用量转换成美元成本,在达到 maxBudgetUsd 时自动中止对话。

📊 遥测事件:每次turn结束时,QueryEngine会通过 tengu_turn_complete 事件上报用量统计,包括input/output token数、耗时、工具调用次数等。


九、高级特性

Snip Compaction:长对话的历史裁剪

当对话变得非常长(数万token),API上下文窗口可能溢出。snipReplay 机制会在适当时机裁剪早期消息,但保留关键决策点,使得后续对话可以"投影"回被裁剪掉的内容。

Session Persistence:对话持久化

flushSessionStorage() 在每次turn结束后调用,将 mutableMessages 写入磁盘(~/.claude/projects/...),使得 claude --resume 可以精确恢复对话状态。

Memory Prompt:动态记忆加载

loadMemoryPrompt() 从项目 .claude/memory/ 目录加载记忆文件,注入到系统提示词中。这让Claude能"记住"跨会话的项目上下文。


十、总结:QueryEngine的设计哲学

读完 QueryEngine.ts 的1295行代码,有三个设计决策令人印象深刻:

  1. AsyncGenerator而非回调:通过异步生成器逐步yield事件,使得REPL UI可以实时渲染流式输出,而SDK消费者也能灵活处理中间事件。

  2. 上下文延迟计算 + 缓存:git status、CLAUDE.md等内容通过memoize延迟计算,在单次会话内零重复开销。

  3. 无头优先(Headless-First):QueryEngine不依赖任何UI框架,所有状态通过AsyncGenerator yield出去,由调用方决定如何渲染。

下一篇预告:我们将深入 src/query.ts ——真正与Anthropic API对话的那一层,看看流式通信、工具Schema注入、以及"200ms首字延迟"是怎么做到的。


Claude Code 源码分析系列 · 第八篇

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐