二、深入 Claude Code CLI 源码:QueryEngine 的设计哲学

如果你用过 Claude Code CLI,一定好奇过它是如何管理对话流程的。今天我们来深入分析它的核心引擎------QueryEngine。

QueryEngine 是什么?

简单说,QueryEngine 是对话生命周期的管理者。它负责:

  • 消息状态管理
  • API 交互协调
  • 工具执行编排
  • 权限追踪
  • 使用统计

它的核心方法 submitMessage() 是一个 AsyncGenerator,支持流式输出、中断、和增量处理。这种设计在 TypeScript 中非常优雅:

typescript 复制代码
async *submitMessage(
  prompt: string | ContentBlockParam[],
  options?: { uuid?: string; isMeta?: boolean }
): AsyncGenerator<SDKMessage, void, unknown>

消息提交的完整流程

让我带你走过一次完整的消息提交旅程。

第一步:初始化和清理

每次提交新消息时,首先要清理 turn-scoped 的追踪状态:

typescript 复制代码
this.discoveredSkillNames.clear()
this.loadedNestedMemoryPaths.clear()

然后包装 canUseTool 函数,用于追踪权限拒绝:

typescript 复制代码
const wrappedCanUseTool: CanUseToolFn = async (...args) => {
  const result = await canUseTool(...args)
  if (result.behavior !== 'allow') {
    this.permissionDenials.push({
      tool_name: sdkCompatToolName(tool.name),
      tool_use_id: toolUseID,
      tool_input: input,
    })
  }
  return result
}

这个包装很巧妙------它不改变工具权限检查的逻辑,只是默默记录所有的拒绝事件。这些记录最终会出现在 SDK 的返回结果中,让调用者知道哪些操作被拒绝了。

第二步:系统提示构建

系统提示不是静态的,而是动态构建的。Claude Code 会根据当前状态组装不同的提示部分:

typescript 复制代码
const { defaultSystemPrompt, userContext, systemContext } =
  await fetchSystemPromptParts({
    tools,
    mainLoopModel: initialMainLoopModel,
    mcpClients,
    customSystemPrompt: customPrompt,
  })

const systemPrompt = asSystemPrompt([
  ...(customPrompt !== undefined ? [customPrompt] : defaultSystemPrompt),
  ...(memoryMechanicsPrompt ? [memoryMechanicsPrompt] : []),
  ...(appendSystemPrompt ? [appendSystemPrompt] : []),
])

这里有一个有趣的设计:内存机制的提示是条件注入的。只有在用户覆盖了自动内存路径时,才会加载内存相关的提示。

第三步:消息持久化(关键时刻)

在进入查询循环之前,有一个关键的持久化步骤:

typescript 复制代码
this.mutableMessages.push(...messagesFromUserInput)

if (persistSession && messagesFromUserInput.length > 0) {
  const transcriptPromise = recordTranscript(messages)
  if (isBareMode()) {
    void transcriptPromise  // fire-and-forget
  } else {
    await transcriptPromise
    if (isEnvTruthy(process.env.CLAUDE_CODE_EAGER_FLUSH)) {
      await flushSessionStorage()
    }
  }
}

为什么要在进入查询循环前持久化?因为如果进程被杀死(比如用户按 Ctrl+C),至少 transcript 里会包含用户刚才输入的内容,而不是只有一堆队列操作记录。

第四步:主查询循环

接下来进入 query() 函数的主循环。这是一个 while (true) 的状态机:

typescript 复制代码
while (true) {
  // 准备消息
  let messagesForQuery = [...getMessagesAfterCompactBoundary(messages)]

  // 应用各种压缩策略
  messagesForQuery = await applyToolResultBudget(messagesForQuery, ...)
  if (feature('HISTORY_SNIP')) {
    const snipResult = snipModule!.snipCompactIfNeeded(messagesForQuery)
    messagesForQuery = snipResult.messages
  }

  // API 调用
  for await (const message of deps.callModel({...})) {
    // 处理流式事件...
  }

  // 工具执行
  const toolUpdates = streamingToolExecutor
    ? streamingToolExecutor.getRemainingResults()
    : runTools(toolUseBlocks, ...)

  // 决定下一个状态或退出
  state = { messages: [...], turnCount: nextTurnCount, ... }
  // 或者
  return { reason: 'completed' }
}

Streaming Tool Execution 的魔法

Claude Code 实现了一个 Streaming Tool Executor,允许工具在 API 流式返回时就开始并行执行:

typescript 复制代码
if (streamingToolExecutor && !toolUseContext.abortController.signal.aborted) {
  for (const toolBlock of msgToolUseBlocks) {
    streamingToolExecutor.addTool(toolBlock, message)
  }
}

// 同时获取已完成的结果
for (const result of streamingToolExecutor.getCompletedResults()) {
  if (result.message) {
    yield result.message
  }
}

这大大减少了等待时间。当 API 还在返回后续内容时,前面的工具调用可能已经开始执行了。

各种压缩策略

QueryEngine 使用了多层压缩策略来管理 token 预算:

  1. Snip - 历史剪裁,移除过旧的消息
  2. Microcompact - 微压缩,保留关键结构
  3. Context Collapse - 上下文折叠,合并相似内容
  4. Autocompact - 自动压缩,超过阈值时触发
  5. Reactive Compact - 响应式压缩,遇到 413 错误时触发

每种策略都有自己的触发条件和保留逻辑,确保重要信息不会丢失。

Tool 接口的设计

最后说说 Tool 接口,这是 QueryEngine 操作的核心对象:

typescript 复制代码
type Tool = {
  name: string
  inputSchema: z.ZodType
  call(args, context, canUseTool, parentMessage, onProgress): Promise<ToolResult>
  prompt(options): Promise<string>
  description(input, options): Promise<string>
  isEnabled(): boolean
  isReadOnly(input): boolean
  isConcurrencySafe(input): boolean
  checkPermissions(input, context): Promise<PermissionResult>
  // ... 渲染方法等
}

每个工具必须实现这些方法。isConcurrencySafe 和 isReadOnly 的区分很重要:

  • 安全的工具可以并行执行
  • 只读工具在 Plan 模式下也能执行

总结

QueryEngine 的设计体现了几个重要的工程原则:

  1. AsyncGenerator 模式:支持流式、可中断、增量处理
  2. 状态机设计:用 while(true) + state 转换,清晰表达循环逻辑
  3. 并行工具执行:Streaming Tool Executor 提升响应速度
  4. 多层压缩策略:适应不同场景的 token 管理
  5. Context 传递模式:每次迭代更新上下文,保持参数不可变

下一篇我会分享 UI 组件系统,看看 Ink 如何在终端中实现 React 渲染。

相关推荐
做萤石二次开发的哈哈1 小时前
视频解码器怎么对接?解码上墙、电视墙开窗与场景切换的ISAPI接入实战
人工智能·物联网·监控·视频编解码·大屏端·萤石开放平台·蓝海aiot一站式工作台
Leo.yuan1 小时前
2026年本地化Data Agent优质厂商盘点:哪些产品更适合企业生产环境
大数据·数据库·人工智能
长谷深风1112 小时前
Tool与Skill:AI能力设计的分水岭
java·人工智能·ai·大模型·aiagent
科技观察哨2 小时前
六足平台选型与纳米定位系统集成:HEB-640六自由度位移台在半导体光刻对准中的参数边界与国产替代评估
前端·人工智能
云上先途2 小时前
任务智能体可以自动完成哪些类型工作,是不是只能做简单重复操作?
大数据·人工智能
明志数科2 小时前
具身智能数据供给的分层:分布式采集与入厂采集的工程边界分析
人工智能·机器学习·机器人
像风一样自由20202 小时前
41.用FastAPI搭建一个RAG后端需要哪些接口
人工智能·大模型·fastapi·rag·智能体
小蒋观天下2 小时前
两轮车检测AI摄像头——2026行业竞争格局、商业模式与核心痛点
大数据·人工智能·安全·计算机视觉·ai大模型
RisunJan2 小时前
【这就是AI】AI每日资讯简报 - 2026-09-28(周一)
大数据·人工智能
也非非也2 小时前
DeepSeek没发公告,但它把Agent装进了你的电脑
人工智能·开源·agent·deepseek·dsh