第3章 对话即编程

2 阅读8分钟

第3章 对话即编程

引言

在计算机编程的历史长河中,人机交互的方式经历了从打孔卡片到命令行界面,再到图形用户界面的演变。然而,Claude Code 代表了一种全新的范式——对话即编程(Conversation as Programming)。这一范式将编程从"编写代码"转变为"与AI对话",通过自然语言交互来完成复杂的编程任务。

本章将深入剖析 Claude Code 的核心引擎——QueryEngine 和 query.ts 中的状态机循环,揭示"对话即编程"背后的技术实现。我们将看到,这一范式转变并非简单的界面包装,而是对编程本质的重新思考:编程不再是显式地告诉计算机每一步该做什么,而是通过对话让AI理解意图并自主决策。

概念讲解:从 REPL 到 AI 对话

REPL 的局限性

传统的编程环境基于 REPL(Read-Eval-Print Loop)循环:

// 传统 REPL 的伪代码
while (true) {
  const input = read();
  const result = eval(input);
  print(result);
}

这种模式要求用户显式地编写代码,计算机只是忠实地执行。用户的思维必须与计算机的执行模型完全对齐,任何细微的语法错误都会导致失败。

AI 对话的革命性

Claude Code 的"对话即编程"范式则完全不同:

// AI 对话的伪代码
while (true) {
  const userMessage = read();
  const context = gatherContext();
  const aiResponse = await queryLLM(userMessage, context);
  const actions = parseActions(aiResponse);
  await executeActions(actions);
  display(aiResponse);
}

在这里,用户只需要用自然语言描述意图,AI 会理解上下文、规划步骤、执行工具调用,并持续对话直到任务完成。这种范式的核心优势在于:

  1. 意图驱动:用户描述"做什么",而非"怎么做"
  2. 上下文感知:AI 自动理解项目结构、文件内容、git状态等
  3. 持续对话:通过多轮交互逐步完善解决方案
  4. 工具调用:AI 可以主动调用各种工具(读取文件、运行命令、搜索等)

源码分析:QueryEngine 的消息提交机制

submitMessage() 方法的核心逻辑

QueryEngine.ts 是整个系统的入口,其 submitMessage() 方法实现了"对话即编程"的核心流程。让我们先看其导入部分:

import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'
import { randomUUID } from 'crypto'
import last from 'lodash-es/last.js'
import {
  getSessionId,
  isSessionPersistenceDisabled,
} from 'src/bootstrap/state.js'
import type {
  PermissionMode,
  SDKCompactBoundaryMessage,
  SDKMessage,
  SDKPermissionDenial,
  SDKStatus,
  SDKUserMessageReplay,
} from 'src/entrypoints/agentSdkTypes.js'
import { accumulateUsage, updateUsage } from 'src/services/api/claude.js'

从导入可以看出,QueryEngine 依赖了多个关键模块:

  • Anthropic SDK 用于与 LLM 通信
  • 状态管理(bootstrap/state.js)
  • API 服务(services/api/claude.js)
  • 工具系统(Tool.js)

submitMessage() 方法的关键步骤包括:

  1. 消息预处理:将用户输入转换为标准消息格式
  2. 上下文收集:调用 getSystemContext()getUserContext()
  3. 查询执行:调用 query() 函数进入状态机循环
  4. 结果处理:将 AI 响应转换为 UI 可显示的格式

上下文收集机制

context.ts 文件展示了上下文收集的精妙设计。系统将上下文分为系统上下文和用户上下文:

export const getSystemContext = memoize(
  async (): Promise<{
    [k: string]: string
  }> => {
    const startTime = Date.now()
    logForDiagnosticsNoPII('info', 'system_context_started')

    // Skip git status in CCR (unnecessary overhead on resume) or when git instructions are disabled
    const gitStatus =
      isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
      !shouldIncludeGitInstructions()
        ? null
        : await getGitStatus()

    // Include system prompt injection if set (for cache breaking, ant-only)
    const injection = feature('BREAK_CACHE_COMMAND')
      ? getSystemPromptInjection()
      : null

    logForDiagnosticsNoPII('info', 'system_context_completed', {
      duration_ms: Date.now() - startTime,
      has_git_status: gitStatus !== null,
      has_injection: injection !== null,
    })

    return {
      ...(gitStatus && { gitStatus }),
      ...(feature('BREAK_CACHE_COMMAND') && injection
        ? {
            cacheBreaker: `[CACHE_BREAKER: ${injection}]`,
          }
        : {}),
    }
  },
)

系统上下文收集了 git 状态、分支信息、提交历史等环境信息。注意这里使用了 memoize 来缓存结果,避免重复计算。

用户上下文则收集了项目特定的信息:

export const getUserContext = memoize(
  async (): Promise<{
    [k: string]: string
  }> => {
    const startTime = Date.now()
    logForDiagnosticsNoPII('info', 'user_context_started')

    // CLAUDE_CODE_DISABLE_CLAUDE_MDS: hard off, always.
    // --bare: skip auto-discovery (cwd walk), BUT honor explicit --add-dir.
    // --bare means "skip what I didn't ask for", not "ignore what I asked for".
    const shouldDisableClaudeMd =
      isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_CLAUDE_MDS) ||
      (isBareMode() && getAdditionalDirectoriesForClaudeMd().length === 0)
    // Await the async I/O (readFile/readdir directory walk) so the event
    // loop yields naturally at the first fs.readFile.
    const claudeMd = shouldDisableClaudeMd
      ? null
      : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
    // Cache for the auto-mode classifier (yoloClassifier.ts reads this
    // instead of importing claudemd.ts directly, which would create a
    // cycle through permissions/filesystem → permissions → yoloClassifier).
    setCachedClaudeMdContent(claudeMd || null)

    logForDiagnosticsNoPII('info', 'user_context_completed', {
      duration_ms: Date.now() - startTime,
      claudemd_length: claudeMd?.length ?? 0,
      claudemd_disabled: Boolean(shouldDisableClaudeMd),
    })

    return {
      ...(claudeMd && { claudeMd }),
      currentDate: `Today's date is ${getLocalISODate()}.`,
    }
  },
)

用户上下文包括:

  • Claude.md 文件(项目说明文档)
  • 当前日期
  • 内存文件(用户提供的上下文信息)

这种分离设计非常优雅:系统上下文提供环境信息,用户上下文提供项目特定信息,两者组合后作为完整的上下文传递给 LLM。

源码分析:query.ts 的状态机循环

状态机的设计思想

query.ts 实现了一个复杂的状态机,这是"对话即编程"范式的核心。状态机负责管理从用户输入到最终响应的整个流程。

从导入部分可以看出,query.ts 依赖了大量功能模块:

import type {
  ToolResultBlockParam,
  ToolUseBlock,
} from '@anthropic-ai/sdk/resources/index.mjs'
import type { CanUseToolFn } from './hooks/useCanUseTool.js'
import { FallbackTriggeredError } from './services/api/withRetry.js'
import {
  calculateTokenWarningState,
  isAutoCompactEnabled,
  type AutoCompactTrackingState,
} from './services/compact/autoCompact.js'
import { buildPostCompactMessages } from './services/compact/compact.js'

状态机的核心是一个 while(true) 循环,不断处理状态转换:

// 伪代码示意(实际实现更复杂)
export async function query(...) {
  let state: State = {
    type: 'initial',
    messages: [],
    context: {},
  };

  while (true) {
    switch (state.type) {
      case 'initial':
        state = await handleInitialState(state);
        break;
      case 'thinking':
        state = await handleThinkingState(state);
        break;
      case 'tool_use':
        state = await handleToolUseState(state);
        break;
      case 'responding':
        state = await handleRespondingState(state);
        break;
      case 'complete':
        return state.result;
    }
  }
}

流式响应处理的生成器模式

流式响应是"对话即编程"体验的关键。用户不需要等待整个响应生成完毕,而是可以实时看到 AI 的思考过程和执行结果。

query.ts 中使用了生成器模式来处理流式响应:

// 伪代码示意
async function* streamResponse(messages: Message[]) {
  const response = await anthropic.messages.stream({
    messages: normalizeMessagesForAPI(messages),
    max_tokens: 4096,
  });

  for await (const event of response) {
    if (event.type === 'content_block_delta') {
      yield event.delta;
    } else if (event.type === 'content_block_stop') {
      // 处理内容块结束
    } else if (event.type === 'message_stop') {
      // 处理消息结束
    }
  }
}

这种设计的优势在于:

  1. 实时反馈:用户可以立即看到 AI 的输出
  2. 内存效率:不需要缓存整个响应
  3. 可中断性:用户可以在任何时刻停止响应

工具调用的集成

状态机的另一个关键功能是处理工具调用。当 AI 决定调用工具时,状态机会:

  1. 解析工具调用请求
  2. 验证权限
  3. 执行工具
  4. 将结果反馈给 AI
  5. 继续对话

从 query.ts 的导入可以看到工具相关的依赖:

import { findToolByName, type ToolUseContext } from './Tool.js'
import { StreamingToolExecutor } from './services/tools/StreamingToolExecutor.js'
import { runTools } from './services/tools/toolOrchestration.js'

ToolUseContext 是一个关键类型,它贯穿整个工具调用流程,携带了执行工具所需的所有上下文信息。

源码分析:工具系统的类型设计

Tool.ts 定义了工具系统的核心类型:

export type ToolInputJSONSchema = {
  [x: string]: unknown
  type: 'object'
  properties?: {
    [x: string]: unknown
  }
}

这是工具输入的 JSON Schema 定义,确保了类型安全。每个工具都必须明确定义其输入参数的结构。

tools.ts 展示了工具注册的方式:

import { toolMatchesName, type Tool, type Tools } from './Tool.js'
import { AgentTool } from './tools/AgentTool/AgentTool.js'
import { SkillTool } from './tools/SkillTool/SkillTool.js'
import { BashTool } from './tools/BashTool/BashTool.js'
import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'

工具系统采用了插件化设计,每个工具都是一个独立的模块,通过统一的接口注册到系统中。这种设计使得扩展新工具变得非常简单。

设计启示

1. 上下文即记忆

Claude Code 的设计哲学之一是"上下文即记忆"。系统不是简单地存储历史对话,而是智能地收集和组织上下文信息。这种设计让 AI 能够"记住"项目结构、文件内容、git 状态等信息,从而在对话中做出更智能的决策。

2. 状态机的威力

query.ts 中的状态机设计展示了状态机在复杂交互系统中的威力。通过将对话流程分解为不同的状态,系统可以:

  • 清晰地管理对话流程
  • 易于扩展新的状态类型
  • 便于测试和调试
  • 支持复杂的状态转换逻辑

3. 流式处理的用户体验

流式响应不仅仅是技术实现,更是用户体验的核心。在"对话即编程"范式中,用户与 AI 的交互应该是连续的、实时的。流式处理让用户能够:

  • 立即看到 AI 的思考过程
  • 在 AI 仍在生成时就开始思考下一步
  • 在任何时候中断或调整对话方向

4. 类型安全的重要性

尽管 TypeScript 增加了代码复杂度,但它带来的类型安全是值得的。Tool.ts 中的类型定义确保了工具调用的安全性,context.ts 中的类型定义确保了上下文的一致性。在大型系统中,类型安全是防止错误的第一道防线。

思考题

  1. 上下文收集的权衡:context.ts 中使用了 memoize 来缓存上下文结果。这种设计有什么优势?在什么情况下会导致缓存失效?如何设计一个更智能的缓存失效策略?

  2. 状态机的扩展性:如果要在 query.ts 的状态机中添加一个新的状态类型(例如"等待用户确认"状态),应该如何设计?需要修改哪些部分?

  3. 流式处理的错误处理:在流式响应过程中,如果网络中断或 AI 服务出错,应该如何优雅地处理?如何保证用户已经看到的内容不会丢失?

  4. 工具调用的安全性:tools.ts 中注册了大量工具,包括可以执行命令的 BashTool。如何设计权限系统来防止 AI 执行危险操作?context.ts 中的权限类型是如何工作的?

  5. 范式转变的挑战:从传统的 REPL 到"对话即编程",最大的挑战是什么?对于习惯了传统编程方式的开发者,如何平滑地过渡到这种新范式?

小结

"对话即编程"代表了人机交互的一次范式转变。通过分析 QueryEngine.ts 和 query.ts 的源码,我们看到这一范式背后的技术实现:

  • QueryEngine 负责消息提交和上下文收集
  • query.ts 的状态机管理整个对话流程
  • 流式响应提供实时反馈
  • 工具系统让 AI 能够执行实际操作

这一范式的核心价值在于:它让编程变得更加直观、高效,降低了编程的门槛,同时保持了强大的功能。随着 AI 技术的不断发展,"对话即编程"可能会成为未来编程的主流方式。