第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 会理解上下文、规划步骤、执行工具调用,并持续对话直到任务完成。这种范式的核心优势在于:
- 意图驱动:用户描述"做什么",而非"怎么做"
- 上下文感知:AI 自动理解项目结构、文件内容、git状态等
- 持续对话:通过多轮交互逐步完善解决方案
- 工具调用: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() 方法的关键步骤包括:
- 消息预处理:将用户输入转换为标准消息格式
- 上下文收集:调用
getSystemContext()和getUserContext() - 查询执行:调用
query()函数进入状态机循环 - 结果处理:将 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') {
// 处理消息结束
}
}
}
这种设计的优势在于:
- 实时反馈:用户可以立即看到 AI 的输出
- 内存效率:不需要缓存整个响应
- 可中断性:用户可以在任何时刻停止响应
工具调用的集成
状态机的另一个关键功能是处理工具调用。当 AI 决定调用工具时,状态机会:
- 解析工具调用请求
- 验证权限
- 执行工具
- 将结果反馈给 AI
- 继续对话
从 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 中的类型定义确保了上下文的一致性。在大型系统中,类型安全是防止错误的第一道防线。
思考题
-
上下文收集的权衡:context.ts 中使用了
memoize来缓存上下文结果。这种设计有什么优势?在什么情况下会导致缓存失效?如何设计一个更智能的缓存失效策略? -
状态机的扩展性:如果要在 query.ts 的状态机中添加一个新的状态类型(例如"等待用户确认"状态),应该如何设计?需要修改哪些部分?
-
流式处理的错误处理:在流式响应过程中,如果网络中断或 AI 服务出错,应该如何优雅地处理?如何保证用户已经看到的内容不会丢失?
-
工具调用的安全性:tools.ts 中注册了大量工具,包括可以执行命令的 BashTool。如何设计权限系统来防止 AI 执行危险操作?context.ts 中的权限类型是如何工作的?
-
范式转变的挑战:从传统的 REPL 到"对话即编程",最大的挑战是什么?对于习惯了传统编程方式的开发者,如何平滑地过渡到这种新范式?
小结
"对话即编程"代表了人机交互的一次范式转变。通过分析 QueryEngine.ts 和 query.ts 的源码,我们看到这一范式背后的技术实现:
- QueryEngine 负责消息提交和上下文收集
- query.ts 的状态机管理整个对话流程
- 流式响应提供实时反馈
- 工具系统让 AI 能够执行实际操作
这一范式的核心价值在于:它让编程变得更加直观、高效,降低了编程的门槛,同时保持了强大的功能。随着 AI 技术的不断发展,"对话即编程"可能会成为未来编程的主流方式。