首页 › tutorials › 断点续跑:打造自愈式Agent实战

断点续跑:用 session 续接打造崩溃自愈的 Claude Agent

概述

把 claude -p 丢进 CI 或定时任务后,最痛的不是它跑得慢,而是跑到一半断在网络抖动、限流(rate_limit)、模型过载(overloaded)或账单异常上,全部上下文归零,从头再来。本教程聚焦一个被多数自动化教程忽略的能力:会话持久化与断点续跑。你将学会用 --output-format json 捕获 session_id、用 --resume 在崩溃后接续同一对话、用 StopFailure 钩子做失败观测、并写一个能在 API 错误后自动续跑的"自愈式"包装脚本。适合已经会用 -p 跑一次性任务、想把 Claude Code 真正塞进长流程自动化的工程师。

前置条件


第一步:先搞清楚"状态"到底存在哪

很多人写自动化脚本失败,是因为默认 claude -p 是"无状态"的——其实不是。每一段对话都被持续落盘成 JSONL 转录文件:

~/.claude/projects/<把工作目录路径编码后的串>/<session-id>.jsonl

也就是说,你的工作目录路径决定了会话存到哪个项目桶里。这带来三条铁律,后面每一步都绕不开:

  1. 续跑必须从同一个目录发起,否则会得到 No conversation found with session ID: ...。会话查找的作用域是"当前项目目录及其 git worktree",换个不相关的目录就找不到。
  2. -p 创建的会话不会出现在 /resume 选择器里,只能用 --resume <session-id> 按 ID 续。这是脚本场景最常踩的坑——你不能指望人去选择器里点。
  3. 转录文件默认 30 天后被清理(cleanupPeriodDays 默认 30)。如果你的巡检要跨月,要么调大这个值,要么把状态目录挪到不会被清的地方。

记住这三点,后面的设计就顺了。整张状态流转如下:

flowchart LR
    subgraph 一次claude-p调用
      A["claude -p '任务'<br/>--output-format json"] -->|stdout| B["JSON: result / session_id /<br/>is_error / stop_reason"]
    end
    B --> C["解析出 session_id<br/>写入状态文件"]
    C --> D[("~/.claude/projects/<cwd>/<sid>.jsonl<br/>持续落盘的对话转录")]
    D -.崩溃后.-> E["claude -p '继续' --resume SID"]
    E --> B

第二步:第一次运行,并把 session_id 落盘

自愈的前提是"能找回现场"。--output-format json 会在响应里带上 session_id,把它捕获下来:

# 跑一次审查任务,拿到 session_id 并落盘
out=$(claude -p "审查 src/ 目录里的性能问题,先列出 top 5" \
  --allowedTools "Read,Grep,Glob" \
  --output-format json)

# 提取关键字段
session_id=$(jq -r '.session_id'    <<<"$out")
cost=$(jq -r '.total_cost_usd // 0' <<<"$out")
result=$(jq -r '.result'            <<<"$out")

# 落盘,供后续续跑或排障使用
echo "$session_id" > .last-session
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$session_id" "$cost" >> .agent-runs.tsv

echo "$result"
echo "session_id = $session_id (已保存到 .last-session)"

预期结果:out 是一个 JSON 对象,至少包含 result(文本结论)、session_id(UUID)、total_cost_usd(本次花费)。.last-session 里写入了这次对话的 ID。

常见问题:


第三步:续跑——--continue vs --resume <id>

拿到 session_id 后,续跑有两条路,别用混:

命令 行为 适用场景
claude -p "..." --continue 续接当前目录最近一次会话 单线推进、你确定没别的并发会话
claude -p "..." --resume <id> 续接指定会话 脚本场景、多仓库并发——唯一可靠的选择
claude --from-pr <url> 续接与某个 PR 绑定的会话 CI 里按 PR 续跑

脚本里永远用 --resume <id>,因为 --continue 的"最近一次"在并发或重试时会被别的调用顶掉,不可靠:

session_id=$(cat .last-session)
claude -p "刚才你列了 top 5,现在重点看数据库查询那两条,给出优化方案" \
  --resume "$session_id" \
  --allowedTools "Read,Grep,Glob" \
  --output-format json | jq -r '.result'

续接后,上一轮的上下文(读过的文件、得出的中间结论)都在,模型不会重复劳动——这正是省 token、省时间的关键。

给会话起名(强烈推荐):用 -n <name> 或 /rename 命名,--resume <name> 就能按名续,且名字会在同仓库的 worktree 间解析:

claude -n dep-upgrade-api -p "升级 services/api 的依赖" --output-format json | jq -r .session_id
# 之后任意位置、同仓库内:
claude -p "继续" --resume dep-upgrade-api

常见问题:


第四步:用 StopFailure 钩子观测每一次崩溃

续跑能力有了,但"什么时候该续"得先能发现。StopFailure 钩子专门在"一轮因 API 错误而结束"时触发,并且 matcher 可以精确到错误类型:

matcher 值 含义
rate_limit 触发限流,通常等一会能好
overloaded 模型过载,服务端 529 类
server_error 服务端错误
billing_error 账单/额度问题——续跑也没用,要人工
authentication_failed 鉴权失败
max_output_tokens 单轮输出超长

在 .claude/settings.json(项目级,可提交)里挂一个失败记录钩子:

{
  "hooks": {
    "StopFailure": [
      {
        "matcher": "rate_limit|overloaded|server_error",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/on-failure.sh",
            "args": []
          }
        ]
      }
    ]
    ,
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/on-stop.sh", "args": [] }
        ]
      }
    ]
  }
}

StopFailure 的关键特性:它的输出和退出码会被忽略——纯做副作用(日志、告警、打标记)。钩子脚本从 stdin 收到 JSON(含 session_id、transcript_path 等公共字段),把"需要续跑"的标记写出来:

#!/usr/bin/env bash
# .claude/hooks/on-failure.sh  —— chmod +x
input=$(cat)
sid=$(jq -r '.session_id'         <<<"$input")
evt=$(jq -r '.hook_event_name'    <<<"$input")
ts=$(date -u +%FT%TZ)

# 写失败日志
printf '%s\t%s\t%s\n' "$ts" "$sid" "$evt" >> "${CLAUDE_PROJECT_DIR}/.agent-failures.log"

# 打一个"待续跑"标记,供包装脚本或定时器读取
echo "$sid" > "${CLAUDE_PROJECT_DIR}/.resume-needed"
exit 0

on-stop.sh(正常结束时清掉待续跑标记、记录成本):

#!/usr/bin/env bash
# .claude/hooks/on-stop.sh  —— chmod +x
input=$(cat)
sid=$(jq -r '.session_id' <<<"$input")
rm -f "${CLAUDE_PROJECT_DIR}/.resume-needed"
printf '%s\t%s\tCLEAN_STOP\n' "$(date -u +%FT%TZ)" "$sid" >> "${CLAUDE_PROJECT_DIR}/.agent-runs.tsv"
exit 0

预期结果:一旦某轮撞上 rate_limit/overloaded/server_error,.agent-failures.log 多一行、.resume-needed 里写下 session_id;正常结束则标记被清。

常见问题:

⚠️ 注意 Stop/UserPromptSubmit/PostToolBatch 等不支持 matcher,写了会被静默忽略;StopFailure 才支持错误类型 matcher。


第五步:核心——一个会自动续跑的"自愈式"包装脚本

把前面几步合起来:包装脚本捕获 session_id,一旦检测到 is_error 或进程异常退出,就用 --resume 把同一个会话接续下去,并告诉它"上次中断了,从断点继续,别重复已完成的步骤"。

#!/usr/bin/env bash
# selfheal.sh —— 在 API 错误后自动用 --resume 续跑,直到完成或超上限
set -uo pipefail

PROMPT="$1"
MAX_ATTEMPTS="${2:-5}"
SESSION_ID=""
PERMISSIONS="Read,Edit,Bash(git *),Bash(npm test)"

for attempt in $(seq 1 "$MAX_ATTEMPTS"); do
  if [[ -z "$SESSION_ID" ]]; then
    # 首次运行
    out=$(claude -p "$PROMPT" \
            --permission-mode acceptEdits \
            --allowedTools "$PERMISSIONS" \
            --output-format json 2>.selfheal.err)
  else
    # 续跑:明确告知中断、要求接着干
    out=$(claude -p "上一次运行因错误中断(第 ${attempt} 次重试)。请从断点继续,不要重复已完成的工作。" \
            --resume "$SESSION_ID" \
            --permission-mode acceptEdits \
            --allowedTools "$PERMISSIONS" \
            --output-format json 2>.selfheal.err)
  fi

  # 进程本身崩了(OOM/网络断/SIGKILL),没有 JSON
  if ! jq -e . >/dev/null 2>&1 <<<"$out"; then
    echo "⚠️ 第 $attempt 次:进程异常退出(见 .selfheal.err)" >&2
    sleep $((attempt * 5))
    continue
  fi

  SESSION_ID=$(jq -r '.session_id // ""' <<<"$out")
  is_error=$(jq -r '.is_error // false'   <<<"$out")
  reason=$(jq -r '.stop_reason // ""'      <<<"$out")

  if [[ "$is_error" == "false" ]]; then
    jq -r '.result' <<<"$out"
    exit 0
  fi

  echo "⚠️ 第 $attempt 次失败 (stop_reason=$reason),session=$SESSION_ID,退避后续跑..." >&2
  sleep $((attempt * 5))   # 简单指数退避
done

echo "❌ 超过最大重试次数 $MAX_ATTEMPTS。最终会话:$SESSION_ID" >&2
echo "手动续跑:claude --resume $SESSION_ID" >&2
exit 1

调用:

./selfheal.sh "把 auth 模块的老式回调改写成 async/await,并保证 npm test 全绿"

预期结果:哪怕中途撞上限流,脚本也会在退避后用同一个 session_id 续上,模型接着干而不是从头改。.selfheal.err 留有每次的 stderr 便于排障。

设计要点:

  1. 续跑提示要"反重复":明确写"从断点继续,不要重复已完成的工作"。否则续跑时模型可能把已改的文件又读一遍、又改一遍,浪费 token。
  2. 退避:rate_limit/overloaded 多半等一会就好,线性/指数退避比硬刚强。
  3. --resume 与权限模式独立:--resume 只恢复对话上下文;--allowedTools/--permission-mode 仍要每次显式传。脚本里我固定传 acceptEdits+白名单,保证续跑后权限行为一致。

常见问题:


第六步:用 stream-json 的 api_retry 事件做退避可视化

--output-format stream-json 会在 API 重试时发出 system/api_retry 事件,字段含 attempt、max_retries、retry_delay_ms、error 类别。你可以一边跑一边把重试进度打到看板:

claude -p "生成迁移脚本" \
  --output-format stream-json --verbose \
  | jq -rc 'select(.type=="system" and .subtype=="api_retry")
            | "[retry] #\(.attempt)/\(.max_retries) 等 \(.retry_delay_ms)ms — \(.error)"' \
  >> .retry-trace.log &

这样你能在 .retry-trace.log 里看到"第 2/10 次重试,等了 1200ms,原因是 rate_limit",据此判断是该继续等、还是该熔断告警。结合第五步的包装脚本,重试由包装脚本掌控(跨进程续跑),观测由 stream-json 提供(单进程内),两者互补。


第七步:硬化——命名、独立状态目录、保留期、可控输入

把自愈 Agent 放到生产前,再做四件事:

1. 给会话起稳定名字,别只靠 UUID。脚本里用业务前缀命名,便于人肉排障:

claude -n "patrol-$(basename "$PWD")-$(date +%Y%m%d)" -p "..." --output-format json

2. 长 batch 用独立的配置目录,避开 30 天清理。CLAUDE_CONFIG_DIR 把所有会话状态挪到一个不会被清、且易备份的位置;cleanupPeriodDays 调大保留期:

export CLAUDE_CONFIG_DIR=/data/agent-state
// .claude/settings.json
{ "cleanupPeriodDays": 90 }

3. 大输入走文件,别走管道。stdin 上限 10MB(v2.1.128)。把大日志/大 diff 写文件,prompt 里给路径:

git diff main > /tmp/d.diff
claude -p "审查 /tmp/d.diff 这个 diff 里的拼写和明显 bug" --allowedTools Read

4. 后台任务别撑住进程。v2.1.163 起,claude -p 结束后约 5 秒会回收它启动的后台 Bash 任务(如 dev server),给了个收尾宽限期。但若你故意要保留一个长驻服务,别指望它活过 -p 调用——那是设计如此。


实战案例:多仓库依赖升级巡检 Agent

把全部能力串起来:对一组仓库逐个跑"升级 patch 依赖 → 测试 → 提交",每个仓库独立捕获 session_id,崩溃自动续跑,最后出一张状态表。

#!/usr/bin/env bash
# repo-upgrade-patrol.sh
set -uo pipefail
ROOT=$(pwd)
REPOS=(services/api services/auth services/billing web/frontend)
STATE="$ROOT/.patrol-state.tsv"
PERMISSIONS="Read,Edit,Bash(npm install),Bash(npm test),Bash(git *)"
printf 'repo\tsession_id\tstatus\tattempts\n' > "$STATE"

upgrade_one() {
  local repo="$1" sid="" n=0
  cd "$ROOT/$repo" || { printf '%s\t-\tSKIP\t-\n' "$repo" >> "$STATE"; return; }

  while (( n < 3 )); do
    n=$((n+1))
    local out
    if [[ -z "$sid" ]]; then
      out=$(claude -n "patrol-$repo" -p \
            "升级 package.json 中所有 patch 版本依赖到最新,npm install 后跑 npm test,全绿就 git commit。" \
            --permission-mode acceptEdits --allowedTools "$PERMISSIONS" \
            --output-format json)
    else
      out=$(claude -p "上次中断,从断点继续,不要重复已完成步骤。" \
            --resume "$sid" --permission-mode acceptEdits \
            --allowedTools "$PERMISSIONS" --output-format json)
    fi

    jq -e . >/dev/null 2>&1 <<<"$out" || { sleep 5; continue; }
    sid=$(jq -r '.session_id // ""' <<<"$out")
    if [[ "$(jq -r '.is_error // false' <<<"$out")" == "false" ]]; then
      printf '%s\t%s\tOK\t%s\n' "$repo" "$sid" "$n" >> "$STATE"
      cd "$ROOT"; return 0
    fi
    sleep $((n*5))
  done
  printf '%s\t%s\tFAILED\t%s\n' "$repo" "$sid" "$n" >> "$STATE"
  cd "$ROOT"
}

for r in "${REPOS[@]}"; do upgrade_one "$r"; done
echo "===== 巡检结果 ====="; column -t -s$'\t' "$STATE"

FAILED 的仓库会留下 session_id,事后直接 cd services/billing && claude --resume <id> 人工介入即可——现场永远不丢。整体编排:

flowchart TD
    S([巡检脚本启动]) --> L{"遍历仓库列表"}
    L -->|每个 repo| R["cd repo; claude -p ... --output-format json"]
    R --> J{解析 JSON}
    J -->|is_error=false / 非 JSON 重试| OK["记 OK<br/>下一个仓库"]
    J -->|is_error=true| CAP["记 session_id"]
    CAP --> RT{重试<3?}
    RT -->|是| RES["claude -p '继续' --resume SID"]
    RES --> J
    RT -->|否| FAIL["记 FAILED + SID<br/>留待人工 --resume"]
    OK --> L
    FAIL --> L
    L -->|全部完成| T[[输出状态表]]

延伸阅读

训练营适用性评估

参考来源

  1. Run Claude Code programmatically (Headless) — 官方 -p/--bare/--output-format/--resume/--continue 与 stdin 上限、后台任务回收等行为权威说明。
  2. Manage sessions — 会话命名、--resume/--from-pr、作用域规则、JSONL 转录存储路径与 cleanupPeriodDays 保留期。
  3. Hooks reference — StopFailure/Stop 事件、matcher 规则、退出码(2 阻断/1 非阻塞)、--bare 跳过 hooks 的行为。
  4. Headless Mode and CI/CD — Tips (SFEIR Institute) — -p 标志、输出格式与多轮会话的 18 条实践技巧。
  5. How to resume, search, and manage Claude Code conversations — --continue/--resume 实操与会话历史检索方法。

[!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学通信知识星球海报-扫码加入