Claude Code 源码解析
返回目录
核心引擎03

查询引擎

SDK 与 Headless 的统一

核心洞察

QueryEngine 是 CLI REPL 和 SDK 编程接口的统一抽象层

学习

概念讲解与核心设计分析

查询引擎概述

QueryEngine 是 Claude Code 的"大脑",它管理着每次对话的完整生命周期——从消息构建到 API 调用,从工具执行到结果记录。每个 REPL 会话拥有一个 QueryEngine 实例,它维护着对话状态、token 追踪和权限管理。

QueryEngine 类的核心状态

QueryEngine 类是一个有状态的查询管理器,内部维护以下关键状态:

  • mutableMessages — 可变的消息数组,存储整个对话历史。这是一个引用类型,外部持有者可以观察到 QueryEngine 对它的修改
  • abortController — 用于取消正在进行的 API 请求。用户按下 Ctrl+C 或 Escape 时触发 abort
  • permissionDenials — 记录用户拒绝过的权限请求,避免在同一会话中重复询问
  • totalUsage — Token 消耗追踪器,按模型分别记录 input/output/cache tokens,用于费用计算和上下文管理
  • readFileState — LRU(最近最少使用)文件缓存,存储最近读取的文件内容,避免重复读取同一文件
  • discoveredSkillNames — 已发现的 Skill 名称集合,用于 Skill 系统的去重
  • loadedNestedMemoryPaths — 已加载的嵌套 CLAUDE.md 记忆文件路径集合,防止循环引用

submitMessage() 异步生成器

submitMessage() 是 QueryEngine 的核心公开方法,它返回一个 AsyncGenerator,逐步 yield 出 SDK 消息供 UI 层消费。其完整生命周期:

  1. 清除追踪状态 — 重置 permissionDenials 等临时状态
  2. 包装 canUseTool — 将权限检查函数与当前上下文绑定,创建带缓存的工具权限检查器
  3. 获取系统提示词片段 — 调用 fetchSystemPromptParts() 组装系统提示词的各个部分
  4. 处理用户输入 — 将用户的文本消息转换为 API 兼容的消息格式
  5. 调用 query() — 进入查询循环,开始与 Claude API 的交互
  6. yield SDK 消息 — 将从 API 返回的消息逐个 yield 出去,供 UI 实时渲染
  7. 记录会话转录 — 将完整的对话记录保存到 transcript 文件

系统提示词组装

系统提示词不是一个静态字符串,而是由多个来源动态组装:

  • 基础提示词 — Claude Code 的核心行为指令
  • 工具说明 — 当前可用工具的使用说明
  • CLAUDE.md 记忆 — 项目级和用户级的记忆文件内容
  • MCP 工具说明 — 已连接的 MCP 服务器提供的工具说明
  • 上下文信息 — 当前工作目录、git 状态等环境信息
  • 用户自定义指令 — 通过配置注入的自定义系统提示词

Coordinator 用户上下文注入

当 Claude Code 以 Coordinator(协调者)模式运行时,系统会注入额外的上下文信息,包括当前项目的目录结构摘要、最近的 git 变更记录、以及其他 Agent 的执行状态。这使得 Coordinator 能够做出更好的任务分配决策。

SDK 消息转换与 yield

QueryEngine 将 Anthropic SDK 返回的原始消息对象转换为统一的内部格式,然后通过 yield* 逐个发射给调用者。这种 AsyncGenerator 模式使得 UI 层可以实时渲染流式响应,而不需要等待整个回复完成。

QueryEngineConfig 类型

QueryEngineConfig 定义了创建 QueryEngine 时所需的配置:

  • model — 使用的模型名称
  • maxTurns — 最大对话轮数
  • systemPrompt — 基础系统提示词
  • tools — 可用工具列表
  • permissionMode — 权限检查模式
  • budget — Token 预算限制
  • onMessage — 消息回调函数

架构

模块关系与设计决策

查询引擎架构

QueryEngine 状态管理

状态字段类型用途生命周期
mutableMessagesMessage[]对话历史整个会话
abortControllerAbortController请求取消每次 submitMessage
permissionDenialsSet<string>权限拒绝记录每次 submitMessage 重置
totalUsageUsageTrackerToken 消耗追踪整个会话累加
readFileStateLRUCache文件内容缓存整个会话(LRU 淘汰)
discoveredSkillNamesSet<string>已发现 Skill整个会话
loadedNestedMemoryPathsSet<string>已加载记忆路径整个会话

submitMessage() 调用链

消息提交的调用链如下:

  • REPL UIqueryEngine.submitMessage(userInput)
    • → 清除临时状态
    • → 包装 canUseTool 权限检查
    • → fetchSystemPromptParts() 组装系统提示词
    • query()(进入查询循环)
      • → queryLoop()(内部循环)
      • → API 调用 + 工具执行
      • → yield StreamEvent
    • → recordTranscript() 保存转录

与其他模块的关系

  • QueryEngine → 调用 query() 进入查询循环
  • QueryEngine → 使用 fetchSystemPromptParts() 组装提示词
  • QueryEngine → 通过 canUseTool() 检查工具权限
  • QueryEngine → 与 UsageTracker 协作追踪 token 消耗
  • REPL → 持有 QueryEngine 实例,消费 AsyncGenerator 输出

源码

3 个关键代码示例

01
QueryEngine 类核心结构
TypeScript
// QueryEngine.ts — 简化版
export class QueryEngine {
  // 核心状态
  private mutableMessages: Message[] = []
  private abortController: AbortController | null = null
  private permissionDenials = new Set<string>()
  private totalUsage: UsageTracker = createUsageTracker()
  private readFileState = new LRUCache<string, FileContent>({
    max: 100  // 最多缓存 100 个文件
  })
  private discoveredSkillNames = new Set<string>()
  private loadedNestedMemoryPaths = new Set<string>()

  // 配置
  private config: QueryEngineConfig

  constructor(config: QueryEngineConfig) {
    this.config = config
  }

  // 取消当前查询
  abort(): void {
    this.abortController?.abort()
  }

  // 获取 token 使用统计
  getUsage(): UsageSummary {
    return this.totalUsage.getSummary()
  }

  // 核心方法:提交消息(见下一个示例)
  async *submitMessage(userInput: string): AsyncGenerator<Message> {
    // ...
  }
}
02
submitMessage() 完整生命周期
TypeScript
// QueryEngine.ts — submitMessage 简化版
async *submitMessage(
  userInput: string
): AsyncGenerator<Message> {
  // 1. 清除上一轮的临时状态
  this.permissionDenials.clear()

  // 2. 创建新的 AbortController
  this.abortController = new AbortController()

  // 3. 包装工具权限检查
  const canUseTool = wrapCanUseTool(
    this.config.permissionMode,
    this.permissionDenials
  )

  // 4. 组装系统提示词(多源合并)
  const systemPromptParts = await fetchSystemPromptParts({
    tools: this.config.tools,
    memories: this.loadedNestedMemoryPaths,
    cwd: process.cwd(),
    skills: this.discoveredSkillNames,
  })

  // 5. 添加用户消息到对话历史
  this.mutableMessages.push({
    role: 'user',
    content: userInput,
  })

  // 6. 进入查询循环,yield 所有响应消息
  const queryGen = query({
    messages: this.mutableMessages,
    systemPrompt: systemPromptParts.join('\n'),
    model: this.config.model,
    tools: this.config.tools,
    canUseTool,
    abortSignal: this.abortController.signal,
    usage: this.totalUsage,
    readFileState: this.readFileState,
  })

  // 7. 逐个 yield SDK 消息供 UI 消费
  for await (const event of queryGen) {
    if (event.type === 'message') {
      this.mutableMessages.push(event.message)
      yield event.message
    }
  }

  // 8. 记录会话转录
  await recordTranscript(this.mutableMessages)
}
03
系统提示词多源组装
TypeScript
// fetchSystemPromptParts — 简化版
async function fetchSystemPromptParts(opts: {
  tools: Tool[]
  memories: Set<string>
  cwd: string
  skills: Set<string>
}): Promise<string[]> {
  const parts: string[] = []

  // 1. 基础系统提示词
  parts.push(BASE_SYSTEM_PROMPT)

  // 2. 工具使用说明
  for (const tool of opts.tools) {
    parts.push(formatToolDescription(tool))
  }

  // 3. CLAUDE.md 项目记忆
  const projectMemory = await loadClaudeMd(opts.cwd)
  if (projectMemory) {
    parts.push(`<project-memory>\n${projectMemory}\n</project-memory>`)
  }

  // 4. 用户级记忆(~/.claude/CLAUDE.md)
  const userMemory = await loadUserClaudeMd()
  if (userMemory) {
    parts.push(`<user-memory>\n${userMemory}\n</user-memory>`)
  }

  // 5. 环境上下文
  parts.push(`Current directory: ${opts.cwd}`)
  parts.push(`Platform: ${process.platform}`)

  // 6. Coordinator 模式额外上下文
  if (isCoordinatorMode()) {
    parts.push(await getCoordinatorContext())
  }

  return parts
}

互动

步进式流程演示

互动演示

查询引擎互动解析

第 1 步:理解 AsyncGenerator 模式

为什么 submitMessage() 返回 AsyncGenerator 而不是 Promise

因为 Claude 的响应是流式的。如果使用 Promise,UI 必须等到整个回复完成才能显示,用户会看到长时间的"思考中..."。AsyncGenerator 让 UI 可以在每个 token 到达时立即渲染,实现打字机效果。

第 2 步:状态的生命周期

注意不同状态字段的生命周期差异:

  • totalUsage 在整个会话中持续累加,因为用户需要看到总费用
  • permissionDenials 在每次 submitMessage清除,因为用户可能改变了主意
  • readFileState 使用 LRU 缓存,自动淘汰最久未使用的文件内容

第 3 步:权限检查的包装

wrapCanUseTool() 为什么要每次 submitMessage 时重新包装?因为它需要捕获当前轮次的 permissionDenials 引用。当用户拒绝某个工具后,该拒绝记录仅在当前轮次有效——下一轮用户可能会允许它。

第 4 步:系统提示词的动态性

每次调用 fetchSystemPromptParts() 都会重新组装系统提示词。这意味着:

  • 如果用户在对话中修改了 CLAUDE.md,下一轮对话就会使用更新后的内容
  • 新发现的 Skill 会自动添加到系统提示词中
  • 环境上下文(如 cd 到新目录)会实时反映

核心设计洞察

  • 有状态但可控:QueryEngine 维护必要的状态,但每个状态都有明确的生命周期规则
  • 流式优先:AsyncGenerator 模式贯穿整个消息管道,从 API 到 UI
  • 权限即时性:权限决策不会跨轮持久化,尊重用户的即时意愿

相关源文件

QueryEngine.ts