<!-- title: 造断点续传批量智能体撞崩可续 -->

## 概述

本教程聚焦 Claude Code 自动化能力的"**耐久性**"这一被低估的维度：如何构建一个能够**断点续传**的批处理智能体——当任务运行到一半遭遇限流、崩溃、网络中断或人为 `Ctrl+C` 时，重跑可以从上次的"存盘点"无缝接续，而非从零重来。我们将把 `-p` 无头模式、`--resume`/`--continue` 会话续传、`Stop` 生命周期钩子三者组装成一条容错流水线，并以"批量生成模块文档"为完整实战项目落地。适合已经写过 Skill 或 MCP、想用 Claude Code 自动化处理**成百上千个文件/任务**、却苦于长任务中途夭折的中高级开发者。

## 前置条件

- Claude Code CLI 已安装并能正常交互运行（`claude --version` 有输出）
- 了解 `-p`（`--print`）无头模式的基本用法
- 已安装 `jq`（用于读写 JSON 进度文件与解析钩子输入）
- Bash 4+（需 `mapfile`，macOS 需 `brew install bash` 升级）
- 一个包含若干源码文件的测试仓库（实战部分会用到 Python，可替换为任意语言）

## 为什么"能续传"才是批量任务的关键

很多人第一次写 Claude 批量脚本，都会写出这样的循环：

```bash
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`：

```bash
#!/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` 的结构：

```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）：

```bash
#!/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`**，这正是续传的钥匙：

```bash
# 首次：开一个长会话处理一批文件，并记下会话 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'
```

**预期输出（截断示意）：**

```json
{ "type": "result", "subtype": "success",
  "session_id": "a1b2c3d4-5678-...",       // ← 存起来
  "result": "已处理 auth.py、cache.py、db.py……共 4 个，余 8 个。" }
```

被打断后，用存下来的会话 ID 一键续命——上下文不丢：

```bash
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`（项目级配置，随仓库走）：

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "bash scripts/on-stop.sh" }
        ]
      }
    ]
  }
}
```

台账脚本 `scripts/on-stop.sh`——记录每次停止，便于事后复盘与对账：

```bash
#!/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：

```bash
#!/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
```

```text
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 里的"可恢复批处理"——把续传做成无人值守

把这套机制放进 CI，就能处理"夜间批量给全仓生成 API 文档"这类长任务。CI 的特点是单 job 有时限、可能因配额重试，**续传不是可选项而是刚需**。

```yaml
# .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 运行的真·断点续传**。

## 实战案例：给一个 12 模块的 Python 服务批量出文档

完整目录结构：

```
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               # 运行台账
```

端到端跑通：

```bash
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 翻译……抽象出三个不变量即可复用：

1. **可枚举的输入项**（文件、issue 编号、API 分页……）
2. **幂等的单项处理器**（同样输入必产同样输出，重复执行无副作用）
3. **持久化的进度状态**（`status` + `attempts` + 原子写）

把 `runner.sh` 里的"生成文档"那一行 `claude -p` 换成你的任务提示，其余结构原样不动。建议进一步封装成自定义斜杠命令（`.claude/commands/resume.md`），把"读 progress.json → 处理下一项 → 更新"的指令固化，让人在交互模式里用 `/resume` 也能驱动同一套续传逻辑，实现交互与无头共用一份状态。

## 延伸阅读

- Claude Code 官方《Manage sessions》——`--continue`/`--resume`/fork 的权威语义
- Claude Code 官方《Run Claude Code programmatically》——`-p`、`--output-format`、`--max-turns` 与流式 JSON
- Claude Code 官方《Hooks reference》——各生命周期事件的 JSON 输入输出 schema 与 exit code 约定
- 《Headless automation: add retry, verification, and resume》社区讨论——长任务恢复的真实痛点
- 《Claude Code Hooks Complete Guide (2026)》——`Stop` 钩子 `decision:block` 自动接力的可复制片段

## 训练营适用性评估

- **适用类型**：中高级工程师实战营、DevOps/平台工程训练营、AI 工程化进阶营。要求学员已具备基础 Shell 与 Claude Code 使用经验，不适合纯零基础（建议先完成入门与 Skill 两轮）。
- **教学时长**：约 4–5 学时。其中概念与三模式对比 1 学时，步骤一、二实操 1.5 学时，钩子与 CI 集成 1.5 学时，扩展封装与答疑 1 学时。
- **考核点**：能否在强制中断 3 次后，最终产物与 token 消耗与"一次跑完"基本一致（验证幂等+续传）；能否解释无状态与有状态两种模型各自的最佳场景。
- **延伸作业**：把模板套用到"批量把回调风格代码改写为 async/await"或"批量给 issue 补复现脚本"，要求产物可重复、进度可审计、崩溃可恢复。

## 参考来源

1. [Manage sessions — Claude Code Docs](https://code.claude.com/docs/en/sessions) — `--continue`/`--resume`/`--fork` 会话管理的官方权威说明
2. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — `-p` 无头模式、`--output-format json`、`--max-turns` 程序化调用参考
3. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — `Stop` 等生命周期钩子的 JSON 输入输出 schema 与 `decision:block` 语义
4. [Headless automation: add retry, verification, and resume](https://github.com/anthropics/claude-code/issues/28489) — 社区对长任务崩溃恢复与续传需求的实战讨论
5. [Claude Code Hooks Complete Guide (March 2026 Edition)](https://smartscope.blog/en/generative-ai/claude/claude-code-hooks-guide/) — `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 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
