本教程面向已经会写最简 SKILL.md 的 Claude Code 使用者,聚焦一个被多数教程忽略的工程核心问题:当 Skill 的逻辑变得复杂时,应该把哪些东西写进 Markdown 指令、哪些东西下沉到 scripts/ 目录的可执行脚本里,以及如何让这些脚本被 Claude 主循环可靠地调用与消费。学完你将掌握 Skill 的三层脚本化模型、PEP 723 自包含脚本写法、以及一套让脚本"对 agent 友好"的接口设计契约,并用一个可运行的日志巡检 Skill 把它们串起来。
/plugin 命令name/description 的最小 Skillpython3 和 uv(pipx 亦可),以及 git、jq初学者写 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 它们,默认不进上下文。这就是渐进式披露的"按需加载"层。
把脚本放进 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 里写的是调用命令,不是算法实现。算法在脚本里。这是"指挥"与"执行"的分界线。
"自包含"的最高境界是:一个 .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 运行你的脚本时,它读 stdout 和 stderr 来决定下一步做什么。脚本是不是"好调",直接决定 Claude 能不能稳定地用你的 Skill。以下六条契约来自官方文档,请逐条遵守。
这是 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
--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
agent 拿到错误时,错误内容直接塑造它的下一次尝试。一个含糊的 Error: invalid input 会浪费一个轮次。要说出错在哪、期望什么、该怎么试:
Error: --format must be one of: json, table.
Received: "xml"
优先用 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"}
--dry-run 让 agent 先预览会发生什么。--help 里写明每个码的含义。--confirm / --force)或其他与风险等级匹配的防护。这条常被忽略却很致命:许多 agent 框架会自动截断超过阈值的工具输出(典型阈值 10–30K 字符),可能丢掉关键信息。因此脚本应当默认输出摘要或合理上限,并用 --offset 之类 flag 支持分页,让 agent 需要更多信息时主动索取。若输出天生很大、又不适合分页,就要求 agent 显式传 --output 指定输出文件(或 - 显式选择 stdout),把"是否产生大量输出"的决定权交给调用方。
把这六条契约放进一次真实的调用过程,时序如下:
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 # 按需加载的字段说明
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 即可。
- 文件不是文本日志(二进制、压缩包)——先解压/转换。
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())
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` 续取
放一个示例 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——这就是"数据与诊断分流"在终端里的直观体现。
把整个 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:同一份日志两次巡检结果不一致。 说明还有逻辑没下沉——仍有部分判断留在自然语言指令里。把那段判断也搬进脚本,让确定性落在代码而非模型。
agentskills.io/specification——格式与字段的权威定义anthropics/skills 仓库 template/ 与 skills/pdf、skills/xlsx——生产级复杂 Skill 的结构参考uv 文档——自包含脚本依赖声明的底层标准log-inspector 完整搭建与验证)。inspect.py 改写为 Deno 自包含版本(npm: 导入)以服务前端团队;为脚本加 --output FILE 大结果落盘模式;编写脱离 Claude 的 pytest 用例验证退出码与 JSON schema。template/、生产级 skills/pdf/skills/xlsx 实现参考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 等场景全覆盖。
