首页 › tutorials › 有状态Skill开发:跨会话续跑

本教程解决一个被普遍忽视的难题:Skill 本身是无状态的。你会学到如何用文件系统 + 状态脚本 + Hook,在无状态架构上"搭出"一个能跨会话续跑的有状态 Skill,并做出一个可运行的批量迁移追踪器。


一、概述

Claude Code 的 Skill 是扩展能力的核心载体,但它有一个被多数教程回避的本质特性:Skill 在会话之间是无状态的——每次启动都是一张白纸。本教程教你突破这一限制。

学习目标

适用人群:已能创建基础 SKILL.md、希望做长流程自动化(批量重构、框架升级、CI 迁移、文档生成)的中高级开发者。


二、前置条件

项 要求
Claude Code v2.1.0+,CLI 可正常启动
基础 会写最简 SKILL.md(frontmatter + 正文)
运行时 Node.js 18+(Hook 脚本用)、jq(状态读写用)
心智模型 理解"上下文窗口""会话""Hook 事件"三个概念

不必精通 Hook,本教程会给出可直接复制的 settings.json 片段。


三、正文

步骤 1:先认清"无状态"这个底牌

很多 Skill 用着用着就"失忆"了:上一次迁移到一半,新开个会话它又从头开始。这不是 bug,而是架构设计。

官方文档与社区技术深度解析一致指出,Skill 具有以下本质特征:

这条最后的事实极其关键:把状态读写的复杂逻辑放进脚本,是"免费的"——因为脚本源码不占上下文,只有它打印的结果才占。这正是有状态 Skill 的工程支点。

预期结果:建立"Skill 无状态 → 用文件 + 脚本补状态"的心智模型。

常见问题

步骤 2:设计状态文件与目录骨架

我们要做一个 migration-tracker:把某个目录下的旧 API 调用批量迁移到新实现(例如 mysql_* → PDO),42 个文件,可能跨多个会话才能干完。

状态文件用一个简单 JSON,字段语义清晰、可被 jq 操作:

{
  "skill_version": "1.0.0",
  "created_at": "2026-06-15T10:00:00Z",
  "updated_at": "2026-06-15T11:30:00Z",
  "goal": "将 src/legacy 下所有 mysql_* 调用迁移到 PDO",
  "status": "in_progress",
  "total": 42,
  "done": 17,
  "queue": ["src/legacy/UserRepo.php", "src/legacy/OrderRepo.php"],
  "completed": ["src/legacy/Auth.php", "src/legacy/Session.php"],
  "current_file": "src/legacy/UserRepo.php",
  "current_line": 128,
  "lessons": ["mysql_query 第三参数是连接句柄,PDO 用 prepare+execute 替代"],
  "last_session_summary": "完成 Auth/Session,修复 3 处 SQL 注入"
}

目录骨架(标准分层,符合渐进披露):

.claude/skills/migration-tracker/
├── SKILL.md                  # 轻量指令,编排工作流
├── scripts/
│   ├── state.sh             # 状态读写(jq 实现,代码不进上下文)
│   ├── session-brief.js     # SessionStart hook:注入续跑简报
│   └── checkpoint.js        # Stop hook:落盘摘要
└── references/
    └── state-schema.md      # 字段语义详解(按需读取)

状态文件本身不放 Skill 目录里(那是"能力"定义),而放业务位置 .claude/migration-state/state.json,与代码一起进版本控制,团队共享进度。

步骤 3:编写状态读写脚本(核心)

这是整个 Skill 的"状态引擎"。用 jq 保证原子性与可组合性:

#!/usr/bin/env bash
# scripts/state.sh — migration-tracker 状态引擎
# 用法: state.sh <init|show|next|done|checkpoint|summary>
set -euo pipefail

STATE_DIR="${MIGRATION_STATE_DIR:-.claude/migration-state}"
STATE_FILE="$STATE_DIR/state.json"
mkdir -p "$STATE_DIR"

stamp() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }

case "${1:-help}" in
  init)
    goal="${2:?usage: state.sh init <goal>}"
    printf '%s' "{\"skill_version\":\"1.0.0\",\"created_at\":\"$(stamp)\",\"goal\":\"$goal\",\"status\":\"in_progress\",\"total\":0,\"done\":0,\"queue\":[],\"completed\":[],\"current_file\":null,\"current_line\":0,\"lessons\":[],\"last_session_summary\":null}" > "$STATE_FILE"
    echo "✓ state initialized at $STATE_FILE"
    ;;
  show) cat "$STATE_FILE" ;;
  enqueue)
    # 从 stdin 接收文件列表,并入 queue 并去重
    tmp=$(mktemp); cat > "$tmp"
    jq --slurpfile q "$tmp" '.queue = ((.queue + $q[0]) | unique) | .total = (.queue|length)' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"; rm -f "$tmp"
    echo "✓ enqueued, total=$(jq '.total' "$STATE_FILE")"
    ;;
  next)
    # 返回第一个"在 queue 但不在 completed"的文件
    jq -r '(.queue - .completed)[0] // "QUEUE_EMPTY"' "$STATE_FILE"
    ;;
  checkpoint)
    file="${2:?}"; line="${3:-0}"
    jq --arg f "$file" --argjson l "$line" --arg t "$(stamp)" \
      '.current_file=$f | .current_line=$l | .updated_at=$t' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ checkpoint $file:$line"
    ;;
  done)
    file="${2:?}"
    jq --arg f "$file" --arg t "$(stamp)" \
      '.completed += [$f] | .done = (.completed|length) | .current_file = null | .updated_at=$t' \
      "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ completed $file, progress=$(jq -r '.done + "/" + (.total|tostring)' "$STATE_FILE")"
    ;;
  summary)
    text="${2:?usage: state.sh summary <text>}"
    jq --arg s "$text" --arg t "$(stamp)" \
      '.last_session_summary=$s | .updated_at=$t' "$STATE_FILE" > "$STATE_FILE.new"
    mv "$STATE_FILE.new" "$STATE_FILE"
    echo "✓ summary saved"
    ;;
  *) echo "commands: init|show|enqueue|next|checkpoint|done|summary"; exit 1 ;;
esac

设计要点(幂等性)

预期输出

$ bash scripts/state.sh next
src/legacy/UserRepo.php
$ bash scripts/state.sh checkpoint src/legacy/UserRepo.php 128
✓ checkpoint src/legacy/UserRepo.php:128

常见问题

步骤 4:编写轻量 SKILL.md 指令

正文要"薄"——把复杂逻辑下沉到脚本,SKILL.md 只负责编排与触发词。description 是触发命中的唯一判据,必须写满场景与关键词:

---
name: migration-tracker
description: 跟踪并执行跨文件的代码迁移、批量重构、框架升级、命名/ API 批量替换等长流程任务。当用户说"批量迁移""重构整个目录""把 X 全部换成 Y""升级到新 API""续跑迁移""继续上次的迁移"时使用。维护跨会话状态文件,支持从断点恢复,不会重复处理已完成文件。
allowed-tools: Read, Edit, Bash, Glob, Grep
---

# 迁移追踪 Skill

## 核心铁律:无状态架构上的状态工程
你跨会话无记忆。本 Skill 用 `.claude/migration-state/state.json` 作为唯一事实源。
**绝不在内存里维护进度——一律落盘,下次一律先读盘。**

## 工作流
1. **恢复或初始化**:`bash scripts/state.sh show`
   - 报错"不存在"→ 询问迁移目标 → `scripts/state.sh init "<目标>"`,
     再用 `Glob`/`Grep` 扫描受影响文件,管道喂给 `scripts/state.sh enqueue`。
2. **取下一项**:`next=$(bash scripts/state.sh next)`。
   若返回 `QUEUE_EMPTY` → 置 `status=done`,汇总报告,结束。
3. **先落断点**:`bash scripts/state.sh checkpoint "$next" 0`(处理前)。
4. **执行**:`Read` 该文件 → `Edit` 完成迁移。
5. **标记完成**:`bash scripts/state.sh done "$next"`;遇到非平凡坑写入 lessons。
6. **回到第 2 步**循环。

字段语义见 `references/state-schema.md`(仅当需要扩展字段时再读)。

为什么这么薄? 因为 Stage 2 加载正文要占 token,正文越薄,常驻成本越低;真正的复杂度都在脚本里(Stage 3 按需,且代码不入上下文)。

常见问题

步骤 5:用 Hook 实现"开盖即续跑"

到这一步,Skill 已经能手动续跑了(新会话里说"继续迁移"即可)。但更优雅的是自动:新会话一开,自己把上次进度念出来。

Skill 不能自己注册 Hook——Hook 是 Harness 层的(settings.json),但 Skill 可以自带脚本,让用户一次性接线:

scripts/session-brief.js(SessionStart 事件,其 stdout 会作为上下文注入):

#!/usr/bin/env node
// SessionStart hook: 读取状态,输出续跑简报(stdout 注入到 Claude 上下文)
const fs = require('fs'); const path = require('path');
const f = path.join(process.cwd(), '.claude/migration-state/state.json');
if (!fs.existsSync(f)) process.exit(0);     // 无状态→静默,不污染新项目
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
if (s.status === 'done') process.exit(0);   // 已完成→不打扰
const remain = (s.queue.length || s.total) - s.done;
console.log(`[migration-tracker 续跑简报] 目标: ${s.goal}`);
console.log(`进度: ${s.done}/${s.total}, 剩余 ${remain} 项, 断点: ${s.current_file || '队首'}`);
if (s.last_session_summary) console.log(`上次: ${s.last_session_summary}`);
console.log(`说"继续迁移"即可从断点恢复。`);

scripts/checkpoint.js(Stop 事件,主线程结束时触发,可调度后台 agent 写摘要):

#!/usr/bin/env node
// Stop hook: 会话结束前提醒落盘摘要(这里仅做轻量提示,避免阻塞)
const fs = require('fs'); const path = require('path');
const f = path.join(process.cwd(), '.claude/migration-state/state.json');
if (!fs.existsSync(f)) process.exit(0);
const s = JSON.parse(fs.readFileSync(f, 'utf8'));
if (s.status !== 'in_progress') process.exit(0);
// stdout 在 Stop 事件会作为 system message 回传给模型,触发它执行 summary 落盘
console.log(`[migration-tracker] 本会话即将结束,进度 ${s.done}/${s.total}。请用 state.sh summary "<本次做了什么>" 落盘后再结束。`);

在 settings.json 接线(项目级,团队共享):

{
  "hooks": {
    "SessionStart": [
      { "matcher": "*", "hooks": [
        { "type": "command",
          "command": "node .claude/skills/migration-tracker/scripts/session-brief.js" }
      ]}
    ],
    "Stop": [
      { "matcher": "", "hooks": [
        { "type": "command",
          "command": "node .claude/skills/migration-tracker/scripts/checkpoint.js" }
      ]}
    ]
  }
}

接线后重启 Claude Code。Skill 文件与 Hook 的修改都不会热生效——发现扫描只在初始化时发生。

常见问题

步骤 6:测试与验证续跑

端到端验证清单:

  1. init → enqueue 喂 3 个测试文件 → show 确认 total=3。
  2. next 得文件 A → checkpoint → 手动 Edit → done A。
  3. Ctrl-C 中断 / 关掉会话 / 重启。
  4. 新会话一开,应自动看到 SessionStart 简报("进度 1/3,断点...")。
  5. 说"继续迁移",Skill 从文件 B 续跑,不会重做 A。
  6. 全部完成后 status=done,简报静默退出。

关键观察:done 集合是去重的、next 是幂等的——这就是续跑正确性的数学保证。


四、无状态 vs 有状态:一张表看清取舍

维度 原生 Skill(无状态) 本教程方案(有状态)
跨会话记忆 ❌ 无 ✅ 文件系统持久化
上下文成本 仅 metadata + 状态文件按需读
续跑能力 ❌ 从头开始 ✅ 断点恢复
复杂度 低 中(脚本 + Hook)
适用场景 单次、原子任务 长流程、批量、可中断
失败模式 无状态可丢 需防"半截 JSON"

判断准则:任务能在单会话内原子完成 → 用原生 Skill;任务必然跨会话、需要进度可见可恢复 → 上有状态方案。


五、实战案例:可运行的 migration-tracker

把上面零碎片段组装成一个完整可复现的最小项目(约 5 分钟跑通):

# 1. 建骨架
mkdir -p .claude/skills/migration-tracker/{scripts,references}
mkdir -p .claude/migration-state
mkdir -p src/legacy && echo '<?php mysql_query("SELECT 1");' > src/legacy/Auth.php

# 2. 落脚本(粘贴步骤3、步骤5 的三个文件)
chmod +x .claude/skills/migration-tracker/scripts/state.sh

# 3. 初始化状态 + 入队
bash .claude/skills/migration-tracker/scripts/state.sh init "mysql_* → PDO"
grep -rl 'mysql_' src/legacy | bash .claude/skills/migration-tracker/scripts/state.sh enqueue

# 4. 接 Hook(粘贴步骤5 的 settings.json),重启 Claude Code

# 5. 在 Claude Code 里说:"用 migration-tracker 继续迁移"
#    → 观察它读状态、取队首、checkpoint、Edit、done、循环

验收标准


六、整体工作流程图

flowchart TD
    A[新会话启动] --> B[SessionStart Hook<br/>读 state.json]
    B --> C{state 存在且未完成?}
    C -- 否 --> Z[静默, 不打扰]
    C -- 是 --> E[注入续跑简报到上下文]
    E --> F[Stage2 加载 SKILL.md 正文]
    F --> G[state.sh next 取队首]
    G --> H{QUEUE_EMPTY?}
    H -- 是 --> K[status=done 汇总结束]
    H -- 否 --> I[checkpoint 落断点]
    I --> J[Read + Edit 执行迁移]
    J --> L[state.sh done 标记完成]
    L --> M{用户中断?}
    M -- 是 --> N[Stop Hook 提醒落 summary]
    M -- 否 --> G
    N --> O[下个会话从 A 续跑]

七、三条进阶优化

  1. 并发安全:多人同时跑会撞状态文件。给 state.sh 加 flock 文件锁,或把 queue 拆成 per-user 分片。
  2. 状态版本演进:字段变更时,在脚本入口做 skill_version 检查与一次性迁移,避免旧状态把新 Skill 带崩。
  3. 蒸馏链:把 lessons 升级为 journal → lesson → rules 三级蒸馏(参考社区 claude-project-skill 的做法),让踩坑经验沉淀为可复用规则,而非一次性笔记。

八、延伸阅读


九、训练营适用性评估

维度 评估
适合训练营 ✅ 中高级研发提效营、AI 工程化营、企业自动化营
前置模块 需先完成"Skill 基础创建""Hook 入门"两节
教学时长 讲解 45 min + 动手实操 90 min,合计约 3.5 课时
学员产出 一个可带回自己项目复用的有状态 Skill 骨架
难点 Hook 接线调试、幂等性思维、原子写

建议作为"Skill 进阶"模块的压轴课——它把"无状态"这个反直觉真相讲透后,学员对整个 Skill 体系的理解会跨一个台阶。


参考来源

  1. Extend Claude with skills — Claude Code Docs — 官方 Skill 创建、管理与共享权威文档
  2. Claude Code Skills: Complete Technical Deep-Dive (gregpriday/claude-code-docs) — 拆解无状态本质、三阶段渐进加载、文件系统持久化与同名优先级(personal > project > plugin)
  3. ZOO-AiiiPM/claude-project-skill — 真实有状态 Skill 范例:8 份规范 + SessionStart/Stop 双 Hook + journal→lesson→rules 蒸馏链
  4. Claude Code Skill Frontmatter: Every YAML Option Explained — name/description/allowed-tools 等字段语义与校验规则
  5. Inside Claude Code Skills: Structure, prompts, invocation — description 触发机制与语义匹配实践

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