当你把 Claude Code 塞进 shell 管道、定时任务或编排器时,-p(headless)模式给出的纯文本输出只是冰山一角。真正的生产级自动化,依赖的是 --output-format stream-json 这一逐行 JSON 事件流:它把"模型思考、工具调用、token 增量、成本、错误"全部结构化地吐到 stdout,让你能用 jq 实时解析、做看板、控预算、断点续跑。本教程面向已经能跑通 claude -p 但想把它升级成可观测、可重试、可计费的命令行 Agent 编排器的开发者。
claude --version 可正常输出claude -p "说一句话" 返回纯文本jq(事件流解析核心工具):jq --version# 环境自检一行流
command -v claude && command -v jq && echo "✅ 就绪" || echo "❌ 缺依赖"
--output-format 决定 Claude Code 把结果怎么交给你,三者用途完全不同:
| 格式 | 形态 | 何时用 |
|---|---|---|
text(默认) |
纯文本字符串 | 人类阅读、简单管道 |
json |
单个 JSON 对象,含 result/session_id/total_cost_usd |
一次性脚本、取 session_id 续跑 |
stream-json |
每行一个 JSON 事件(NDJSON) | 实时观测、看板、长任务进度 |
操作说明:用三种格式各跑一次同一个 prompt,对比 stdout。
echo "--- text ---"
claude -p "用一句话解释递归"
echo "--- json ---"
claude -p "用一句话解释递归" --output-format json | jq '{result, session_id, cost: .total_cost_usd, turns: .num_turns}'
echo "--- stream-json(只看事件类型) ---"
claude -p "用一句话解释递归" --output-format stream-json --verbose | jq -r '.type' | sort | uniq -c
预期结果:json 会给出类似 { "result": "递归是…", "session_id": "abc123…", "cost": 0.0021, "turns": 1 } 的完整对象;stream-json 会打印一串事件 type 计数,例如若干个 system、assistant、一个 result。
常见问题:
stream-json 没有任何输出就结束了? A:很可能漏了 --verbose。官方文档明确:要用 --output-format stream-json 配合 --verbose(以及需要 token 级增量时加 --include-partial-messages)才能拿到完整事件流。json 模式结果污染了输出? A:加上 --bare,让脚本不被本地的 settings/CLAUDE.md 干扰,得到干净的 CI 语义。stream-json 是换行分隔的 NDJSON,每行一个事件。理解事件分类是后续一切解析的基础。
操作说明:把一次真实调用的事件流落盘,逐行观察字段。
claude -p "读一下当前目录的 README 并总结" --output-format stream-json --verbose \
> /tmp/events.ndjson
# 1) 看每行事件类型分布
jq -r '.type' /tmp/events.ndjson | sort | uniq -c
# 2) 看第一个事件(通常是 system/init)
head -n1 /tmp/events.ndjson | jq '.'
预期结果:你会看到事件的 type 至少包含这几类:
system(含子类型 init):事件流的第一个,报告本次会话的 model、可用 tools、MCP servers、已加载 plugins。它是会话的"身份证"。assistant:模型回复,content 里可能同时有 text 块和 tool_use 块。user:工具结果回填给模型的那一轮(tool_result)。stream_event:token 级增量(需 --include-partial-messages),event.delta.type == "text_delta" 就是逐字吐出的文本。result:事件流的收尾,包含最终文本、total_cost_usd、duration_ms、num_turns、is_error。常见问题:
system/init? A:当设置了 CLAUDE_CODE_SYNC_PLUGIN_INSTALL 等同步安装逻辑时,首事件顺序可能变。脚本里别假设"第一行一定是 init",要用 select(.type=="system" and .subtype=="init") 过滤。事件流是给机器看的,但 jq 能随时把它转成你想要的任何视图。这是看板的灵魂。
操作说明:四段经典 jq 过滤器,分别取"逐字流式文本""模型说的话""工具调用清单""最终成本"。
# (a) 逐字流式文本(像 ChatGPT 打字机效果)
claude -p "写一首关于管道符的诗" \
--output-format stream-json --verbose --include-partial-messages \
| jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'
# (b) 只看模型的文本回复(不含工具调用)
cat /tmp/events.ndjson \
| jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text'
# (c) 提取所有工具调用(名称 + 关键参数)
cat /tmp/events.ndjson \
| jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | {name, input}'
# (d) 最终成本与耗时
cat /tmp/events.ndjson \
| jq '{cost: .total_cost_usd, ms: .duration_ms, turns: .num_turns, ok: (.is_error|not)} | select(.cost!=null)'
预期结果:
- (a) 终端边跑边打印诗句,无引号无换行污染(靠 jq -rj 的 -r 原始输出 + -j 不加换行)。
- (c) 输出形如 {"name":"Read","input":{"file_path":"README.md"}} 的工具调用序列。
- (d) 输出 {"cost":0.0142,"ms":8341,"turns":4,"ok":true}。
常见问题:
(c) 报 null? A:没加 ? 容错。content 数组里可能混着 text 块,select(.type=="tool_use") 前的 .message.content[]? 那个 ? 是关键,遇到非数组 content 时跳过而非报错。-rj 不认识? A:旧版 jq 没有 -j,升级到 1.6+,或用 -r 后 tr -d '\n' 替代。--json-schema 拿到结构化结果,告别正则解析自动化最大的痛苦是"模型返回自由文本,你还得正则抠字段"。--output-format json --json-schema 直接让模型吐符合 schema 的结构化对象,落在 structured_output 字段。
操作说明:让 Claude 从一个文件里抽取函数名,强制返回字符串数组。
claude -p "提取 auth.py 里的主要函数名" \
--output-format json \
--json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
| jq '.structured_output'
预期结果:返回 {"functions": ["login","verify_token","refresh_session"]}。result 字段仍保留人类可读文本,而 structured_output 是机器消费的干净对象,配合 session_id、total_cost_usd 等元数据一起返回。
常见问题:
jq 校验你的 schema 字符串本身能解析:echo '...' | jq .。required 里的字段模型一定会给,未 required 的可能缺失,下游用 // [] 兜底。{"enum":["bug","feature","question"]} 非常适合做分类任务的护栏。无人值守跑久了,必须能感知"花了多少钱、有没有出错"。json 模式的 payload 自带 total_cost_usd 和按模型拆分的成本明细,result 事件则带 is_error 及错误子类型。
操作说明:写一个 wrapper 函数,跑完后打印成本,并在出错时按子类型分诊。
run_with_cost () {
claude -p "$1" --output-format json --bare > /tmp/out.json
local rc=$?
# 成本
jq -r '"💰 cost=$\(.total_cost_usd) turns=\(.num_turns) ms=\(.duration_ms)"' /tmp/out.json
# 错误分诊
if jq -e '.is_error == true' /tmp/out.json >/dev/null; then
jq -r '"❌ 错误子类型: \(.subtype // "unknown")"' /tmp/out.json
fi
# 结构化结果
jq -r '.result' /tmp/out.json
return $rc
}
run_with_cost "检查 package.json 里有没有未使用的依赖"
预期结果:一行成本(cost=$0.0312 turns=3 ms=12044),正常时跟结果文本;如果触发配额/限流,会打印 ❌ 错误子类型: rate_limit。
官方文档列出的错误子类型包括:rate_limit、overloaded、billing_error、invalid_request、model_not_found、server_error、max_output_tokens、oauth_org_not_allowed、unknown。编排器应据此决定退避策略——rate_limit/overloaded 可指数退避重试,billing_error/oauth_org_not_allowed 应立刻熔断报警。
常见问题:
is_error 字段在 stream-json 的 result 事件里也有吗? A:有。无论 json 还是 stream-json,收尾的 result 事件都携带这套字段。stream-json 场景用 jq 'select(.type=="result")' 取它。@文件 或先落盘再在 prompt 里引用。--resume 与 --continue,把单次调用串成有状态流水线headless 不等于无状态。--continue 续跑最近一次会话,--resume <session_id> 续跑指定会话。关键限制:session_id 查找的作用域是当前项目目录及其 git worktree,所以多次调用必须在同一目录下跑。
操作说明:第一步起一个 review 会话并抓 session_id,第二步带着它追问。
# 第一步:开 review,抓 session_id
session_id=$(claude -p "审查这个代码库的性能问题" \
--output-format json | jq -r '.session_id')
# 第二步:在同一目录下,续跑同一会话
claude -p "把刚才发现的前 3 个问题各给出修复方案" \
--resume "$session_id" --output-format json | jq -r '.result'
# 或者最省事:直接续最近一次
claude -p "生成一份所有问题的汇总报告" --continue | cat
预期结果:第二步能引用第一步的上下文("刚才发现的问题"),无需重新喂背景。把多个 -p 调用串成一条有状态的 Agent 工作流,而每一步都能独立观测、独立计费。
常见问题:
--resume 报"找不到会话"? A:99% 是目录变了。session_id 与目录绑定,CI 里务必 cd 到仓库根再跑。worktree 内的会话对主仓库可见,反之不行。session_id 存变量/存文件,后续一律 --resume 指定 ID,绝不用 --continue(它会抢最近一次,并发下会串台)。--max-turns,给自动化装护栏无人值守的最大风险是"模型干了自己不该干的事"。三道闸门:--permission-mode、--max-turns、--bare。
操作说明:按风险等级选用权限模式。
# 只读巡视(最安全):default 模式在 CI 下退化为只读命令集,适合巡检
claude -p "检查 Dockerfile 有没有安全坏味道" --permission-mode default --bare
# 自动改文件但不执行危险命令:acceptEdits
# 文档原文:acceptEdits 让 Claude 无需提示写文件,并自动批准 mkdir/touch 等常见文件系统命令
claude -p "应用 eslint --fix 的所有修复" --permission-mode acceptEdits
# 硬上限:最多 5 轮工具调用,防止失控
claude -p "重构这个模块" --permission-mode acceptEdits --max-turns 5
bypassPermissions(俗称 yolo)会跳过一切确认,只在一次性沙箱容器/VM 里用,生产链路严禁。
预期结果:acceptEdits 下文件改动直接生效不卡审批;--max-turns 5 下任务在第 5 轮工具调用后被强制收尾,result 事件照常带成本。
常见问题:
acceptEdits + --max-turns + --bare 组合,再配合 settings.json 的 deny 规则封死 rm -rf、git push 等。cch(命令行 Agent 看板)把前面 7 步拼成一个可复用脚本:实时进度看板 + 成本累计 + 错误分诊 + 指数退避重试。
操作说明:保存为 ~/.local/bin/cch 并 chmod +x。
#!/usr/bin/env bash
# cch — Claude Code headless orchestrator with live dashboard
set -uo pipefail
PROMPT="$1"; shift
MAX_TURNS="${CCH_MAX_TURNS:-12}"
BUDGET="${CCH_BUDGET:-2.0}" # 单次预算上限(美元)
MAX_RETRY=3
run_once () {
claude -p "$PROMPT" "$@" \
--output-format stream-json --verbose --include-partial-messages \
--permission-mode acceptEdits --max-turns "$MAX_TURNS" --bare \
| tee /tmp/cch_events.ndjson \
| jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'
}
attempt=0
while [ $attempt -lt $MAX_RETRY ]; do
attempt=$((attempt+1))
run_once "$@" || true
# 从 result 事件读成本与错误
result=$(jq -c 'select(.type=="result")' /tmp/cch_events.ndjson | tail -n1)
cost=$(echo "$result" | jq -r '.total_cost_usd // 0')
err=$(echo "$result" | jq -r '.is_error // false')
subtype=$(echo "$result" | jq -r '.subtype // ""')
echo # 换行,结束流式文本
printf '📊 尝试 %d 成本 $%.4f 错误=%s %s\n' "$attempt" "$cost" "$err" "$subtype"
# 预算熔断
awk "BEGIN{exit !($cost > $BUDGET)}" && { echo "🛑 超预算($BUDGET),熔断"; break; }
# 成功或不可重试错误 → 退出
[ "$err" = "false" ] && break
case "$subtype" in
rate_limit|overloaded) sleep $((2**attempt));; # 指数退避
billing_error|oauth_org_not_allowed) break;; # 立即报警
esac
done
预期结果:执行 cch "给 src 下所有函数补类型注解",终端实时打印模型生成的文本(打字机效果),结束后输出一行 📊 尝试 1 成本 $0.2143 错误=false。限流时自动退避重试,超预算自动熔断。/tmp/cch_events.ndjson 留存完整事件流供事后审计。
常见问题:
tee 后的管道再分一路:tee >(jq -c 'select(.type=="assistant")|.message.usage' >> /tmp/usage.log),实时累计 token。/tmp/cch_events.ndjson? A:并发时改用 mktemp:EV=$(mktemp),把它传进去,结束时清理。把整套能力串成一个可运行的端到端管道:扫描一个仓的开放 issue → 用 --json-schema 强制分类成 bug/feature/question 并打分 → 用 --resume 续跑同一会话让模型写处置建议 → 解析成本写日志。全程 headless、可观测、有预算护栏。
#!/usr/bin/env bash
# issue-triage.sh — 用法: ./issue-triage.sh <repo> 例 myorg/myrepo
set -uo pipefail
REPO="$1"; BUDGET=3.0
# 1) 拉取未关闭 issue 的标题(gh CLI)
mapfile -t TITLES < <(gh issue list -R "$REPO" --state open --limit 20 \
--json number,title -q '.[] | "\(.number)\t\(.title)"')
# 2) 对每个 issue:结构化分类 + 抓 session_id
: > /tmp/triage.log
for line in "${TITLES[@]}"; do
num="${line%%$'\t'*}"; title="${line#*$'\t'}"
out=$(claude -p "这是一条 GitHub issue,请分类。标题: $title" \
--output-format json --bare \
--json-schema '{"type":"object","properties":{
"label":{"enum":["bug","feature","question"]},
"confidence":{"type":"number"},
"suggestion":{"type":"string"}},
"required":["label","confidence","suggestion"]}')
sid=$(echo "$out" | jq -r '.session_id')
cost=$(echo "$out" | jq -r '.total_cost_usd')
parsed=$(echo "$out" | jq -c '.structured_output')
# 3) 续跑同一会话,让模型基于分类写出 200 字内处置建议
reply=$(claude -p "基于你刚才的分类,用 200 字内给出处置建议" \
--resume "$sid" --output-format json | jq -r '.result')
# 4) 回填标签(演示用,真实环境按需放开)
label=$(echo "$parsed" | jq -r '.label')
echo "gh issue edit $num -R $REPO --add-label \"$label\" # dry-run"
printf '#%s [%s %.2f] $%s\n %s\n\n' "$num" "$label" \
"$(echo $parsed|jq -r .confidence)" "$cost" "$reply" >> /tmp/triage.log
# 预算护栏
total=$(awk '{s+=$1}END{print s}' /tmp/triage_costs 2>/dev/null || echo 0)
awk "BEGIN{exit !($total > $BUDGET)}" && { echo "🛑 预算 $BUDGET 用尽,停止"; break; }
done
echo "✅ 完成,详见 /tmp/triage.log"
运行预期:./issue-triage.sh myorg/myrepo 会逐条打印 #42 [bug 0.91] $0.0088 加一段处置建议,并在 /tmp/triage.log 汇总。每条 issue 都用 --resume 复用分类时的会话上下文,省 token、保连贯;成本逐条累加,超 $3.0 自动停。
flowchart TD
A[Shell / CI 触发] --> B["claude -p --output-format stream-json<br/>--verbose --include-partial-messages"]
B -->|NDJSON 事件流| C{事件 type}
C -->|system/init| D[会话身份证:<br/>model / tools / MCP / plugins]
C -->|stream_event text_delta| E[jq -rj 实时打字机]
C -->|assistant tool_use| F[工具调用清单]
C -->|result| G[收尾:<br/>total_cost_usd / num_turns / is_error / subtype]
G --> H{is_error?}
H -->|false| I[输出结构化结果]
H -->|true| J{subtype 分诊}
J -->|rate_limit / overloaded| K[指数退避重试]
J -->|billing / oauth| L[立即熔断报警]
K --> B
G -. 成本累计 .-> M[(预算护栏)]
M -. 超 BUDGET .-> L
I --> N[--resume session_id 续跑<br/>串成有状态流水线]
N --> B
Stream responses in real-time)和更优雅的错误处理。--continue/--resume 之外还有 --from-pr、交互式 /resume 选择器、session 命名与分支,见 Sessions 文档。bypassPermissions(yolo)必须配沙箱,官方推荐 Bubblewrap/Seatbelt/Docker/devcontainer,见 Sandbox environments。ENABLE_PROMPT_CACHING_1H、cache_read_input_tokens 等可把批量调用的 token 成本再砍一截,见 Cost tracking。| 维度 | 评估 |
|---|---|
| 适合的训练营 | DevOps/SRE 自动化营、AI 工程化营、平台工程营;不适合零基础(需 shell+jq 基础) |
| 学员前置 | 能写 50 行 bash、会用 jq、理解管道与退出码 |
| 建议教学时长 | 4 课时:1 课时事件流理论(步骤1-3)、1 课时结构化与成本(步骤4-5)、1 课时会话与护栏(步骤6-7)、1 课时实战案例(步骤8+案例) |
| 可裁剪点 | 时间紧可跳过步骤4(json-schema),但步骤3和步骤5是核心,不建议省 |
| 进阶作业 | 让学员把 cch 扩展成支持多任务并发的 worker(用 mktemp 隔离 + xargs -P),并加一个 token 实时累计看板 |
--output-format/stream-json/--json-schema/--resume/权限模式的权威说明--continue/--resume/--from-pr 及 session_id 目录作用域规则default/acceptEdits/plan/bypassPermissions 四模式语义与 Shift+Tab 切换total_cost_usd、按模型成本拆分、prompt caching 配置[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
