DOCAI · 课程 · L3 · 21 分钟 · 命令参考
命令执行流程
同一条命令本地能跑、进了 CI 却卡住,差别常常藏在链路的某一层。这一节按顺序拆开参数解析、配置叠加、上下文组装、模型请求、工具回合与结果输出,并给出定位卡点的办法。
你将学会
- 能按阶段复述一次调用从输入到退出的完整路径
- 能判断卡顿发生在解析、权限还是工具执行
- 能用输出格式和轮次上限控制脚本里的行为
敲下回车之后,链路已经开始了
同一条命令,你在终端里跑得顺利,挪到流水线就报「没有权限」,或者干脆停在某个确认框前。多数人先怀疑模型不稳定,其实更常见的原因是:这条命令在中段撞上了和本地不同的配置。
把整个过程想成一条时间线,卡点就能落到具体某一层。下面顺着这条线走一遍,同时标出每层对应的开关。
前三步:解析、叠加配置、拼上下文
这些动作都发生在请求模型之前。
解析参数。 命令行里的子命令、标志、提示词先被切开。提示词含空格时必须整体加引号,否则后面的词会被当成新参数;写成 --model=值 也能把名字和值绑在一起。名字打错时进程会直接退出,不会带着半截配置继续跑。
叠加配置。 内置默认在最底层,往上依次是用户级设置、项目共享设置、项目本地设置,最后是命令行标志,越靠后越优先。企业托管策略通常还另有一层更高的位置,实际顺序以你所在环境为准。排查配置问题时,先看命令里有没有临时传参,它压过所有文件;设置文件本身写错时,claude doctor 会给出校验错误。
拼上下文。 系统提示、项目说明文件(CLAUDE.md)、工具清单、本轮输入,以及续接会话时带上的历史消息,共同组成这次请求的载荷。历史越长,输入 token 越多,这也解释了续接会话为什么比新开会话更贵。
中段:流式回话与工具回合
请求发出后,回答是流式回来的。文字先到先显示,不用等整段生成完。
真正的工作发生在工具回合里。模型不会直接动你的文件,它先输出一个结构化请求:要调哪个工具、参数是什么。程序执行工具,把结果塞回对话,模型看到结果再决定下一步。读文件、改文件、跑命令,都靠这样一轮一轮推进。
每一轮算一个 turn;在非交互模式里,--max-turns 限制的就是轮数。设上限不是保守,而是防止模型在同一个工具上来回打转;到了上限进程会停下,你拿到已有结果,而不是无限等下去。
权限判断插在工具执行之前
工具请求不是拿到就执行,程序先查规则:
- 命中允许列表,直接跑;
- 命中拒绝列表,回一条拒绝结果,模型只能换方案;
- 两边都没有,交互模式弹出确认框,由你选。
这解释了 -p 模式里最常见的一种失败:脚本运行没人点确认,未获授权的写操作被拒,模型于是回你一段「我改不了,但建议这样改」。解法不是关掉所有防护,而是把这次任务真正需要的工具写进允许列表,范围尽量窄。
--dangerously-skip-permissions 会跳过绝大部分确认,只适合隔离环境里的明确任务。生产脚本里用它,等于把风险交给模型的判断力。
末段:结果形态与退出方式
最后一步由输出格式决定:
| 格式 | 形态 | 适合 |
|---|---|---|
| text | 人读的文本,逐字到达 | 终端里直接看 |
| json | 结束后给一个完整对象 | 脚本取结果 |
| stream-json | 一行一个事件,持续输出 | 前端或管道实时消费 |
非交互模式输出完即退出,退出码可供脚本判断;交互模式把光标还给你,会话留在内存里等下一句。
用链路定位一次失败
假设下面这条命令在流水线里没有改动任何文件:
claude -p "在 src/utils.ts 里补一个 debounce 函数" --max-turns 5
按阶段排查:解析正常;配置读到了,但允许列表只有读类工具;上下文里项目说明也在;模型确实发出了编辑请求;断点在权限判断,被自动拒绝。修法是把这个权限明确交出去:
claude -p "在 src/utils.ts 里补一个 debounce 函数" \
--max-turns 5 \
--allowedTools "Read" "Edit" "Glob"
同样的提示词再跑一次,就会走到工具执行并落地。看懂链路的价值在这里:不靠猜,直接找到断点。
Hook 挂在链路的哪一段
整条链路里有几个固定插槽,配置好的外部命令会在对应时刻被调用:
| Hook | 触发位置 | 适合做什么 |
|---|---|---|
| SessionStart | 进程启动、会话建立时 | 打印环境信息、检查依赖是否装好 |
| UserPromptSubmit | 输入已提交、上下文尚未组装时 | 补充项目状态、拦截明显有问题的请求 |
| PreToolUse | 工具即将执行前 | 审计危险命令、按路径拦截写入 |
| PostToolUse | 工具执行完成之后 | 格式化、跑快速检查、记录改动 |
| Stop | 模型给出最终回答、回合结束前 | 提醒未完成事项、生成摘要 |
| SessionEnd | 退出或会话关闭时 | 清理临时文件、保存统计 |
以「每次改完文件自动跑测试」为例,配置形态大致如下:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [{ "type": "command", "command": "npm test --silent" }]
}
]
}
}
matcher 决定匹配哪些工具,命令在匹配事件之后执行。两点边界要记住:一是 hook 里不要放长跑任务,它会把主流程拖住,整套测试很慢就改成只跑相关文件;二是 hook 失败是否阻断后续步骤,取决于当前版本和配置,升级后先验证一次实际行为。更完整的机制见钩子。
四个高频故障:症状、原因、处理
症状一:脚本模式下它只给建议,文件一点没改。 先从输出里确认有没有权限拒绝的提示,再看允许列表的粒度:只放行到 Bash(git diff:*) 这种命令前缀,比整类放开安全;需要写文件时,逐项列出 Edit、Write 和具体目录,不要用通配把风险一起兜进来。改完用同一条提示词复跑,确认改动真的落盘。
症状二:命令迟迟不结束。 先分清是模型在反复调用,还是某个工具本身在长时间执行。前者看轮次,给 --max-turns 设一个 5 到 15 之间的值;后者把长命令放后台,让模型继续做别的事,做法见后台 bash 命令。两种情况都不要靠加大超时硬扛。
症状三:拿到的输出无法按 JSON 解析。 常见原因是把流式格式当成了单个对象。需要一次性拿到完整结果时,选 json;需要边生成边处理时,选 stream-json,并按行读取、逐行解析。两者都要求关掉那些会混入提示文字的装饰输出。
症状四:改了设置却不生效。 按生效顺序倒查:命令行临时传参压过一切,项目本地设置压过项目共享设置,用户级设置只在前面都没有时才轮得到。另一个常被忽略的是路径作用域——在 A 目录写的项目设置,到了 B 目录不会加载;确认文件位置无误后,还要重启进程让它重新读取。
还有一种表现是退出码正常、文件却没变:这多半说明提示词里只有讨论,没有可执行的验收条件,模型把任务当成咨询处理了。把「改哪个文件、跑哪条命令、看到什么算成功」写清楚,空转就会少很多。
另外两种情况也常见:续接会话后第一轮就超出上下文,是因为旧消息被整段带上,解决方式是在上次结束前压缩一次;按了中断键后发现回答缺了一截,是因为未完成的流被丢弃,重要任务尽量一次跑完,或把中间结果写进文件。
一个可以直接抄进流水线的例子:
claude -p "审查当前分支相对 main 的改动,按严重程度列出问题,指出文件与行号" \
--allowedTools "Read" "Grep" "Bash(git diff:*)" \
--output-format json \
--max-turns 8 > review.json
jq -r '.result // .' review.json | head -n 40
这段命令把读文件、搜代码和查看差异放开,其余写操作仍需确认;输出落进文件,后续步骤再解析。字段名若与上面不同,先打印一次原始对象再调整取值。
记住这条线,后面每个开关在什么位置生效都能对上号。想先补工具调用的概念,可以看什么是工具调用;流式响应本身见流式输出。下一节解决「忘了参数怎么办」——命令帮助系统。
自测
先自己回答,再展开对照;答不上来就回到对应小节。01为什么同一条命令在本地能改文件,在 CI 里却被拒绝?看答案
非交互模式没有人确认权限,未被允许的写工具会被自动拒掉;需要在启动时用允许列表明确放行。
02工具回合的轮数上限解决什么问题?看答案
防止模型反复调用同一个工具打转,到上限就停下并交回已有结果。