本教程解决一个真实痛点:用 claude -p 跑一个长任务(重构整个模块、批量改文档、生成测试套件),跑到一半限流了、CI 超时了、或者你手滑 Ctrl+C 了——上下文全丢,只能从头再来。我们将用会话续跑(--resume/--continue)+ 流式输出解析(stream-json)搭一个"可重启、可观测"的自主代理运行器:每次运行自动捕获 session_id,失败后重跑命令即可从断点续上,全程实时打印进度、工具调用与限流重试事件。适合做夜间批处理、CI 长任务、大型重构的工程师。
--json-schema、--bare、--fork-session 均需较新版本(下文会标注具体小版本)。jq:解析 JSON 输出。sudo apt install jq 或 brew install jq。ANTHROPIC_API_KEY:本教程默认用 --bare 模式,它跳过 OAuth 与钥匙串读取,鉴权必须来自环境变量。export ANTHROPIC_API_KEY=sk-ant-...
claude --version # 确认版本
Claude Code 把每一次对话存成一条会话(session),绑定到当前项目目录,随着你工作持续写入本地 transcript 文件。会话有两层价值,本教程吃的是第二层:
/clear 后还能 /resume 找回来;claude -p 跑的非交互任务也有 session_id,可以被同一条命令再次 --resume。关键事实(官方文档原文):
Sessions created with
claude -por the Agent SDK do not appear in the session picker, but you can still resume one by passing its session ID toclaude --resume <session-id>.
也就是说,-p 模式的会话不会出现在选择器里,但只要你有 session_id,照样能续。这正是构建运行器的基础。
transcript 落盘位置(了解即可,不建议直接解析,格式随版本变):
~/.claude/projects/<工作目录路径-非字母数字替换为->/<session-id>.jsonl
默认保留 30 天,可用 settings.json 里的 cleanupPeriodDays 改。
| 命令 | 作用 | 适用场景 |
|---|---|---|
claude --continue |
续跑当前目录最近一条会话 | 交互式"接着干" |
claude --resume |
打开会话选择器 | 人工挑历史会话 |
claude --resume <id或名字> |
按 ID/名字直接续 | 脚本自动化(本教程核心) |
claude --from-pr <url> |
续跑绑定到某个 PR 的会话 | PR 机器人 |
⚠️ 作用域陷阱:--resume <id> 的查找只限当前项目目录及其 git worktree。在 A 目录创建的会话,到 B 目录 --resume 会报 No conversation found with session ID。所以运行器脚本必须每次在同一目录执行——把状态文件放在项目根,配合固定的工作目录即可避免。
长任务并行时,命名是找回会话的最快方式:
# 启动时命名
claude -n auth-refactor
# 会话内改名
/rename auth-refactor
# 选择器里高亮后按 Ctrl+R
命名后,claude --resume auth-refactor 比 --resume abc123... 可读得多。
预期结果:--resume <name> 命中唯一会话则直接进入;名字冲突时打开选择器并预填搜索词。
常见问题:名字在多 worktree 间重复会变"歧义",选择器会要求你二选一。
续跑的前提是先拿到 ID。--output-format json 会把结果包成结构化对象,含 result、session_id、total_cost_usd、usage 等:
claude -p "审查这个代码库的性能问题" --output-format json > run.json
jq '{session_id, cost: .total_cost_usd, result: .result[0:80]}' run.json
预期输出:
{
"session_id": "a1b2c3d4-...",
"cost": 0.0123,
"result": "发现 3 个 N+1 查询,集中在 user_service.py..."
}
一行捕获,供后续命令复用:
session_id=$(claude -p "开始一次审查" --output-format json | jq -r '.session_id')
echo "拿到会话: $session_id"
常见问题:忘记加 --output-format json 时,session_id 字段不存在,jq 返回 null,下一步 --resume null 直接报错。养成"先 --output-format json 兜底拿 ID"的习惯。
拿到 session_id 后,发后续 prompt 时带上它,Claude 就带着完整上下文继续:
# 第一条
session_id=$(claude -p "审查这个代码库的性能问题" --output-format json | jq -r '.session_id')
# 续问 1
claude -p "现在重点看数据库查询" --resume "$session_id"
# 续问 2
claude -p "汇总所有发现的问题" --resume "$session_id"
注意:续跑的是"会话状态",不是"工作区文件"——文件改动在磁盘上,本来就在;--resume 恢复的是对话记忆。所以"跑了一半挂掉 → 重跑续上"这条链路是成立的。
json 模式只在结尾给你一个对象。长任务跑 10 分钟,中间发生什么一无所知。stream-json 是逐行换行分隔的 JSON 事件流,每个事件一行,能实时看到模型在想什么、在调什么工具、是否在限流重试。
claude -p "解释递归" \
--output-format stream-json \
--verbose \
--include-partial-messages
--include-partial-messages 让 token 逐字吐出(不加它只有整段消息)。三类高价值事件:
| 事件类型 | 关键字段 | 用途 |
|---|---|---|
system / init |
session_id, model, tools, mcp_servers, plugins |
一开头就拿到 session_id,不用等结尾 |
system / api_retry |
attempt, max_retries, retry_delay_ms, error_status, error |
限流/过载时主动告警 + 自定义退避 |
result |
session_id, total_cost_usd, is_error |
权威的最终状态与 session_id |
api_retry 的 error 字段会是这些类别之一:rate_limit、overloaded、authentication_failed、billing_error、invalid_request、model_not_found、server_error、max_output_tokens、unknown。
用 jq 只看流式文本(-r 去引号、-j 不加换行,让 token 连续):
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'
版本注意:--input-format stream-json(反向把事件喂回 Claude、做双向程序化控制)目前官方文档说明不全(见 GitHub issue #24594 / #44911)。CLI 单向消费用 --output-format stream-json 已足够;需要双向回调请用 Agent SDK(Python/TS)。
除了自然语言,还能强制 Claude 按 JSON Schema 吐结果,落在 structured_output 字段:
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"]}
运行器里常用这招:让代理自己产出"任务是否完成 / 还差什么"的结构化结论,脚本据此决定要不要续跑下一轮。
想在不污染主线的前提下试另一种思路?分支(fork)复制一份对话副本,原会话原封不动:
# 命令行:续跑 + 分支
claude --resume "$session_id" --fork-session
# 会话内
/branch try-streaming-approach
/branch 会打印两个 ID:你正进入的新分支、和留在原地的原始会话。注意:"仅本次会话允许"的权限不会带进新分支;不 fork 而在两个终端 --resume 同一会话,消息会交错进同一条 transcript,避免这么做。
把上面拼成一套真正能跑的工程。目标:跑一个长任务,崩了重跑能续上,全程可观测,还能记录成本。
目录结构:
my-project/
├─ run.sh # 运行器
├─ stream_parse.py # 流式解析器
└─ .agent_state.json # 上次会话状态(自动生成)
stream_parse.py(流式事件解析器)逐行读 stream-json,打印实时进度,把最终 session_id/成本/是否出错写到状态文件。
#!/usr/bin/env python3
# 读 claude 的 stream-json,打印实时进度,并把最终状态写盘
import sys, json
state_path = sys.argv[1] if len(sys.argv) > 1 else None
session_id, cost, is_error = None, None, False
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
evt = json.loads(line)
except json.JSONDecodeError:
continue
t = evt.get("type")
# 1) 开场即拿到 session_id、模型、加载的工具/MCP/插件
if t == "system" and evt.get("subtype") == "init":
session_id = evt.get("session_id")
print(f"[init] session={session_id} model={evt.get('model')} "
f"tools={len(evt.get('tools', []))}", flush=True)
# 2) 限流/过载重试:可据此发告警或自定义退避
elif t == "system" and evt.get("subtype") == "api_retry":
print(f"[retry] 第{evt.get('attempt')}/{evt.get('max_retries')}次 "
f"{evt.get('error')} (http {evt.get('error_status')}) "
f"等待 {evt.get('retry_delay_ms')}ms", flush=True)
# 3) 助手消息:只打印工具调用(文本已由下面的 delta 实时打印)
elif t == "assistant":
for c in evt.get("message", {}).get("content", []):
if c.get("type") == "tool_use":
print(f"\n[tool] {c.get('name')} -> " +
json.dumps(c.get("input", {}), ensure_ascii=False)[:120],
flush=True)
# 4) 逐 token 文本流(需 --include-partial-messages)
elif t == "stream_event":
d = evt.get("event", {}).get("delta", {})
if d.get("type") == "text_delta":
sys.stdout.write(d.get("text", ""))
sys.stdout.flush()
# 5) 权威收尾:最终 session_id / 成本 / 是否出错
elif t == "result":
session_id = evt.get("session_id")
cost = evt.get("total_cost_usd")
is_error = evt.get("is_error", False)
print(f"\n[done] session={session_id} cost=${cost} error={is_error}", flush=True)
if state_path:
with open(state_path, "w") as f:
json.dump({"session_id": session_id, "cost": cost,
"is_error": is_error}, f)
sys.exit(1 if is_error else 0) # 退出码驱动运行器
设计要点:session_id 既在 init 又在 result 里取(后者更权威),保证即使中途崩了 init 也兜住。
run.sh(运行器)#!/usr/bin/env bash
set -uo pipefail
PROMPT="${1:?用法: ./run.sh \"<任务>\" [额外 claude 参数]}"
STATE=".agent_state.json"
RESUME=""
# 有上次会话就续跑,没有就新开
if [[ -f "$STATE" ]] && SID=$(jq -r '.session_id // empty' "$STATE") && [[ -n "$SID" ]]; then
echo "↩ 续跑会话 $SID"
RESUME="--resume $SID"
else
echo "✦ 新建会话"
fi
# --bare:跳过 hooks/skills/plugins/MCP/CLAUDE.md,保证多机一致(官方推荐脚本默认)
# 注意 bare 跳过 OAuth,鉴权须来自 ANTHROPIC_API_KEY
# shellcheck disable=SC2086
claude --bare -p "$PROMPT" $RESUME \
--output-format stream-json \
--verbose \
--include-partial-messages \
--permission-mode acceptEdits \
${2:-} \
| python3 stream_parse.py "$STATE"
code=$?
if (( code != 0 )); then
echo "✗ 失败(退出码 $code)。状态已存于 $STATE,重跑本命令即可从断点续上。"
exit 1
fi
echo "✓ 完成。会话 $(jq -r '.session_id' "$STATE") 已记录,累计成本 \$$(jq -r '.cost' "$STATE")。"
chmod +x run.sh stream_parse.py
# 第一次:新开会话
./run.sh "把 src/ 下所有 console.log 换成项目统一的 logger,并跑一遍测试"
# 中途被限流/超时/你按了 Ctrl+C —— 状态已落盘,直接重跑:
./run.sh "继续刚才的任务"
第二次执行时,run.sh 读到 .agent_state.json 里的 session_id,自动加 --resume,Claude 带着完整上下文从断点继续。stream_parse.py 在终端实时打印 [tool] Edit -> ...、[retry] 第2/10次 rate_limit ...,你看得见代理在干什么、卡在哪。
--bare vs 非 bare:bare 是脚本/SDK 的官方推荐模式,快且确定(队友 ~/.claude 里的 hook、项目 .mcp.json 都不会偷偷跑)。代价是不自动加载 CLAUDE.md/技能。需要项目上下文时,要么在 prompt 里显式说明,要么用 --settings/--append-system-prompt 注入,要么去掉 --bare。acceptEdits 的边界:它放行文件写入和 mkdir/touch/mv/cp 等,但其它 shell 命令和网络请求仍需 --allowedTools 或 permissions.allow,否则一执行就中止。需要 npm test 这类,补 --allowedTools "Bash(npm *)".claude -p 里若起了 dev server,Claude 返回后约 5 秒宽限期就杀掉该 shell。v2.1.182 起对卡住的子代理等待上限默认 10 分钟,用 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 调。运行器若依赖后台进程,别指望它跨次续跑存活。total_cost_usd 累计,必要时 /compact 后再续。flowchart TD
A["运行 ./run.sh '任务'"] --> B{".agent_state.json 存在?"}
B -- 是 --> C["读取上次 session_id"]
C --> D["claude --resume SID 续跑"]
B -- 否 --> E["claude 新开会话"]
D --> F["stream-json 实时事件流"]
E --> F
F --> G["stream_parse.py 解析"]
G --> H["打印 init/tool/retry/文本"]
H --> I["捕获 result 事件"]
I --> J["写 session_id 到状态文件"]
J --> K{"is_error?"}
K -- 是 --> L["退出非 0,等待重跑"]
K -- 否 --> M["✓ 完成"]
L --> B
整条链路的精髓:状态文件 .agent_state.json 是断点续跑的"存档点",失败不丢上下文,重跑即可续,闭环。
--resume 粒度更细。--from-pr)。| 维度 | 评估 |
|---|---|
| 适合训练营 | DevOps / SRE 自动化营、AI 工程实战营、CI/CD 进阶营 |
| 学员基础 | 已会用 claude -p 跑过单次任务,懂 Bash 与 JSON |
| 教学时长 | 2.5–3 小时(含动手搭建运行器 + 触发一次限流重跑续跑) |
| 难点 | stream-json 事件类型识别、bare 模式鉴权、permission-mode 边界 |
| 验收标准 | 学员能演示"任务跑到一半被中断 → 重跑命令 → 代理从断点带上下文续上",并能从日志指出至少一次 retry 事件 |
--continue/--resume/--fork-session/命名/transcript 落盘。-p/--bare/--output-format/--json-schema/续跑与权限模式。stream-json 事件流与程序化消费。--resume 的 CI/CD 脚本套路。--input-format stream-json undocumented — 双向程序化驱动的文档缺口说明,提示何时该转用 Agent SDK。[!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 等场景全覆盖。
