首页 › tutorials › Skill脚本化:写agent友好的脚本

Claude Code Skill 脚本化实战:把复杂逻辑下沉到可执行脚本,设计 agent 友好的接口

概述

本教程面向已经会写最简 SKILL.md 的 Claude Code 使用者,聚焦一个被多数教程忽略的工程核心问题:当 Skill 的逻辑变得复杂时,应该把哪些东西写进 Markdown 指令、哪些东西下沉到 scripts/ 目录的可执行脚本里,以及如何让这些脚本被 Claude 主循环可靠地调用与消费。学完你将掌握 Skill 的三层脚本化模型、PEP 723 自包含脚本写法、以及一套让脚本"对 agent 友好"的接口设计契约,并用一个可运行的日志巡检 Skill 把它们串起来。

前置条件


一、为什么要"下沉":把逻辑全塞进 SKILL.md 的三个陷阱

初学者写 Skill 的典型路径是:把一切想教会 Claude 的知识,事无巨细地铺进 SKILL.md。当 Skill 只做"提示词注入"时这没问题;可一旦涉及确定性的数据处理(解析日志、统计依赖、校验结构、生成报告),纯 Markdown 指令会撞上三个陷阱:

陷阱一:确定性任务用自然语言描述,等于让模型每次重新"猜"算法。 你在 SKILL.md 里写"统计每个模块的 ERROR 数量并按降序排列",模型每次都要现场理解这段文字、现场决定怎么遍历、现场决定排序键。同一段日志跑三次,可能得到三个略有不同的结果。脚本则是确定性的——同样的输入永远产出同样的输出。

陷阱二:上下文膨胀与 token 浪费。 Skill 遵循渐进式披露(progressive disclosure)原则,SKILL.md 应当是"廉价的入口",越短越好(社区经验值常建议控制在 500 行以内)。把冗长的算法描述、边界条件、示例全堆进 SKILL.md,等于让这个 Skill 一旦被触发就吃掉大量上下文窗口。

陷阱三:无法脱离 Claude 单独测试。 一段写在 Markdown 里的逻辑,你没法在终端里 bash 它、没法加进 CI、没法用 pytest 覆盖。它只能在 Claude 会话里"靠人肉验证"。而抽到 scripts/ 的脚本是普通可执行文件,可以独立运行、独立测试、独立版本化。

这三个陷阱的共同解药就是脚本化:让 SKILL.md 负责"指挥"(告诉 Claude 何时调用、按什么顺序、传什么参数),让 scripts/ 负责"执行"(确定性的、可测试的、自包含的计算)。


二、三层脚本化模型

Anthropic 官方文档把 Skill 里"可执行的东西"分成三个层级,按复杂度递增:

flowchart TD
    A[Skill 被触发] --> B{任务复杂度?}
    B -->|"调用现成工具+几个flag"| C[第一层: one-off 命令<br/>直接写在 SKILL.md 里<br/>如 npx eslint@9 --fix .]
    B -->|"可复用逻辑、需自含依赖"| D[第二层: scripts/ 自包含脚本<br/>PEP 723 / Deno / Bun 内联依赖]
    C --> E[CLaude 执行<br/>消费 stdout]
    D --> E
    E --> F{需要深度参考?}
    F -->|是| G[第三层: references/*.md<br/>按需 Read, 不进默认上下文]
    F -->|否| H[完成]
    G --> E

第一层 · one-off 命令:当某个现成包已经能干活,直接在 SKILL.md 的代码块里写命令即可,连 scripts/ 目录都不需要。生态里有一批工具能在运行时自动解析依赖:uvx(Python,隔离环境+激进缓存)、pipx run、npx(Node)、bunx、deno run、go run。它们的特点是"按需下载、缓存复用"。关键纪律有两条:固定版本(写 npx [email protected] 而非 npx eslint,让命令随时间行为不变);声明前置条件(在 SKILL.md 里写明"需要 Node.js 18+",而不是假设 agent 的环境里有)。运行时级别的兼容性约束,官方还提供了 compatibility 这个 frontmatter 字段来表达。

第二层 · scripts/ 自包含脚本:当命令复杂到"第一次很难写对"时,就该把测试过的脚本放进 scripts/。脚本应当自包含(self-contained)——把依赖声明在脚本文件内部,不需要额外的 manifest 或安装步骤。下文会详述。

第三层 · references/ 参考资料:深度文档(长篇规范、大量示例、对照表)放在 references/*.md,用相对路径从 SKILL.md 链接进去。Claude 只在真正需要时才 Read 它们,默认不进上下文。这就是渐进式披露的"按需加载"层。


三、从 SKILL.md 引用脚本:路径与清单约定

把脚本放进 scripts/ 之后,关键问题是 Claude 怎么"知道它们存在、知道怎么调"。官方约定很简单——在 SKILL.md 里显式列清单 + 给出调用方式。

脚本路径用相对于 Skill 目录根的相对路径,agent 会自动解析,不需要绝对路径。同一个相对路径约定也适用于 references/*.md 里的代码块——因为 agent 是从 Skill 目录根执行命令的。

一个规范的"可用脚本"清单长这样:

## Available scripts

- **`scripts/inspect.py`** — 解析日志,按级别统计,提取 ERROR 上下文窗口
- **`scripts/burst_detect.py`** — 检测错误突发(单位时间错误数突增)

随后用编号的 workflow 告诉 agent 调用顺序:

## Workflow

1. 先巡检日志,输出结构化摘要:
   ```bash
   python3 scripts/inspect.py --format json "$LOG_FILE"
   ```
2. 若摘要显示有 ERROR,再做突发检测:
   ```bash
   python3 scripts/burst_detect.py --input "$LOG_FILE" --window 60s
   ```
3. 综合两份结果,生成给人看的结论。

注意:SKILL.md 里写的是调用命令,不是算法实现。算法在脚本里。这是"指挥"与"执行"的分界线。


四、用 PEP 723 写自包含 Python 脚本

"自包含"的最高境界是:一个 .py 文件既包含逻辑、又声明自己的依赖,agent 一条命令就能跑起来,不需要 requirements.txt、不需要 pip install、不需要虚拟环境。Python 的 PEP 723 正是为此而生——它定义了一个在脚本内部用 TOML 块声明依赖的标准格式,放在 # /// 标记之间:

# /// script
# dependencies = [
#   "rich>=13.7,<14",
# ]
# ///

import sys
from rich.console import Console
from rich.table import Table

console = Console()
# ...业务逻辑...

然后用 uv run(推荐)执行:

uv run scripts/inspect.py --format json app.log

uv run 会创建隔离环境、安装声明的依赖、运行脚本,全程对调用方透明。pipx run scripts/inspect.py 也支持 PEP 723。版本用 PEP 508 规范符固定("rich>=13.7,<14"),用 requires-python 约束解释器版本,需要完全可复现时还能 uv lock --script 生成锁文件。

同样的"内联依赖"模式在其他语言里也有标准答案:Deno 用 npm: / jsr: 导入说明符(脚本顶部 #!/usr/bin/env -S deno run,依赖如 import * as cheerio from "npm:[email protected]");Bun 直接在 import 路径里钉版本(from "[email protected]",且 node_modules 不存在时自动安装);Ruby 用 bundler/inline 在脚本里写 gemfile 块。选哪种语言取决于目标用户的环境里最可能装了什么运行时——给前端团队写 Skill 优先 Deno/Bun,给运维/数据团队写优先 PEP 723 + uv。


五、设计 agent 友好的接口:六条契约

这是本教程最关键的一节。当 agent 运行你的脚本时,它读 stdout 和 stderr 来决定下一步做什么。脚本是不是"好调",直接决定 Claude 能不能稳定地用你的 Skill。以下六条契约来自官方文档,请逐条遵守。

契约 1 · 绝对不要交互式提示(硬性要求)

这是 agent 执行环境的硬性约束。agent 跑在非交互式 shell 里,没法回应 TTY 提示、密码框、确认菜单。一个阻塞在交互输入上的脚本会无限挂起,整个会话卡死。

所有输入都要通过命令行 flag、环境变量或 stdin 传入。反例与正例对比:

# 错误:挂起等待输入
$ python scripts/deploy.py
Target environment: _

# 正确:缺参就报清晰的错并给指引
$ python scripts/deploy.py
Error: --env is required. Options: development, staging, production.
Usage: python scripts/deploy.py --env staging --tag v1.2.3

契约 2 · 用 --help 文档化接口

--help 的输出是 agent 学习你脚本接口的主要途径。要包含简短描述、所有 flag、用法示例,且保持简洁(因为它会和其他内容一起进入上下文窗口):

Usage: scripts/inspect.py [OPTIONS] LOG_FILE
Parse a log file and emit a structured summary.

Options:
  --format FORMAT    Output format: json, table (default: json)
  --offset N         Skip first N findings (for pagination)
  --limit N          Cap number of findings (default: 50)
  --dry-run          Show what would be analyzed, exit 0
Examples:
  scripts/inspect.py app.log
  scripts/inspect.py --format table --offset 50 app.log

契约 3 · 写有帮助的错误信息

agent 拿到错误时,错误内容直接塑造它的下一次尝试。一个含糊的 Error: invalid input 会浪费一个轮次。要说出错在哪、期望什么、该怎么试:

Error: --format must be one of: json, table.
       Received: "xml"

契约 4 · 用结构化输出,并把数据与诊断分流

优先用 JSON / CSV / TSV 这种结构化格式,而不是自由文本——结构化输出既能被 agent 消费,也能被 jq/cut/awk 这类标准工具消费,让脚本可以编进管道。

更重要的纪律是数据与诊断分流:结构化数据走 stdout,进度、警告、其他诊断走 stderr。这样 agent 能捕获干净可解析的输出,同时需要时还能看到诊断信息。对比:

# 空格对齐——难以程序化解析
NAME          STATUS    CREATED
my-service    running   2025-01-15

# 定界——字段边界毫无歧义
{"name": "my-service", "status": "running", "created": "2025-01-15"}

契约 5 · 幂等、输入约束、干跑、退出码、安全默认

契约 6 · 可预测的输出大小(防截断)

这条常被忽略却很致命:许多 agent 框架会自动截断超过阈值的工具输出(典型阈值 10–30K 字符),可能丢掉关键信息。因此脚本应当默认输出摘要或合理上限,并用 --offset 之类 flag 支持分页,让 agent 需要更多信息时主动索取。若输出天生很大、又不适合分页,就要求 agent 显式传 --output 指定输出文件(或 - 显式选择 stdout),把"是否产生大量输出"的决定权交给调用方。


六、stdout 与 stderr 的分流契约(时序视角)

把这六条契约放进一次真实的调用过程,时序如下:

sequenceDiagram
    participant A as Claude 主循环
    participant S as scripts/inspect.py
    A->>S: 读 SKILL.md,看到脚本清单
    A->>S: bash inspect.py --help(学习接口)
    S-->>A: stdout: Usage + Options
    A->>S: bash inspect.py --format json --limit 50 app.log
    S-->>S: 解析、统计(stderr 打印进度 "parsing 1.2MB…")
    S-->>A: stdout: 干净的 JSON 数组
    S-->>A: stderr: 警告 "3 lines unparseable"
    A->>A: 用 JSON 决策(是否有 ERROR?是否要分页?)
    A->>S: bash inspect.py --offset 50 app.log(取下一页)
    S-->>A: stdout: 下一批 JSON
    Note over A,S: 数据走 stdout 可 jq 管道;诊断走 stderr 不污染数据

记住一句话:stdout 是给程序的,stderr 是给人和诊断的,--help 和退出码是给 agent 决策的。


七、实战案例:构建 log-inspector 日志巡检 Skill

我们把前面所有概念落到一个完整可运行的项目里。这个 Skill 干一件具体的事:给定一个日志文件,产出结构化的健康报告,让 Claude 能据此判断"要不要升级告警"。完整目录结构:

log-inspector/
├── SKILL.md
├── scripts/
│   └── inspect.py        # PEP 723 自包含,agentic 接口
└── references/
    └── report-schema.md  # 按需加载的字段说明

7.1 SKILL.md(轻量入口,只指挥)

---
name: log-inspector
description: Parses a log file and emits a structured health report
  (level counts, ERROR context windows, burst detection). Use when the
  user asks to inspect, triage, or summarize a .log file, or to find
  errors/anomalies in logs. Emits JSON to stdout by default.
---

# Log Inspector

巡检一个日志文件,输出结构化健康报告。

## Available scripts

- **`scripts/inspect.py`** — 解析日志:级别计数、提取 ERROR 上下文窗口、突发检测。自包含,PEP 723 声明依赖。

## Workflow

1. 先拿总览(默认 JSON,限 50 条发现,防截断):
   ```bash
   uv run scripts/inspect.py --format json --limit 50 "$LOG_FILE"
   ```
2. 若 JSON 里 `error_count > 0`,取下一页细查:
   ```bash
   uv run scripts/inspect.py --offset 50 --limit 50 "$LOG_FILE"
   ```
3. 字段含义见 `references/report-schema.md`(仅在需要解释字段时再读)。
4. 基于报告结论给人建议;不要复述原始日志行,只引用必要上下文窗口。

## Do NOT use when

- 用户只是想 `grep` 某个关键词——直接用 shell 即可。
- 文件不是文本日志(二进制、压缩包)——先解压/转换。

7.2 scripts/inspect.py(自包含 + agentic 接口)

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
"""
Log Inspector — parse a text log and emit a structured health report.
Designed for agentic use: non-interactive, --help documented,
structured stdout, diagnostics on stderr, paginated, meaningful exit codes.
"""
import argparse
import json
import re
import sys
from collections import Counter

# 形如: 2026-06-19T10:22:31Z ERROR [auth] login failed for user=alice
LINE_RE = re.compile(
    r'^(?P<ts>\S+)\s+(?P<level>\w+)\s+(?:\[(?P<ctx>[^\]]+)\]\s+)?(?P<msg>.*)$'
)
LEVELS = {"DEBUG", "INFO", "WARN", "ERROR"}
EXIT_OK, EXIT_HAS_ERRORS, EXIT_BAD_ARGS = 0, 2, 3


def eprint(*a):  # 诊断一律走 stderr
    print(*a, file=sys.stderr)


def parse_args(argv):
    p = argparse.ArgumentParser(
        prog="scripts/inspect.py",
        description="Parse a log file and emit a structured summary.",
        epilog="Exit codes: 0 ok | 2 found errors | 3 bad arguments",
    )
    p.add_argument("log_file", help="Path to the .log file to inspect")
    p.add_argument("--format", default="json", choices=["json", "table"],
                   help="Output format (default: json)")
    p.add_argument("--offset", type=int, default=0,
                   help="Skip first N error findings (pagination)")
    p.add_argument("--limit", type=int, default=50,
                   help="Cap error findings shown (default: 50)")
    p.add_argument("--window", type=int, default=2,
                   help="Context lines around each ERROR (default: 2)")
    p.add_argument("--dry-run", action="store_true",
                   help="Show planned analysis, emit nothing, exit 0")
    return p.parse_args(argv)


def main(argv=None):
    args = parse_args(argv)
    # —— 契约 5:输入约束 ——
    if args.limit < 1:
        eprint('Error: --limit must be >= 1. Use --limit 50 for the default.')
        return EXIT_BAD_ARGS

    # —— 契约 6:输出大小可预测,默认摘要 + 分页 ——
    try:
        with open(args.log_file, "r", errors="replace") as fh:
            raw_lines = fh.readlines()
    except FileNotFoundError:
        eprint(f'Error: file not found: {args.log_file}\n'
               f'Usage: scripts/inspect.py --format json <LOG_FILE>')
        return EXIT_BAD_ARGS

    if args.dry_run:  # 契约 5:干跑
        eprint(f"[dry-run] would parse {len(raw_lines)} lines "
               f"from {args.log_file}, window={args.window}, "
               f"limit={args.limit}, offset={args.offset}")
        return EXIT_OK

    eprint(f"[progress] parsing {len(raw_lines)} lines …")

    counts = Counter()
    errors = []  # 收集带上下文的 ERROR
    for i, line in enumerate(raw_lines):
        line = line.rstrip("\n")
        m = LINE_RE.match(line)
        if not m:
            continue
        level = m.group("level").upper()
        if level not in LEVELS:
            continue
        counts[level] += 1
        if level == "ERROR":
            start = max(0, i - args.window)
            end = min(len(raw_lines), i + args.window + 1)
            errors.append({
                "ts": m.group("ts"),
                "ctx": m.group("ctx") or "",
                "msg": m.group("msg"),
                "window": raw_lines[start:end],
            })

    total = len(errors)
    paged = errors[args.offset: args.offset + args.limit]
    eprint(f"[progress] found {total} errors; "
           f"showing {len(paged)} (offset={args.offset}, limit={args.limit})")

    summary = {
        "file": args.log_file,
        "lines": len(raw_lines),
        "counts": {lvl: counts[lvl] for lvl in LEVELS},
        "error_count": total,
        "errors": paged,
        "truncated": total > args.offset + args.limit,
    }

    # —— 契约 4:结构化数据走 stdout ——
    if args.format == "json":
        print(json.dumps(summary, ensure_ascii=False))
    else:
        print(f"{'LEVEL':<8}{'COUNT':>8}")
        for lvl in LEVELS:
            print(f"{lvl:<8}{counts[lvl]:>8}")

    # —— 契约 5:有意义的退出码 ——
    return EXIT_HAS_ERRORS if total > 0 else EXIT_OK


if __name__ == "__main__":
    sys.exit(main())

7.3 references/report-schema.md(按需加载)

# Report field schema

- `counts`: 各级别行数(DEBUG/INFO/WARN/ERROR)
- `error_count`: ERROR 总数(未受 offset/limit 影响)
- `errors[]`: 当前页的 ERROR,每条含 `ts`、`ctx`(方括号标签)、`msg`、`window`(上下文行)
- `truncated`: true 表示还有更多未展示,应用 `--offset` 续取

7.4 预期输出与验证

放一个示例 app.log(含几条 ERROR)后,脱离 Claude 直接验证脚本:

$ uv run scripts/inspect.py --help          # 学习接口
$ uv run scripts/inspect.py --dry-run app.log   # 干跑预览
[progress] would parse 1240 lines from app.log, window=2, limit=50, offset=0

$ uv run scripts/inspect.py --format json app.log   # 正式巡检
[progress] parsing 1240 lines …
[progress] found 7 errors; showing 7 (offset=0, limit=50)
{"file":"app.log","lines":1240,"counts":{"DEBUG":...,"ERROR":7},...,"truncated":false}

$ echo $?     # 退出码 2 = 有错误(agent 可据此分支)
2

$ uv run scripts/inspect.py app.log | jq '.error_count'   # 结构化可入管道
7

注意 --help/--dry-run/[progress] 都走 stderr 或受控 stdout,jq 只解析到干净的 JSON——这就是"数据与诊断分流"在终端里的直观体现。

7.5 注册到 Claude Code

把整个 log-inspector/ 目录放进项目级 .claude/skills/(或用户级 ~/.claude/skills/)。若你的 Skill 来自一个仓库,也可用插件市场方式:

/plugin marketplace add anthropics/skills
/plugin install example-skills@anthropic-agent-skills

装好后,在会话里直接说"巡检一下 ./app.log,告诉我有没有异常",Claude 会根据 description 自动触发该 Skill,读 SKILL.md,按 workflow 调脚本,消费 JSON,给出结论——全程不碰原始算法实现。


八、常见问题与排查

Q1:脚本第一次 uv run 很慢,agent 会不会超时? 首次需要建隔离环境、装依赖,确实慢一些;之后 uv 激进缓存,重复运行近乎瞬时。若依赖很重,考虑改用第一层 one-off 命令(如 uvx [email protected] check .),让缓存粒度落在工具本身。

Q2:脚本有时被 Claude"重写"或绕过,直接用 Python 现写逻辑。 通常是因为 SKILL.md 没有显式列出脚本清单与调用命令(见第三节)。Claude 只能调用它"看见"的东西。确保有 ## Available scripts 和带相对路径代码块的 ## Workflow。

Q3:调用返回了输出,但 agent 似乎只看到前半截。 命中了截断阈值(10–30K)。缩小默认 --limit,或强制要求 --output FILE 把大结果落盘,stdout 只回一个指针(如 {"report": "out.json", "rows": 9821})。

Q4:脚本偶尔挂死、会话卡住。 几乎一定是某个交互式 prompt(输入框、确认菜单、SSH 密码)。排查 input()、git push 的凭证提示、apt 的 Y/n。全部改成 flag / 环境变量 / --yes。

Q5:同一份日志两次巡检结果不一致。 说明还有逻辑没下沉——仍有部分判断留在自然语言指令里。把那段判断也搬进脚本,让确定性落在代码而非模型。


延伸阅读


训练营适用性评估


参考来源

  1. Using scripts in skills — Agent Skills 官方文档 — one-off 命令、自包含脚本(PEP 723/Deno/Bun/Ruby)、agentic 接口设计六条契约的权威来源
  2. Equipping agents for the real world with Agent Skills — Anthropic Engineering — progressive disclosure 设计原则与 Skill 架构官方深度解读
  3. anthropics/skills — GitHub 官方仓库 — Skill 目录结构、template/、生产级 skills/pdf/skills/xlsx 实现参考
  4. Skill authoring best practices — Claude API Docs — SKILL.md 篇幅控制、描述优化、触发准确率最佳实践
  5. Agent Skills Specification — agentskills.io — frontmatter 字段(含 compatibility)、路径约定与渐进式披露的开放规范定义

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