Claude Code命令执行流程原理

Claude 命令执行流程:从用户输入到结果输出的完整链路

拆解 Claude Code 一次命令执行的完整流程——从 CLI 解析、配置加载、LLM 调用,到工具调用循环、权限确认、流式输出。

· 阅读约 14 分钟

很多人用 Claude Code 一段时间后会有这样的疑惑:我敲了一行命令,Claude 怎么知道要调哪些工具?为什么有时候它会停下来问我”是否允许执行 rm 命令”?为什么 stream 输出有时候断在中间?

这些问题的答案都藏在”一次命令的执行流程”里。这篇文章按时间顺序,把从你按下回车到结果输出的整个链路拆开讲清楚。理解了这个流程,你才知道每个 CLI 标志、每个权限模式、每个 hook 的位置在哪、什么时候生效。


一次命令执行的全景图

先看完整的链路:

用户输入
   |
   v
┌─────────────────┐
│ 1. CLI 解析     │  解析子命令、标志、prompt
└────────┬────────┘
         |
         v
┌─────────────────┐
│ 2. 加载配置     │  全局 → 项目 → 命令行(优先级递增)
└────────┬────────┘
         |
         v
┌─────────────────┐
│ 3. 构建上下文   │  CLAUDE.md + 历史 + 系统提示
└────────┬────────┘
         |
         v
┌─────────────────┐
│ 4. 调用 LLM     │  发起 API 请求,开始流式接收
└────────┬────────┘
         |
         v
┌─────────────────────────────┐
│ 5. 工具调用循环             │
│    LLM 返回 → 解析 →        │
│    权限确认 → 执行工具 →    │
│    工具结果回喂 → 再调 LLM  │
│    (直到 LLM 不再调用工具)│
└────────┬────────────────────┘
         |
         v
┌─────────────────┐
│ 6. 输出结果     │  text / json / stream-json
└────────┬────────┘
         |
         v
┌─────────────────┐
│ 7. 退出或继续   │  -p 模式退出,REPL 等下一轮
└─────────────────┘

下面把每一步拆开讲。


第 1 步:CLI 解析

你在终端敲:

claude -p "重构 src/utils.ts" --model claude-opus-4-7 --max-turns 10

Claude Code 启动后做的第一件事是解析这行命令:

  • 子命令:无(默认进入 prompt 模式)
  • 标志:-p(打印模式)、--model--max-turns
  • Prompt:"重构 src/utils.ts"

解析失败会立即报错退出。比如打错标志名 --modle,会提示:

error: unknown option '--modle'
Did you mean '--model'?

这一步常见坑: 标志的顺序、标志值有空格时要不要加引号。

# 错(空格会被当成新参数)
claude -p 重构 utils

# 对
claude -p "重构 utils"

# 也对(用 = 等号绑定)
claude --model=claude-sonnet-4-7 -p "..."

第 2 步:加载配置

CLI 解析完后,Claude Code 按顺序加载多层配置:

1. 默认配置(内置)
2. 用户全局:~/.claude/settings.json
3. 项目级:./.claude/settings.json
4. 项目本地:./.claude/settings.local.json(gitignore)
5. 命令行标志(最高优先级)

后面的会覆盖前面的。比如:

来源model 设置
默认claude-sonnet-4-7
全局 settings.jsonclaude-haiku-4-7
项目 settings.json(未设置)
命令行 --modelclaude-opus-4-7
最终生效claude-opus-4-7

同时这一步还会读:

  • CLAUDE.md:项目说明(告诉 Claude 这个项目的约定)
  • ~/.claude/CLAUDE.md:全局个人偏好
  • .mcp.json:MCP 服务器配置
  • permissions 字段:哪些工具自动允许、哪些拒绝

调试技巧:--verbose 看每一层配置加载的过程:

claude --verbose -p "..."
# [config] loading defaults
# [config] reading ~/.claude/settings.json
# [config] reading ./.claude/settings.json
# [config] CLI flag --model overrides settings.model
# [config] resolved model = claude-opus-4-7

第 3 步:构建上下文

配置加载完,Claude Code 准备发给 LLM 的”上下文包”:

[System Prompt]
  - Claude Code 内置的系统提示
  - + --append-system-prompt 追加内容
  - 或 --system-prompt 完全替换

[CLAUDE.md 内容]
  - 全局 CLAUDE.md
  - 项目 CLAUDE.md(合并进 system 区域)

[Tools 定义]
  - 内置工具(Read、Edit、Write、Bash、Glob、Grep 等)
  - MCP 提供的外部工具

[历史消息](如果是 -c 续接)
  - 上一次会话的 messages

[当前用户输入]
  - 你刚敲的 prompt

这个包通过 Anthropic API 发出去。如果是续接对话(-c),还会带上历史消息——这就是为什么续接对话比新对话 Token 用得多。


第 4 步:调用 LLM 与流式接收

Claude Code 发起 API 请求时,默认开启流式(streaming)。LLM 一边生成 token,一边推回来。

LLM:  "好的,我来重构..."  ← 文字流
LLM:  ↑ 这部分会立即显示在终端
LLM:  [tool_use: Read("src/utils.ts")]  ← 工具调用 token

终端里你会看到文字一个一个蹦出来。这叫流式输出。

为什么流式重要:

  1. 反应快,不用等几十秒看到完整结果
  2. 大型回复(生成代码)可以早点看到开头判断方向
  3. -p --output-format stream-json 模式下,可以管道给前端

流式的中断: 你按 Ctrl+C 时,本质是中断了当前的流式接收。Claude Code 会丢弃未完成的响应,回到等待用户输入的状态。


第 5 步:工具调用循环(核心)

这是 Claude Code 最关键的一步。LLM 不只是回文字,它还会”调用工具”。

工具调用是什么

LLM 输出一个特殊的 JSON 块,告诉 Claude Code:“请帮我执行这个操作”:

{
  "type": "tool_use",
  "name": "Read",
  "input": {
    "file_path": "/home/user/project/src/utils.ts"
  }
}

Claude Code 拿到这个块,就去执行 Read 工具:读文件、把内容拼进结果。然后把结果作为 tool_result 喂回给 LLM:

{
  "type": "tool_result",
  "tool_use_id": "...",
  "content": "export function foo() { ... }"
}

LLM 拿到结果,继续生成。可能再调一个工具(Edit),可能直接给你最终回答。

循环的过程

[第 1 轮]
  LLM: "我需要先看看文件" + tool_use(Read)
  Claude Code: 执行 Read → 返回文件内容
  
[第 2 轮]
  LLM: 看到内容,决定改 → tool_use(Edit)
  Claude Code: 检查权限 → 弹确认 → 执行 → 返回 success
  
[第 3 轮]
  LLM: 改完了,再读一遍验证 → tool_use(Read)
  Claude Code: 执行 → 返回新内容
  
[第 4 轮]
  LLM: "完成了,做了以下改动..."(不再调工具)

→ 循环结束

每一轮叫一个 turn。--max-turns 10 就是限制最多 10 轮。

权限确认

工具调用不是无脑执行的。Claude Code 在执行前会判断:

工具调用 (Edit, Bash, Write 等)
   |
   v
查 permissions 配置
   |
   ├─ 在 allowList?  → 直接执行
   ├─ 在 denyList?   → 拒绝并告诉 LLM
   └─ 都不在?        → 弹出确认 UI

                       用户选 [允许/拒绝/总是允许]

--dangerously-skip-permissions 等于把所有工具都当成 allowList 处理。

acceptEdits 模式只对 EditWrite 自动放行,对 Bash 还是会确认。

工具调用的中断

执行慢的工具(比如 Bashnpm install)你可以按 Ctrl+C 中断。中断后:

  • 工具结果会标为 interrupted
  • LLM 收到中断信号,决定下一步(通常会问你怎么办)
  • 你可以输入新指令,循环继续

第 6 步:输出结果

LLM 不再调用工具,开始生成最终回答。Claude Code 根据 --output-format 决定输出格式:

格式适用场景示例
text(默认)终端阅读直接打印文字
json脚本解析最终结果{"result": "..."} 一次性输出
stream-json实时管道给前端每个事件一行 NDJSON

stream-json 的输出长这样:

{"type":"message_start","message":{...}}
{"type":"content_block_delta","delta":{"text":"好"}}
{"type":"content_block_delta","delta":{"text":"的"}}
{"type":"tool_use","name":"Read","input":{...}}
{"type":"tool_result","content":"..."}
{"type":"message_stop"}

每行是一个 JSON。前端可以一边读一边渲染。


第 7 步:退出或等待

到这里有两种走向:

-p 模式: 输出完最终结果,进程退出。这就是为什么 -p 适合脚本——一次进一次出,没有交互。

REPL 模式: 输出完,光标回到输入栏,等你下一句话。整个会话上下文(messages)保留在内存里,下一句继续接上。

REPL 模式按 /exitCtrl+D、关终端都会退出。退出前如果你启用了”会话保存”,下次能用 -c 接回来。


Hook 在哪个阶段触发

如果你配置了 hooks(在 ~/.claude/settings.json 里),它们会在特定阶段被自动调用:

Hook 名触发时机
UserPromptSubmit第 3 步之前(用户提交输入后)
PreToolUse第 5 步内,工具执行前
PostToolUse第 5 步内,工具执行后
Stop第 7 步前(LLM 输出完成)
SessionStart第 1 步(启动会话)
SessionEnd退出时

比如想在 Claude 改完代码后自动跑测试,就用 PostToolUse hook 监听 Edit/Write

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "npm test" }]
      }
    ]
  }
}

一个完整的实例

我们看一次真实命令的全过程:

$ claude -p "在 utils.ts 里加一个 debounce 函数" --max-turns 5
[1] CLI 解析
    -p: 非交互模式
    --max-turns: 5
    prompt: "在 utils.ts 里加一个 debounce 函数"

[2] 加载配置
    ~/.claude/settings.json: model = sonnet, permissions.allow = [Read]
    ./.claude/settings.json: 无
    最终: model = sonnet

[3] 构建上下文
    System: Claude Code 默认提示
    CLAUDE.md: "项目用 TypeScript, 严格类型"
    Tools: Read, Edit, Write, Bash, Glob, Grep
    User: "在 utils.ts 里加一个 debounce 函数"

[4] 调用 LLM (流式)
    流式接收开始...

[5] 工具调用循环
    Turn 1: LLM 调用 Glob("**/utils.ts")
            → 找到 src/utils.ts
    Turn 2: LLM 调用 Read("src/utils.ts")
            → 读到现有内容
    Turn 3: LLM 调用 Edit(...)
            → 权限检查: Edit 不在 allowList
            → -p 模式 + 没 --allowedTools = 自动拒绝
            → LLM 收到 "permission denied"
    Turn 4: LLM 输出最终回答
            "我无法直接修改文件,但下面是建议的代码..."

[6] 输出
    text 格式打印到 stdout

[7] 退出
    exit code 0

发现问题了——-p 模式下我没加 --allowedTools,所以 Edit 被自动拒绝。修正:

claude -p "在 utils.ts 里加一个 debounce 函数" \
  --max-turns 5 \
  --allowedTools "Edit,Read,Glob"

这次 Turn 3 会成功执行 Edit。这就是流程知识对实际使用的帮助:知道哪一步会卡住,提前给配置。


常见问题

Q: 为什么有时候 Claude 会停下来等很久?

通常是在第 5 步等工具执行。比如 Bashnpm install 要几分钟。可以配 Bash(npm install:*) 到 allowList 让它直接跑,或者在另一个终端跑然后告诉 Claude 结果。

Q: 工具调用会死循环吗?

会。LLM 可能反复调用同一个工具陷入循环。--max-turns 就是为了防止这种情况——超过上限直接停。生产脚本里建议设置 5-15。

Q: 流式输出和 --output-format json 矛盾吗?

不矛盾但有差别。text 默认流式,stream-json 也是流式(一行一个事件),但 json 是非流式——它会等所有结果完成后一次性吐一个完整 JSON。脚本里要”完整结果”用 json,要”实时反馈”用 stream-json

Q: -c 续接对话时,前 N 步会重做吗?

不会。第 1-2 步还是要做(解析 CLI、加载配置)。但第 3 步会读上次的会话历史一起塞进去,所以上下文 Token 一开始就多。如果上下文太长,建议在上次会话末尾用 /compact 压缩一下再退出。


快速参考:流程关键点

阶段涉及的 CLI 标志涉及的 Hook
CLI 解析所有 --flag-
加载配置--config--verboseSessionStart
构建上下文--system-prompt--append-system-promptUserPromptSubmit
LLM 调用--model-
工具循环--allowedTools--max-turns--permission-modePreToolUsePostToolUse
输出--output-formatStop
退出-p 或 REPLSessionEnd

碰到问题先定位是哪一阶段出的问题,再去找对应的标志或 hook 修。