Claude Code 源码剖析 模块一 · 第四节:REPL 与 SDK 模式

模块一 · 第四节: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 的启动器,它:

  1. 动态导入 AppREPL 组件
  2. 使用 Ink(React for CLI)渲染 TUI
  3. 启动交互式界面
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 的调用者,负责:

  1. 创建 QueryEngine 实例
  2. 调用 submitMessage()
  3. 渲染返回的消息

问 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 提供了:

  1. 状态维护:自动追加消息到 mutableMessages
  2. 权限追踪:记录 permissionDenials
  3. 使用量追踪:累加 totalUsage
  4. 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 将中断信号传递到:

  1. fetch() 请求
  2. 工具执行
  3. 任何支持 AbortSignal 的异步操作

七、延伸阅读

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

相关推荐
Alvin千里无风5 小时前
在 Ubuntu 上从源码安装 Nanobot:轻量级 AI 助手完整指南
linux·人工智能·ubuntu
环黄金线HHJX.5 小时前
龙虾钳足启发的AI集群语言交互新范式
开发语言·人工智能·算法·编辑器·交互
Omics Pro5 小时前
虚拟细胞:开启HIV/AIDS治疗新纪元的关键?
大数据·数据库·人工智能·深度学习·算法·机器学习·计算机视觉
悦来客栈的老板6 小时前
AI逆向|猿人学逆向反混淆练习平台第七题加密分析
人工智能
KOYUELEC光与电子努力加油6 小时前
JAE日本航空端子推出支持自走式机器人的自主充电功能浮动式连接器“DW15系列“方案与应用
服务器·人工智能·机器人·无人机
萤火阳光6 小时前
13|自定义 Skill 创作:打造专属自动化利器
人工智能
我哪会这个啊6 小时前
SpringAlibaba Ai基础入门
人工智能
掘根6 小时前
【微服务即时通讯项目】系统联调
微服务·云原生·架构
tianbaolc6 小时前
Claude Code 源码剖析 模块一 · 第六节:autoDream 自动记忆整合
人工智能·ai·架构·claude code
蓝色的杯子7 小时前
从 LLM 到 Agent Skill,龙虾的技术基础 · ② Token
人工智能