本教程聚焦 Claude Code 自动化能力的"耐久性"这一被低估的维度:如何构建一个能够断点续传的批处理智能体——当任务运行到一半遭遇限流、崩溃、网络中断或人为 Ctrl+C 时,重跑可以从上次的"存盘点"无缝接续,而非从零重来。我们将把 -p 无头模式、--resume/--continue 会话续传、Stop 生命周期钩子三者组装成一条容错流水线,并以"批量生成模块文档"为完整实战项目落地。适合已经写过 Skill 或 MCP、想用 Claude Code 自动化处理成百上千个文件/任务、却苦于长任务中途夭折的中高级开发者。
claude --version 有输出)-p(--print)无头模式的基本用法jq(用于读写 JSON 进度文件与解析钩子输入)mapfile,macOS 需 brew install bash 升级)很多人第一次写 Claude 批量脚本,都会写出这样的循环:
for f in $(find src -name '*.py'); do
claude -p "给 $f 写文档" > docs/$(basename $f .py).md
done
这在 10 个文件时没问题。但当目录里有 800 个文件,跑到第 513 个时遇到 API 限流——脚本退出,前 512 个的成果还在文件里,但脚本不知道哪些做完了。盲目重跑会重复消耗 token(且覆盖已生成的好结果);人工挑出未完成的又耗时。痛点不在于"会不会跑",而在于"记不记得自己跑到哪了"。
这正是工业级批处理与玩具脚本的分水岭:外部化状态(externalized state)+ 幂等执行(idempotency)+ 崩溃恢复(crash recovery)。下面用 Claude Code 的三个原生能力把它们拼起来。
progress.json 把进度"存盘"核心思想:每处理完一个任务项,立刻把它的状态写进一个 JSON 文件。这个文件就是"存盘点"。
先初始化进度文件。把所有待处理文件扫描进来,状态置为 pending:
#!/usr/bin/env bash
# init-progress.sh —— 扫描源码,生成进度骨架
SRC_DIR="${1:-src}"
PROGRESS="progress.json"
find "$SRC_DIR" -name '*.py' | sort \
| jq -R . \
| jq -s 'map({path: ., status: "pending", attempts: 0}) | {files: ., updated: "init"}' \
> "$PROGRESS"
echo "已登记 $(jq '.files|length' "$PROGRESS") 个待处理文件"
预期输出:
已登记 12 个待处理文件
progress.json 的结构:
{
"files": [
{ "path": "src/auth.py", "status": "pending", "attempts": 0 },
{ "path": "src/cache.py", "status": "pending", "attempts": 0 }
],
"updated": "init"
}
关键设计点:
- status 取值 pending / done / failed——幂等判断的依据
- attempts 记录尝试次数——可用于失败重试上限保护,避免某个坏文件把整个任务拖死
- 用 jq -R . | jq -s 把多行路径"打包成数组",比写 for 循环拼字符串更不易出错
驱动脚本只挑 pending 项处理,每完成一个就原子地更新进度。所谓"原子",指先写临时文件再用 mv 替换——避免写到一半被打断导致进度文件损坏(这是断点续传里最常见的隐性 bug):
#!/usr/bin/env bash
# runner.sh —— 断点续传批处理驱动器
set -euo pipefail
SRC_DIR="src"
DOCS_DIR="docs"
PROGRESS="progress.json"
mkdir -p "$DOCS_DIR"
# 提取所有 pending 项的数组下标(用下标而非路径,便于 jq 原位更新)
mapfile -t IDX < <(jq -r '.files | to_entries[] | select(.value.status=="pending") | .key' "$PROGRESS")
for idx in "${IDX[@]}"; do
file=$(jq -r ".files[$idx].path" "$PROGRESS")
out="$DOCS_DIR/$(basename "${file%.py}").md"
echo "▶ [$idx] $(basename "$file") → $out"
# 幂等兜底:输出文件已存在则直接标记完成,跳过 LLM 调用
if [[ -f "$out" ]]; then
jq --argjson i "$idx" '.files[$i].status="done"' "$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
continue
fi
# —— 核心:无头单次调用 ——
if claude -p \
"阅读 $(realpath "$file"),生成简洁中文模块文档写入 $out,包含:职责、导出的函数/类清单、一个典型用法示例。" \
> "/tmp/doc-$idx.log" 2>&1; then
# 成功:状态置 done,尝试次数 +1
jq --argjson i "$idx" '.files[$i].status="done" | .files[$i].attempts += 1' \
"$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
echo " ✓ 完成"
else
# 失败(限流/网络/Ctrl+C):只累加 attempts,不改状态——下次重跑自动重试
jq --argjson i "$idx" '.files[$i].attempts += 1' \
"$PROGRESS" > "$PROGRESS.tmp" && mv "$PROGRESS.tmp" "$PROGRESS"
echo " ✗ 失败,已记 attempt,将在下次运行时重试(当前无需手动干预)"
fi
done
echo "进度:$(jq '[.files[]|select(.status=="done")]|length' "$PROGRESS") / $(jq '.files|length' "$PROGRESS")"
运行演示——故意制造中断:
$ ./runner.sh
▶ [0] auth.py → docs/auth.md
✓ 完成
▶ [1] cache.py → docs/cache.md
^C # 模拟限流或主动打断
$ ./runner.sh # 再次运行
▶ [1] cache.py → docs/cache.md # 自动从未完成的 cache.py 续跑
✓ 完成
▶ [2] db.py → docs/db.md
✓ 完成
进度:3 / 12
注意第二次运行 auth.py 没有被重新调用——因为 runner.sh 一开始已经把它的状态写成 done。即使状态没及时写(极端情况),第二道幂等兜底([[ -f "$out" ]])也会拦截重复工作。双保险是耐久性脚本的设计准则。
常见问题:为什么不用
set -e直接让循环失败即停? 因为set -e配合if判断时不会触发退出(命令在if条件位时失败码被"消费")。这里正是利用了这一特性:失败走else分支记录而非崩溃,由你决定是否重跑。
--resume 让"单条长会话"也能续命步骤二是无状态模型——每个文件一次独立的 -p 调用,彼此不共享上下文。它最健壮,但 token 开销大(每次都重新加载系统提示),且无法利用"我已经看过上一个文件"的跨文件理解能力。
另一种是有状态模型:用一个长会话连续处理多个文件,共享上下文。它的续命工具是 --resume(接指定会话 ID)和 --continue(续 cwd 内最近一次会话)。
关键事实:claude -p --output-format json 的返回体里自带 session_id,这正是续传的钥匙:
# 首次:开一个长会话处理一批文件,并记下会话 ID
RES=$(claude -p --output-format json --max-turns 40 \
"逐个处理 progress.json 中 status=pending 的文件,每完成一个就更新该文件状态为 done,把结果写入 docs/。完成后告诉我处理到第几个。")
echo "$RES" | jq -r '.session_id' > .session-id
echo "$RES" | jq -r '.result'
预期输出(截断示意):
{ "type": "result", "subtype": "success",
"session_id": "a1b2c3d4-5678-...", // ← 存起来
"result": "已处理 auth.py、cache.py、db.py……共 4 个,余 8 个。" }
被打断后,用存下来的会话 ID 一键续命——上下文不丢:
SID=$(cat .session-id)
claude -p --output-format json --resume "$SID" --max-turns 40 \
"继续处理 progress.json 中剩余的 pending 文件,规则同前。"
两种模型的取舍:
| 维度 | 无状态(步骤二) | 有状态(本步骤) |
|---|---|---|
| 健壮性 | 极高,单项失败不影响其他 | 中等,会话损坏需换新会话 |
| 上下文共享 | 无 | 有,跨文件理解更连贯 |
| Token 成本 | 较高(重复加载) | 较低 |
| 适用场景 | 文件彼此独立 | 任务有强先后依赖、需要累积理解 |
常见问题:
--continue和--resume怎么选?--continue(-c)续"当前目录最近一次会话",无需记 ID,适合人在终端临时恢复;--resume(-r)续指定会话,适合脚本里精准恢复。批量脚本一律用--resume,避免续错会话。
Stop 钩子做"运行台账"与自动接力钩子是 Claude Code 生命周期里的"事件回调"。Stop 在每次响应结束、即将把控制权交还时触发,并通过 stdin 传入一段 JSON(含 session_id、transcript_path、hook_event_name)。我们用它做两件事:审计台账、可控自动接力。
先在仓库根目录建 .claude/settings.json(项目级配置,随仓库走):
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": "bash scripts/on-stop.sh" }
]
}
]
}
}
台账脚本 scripts/on-stop.sh——记录每次停止,便于事后复盘与对账:
#!/usr/bin/env bash
# on-stop.sh —— Stop 钩子:写运行台账
input=$(cat) # 必须读取 stdin,否则管道阻塞
sid=$(echo "$input" | jq -r '.session_id // "?"')
ts=$(date -u +%FT%TZ)
done_n=$(jq '[.files[]|select(.status=="done")]|length' progress.json 2>/dev/null || echo 0)
total=$(jq '.files|length' progress.json 2>/dev/null || echo 0)
echo "{\"ts\":\"$ts\",\"session\":\"$sid\",\"progress\":\"$done_n/$total\"}" >> run.log
钩子的第二种、也更强大的用法是自动接力:让 Stop 返回一个"阻止停止"的决策,把一段指令喂回给 Claude,使其继续工作。机制是钩子向 stdout 输出 JSON:
#!/usr/bin/env bash
# on-stop-continue.sh —— 还有 pending 就让 Claude 接着干
done_n=$(jq '[.files[]|select(.status=="done")]|length' progress.json)
total=$(jq '.files|length' progress.json)
if [ "$done_n" -lt "$total" ]; then
# 关键:decision=block 会让 Claude 继续这一轮,reason 作为新指令注入
printf '{"decision":"block","reason":"还有 %d 个文件未处理,读取 progress.json 找到下一个 pending 项继续生成文档。全部完成后再结束。"}\n' $((total - done_n))
else
echo '{}' # 全部完成,正常放行
fi
flowchart TD
A["runner.sh / 长会话启动"] --> B{"Claude 处理一批文件"}
B --> C["完成一个 → 原子更新 progress.json"]
C --> D["Stop 钩子触发"]
D --> E{"on-stop.sh 判断"}
E -- "仍有 pending" -->|"decision:block + reason"| B
E -- "全部 done" --> F["放行停止"]
B -. "限流/Ctrl+C/崩溃" .-> G["进程退出,进度已存盘"]
G --> H["重跑 runner.sh 或 claude -p --resume SID"]
H --> B
D --> I["写 run.log 台账"]
注意自动接力有死循环风险。两道保险必须配齐:
1. --max-turns N:硬上限,到点强制停。
2. 读取钩子输入里的 stop_hook_active 字段——当它在同一停止事件内为 true 时不要再 block,避免嵌套。Claude Code 本身也对此有保护,但显式判断更稳。
把这套机制放进 CI,就能处理"夜间批量给全仓生成 API 文档"这类长任务。CI 的特点是单 job 有时限、可能因配额重试,续传不是可选项而是刚需。
# .github/workflows/batch-docs.yml(节选)
jobs:
gen-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: actions/setup-node@v4
- run: npm i -g @anthropic-ai/claude-code
- env: { ANTHROPIC_API_KEY: '${{ secrets.ANTHROPIC_API_KEY }}' }
run: |
# 进度文件 progress.json 随仓库或作为 artifact 持久化
./init-progress.sh src
# 限时:跑到哪算哪,下次 CI 重跑自动续
timeout 50m ./runner.sh || true
# 把更新后的 progress.json 提交回去,供下次续传
git config user.email ci@bot && git config user.name ci-bot
git add progress.json docs/ && git commit -m "docs: batch update (resumable)" && git push || true
timeout 50m ... || true 是关键:到点强杀也不报错退出,因为进度已落盘;下次 workflow 触发时,runner.sh 从 progress.json 续跑。把 progress.json 提交回仓库,就实现了跨 CI 运行的真·断点续传。
完整目录结构:
my-service/
├── src/ # 12 个 .py 模块
│ ├── auth.py
│ ├── cache.py
│ └── ...
├── scripts/
│ ├── init-progress.sh
│ ├── runner.sh
│ ├── on-stop.sh
│ └── on-stop-continue.sh
├── .claude/
│ └── settings.json # Stop 钩子配置
├── docs/ # 生成产物
├── progress.json # 存盘点
└── run.log # 运行台账
端到端跑通:
cd my-service
./scripts/init-progress.sh src # ① 登记 12 个文件
./scripts/runner.sh # ② 开跑(途中 Ctrl+C 模拟崩溃)
./scripts/runner.sh # ③ 续跑,自动跳过已完成项
cat run.log # ④ 查台账,核对每次停止点
jq '.files[] | {path, status, attempts}' progress.json # ⑤ 看明细
一个健壮性自测:故意删掉 docs/cache.md 但保留 progress.json 里 cache.py: done。重跑 runner.sh,步骤二里的幂等兜底([[ -f "$out" ]])会发现输出丢失,重新生成而非假装完成。这就是"状态与产物一致性"的双向校验,值得作为长期维护脚本的标准动作。
上述模式可以泛化为处理任何"一组输入 → 一组输出"的任务:代码迁移、日志归因、测试补全、Markdown 翻译……抽象出三个不变量即可复用:
status + attempts + 原子写)把 runner.sh 里的"生成文档"那一行 claude -p 换成你的任务提示,其余结构原样不动。建议进一步封装成自定义斜杠命令(.claude/commands/resume.md),把"读 progress.json → 处理下一项 → 更新"的指令固化,让人在交互模式里用 /resume 也能驱动同一套续传逻辑,实现交互与无头共用一份状态。
--continue/--resume/fork 的权威语义-p、--output-format、--max-turns 与流式 JSONStop 钩子 decision:block 自动接力的可复制片段--continue/--resume/--fork 会话管理的官方权威说明-p 无头模式、--output-format json、--max-turns 程序化调用参考Stop 等生命周期钩子的 JSON 输入输出 schema 与 decision:block 语义Stop 钩子自动接力的可复制配置与防死循环实践[!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 等场景全覆盖。
