当用户在 Claude Code 里输入一个命令或 prompt 后,系统是如何分流、执行、循环调用模型与工具,直到把最终结果返回给用户。
总流程图
flowchart TD
A["CLI 启动 main.tsx"] --> B["launchRepl 启动 REPL"]
B --> C["用户在 PromptInput 输入并提交"]
C --> D{"输入走哪条路"}
D -->|立即执行的 slash 命令<br/>local-jsx 或 immediate| E["本地直接执行命令<br/>显示本地 UI 或通知"]
D -->|Remote 模式<br/>且不是本地 JSX| F["包装成消息发给远端 session"]
D -->|本地正常路径| G["handlePromptSubmit"]
G --> H{"当前已有 turn 在运行"}
H -->|是| I["入队 queue<br/>必要时中断当前 turn"]
H -->|否| J["executeUserInput"]
J --> K["processUserInput"]
K --> L{"输入类型"}
L -->|bash 模式| M["processBashCommand"]
L -->|slash 命令| N["processSlashCommand"]
L -->|普通 prompt| O["processTextPrompt"]
N --> N1{"slash 命令类型"}
N1 -->|local-jsx| E
N1 -->|prompt command 或 skill| P["转成 user meta attachment 消息<br/>并附加 allowedTools model effort"]
M --> Q["产出 messages 和 shouldQuery"]
O --> Q
P --> Q
Q --> R["执行 UserPromptSubmit hooks"]
R --> S{"还需要调模型吗"}
S -->|否| T["只更新本地消息和 UI,然后结束"]
S -->|是| U["onQuery 和 onQueryImpl"]
U --> V["构造 ToolUseContext<br/>SystemPrompt UserContext SystemContext"]
V --> W["进入 query 主循环"]
W --> X["历史裁剪压缩<br/>budget 检查<br/>memory 和 skill prefetch"]
X --> Y["调用模型,流式返回 assistant 内容"]
Y --> Z{"模型是否发出 tool_use"}
Z -->|否| AA["stop hooks token budget recovery"]
Z -->|是| AB["执行工具 runTools 或 StreamingToolExecutor"]
AB --> AC["权限检查 canUseTool"]
AC --> AD["串行或并发执行工具<br/>Bash File Web MCP Agent 等"]
AD --> AE["生成 tool_result attachment progress"]
AE --> AF["把 assistant 和 tool_result<br/>拼回 messages"]
AF --> W
Y -->|stream_event 或 assistant| UI["onQueryEvent 持续更新 transcript 和 UI"]
AE -->|progress 或 tool_result| UI
AA -->|final assistant| UI
AA --> AG["turn 完成"]
UI --> AG
AG --> AH["resetLoadingState 和 onTurnComplete"]
AH --> AI["最终结果展示给用户"]
核心理解
这不是“用户输入一次,模型返回一次”的单次请求模型,而是一个多轮闭环:
-
用户输入先被分流。 普通 prompt、slash 命令、bash、remote、本地即时命令,走不同路径。
-
需要进模型的输入,会先整理成消息。 这里会附加图片、文件、IDE 上下文、技能消息、hook 结果、权限信息。
-
query()是核心循环。 模型先流式输出;如果输出里包含tool_use,系统就执行工具,把tool_result再拼回上下文,继续下一轮模型调用。 -
UI 是流式更新的。 文本 token、工具进度、tool result、最终答复,都会持续进入 transcript,而不是等最后一次性显示。
分阶段说明
1. 启动与进入 REPL
- CLI 启动后进入
main.tsx - 初始化完成后调用
launchRepl(...) - REPL 负责渲染输入框、消息区、权限弹窗、工具进度和最终结果
2. 用户提交输入
用户输入提交后,REPL 的 onSubmit(...) 负责第一层分流:
- 如果是“立即执行”的本地 slash 命令,直接本地执行
- 如果当前是 remote 模式,则转发给远端 session
- 否则交给
handlePromptSubmit(...)
3. handlePromptSubmit:排队或立刻执行
这一层主要做三件事:
- 处理粘贴文本和图片引用
- 判断当前是否已有 turn 正在运行
- 决定是直接执行,还是先入队
如果当前已有 turn 在跑:
- 新输入会进入命令队列
- 某些可中断场景下,还会主动中断当前 turn
如果当前空闲:
- 会进入
executeUserInput(...)
4. processUserInput:按输入类型分流
这里是第二层真正的业务分发:
bash模式走processBashCommand- slash 命令走
processSlashCommand - 普通 prompt 走
processTextPrompt
在分流之前,还会做这些工作:
- 处理图片和 pasted image
- 抽取 attachment
- bridge / remote 安全判断
- 特殊关键字改写
5. slash 命令并不都一样
slash 命令有两类:
local-jsx:本地 UI 命令,不一定进模型prompt command / skill:会被转成一组消息,再送给模型
第二类通常会附带:
- meta message
- attachment
- skill 内容
- 命令允许的
allowedTools - 指定的
model或effort
6. hooks 先过一遍
在真正进入模型前,还会跑 UserPromptSubmit hooks:
- 可以附加上下文
- 可以阻止继续
- 可以生成警告消息
所以用户输入并不一定会直接进入模型。
7. onQuery:准备模型调用上下文
如果 shouldQuery = true,则进入 onQuery(...) / onQueryImpl(...)。
这里会准备:
ToolUseContext- 当前 turn 的工具集
systemPromptuserContextsystemContext- 权限模式
- MCP / IDE / agent 相关上下文
然后调用 query(...)。
8. query():主循环
query() 才是整个系统最核心的调度器。
每一轮大致会做:
- 整理消息上下文
- 做裁剪、压缩、budget 控制
- 流式调用模型
- 收集 assistant 输出
- 如果出现
tool_use,执行工具 - 把
tool_result拼回消息,再继续下一轮 - 直到不再需要工具调用
9. 工具执行闭环
当模型返回 tool_use 时:
- 系统会检查权限
- 根据工具的并发安全性,决定串行或并发执行
- 执行 Bash / File / Web / MCP / Agent 等工具
- 生成
tool_result、attachment、progress - 再把这些结果拼回上下文
- 回到下一轮
query()
所以“任务完成”通常不是单次模型输出完成,而是:
- 模型输出
- 工具执行
- 模型继续推理
- 再次工具执行
- 最终收敛
10. 结果如何返回给用户
返回不是一步完成,而是流式进行:
assistant文本块持续显示progress显示工具进度tool_result显示工具输出- 出错时显示 API 错误或恢复信息
- 最终结束时调用
resetLoadingState和onTurnComplete
用户看到的是一个持续更新的 transcript。
子图 1:slash command 细分流程
flowchart TD
A["用户输入 slash 命令"] --> B["REPL onSubmit"]
B --> C{"是不是 immediate local-jsx"}
C -->|是| D["本地直接执行"]
D --> E["更新本地 UI 或通知"]
E --> F["结束"]
C -->|否| G["handlePromptSubmit"]
G --> H["executeUserInput"]
H --> I["processUserInput"]
I --> J["processSlashCommand"]
J --> K{"命令类型"}
K -->|local-jsx| L["load command 并渲染本地 JSX"]
L --> M["command onDone"]
M --> F
K -->|prompt command| N["getPromptForCommand"]
N --> O["生成 user message"]
O --> P["生成 meta message"]
P --> Q["生成 attachment message"]
Q --> R["附加 allowedTools model effort"]
R --> S["shouldQuery true"]
S --> T["onQuery"]
K -->|unknown 或非法| U["生成错误或帮助消息"]
U --> V["shouldQuery false"]
V --> F
这个子图的重点
- 不是所有 slash 命令都会进入模型
local-jsx主要是本地交互命令- prompt command / skill 会被展开成一组消息后再送入
query() - skill 还可能附带额外工具权限和模型配置
子图 2:tool_use 闭环流程
flowchart TD
A["query 主循环开始"] --> B["准备 messagesForQuery"]
B --> C["调用模型 deps.callModel"]
C --> D["流式收到 assistant 输出"]
D --> E{"是否出现 tool_use"}
E -->|否| F["进入 stop hooks recovery token budget"]
F --> G["返回最终 assistant"]
G --> H["turn 完成"]
E -->|是| I["收集 toolUseBlocks"]
I --> J{"是否启用 StreamingToolExecutor"}
J -->|是| K["streaming executor 执行工具"]
J -->|否| L["runTools 执行工具"]
K --> M["权限检查 canUseTool"]
L --> M
M --> N{"工具是否可并发"}
N -->|可并发| O["并发执行"]
N -->|不可并发| P["串行执行"]
O --> Q["产生 progress message"]
O --> R["产生 tool_result"]
P --> Q
P --> R
Q --> S["更新 UI"]
R --> T["normalizeMessagesForAPI"]
T --> U["把 tool_result attachment progress 拼回上下文"]
U --> V["必要时补 memory attachment 或 skill discovery attachment"]
V --> W["刷新 tools 与 ToolUseContext"]
W --> X["递归进入下一轮 query"]
X --> C
这个子图的重点
tool_use不是终点,而是下一轮推理的中间态- 工具执行后生成的结果,会重新进入上下文
- 所以完整任务常常是“模型 -> 工具 -> 模型 -> 工具 -> 模型”多轮收敛
子图 3:权限与中断流程
flowchart TD
A["模型请求调用工具"] --> B["canUseTool 权限检查"]
B --> C{"当前规则是否允许"}
C -->|允许| D["执行工具"]
C -->|不允许| E["弹出权限请求"]
E --> F{"用户如何选择"}
F -->|允许一次| D
F -->|允许并记住规则| G["写入 permission rule"]
G --> D
F -->|拒绝| H["返回拒绝结果给模型"]
D --> I{"执行期间用户是否再次提交"}
I -->|否| J["正常完成并返回 tool_result"]
I -->|是| K{"当前工具可中断吗"}
K -->|可中断| L["abort 当前 turn"]
K -->|不可中断| M["新输入进入 queue"]
L --> N["生成 interruption 或 synthetic result"]
N --> O["后续 turn 继续处理新输入"]
M --> O
H --> O
J --> O
这个子图的重点
- 工具调用不是直接执行,前面还有权限门
- 用户在执行中再次提交输入时,系统不一定立即丢弃当前任务
- 有些场景是中断当前 turn,有些场景是把新输入先排队
关键源码入口
- CLI 进入 REPL:
src/main.tsx
- 提交入口:
src/screens/REPL.tsx
- 提交后的统一处理:
src/utils/handlePromptSubmit.ts
- 输入类型分发:
src/utils/processUserInput/processUserInput.ts
- slash 命令处理:
src/utils/processUserInput/processSlashCommand.tsx
- 普通 prompt 处理:
src/utils/processUserInput/processTextPrompt.ts
- bash 命令处理:
src/utils/processUserInput/processBashCommand.tsx
- 模型与工具主循环:
src/query.ts
- 工具编排:
src/services/tools/toolOrchestration.ts
重点源码定位
- REPL 提交入口:
src/screens/REPL.tsx:3142
- REPL 真正进入
query():src/screens/REPL.tsx:2661src/screens/REPL.tsx:2793
- 提交后的统一入口:
src/utils/handlePromptSubmit.ts:120
- 空闲时直接执行:
src/utils/handlePromptSubmit.ts:368
- 输入类型分发:
src/utils/processUserInput/processUserInput.ts:281
- slash / bash / prompt 分支:
src/utils/processUserInput/processUserInput.ts:516src/utils/processUserInput/processUserInput.ts:531src/utils/processUserInput/processUserInput.ts:576
query()主循环开始:src/query.ts:337
- 模型流式调用:
src/query.ts:659
- 工具执行:
src/query.ts:1380
- 工具执行后的递归下一轮:
src/query.ts:1714
一句话总结
Claude Code 的执行模型本质上是:
“用户输入 -> 分流成 prompt / command / bash / remote -> 组装上下文 -> 模型流式输出 -> 发现 tool_use -> 执行工具 -> 回填 tool_result -> 再次调用模型 -> 收敛后把最终结果流式展示给用户。”
名称说明
-
prompt- 指普通自然语言输入,也就是用户直接输入一句话,让模型理解并继续处理。
-
command- 指命令式输入。在 Claude Code 里,通常是以特定形式触发某种预定义能力。
-
slash command- 指以
/开头的命令,比如/review、/compact、/config。 - 它和普通 prompt 不一样,不是直接把原文送给模型,而是先走命令分发逻辑。
- 指以
-
bash- 指 shell 命令模式,也就是把输入当成终端命令来处理,而不是当成普通自然语言。
-
JSX- JSX 是一种“用类似 HTML 的语法描述界面”的写法,常见于 React。
- 在这个项目里,
local-jsx基本可以理解成:命令执行后,不一定去调模型,而是在本地直接渲染一个交互界面。
-
local-jsx- 指“本地 UI 命令”。
- 这类命令执行后,通常会在当前终端 UI 里打开一个选择器、面板、对话框或配置界面。
- 例如某些设置类、选择类命令,不需要模型推理,也不需要进入
query()主循环。
-
prompt command- 指“命令本身会先展开成一段 prompt 或一组消息,然后再交给模型继续处理”的命令。
- 它不是纯本地 UI,也不是单纯把
/xxx原文发给模型,而是先转换成系统定义好的上下文。
-
skill- 可以理解成“结构化的能力包”或“专门的工作说明”。
- 某些 skill 会给模型补充额外规则、步骤、工具权限,告诉它应该按什么流程做事。
-
attachment- 指附加到当前上下文里的额外信息。
- 例如文件内容、图片、IDE 上下文、hook 附加说明、记忆内容,都可能作为 attachment 进入模型上下文。
-
meta message- 指“模型可见,但用户通常不直接当成普通聊天内容来看的消息”。
- 它常用于塞额外指令、恢复提示、技能展开内容、内部控制信息。
-
ToolUseContext- 可以理解成“一次工具调用和模型调用共享的运行时上下文”。
- 它里面通常包含当前消息、工具列表、权限状态、abort controller、agent 信息等。
-
tool_use- 指模型在回答过程中,不是直接给最终答案,而是先提出“我要调用某个工具”。
- 例如读取文件、执行 shell、搜索内容、调用子 agent。
-
tool_result- 指工具执行完成后返回的结果。
- 这个结果不会只显示给用户,还会再次回填给模型,供下一轮推理继续使用。
-
streaming- 指流式返回。
- 也就是模型不是最后一次性返回完整结果,而是边生成边显示文本、进度和工具事件。
-
turn- 指一次完整的交互轮次。
- 但在 Claude Code 里,一个用户输入可能会触发多个内部 turn,因为模型可能会多次调用工具再继续推理。
-
query()- 这里的
query()不是普通数据库查询,而是这个项目里最核心的“模型与工具调度主循环”。 - 它负责发起模型请求、接收流式输出、执行工具、回填结果、进入下一轮,直到收敛。
- 这里的
术语对照表
这个表的目标不是给出最严格的源码定义,而是帮你在读流程图和代码时,快速判断“这个词大致站在哪个层面说话”。
| 术语 | 用户视角 | UI 视角 | 模型视角 | 工具视角 | 简短理解 |
|---|---|---|---|---|---|
prompt | 是 | 是 | 是 | 否 | 用户输入的一段自然语言请求 |
slash command | 是 | 是 | 不一定 | 不一定 | 以 / 开头的命令入口 |
bash | 是 | 是 | 有时 | 是 | 以 shell 命令方式处理输入 |
local-jsx | 否 | 是 | 否 | 否 | 在本地直接渲染交互界面 |
prompt command | 否 | 是 | 是 | 间接相关 | 先展开成消息,再进入模型 |
skill | 部分可见 | 部分可见 | 是 | 间接相关 | 给模型补充工作方法和约束 |
attachment | 部分可见 | 是 | 是 | 否 | 附加到上下文里的额外材料 |
meta message | 通常不可见 | 内部可见 | 是 | 否 | 给模型看的隐藏控制消息 |
assistant message | 是 | 是 | 是 | 否 | 模型生成的回答内容 |
tool_use | 通常不可直接输入 | 会显示过程 | 是 | 是 | 模型请求调用某个工具 |
tool_result | 部分可见 | 是 | 是 | 是 | 工具执行后的结果回填 |
progress message | 是 | 是 | 通常否 | 是 | 工具执行过程中的进度更新 |
ToolUseContext | 否 | 否 | 间接相关 | 是 | 当前 turn 的运行时上下文 |
query() | 否 | 否 | 是 | 是 | 模型和工具的主调度循环 |
turn | 是 | 是 | 是 | 是 | 一轮处理过程 |
streaming | 是 | 是 | 是 | 间接相关 | 边生成边返回,而不是一次性返回 |
按视角理解
1. 用户视角
从用户角度,最重要的词通常是:
promptslash commandbashturnstreaming
因为这些词决定的是:我输入了什么、系统现在在做什么、结果怎么显示出来。
2. UI 视角
从终端界面角度,最常见的是:
local-jsxattachmentassistant messageprogress messagestreaming
因为 UI 关心的是:当前该渲染哪种消息、是否要显示一个本地交互面板、是不是还在流式更新。
3. 模型视角
从模型上下文角度,最关键的是:
promptprompt commandskillattachmentmeta messagetool_usetool_result
因为模型真正“看到”的,不只是用户原始输入,还包括系统拼进去的各种隐藏消息和工具结果。
4. 工具视角
从工具调度角度,最关键的是:
tool_usetool_resultprogress messageToolUseContextquery()
因为工具不是单独跑的,它们是在 query() 驱动下被模型请求,再通过 ToolUseContext 和整轮状态串起来。
一个最容易混淆的区别
slash command是“输入入口”prompt command是“slash command 里的一种执行方式”local-jsx是“slash command 里另一种执行方式”
也就是说:
- 用户输入
/review,这是一个slash command - 它最后可能被展开成 prompt 去问模型,这时它属于
prompt command - 用户输入
/config,也可能根本不进模型,而是在本地打开一个交互界面,这时它属于local-jsx