Claude 命令执行流程:从用户输入到结果输出的完整链路
拆解 Claude Code 一次命令执行的完整流程——从 CLI 解析、配置加载、LLM 调用,到工具调用循环、权限确认、流式输出。
很多人用 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.json | claude-haiku-4-7 |
| 项目 settings.json | (未设置) |
命令行 --model | claude-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
终端里你会看到文字一个一个蹦出来。这叫流式输出。
为什么流式重要:
- 反应快,不用等几十秒看到完整结果
- 大型回复(生成代码)可以早点看到开头判断方向
- 在
-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 模式只对 Edit 和 Write 自动放行,对 Bash 还是会确认。
工具调用的中断
执行慢的工具(比如 Bash 跑 npm 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 模式按 /exit、Ctrl+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 步等工具执行。比如 Bash 跑 npm 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、--verbose | SessionStart |
| 构建上下文 | --system-prompt、--append-system-prompt | UserPromptSubmit |
| LLM 调用 | --model | - |
| 工具循环 | --allowedTools、--max-turns、--permission-mode | PreToolUse、PostToolUse |
| 输出 | --output-format | Stop |
| 退出 | -p 或 REPL | SessionEnd |
碰到问题先定位是哪一阶段出的问题,再去找对应的标志或 hook 修。