第四节:REPL 与 SDK 模式
交互式编程环境

作者:小学子 📚 | 日期:2026年4月2日 | 第一阶段 · 模块一


# 第一阶段 · 模块一 · 第四节: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`1295SDK 封装
`src/query.ts`1729核心循环


二、REPL.tsx 详解

2.1 源码实现

源码位置:`src/replLauncher.tsx`


        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 的启动器,它:

1. 动态导入 `App` 和 `REPL` 组件

2. 使用 Ink(React for CLI)渲染 TUI

3. 启动交互式界面


        // launchRepl 被 main.tsx 的 action 调用
        .action(async (prompt, options) => {
          await launchRepl(root, appProps, replProps, renderAndRun);
        });
        

问 2:REPL.tsx 为什么这么大(5005 行)?

功能行数估计说明
组件定义~1000React 组件
事件处理~800键盘、鼠标事件
状态管理~600AppStateStore
渲染逻辑~1500消息、工具、UI
其他~1100样式、工具函数

REPL.tsx 是一个完整的交互式应用,需要处理:

- 消息渲染

- 工具调用显示

- 用户输入

- 命令历史

- 自动补全

问 3:REPL 如何使用 QueryEngine?


        // REPL.tsx 内部
        const queryEngine = new QueryEngine(config);
        
        // 用户发送消息时
        for await (const message of queryEngine.submitMessage(prompt)) {
          // 流式处理消息
          renderMessage(message);
        }
        

REPL 是 QueryEngine 的调用者,负责:

1. 创建 QueryEngine 实例

2. 调用 submitMessage()

3. 渲染返回的消息

问 4:renderAndRun 是什么?


        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 行


        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


        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. 状态转移                                                    │
        │  }                                                                  │
        └─────────────────────────────────────────────────────────────────────┘
        

区别

方面QueryEnginequery()
职责SDK 封装核心循环
状态会话状态对话状态
模式headless/SDK内部使用
接口AsyncGeneratorAsyncGenerator

问 3:为什么需要两层分离?


        // 问题:如果只有 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 的区别?


        // 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 负责格式转换


        for await (const message of query({ ... })) {
          yield toSDKMessage(message);  // 转换为 SDK 格式
        }
        

问 5:AsyncGenerator 的好处是什么?


        async *submitMessage(prompt): AsyncGenerator<SDKMessage> {
          // 流式 yield,每收到一个消息块就 yield
          for await (const message of query({ ... })) {
            yield toSDKMessage(message);  // 立即返回,不等待全部完成
          }
        }
        

好处:

- 流式处理:消息边产生边返回

- 内存效率:不需要缓存全部消息

- 低延迟:用户尽快看到响应


四、CLI 模式 vs SDK 模式

4.1 模式选择

场景推荐模式原因
终端交互CLI 模式完整的 TUI 体验
自动化脚本SDK 模式程序化控制
集成到 IDESDK 模式嵌入插件
一次性任务CLI 模式(-p)非交互输出

4.2 CLI 模式的 -p/--print 选项

源码位置:`src/main.tsx`


        // --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 模式示例


        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 生成器模式


        // QueryEngine.submitMessage 返回 AsyncGenerator
        async *submitMessage(prompt): AsyncGenerator<SDKMessage> {
          for await (const message of query({ ... })) {
            yield toSDKMessage(message);
          }
        }
        

好处:流式处理、内存效率。

5.2 封装模式


        QueryEngine 封装 query()
            │
            ├── 状态管理
            ├── 权限追踪
            ├── 格式转换
            └── 对外接口
        

5.3 策略模式


        // QueryEngine 可以注入不同的 canUseTool 实现
        const engine = new QueryEngine({
          canUseTool: customCanUseTool,  // 可替换的策略
        });
        


六、思考题

思考题 1:REPL 能使用 query() 直接吗?

问题:REPL.tsx 为什么通过 QueryEngine 调用 query(),而不是直接调用?

答案


        // 方案 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 提供了:

1. 状态维护:自动追加消息到 mutableMessages

2. 权限追踪:记录 permissionDenials

3. 使用量追踪:累加 totalUsage

4. session 持久化:自动调用 recordTranscript


思考题 2:SDK 的 AsyncGenerator 有什么限制?

问题:AsyncGenerator 作为 SDK 接口有什么限制?如何处理?

答案

限制


        // 1. 无法回退到之前的消息
        for await (const msg of engine.submitMessage("hello")) {
          // msg 只能顺序处理
        }
        
        // 2. 无法中途修改已 yield 的消息
        // 3. 错误处理复杂
        

解决方案


        // 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?

答案


        // 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 将中断信号传递到:

1. fetch() 请求

2. 工具执行

3. 任何支持 AbortSignal 的异步操作


七、延伸阅读

文件行数核心内容
`src/replLauncher.tsx`22REPL 启动器
`src/screens/REPL.tsx`5005交互式 TUI
`src/QueryEngine.ts`1295SDK 封装
`src/query.ts`1729核心循环


八、下节预告

下一节我们将深入 工具系统

- 工具是如何注册的?

- 内置工具有哪些?

- 如何自定义工具?


*- 第一轮:□ 事实准确性*

*- 第二轮:□ 深度与洞见*

*- 第三轮:□ 可读性与价值*