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.tsxQueryEngine界面交互式 TUIInk程序化 API用途终端对话嵌入其他应用代表命令claude helloSDK 的submitMessage()1.2 源码位置文件行数职责src/replLauncher.tsx22启动 REPL 界面src/screens/REPL.tsx5005交互式 TUIsrc/QueryEngine.ts1295SDK 封装src/query.ts1729核心循环二、REPL.tsx 详解2.1 源码实现源码位置src/replLauncher.tsxexportasyncfunctionlaunchRepl(root:Root,appProps:AppWrapperProps,replProps:REPLProps,renderAndRun:(root:Root,element:React.ReactNode)Promisevoid,):Promisevoid{const{App}awaitimport(./components/App.js);const{REPL}awaitimport(./screens/REPL.js);awaitrenderAndRun(root,App{...appProps}REPL{...replProps}//App,);}2.2 五问分析问 1launchRepl() 的核心职责是什么launchRepl() 是REPL 的启动器它动态导入App和REPL组件使用 InkReact for CLI渲染 TUI启动交互式界面// launchRepl 被 main.tsx 的 action 调用.action(async(prompt,options){awaitlaunchRepl(root,appProps,replProps,renderAndRun);});问 2REPL.tsx 为什么这么大5005 行功能行数估计说明组件定义~1000React 组件事件处理~800键盘、鼠标事件状态管理~600AppStateStore渲染逻辑~1500消息、工具、UI其他~1100样式、工具函数REPL.tsx 是一个完整的交互式应用需要处理消息渲染工具调用显示用户输入命令历史自动补全问 3REPL 如何使用 QueryEngine// REPL.tsx 内部constqueryEnginenewQueryEngine(config);// 用户发送消息时forawait(constmessageofqueryEngine.submitMessage(prompt)){// 流式处理消息renderMessage(message);}REPL 是 QueryEngine 的调用者负责创建 QueryEngine 实例调用 submitMessage()渲染返回的消息问 4renderAndRun 是什么renderAndRun:(root:Root,element:React.ReactNode)Promisevoid这是 Ink 的渲染函数root终端根节点elementReact 元素JSX返回 Promise在 REPL 退出时 resolve问 5为什么使用 Ink 而不是原生 Node.js TUI方案优点缺点InkReact组件化、生态丰富包大小大原生 TUI包大小小、性能好开发效率低Claude Code 选择 Ink 是因为已有成熟的 React 组件生态状态管理AppStateStore易于维护快速迭代三、QueryEngine 详解3.1 源码实现源码位置src/QueryEngine.ts第 184 行exportclassQueryEngine{privateconfig:QueryEngineConfigprivatemutableMessages:Message[]privateabortController:AbortControllerprivatepermissionDenials:SDKPermissionDenial[]privatetotalUsage:NonNullableUsageprivatereadFileState:FileStateCacheconstructor(config:QueryEngineConfig){this.configconfigthis.mutableMessagesconfig.initialMessages??[]this.abortControllerconfig.abortController??createAbortController()this.permissionDenials[]this.readFileStateconfig.readFileCachethis.totalUsageEMPTY_USAGE}async*submitMessage(prompt:string|ContentBlockParam[],options?:{uuid?:string;isMeta?:boolean},):AsyncGeneratorSDKMessage,void,unknown{// ...}}3.2 五问分析问 1QueryEngine 的核心职责是什么职责说明管理会话状态mutableMessages、usage权限追踪permissionDenials调用核心循环委托给 query()流式输出yield SDKMessageasync*submitMessage(prompt,options):AsyncGeneratorSDKMessage{// 1. 包装 canUseTool记录权限拒绝constwrappedCanUseToolasync(tool,input,...){constresultawaitcanUseTool(tool,input,...);if(!result.allowed){this.permissionDenials.push({tool,input,...});// 记录}returnresult;};// 2. 调用 query() 核心循环forawait(constmessageofquery({canUseTool:wrappedCanUseTool,...})){// 3. session 持久化if(options?.persistSession){awaitrecordTranscript(this.mutableMessages);}// 4. yield SDK 格式消息yieldtoSDKMessage(message);}}问 2QueryEngine 和 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 封装constenginenewQueryEngine({initialMessages:[...]});// Turn 1forawait(constmsgofengine.submitMessage(hello)){// 处理消息}// Turn 2session 保持forawait(constmsgofengine.submitMessage(follow up)){// 处理消息}// QueryEngine 自动维护 mutableMessages 状态问 4SDKMessage 和 Message 的区别// Message内部格式interfaceMessage{type:user|assistant|tool_use|tool_result;content:string|ContentBlock[];}// SDKMessage外部格式interfaceSDKMessage{type:user|assistant|tool_use|tool_result|error;content:string|ContentBlock[];// SDK 特有字段usage?:Usage;stopReason?:string;}QueryEngine 负责格式转换forawait(constmessageofquery({...})){yieldtoSDKMessage(message);// 转换为 SDK 格式}问 5AsyncGenerator 的好处是什么async*submitMessage(prompt):AsyncGeneratorSDKMessage{// 流式 yield每收到一个消息块就 yieldforawait(constmessageofquery({...})){yieldtoSDKMessage(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 方式awaitrunPrintMode(prompt,options);}else{// 交互模式启动 REPLawaitlaunchRepl(root,appProps,replProps,renderAndRun);}});4.3 SDK 模式示例import{ClaudeCode}fromanthropic-ai/claude-code-sdk;constclaudecodenewClaudeCode();conststreamclaudecode.messages.stream({model:claude-opus-4-5,max_tokens:1024,messages:[{role:user,content:Hello!}],});forawait(consteventofstream){console.log(event);}五、设计模式5.1 生成器模式// QueryEngine.submitMessage 返回 AsyncGeneratorasync*submitMessage(prompt):AsyncGeneratorSDKMessage{forawait(constmessageofquery({...})){yieldtoSDKMessage(message);}}好处流式处理、内存效率。5.2 封装模式QueryEngine 封装 query() │ ├── 状态管理 ├── 权限追踪 ├── 格式转换 └── 对外接口5.3 策略模式// QueryEngine 可以注入不同的 canUseTool 实现constenginenewQueryEngine({canUseTool:customCanUseTool,// 可替换的策略});六、思考题思考题 1REPL 能使用 query() 直接吗问题REPL.tsx 为什么通过 QueryEngine 调用 query()而不是直接调用答案// 方案 1REPL 直接调用 query()forawait(constmessageofquery({messages:[...],tools:[...]})){// 问题 1每次调用都要传完整的 messages// 问题 2无法追踪权限拒绝// 问题 3session 持久化要自己实现}// 方案 2通过 QueryEngineconstenginenewQueryEngine({initialMessages:[...]});forawait(constmessageofengine.submitMessage(prompt)){// QueryEngine 自动维护状态}QueryEngine 提供了状态维护自动追加消息到 mutableMessages权限追踪记录 permissionDenials使用量追踪累加 totalUsagesession 持久化自动调用 recordTranscript思考题 2SDK 的 AsyncGenerator 有什么限制问题AsyncGenerator 作为 SDK 接口有什么限制如何处理答案限制// 1. 无法回退到之前的消息forawait(constmsgofengine.submitMessage(hello)){// msg 只能顺序处理}// 2. 无法中途修改已 yield 的消息// 3. 错误处理复杂解决方案// 1. 批量处理而非流式constmessages[];forawait(constmsgofengine.submitMessage(hello)){messages.push(msg);}// 2. 使用 complete() 获取最终状态constresultawaitengine.complete(hello);// 如果 SDK 提供// 3. 错误包装try{forawait(constmsgofengine.submitMessage(hello)){// 处理消息}}catch(error){// 处理错误}思考题 3CLI 模式如何处理中断问题用户按 CtrlC 时CLI 模式如何中断正在执行的 query答案// QueryEngine 创建 AbortControllerclassQueryEngine{privateabortControllernewAbortController();async*submitMessage(prompt):AsyncGeneratorSDKMessage{forawait(constmessageofquery({signal:this.abortController.signal,// 传递 abort signal})){yieldmessage;}}abort(){this.abortController.abort();// 中断}}// REPL 处理 CtrlCdocument.addEventListener(keydown,(e){if(e.ctrlKeye.keyc){engine.abort();// 中断 query}});AbortController 将中断信号传递到fetch() 请求工具执行任何支持 AbortSignal 的异步操作七、延伸阅读文件行数核心内容src/replLauncher.tsx22REPL 启动器src/screens/REPL.tsx5005交互式 TUIsrc/QueryEngine.ts1295SDK 封装src/query.ts1729核心循环