第八篇:QueryEngine查询引擎,Claude Code的核心对话循环
第八篇: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步)
- 清理与初始化 — 清除discoveredSkillNames,重置cwd,记录开始时间
- 处理用户输入(processUserInput) — 解析prompt字符串或ContentBlockParam[],展开斜杠命令、技能调用、@文件引用
- 构建上下文(fetchSystemPromptParts) — 加载系统提示词片段、用户上下文(CLAUDE.md)、git状态
- 调用Anthropic API(query) — 将messages + system + tools发送给Anthropic Messages API,开启流式响应
- 处理流式响应 — 逐块解析SSE事件:文本内容→输出;tool_use→收集输入;thinking→存储思维链
- 执行工具调用 — 对AI返回的每一个tool_use,调用对应Tool的execute方法
- 注入工具结果,继续下一轮 — 将tool_result消息追加到mutableMessages,如果未达到maxTurns则回到步骤4
- 收尾:记录用量、保存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行代码,有三个设计决策令人印象深刻:
-
AsyncGenerator而非回调:通过异步生成器逐步yield事件,使得REPL UI可以实时渲染流式输出,而SDK消费者也能灵活处理中间事件。
-
上下文延迟计算 + 缓存:git status、CLAUDE.md等内容通过memoize延迟计算,在单次会话内零重复开销。
-
无头优先(Headless-First):QueryEngine不依赖任何UI框架,所有状态通过AsyncGenerator yield出去,由调用方决定如何渲染。
下一篇预告:我们将深入 src/query.ts ——真正与Anthropic API对话的那一层,看看流式通信、工具Schema注入、以及"200ms首字延迟"是怎么做到的。
Claude Code 源码分析系列 · 第八篇
更多推荐


所有评论(0)