模块一 · 第四节:REPL 与 SDK 模式
核心问题
REPL 和 SDK 模式的区别是什么?launchRepl() 和 QueryEngine 的关系是什么?CLI 模式和 SDK 模式如何选择?
◇ 本节位置
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 入口层(entrypoints/) │
│ │
│ cli.tsx ──> main.tsx ──> REPL.tsx (交互模式) ← CLI 模式 │
│ └──> QueryEngine.ts (SDK/headless) ← SDK 模式 │
│ │
│ ← 本节内容 │
└──────────────────────────────┬──────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 查询引擎层(query.ts / QueryEngine.ts) │
└─────────────────────────────────────────────────────────────────────┘
一、两种模式概览
1.1 CLI 模式 vs SDK 模式
| 特性 | CLI 模式 | SDK 模式 |
|---|---|---|
| 入口 | REPL.tsx |
QueryEngine |
| 界面 | 交互式 TUI(Ink) | 程序化 API |
| 用途 | 终端对话 | 嵌入其他应用 |
| 代表命令 | claude "hello" |
SDK 的 submitMessage() |
1.2 源码位置
| 文件 | 行数 | 职责 |
|---|---|---|
src/replLauncher.tsx |
22 | 启动 REPL 界面 |
src/screens/REPL.tsx |
5005 | 交互式 TUI |
src/QueryEngine.ts |
1295 | SDK 封装 |
src/query.ts |
1729 | 核心循环 |
二、REPL.tsx 详解
2.1 源码实现
源码位置 :src/replLauncher.tsx
typescript
export async function launchRepl(
root: Root,
appProps: AppWrapperProps,
replProps: REPLProps,
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>,
): Promise<void> {
const { App } = await import('./components/App.js');
const { REPL } = await import('./screens/REPL.js');
await renderAndRun(
root,
<App {...appProps}>
<REPL {...replProps} />
</App>,
);
}
2.2 五问分析
问 1:launchRepl() 的核心职责是什么?
launchRepl() 是 REPL 的启动器,它:
- 动态导入
App和REPL组件 - 使用 Ink(React for CLI)渲染 TUI
- 启动交互式界面
typescript
// launchRepl 被 main.tsx 的 action 调用
.action(async (prompt, options) => {
await launchRepl(root, appProps, replProps, renderAndRun);
});
问 2:REPL.tsx 为什么这么大(5005 行)?
| 功能 | 行数估计 | 说明 |
|---|---|---|
| 组件定义 | ~1000 | React 组件 |
| 事件处理 | ~800 | 键盘、鼠标事件 |
| 状态管理 | ~600 | AppStateStore |
| 渲染逻辑 | ~1500 | 消息、工具、UI |
| 其他 | ~1100 | 样式、工具函数 |
REPL.tsx 是一个完整的交互式应用,需要处理:
- 消息渲染
- 工具调用显示
- 用户输入
- 命令历史
- 自动补全
问 3:REPL 如何使用 QueryEngine?
typescript
// REPL.tsx 内部
const queryEngine = new QueryEngine(config);
// 用户发送消息时
for await (const message of queryEngine.submitMessage(prompt)) {
// 流式处理消息
renderMessage(message);
}
REPL 是 QueryEngine 的调用者,负责:
- 创建 QueryEngine 实例
- 调用 submitMessage()
- 渲染返回的消息
问 4:renderAndRun 是什么?
typescript
renderAndRun: (root: Root, element: React.ReactNode) => Promise<void>
这是 Ink 的渲染函数:
root:终端根节点element:React 元素(JSX)- 返回 Promise,在 REPL 退出时 resolve
问 5:为什么使用 Ink 而不是原生 Node.js TUI?
| 方案 | 优点 | 缺点 |
|---|---|---|
| Ink(React) | 组件化、生态丰富 | 包大小大 |
| 原生 TUI | 包大小小、性能好 | 开发效率低 |
Claude Code 选择 Ink 是因为:
- 已有成熟的 React 组件生态
- 状态管理(AppStateStore)易于维护
- 快速迭代
三、QueryEngine 详解
3.1 源码实现
源码位置 :src/QueryEngine.ts 第 184 行
typescript
export class QueryEngine {
private config: QueryEngineConfig
private mutableMessages: Message[]
private abortController: AbortController
private permissionDenials: SDKPermissionDenial[]
private totalUsage: NonNullableUsage
private readFileState: FileStateCache
constructor(config: QueryEngineConfig) {
this.config = config
this.mutableMessages = config.initialMessages ?? []
this.abortController = config.abortController ?? createAbortController()
this.permissionDenials = []
this.readFileState = config.readFileCache
this.totalUsage = EMPTY_USAGE
}
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {
// ...
}
}
3.2 五问分析
问 1:QueryEngine 的核心职责是什么?
| 职责 | 说明 |
|---|---|
| 管理会话状态 | mutableMessages、usage |
| 权限追踪 | permissionDenials |
| 调用核心循环 | 委托给 query() |
| 流式输出 | yield SDKMessage |
typescript
async *submitMessage(prompt, options): AsyncGenerator<SDKMessage> {
// 1. 包装 canUseTool,记录权限拒绝
const wrappedCanUseTool = async (tool, input, ...) => {
const result = await canUseTool(tool, input, ...);
if (!result.allowed) {
this.permissionDenials.push({ tool, input, ... }); // 记录
}
return result;
};
// 2. 调用 query() 核心循环
for await (const message of query({ canUseTool: wrappedCanUseTool, ... })) {
// 3. session 持久化
if (options?.persistSession) {
await recordTranscript(this.mutableMessages);
}
// 4. yield SDK 格式消息
yield toSDKMessage(message);
}
}
问 2:QueryEngine 和 query() 的关系?
┌─────────────────────────────────────────────────────────────────────┐
│ QueryEngine (SDK 封装) │
│ │
│ class QueryEngine { │
│ mutableMessages // 会话状态 │
│ permissionDenials // 权限追踪 │
│ totalUsage // 使用量 │
│ │
│ submitMessage() { │
│ // 1. 包装 canUseTool │
│ // 2. 调用 query() │
│ // 3. session 持久化 │
│ // 4. yield SDK 消息 │
│ } │
│ } │
└──────────────────────────────┬──────────────────────────────────────┘
│ 调用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ query() (核心循环) │
│ │
│ while (true) { │
│ // 1. 调用 Claude API │
│ // 2. 执行工具 │
│ // 3. 状态转移 │
│ } │
└─────────────────────────────────────────────────────────────────────┘
区别:
| 方面 | QueryEngine | query() |
|---|---|---|
| 职责 | SDK 封装 | 核心循环 |
| 状态 | 会话状态 | 对话状态 |
| 模式 | headless/SDK | 内部使用 |
| 接口 | AsyncGenerator | AsyncGenerator |
问 3:为什么需要两层分离?
typescript
// 问题:如果只有 query()
query({ messages: [...], tools: [...] })
// SDK 用户的问题:
// 1. 如何持久化 session?
// 2. 如何追踪权限拒绝?
// 3. 如何管理多个 Turn?
// 解决方案:QueryEngine 封装
const engine = new QueryEngine({ initialMessages: [...] });
// Turn 1
for await (const msg of engine.submitMessage("hello")) {
// 处理消息
}
// Turn 2(session 保持)
for await (const msg of engine.submitMessage("follow up")) {
// 处理消息
}
// QueryEngine 自动维护 mutableMessages 状态
问 4:SDKMessage 和 Message 的区别?
typescript
// Message(内部格式)
interface Message {
type: 'user' | 'assistant' | 'tool_use' | 'tool_result';
content: string | ContentBlock[];
}
// SDKMessage(外部格式)
interface SDKMessage {
type: 'user' | 'assistant' | 'tool_use' | 'tool_result' | 'error';
content: string | ContentBlock[];
// SDK 特有字段
usage?: Usage;
stopReason?: string;
}
QueryEngine 负责格式转换:
typescript
for await (const message of query({ ... })) {
yield toSDKMessage(message); // 转换为 SDK 格式
}
问 5:AsyncGenerator 的好处是什么?
typescript
async *submitMessage(prompt): AsyncGenerator<SDKMessage> {
// 流式 yield,每收到一个消息块就 yield
for await (const message of query({ ... })) {
yield toSDKMessage(message); // 立即返回,不等待全部完成
}
}
好处:
- 流式处理:消息边产生边返回
- 内存效率:不需要缓存全部消息
- 低延迟:用户尽快看到响应
四、CLI 模式 vs SDK 模式
4.1 模式选择
| 场景 | 推荐模式 | 原因 |
|---|---|---|
| 终端交互 | CLI 模式 | 完整的 TUI 体验 |
| 自动化脚本 | SDK 模式 | 程序化控制 |
| 集成到 IDE | SDK 模式 | 嵌入插件 |
| 一次性任务 | CLI 模式(-p) | 非交互输出 |
4.2 CLI 模式的 -p/--print 选项
源码位置 :src/main.tsx
typescript
// --print 模式下不启动 REPL
.option('-p, --print', 'non-interactive mode, output response only')
.action(async (prompt, options) => {
if (options.print) {
// 非交互模式,使用 SDK 方式
await runPrintMode(prompt, options);
} else {
// 交互模式,启动 REPL
await launchRepl(root, appProps, replProps, renderAndRun);
}
});
4.3 SDK 模式示例
typescript
import { ClaudeCode } from '@anthropic-ai/claude-code-sdk';
const claudecode = new ClaudeCode();
const stream = claudecode.messages.stream({
model: 'claude-opus-4-5',
max_tokens: 1024,
messages: [{ role: 'user', content: 'Hello!' }],
});
for await (const event of stream) {
console.log(event);
}
五、设计模式
5.1 生成器模式
typescript
// QueryEngine.submitMessage 返回 AsyncGenerator
async *submitMessage(prompt): AsyncGenerator<SDKMessage> {
for await (const message of query({ ... })) {
yield toSDKMessage(message);
}
}
好处:流式处理、内存效率。
5.2 封装模式
QueryEngine 封装 query()
│
├── 状态管理
├── 权限追踪
├── 格式转换
└── 对外接口
5.3 策略模式
typescript
// QueryEngine 可以注入不同的 canUseTool 实现
const engine = new QueryEngine({
canUseTool: customCanUseTool, // 可替换的策略
});
六、思考题
思考题 1:REPL 能使用 query() 直接吗?
问题:REPL.tsx 为什么通过 QueryEngine 调用 query(),而不是直接调用?
答案:
typescript
// 方案 1:REPL 直接调用 query()
for await (const message of query({ messages: [...], tools: [...] })) {
// 问题 1:每次调用都要传完整的 messages
// 问题 2:无法追踪权限拒绝
// 问题 3:session 持久化要自己实现
}
// 方案 2:通过 QueryEngine
const engine = new QueryEngine({ initialMessages: [...] });
for await (const message of engine.submitMessage(prompt)) {
// QueryEngine 自动维护状态
}
QueryEngine 提供了:
- 状态维护:自动追加消息到 mutableMessages
- 权限追踪:记录 permissionDenials
- 使用量追踪:累加 totalUsage
- session 持久化:自动调用 recordTranscript
思考题 2:SDK 的 AsyncGenerator 有什么限制?
问题:AsyncGenerator 作为 SDK 接口有什么限制?如何处理?
答案:
限制:
typescript
// 1. 无法回退到之前的消息
for await (const msg of engine.submitMessage("hello")) {
// msg 只能顺序处理
}
// 2. 无法中途修改已 yield 的消息
// 3. 错误处理复杂
解决方案:
typescript
// 1. 批量处理而非流式
const messages = [];
for await (const msg of engine.submitMessage("hello")) {
messages.push(msg);
}
// 2. 使用 complete() 获取最终状态
const result = await engine.complete("hello"); // 如果 SDK 提供
// 3. 错误包装
try {
for await (const msg of engine.submitMessage("hello")) {
// 处理消息
}
} catch (error) {
// 处理错误
}
思考题 3:CLI 模式如何处理中断?
问题:用户按 Ctrl+C 时,CLI 模式如何中断正在执行的 query?
答案:
typescript
// QueryEngine 创建 AbortController
class QueryEngine {
private abortController = new AbortController();
async *submitMessage(prompt): AsyncGenerator<SDKMessage> {
for await (const message of query({
signal: this.abortController.signal, // 传递 abort signal
})) {
yield message;
}
}
abort() {
this.abortController.abort(); // 中断
}
}
// REPL 处理 Ctrl+C
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'c') {
engine.abort(); // 中断 query
}
});
AbortController 将中断信号传递到:
- fetch() 请求
- 工具执行
- 任何支持 AbortSignal 的异步操作
七、延伸阅读
| 文件 | 行数 | 核心内容 |
|---|---|---|
src/replLauncher.tsx |
22 | REPL 启动器 |
src/screens/REPL.tsx |
5005 | 交互式 TUI |
src/QueryEngine.ts |
1295 | SDK 封装 |
src/query.ts |
1729 | 核心循环 |