查询引擎
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 层消费。其完整生命周期:
- 清除追踪状态 — 重置 permissionDenials 等临时状态
- 包装 canUseTool — 将权限检查函数与当前上下文绑定,创建带缓存的工具权限检查器
- 获取系统提示词片段 — 调用
fetchSystemPromptParts()组装系统提示词的各个部分 - 处理用户输入 — 将用户的文本消息转换为 API 兼容的消息格式
- 调用 query() — 进入查询循环,开始与 Claude API 的交互
- yield SDK 消息 — 将从 API 返回的消息逐个 yield 出去,供 UI 实时渲染
- 记录会话转录 — 将完整的对话记录保存到 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 状态管理
| 状态字段 | 类型 | 用途 | 生命周期 |
|---|---|---|---|
| mutableMessages | Message[] | 对话历史 | 整个会话 |
| abortController | AbortController | 请求取消 | 每次 submitMessage |
| permissionDenials | Set<string> | 权限拒绝记录 | 每次 submitMessage 重置 |
| totalUsage | UsageTracker | Token 消耗追踪 | 整个会话累加 |
| readFileState | LRUCache | 文件内容缓存 | 整个会话(LRU 淘汰) |
| discoveredSkillNames | Set<string> | 已发现 Skill | 整个会话 |
| loadedNestedMemoryPaths | Set<string> | 已加载记忆路径 | 整个会话 |
submitMessage() 调用链
消息提交的调用链如下:
- REPL UI →
queryEngine.submitMessage(userInput)- → 清除临时状态
- → 包装 canUseTool 权限检查
- → fetchSystemPromptParts() 组装系统提示词
- → query()(进入查询循环)
- → queryLoop()(内部循环)
- → API 调用 + 工具执行
- → yield StreamEvent
- → recordTranscript() 保存转录
与其他模块的关系
- QueryEngine → 调用
query()进入查询循环 - QueryEngine → 使用
fetchSystemPromptParts()组装提示词 - QueryEngine → 通过
canUseTool()检查工具权限 - QueryEngine → 与
UsageTracker协作追踪 token 消耗 - REPL → 持有 QueryEngine 实例,消费 AsyncGenerator 输出
源码
共 3 个关键代码示例
// 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> {
// ...
}
}// 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)
}// 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
- 权限即时性:权限决策不会跨轮持久化,尊重用户的即时意愿