Claude Code: 从用户输入到返回结果的整体流程

11 阅读10分钟

当用户在 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["最终结果展示给用户"]

核心理解

这不是“用户输入一次,模型返回一次”的单次请求模型,而是一个多轮闭环:

  1. 用户输入先被分流。 普通 prompt、slash 命令、bash、remote、本地即时命令,走不同路径。

  2. 需要进模型的输入,会先整理成消息。 这里会附加图片、文件、IDE 上下文、技能消息、hook 结果、权限信息。

  3. query() 是核心循环。 模型先流式输出;如果输出里包含 tool_use,系统就执行工具,把 tool_result 再拼回上下文,继续下一轮模型调用。

  4. 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
  • 指定的 modeleffort

6. hooks 先过一遍

在真正进入模型前,还会跑 UserPromptSubmit hooks:

  • 可以附加上下文
  • 可以阻止继续
  • 可以生成警告消息

所以用户输入并不一定会直接进入模型。

7. onQuery:准备模型调用上下文

如果 shouldQuery = true,则进入 onQuery(...) / onQueryImpl(...)

这里会准备:

  • ToolUseContext
  • 当前 turn 的工具集
  • systemPrompt
  • userContext
  • systemContext
  • 权限模式
  • MCP / IDE / agent 相关上下文

然后调用 query(...)

8. query():主循环

query() 才是整个系统最核心的调度器。

每一轮大致会做:

  1. 整理消息上下文
  2. 做裁剪、压缩、budget 控制
  3. 流式调用模型
  4. 收集 assistant 输出
  5. 如果出现 tool_use,执行工具
  6. tool_result 拼回消息,再继续下一轮
  7. 直到不再需要工具调用

9. 工具执行闭环

当模型返回 tool_use 时:

  • 系统会检查权限
  • 根据工具的并发安全性,决定串行或并发执行
  • 执行 Bash / File / Web / MCP / Agent 等工具
  • 生成 tool_resultattachmentprogress
  • 再把这些结果拼回上下文
  • 回到下一轮 query()

所以“任务完成”通常不是单次模型输出完成,而是:

  • 模型输出
  • 工具执行
  • 模型继续推理
  • 再次工具执行
  • 最终收敛

10. 结果如何返回给用户

返回不是一步完成,而是流式进行:

  • assistant 文本块持续显示
  • progress 显示工具进度
  • tool_result 显示工具输出
  • 出错时显示 API 错误或恢复信息
  • 最终结束时调用 resetLoadingStateonTurnComplete

用户看到的是一个持续更新的 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:2661
    • src/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:516
    • src/utils/processUserInput/processUserInput.ts:531
    • src/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. 用户视角

从用户角度,最重要的词通常是:

  • prompt
  • slash command
  • bash
  • turn
  • streaming

因为这些词决定的是:我输入了什么、系统现在在做什么、结果怎么显示出来。

2. UI 视角

从终端界面角度,最常见的是:

  • local-jsx
  • attachment
  • assistant message
  • progress message
  • streaming

因为 UI 关心的是:当前该渲染哪种消息、是否要显示一个本地交互面板、是不是还在流式更新。

3. 模型视角

从模型上下文角度,最关键的是:

  • prompt
  • prompt command
  • skill
  • attachment
  • meta message
  • tool_use
  • tool_result

因为模型真正“看到”的,不只是用户原始输入,还包括系统拼进去的各种隐藏消息和工具结果。

4. 工具视角

从工具调度角度,最关键的是:

  • tool_use
  • tool_result
  • progress message
  • ToolUseContext
  • query()

因为工具不是单独跑的,它们是在 query() 驱动下被模型请求,再通过 ToolUseContext 和整轮状态串起来。

一个最容易混淆的区别

  • slash command 是“输入入口”
  • prompt command 是“slash command 里的一种执行方式”
  • local-jsx 是“slash command 里另一种执行方式”

也就是说:

  • 用户输入 /review,这是一个 slash command
  • 它最后可能被展开成 prompt 去问模型,这时它属于 prompt command
  • 用户输入 /config,也可能根本不进模型,而是在本地打开一个交互界面,这时它属于 local-jsx