首页 › tutorials › 会话续跑:造可重启自主代理

概述

本教程解决一个真实痛点:用 claude -p 跑一个长任务(重构整个模块、批量改文档、生成测试套件),跑到一半限流了、CI 超时了、或者你手滑 Ctrl+C 了——上下文全丢,只能从头再来。我们将用会话续跑(--resume/--continue)+ 流式输出解析(stream-json)搭一个"可重启、可观测"的自主代理运行器:每次运行自动捕获 session_id,失败后重跑命令即可从断点续上,全程实时打印进度、工具调用与限流重试事件。适合做夜间批处理、CI 长任务、大型重构的工程师。

前置条件

export ANTHROPIC_API_KEY=sk-ant-...
claude --version   # 确认版本

第一部分:先搞懂"会话"是什么

Claude Code 把每一次对话存成一条会话(session),绑定到当前项目目录,随着你工作持续写入本地 transcript 文件。会话有两层价值,本教程吃的是第二层:

  1. 交互层:/clear 后还能 /resume 找回来;
  2. 脚本层:claude -p 跑的非交互任务也有 session_id,可以被同一条命令再次 --resume。

关键事实(官方文档原文):

Sessions created with claude -p or the Agent SDK do not appear in the session picker, but you can still resume one by passing its session ID to claude --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。所以运行器脚本必须每次在同一目录执行——把状态文件放在项目根,配合固定的工作目录即可避免。

步骤 1:给会话起名字

长任务并行时,命名是找回会话的最快方式:

# 启动时命名
claude -n auth-refactor

# 会话内改名
/rename auth-refactor

# 选择器里高亮后按 Ctrl+R

命名后,claude --resume auth-refactor 比 --resume abc123... 可读得多。

预期结果:--resume <name> 命中唯一会话则直接进入;名字冲突时打开选择器并预填搜索词。

常见问题:名字在多 worktree 间重复会变"歧义",选择器会要求你二选一。


第三部分:从 JSON 输出里"捞"出 session_id

续跑的前提是先拿到 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"的习惯。

步骤 2:续跑同一条会话

拿到 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 恢复的是对话记忆。所以"跑了一半挂掉 → 重跑续上"这条链路是成立的。


第四部分:用 stream-json 做"可观测性"

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)。


第五部分:拿结构化结果(--json-schema)

除了自然语言,还能强制 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"]}

运行器里常用这招:让代理自己产出"任务是否完成 / 还差什么"的结构化结论,脚本据此决定要不要续跑下一轮。


第六部分:分支会话做 A/B 方案对比

想在不污染主线的前提下试另一种思路?分支(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 # 上次会话状态(自动生成)

文件 1: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 也兜住。

文件 2: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 ...,你看得见代理在干什么、卡在哪。

关键权衡与陷阱


工作流程图

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 是断点续跑的"存档点",失败不丢上下文,重跑即可续,闭环。


延伸阅读


训练营适用性评估

维度 评估
适合训练营 DevOps / SRE 自动化营、AI 工程实战营、CI/CD 进阶营
学员基础 已会用 claude -p 跑过单次任务,懂 Bash 与 JSON
教学时长 2.5–3 小时(含动手搭建运行器 + 触发一次限流重跑续跑)
难点 stream-json 事件类型识别、bare 模式鉴权、permission-mode 边界
验收标准 学员能演示"任务跑到一半被中断 → 重跑命令 → 代理从断点带上下文续上",并能从日志指出至少一次 retry 事件

参考来源

  1. Manage sessions — Claude Code Docs — 官方会话管理,覆盖 --continue/--resume/--fork-session/命名/transcript 落盘。
  2. Run Claude Code programmatically — Claude Code Docs — 官方 Headless 文档,覆盖 -p/--bare/--output-format/--json-schema/续跑与权限模式。
  3. Claude Code --continue and --resume: Never Lose Your Context Again — 续跑机制的完整实战指南。
  4. Claude Code stream-json: the output format that changes everything — 深入解析 stream-json 事件流与程序化消费。
  5. Mastering Claude Code Sessions: -continue & -resume — 含 4 个捕获 session ID 传给 --resume 的 CI/CD 脚本套路。
  6. GitHub Issue #24594: --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 等场景全覆盖。

51学通信知识星球海报-扫码加入