实操:把一个 SOP 做成 Skill
从人工流程到可被 Agent 调用的能力。
一句话理解:把 SOP 做成 Skill 分四步:先摸清人工流程和 Agent 的缺口,再搭文件夹、写 SKILL.md 和脚本,然后分别验证触发和效果,最后把每次纠错补回 Skill。
前面几课讲了 Skill 是什么、怎么组织、怎么加载、怎么写。这一课动手做一个完整的 Skill:把「SEO 站内诊断」这份人工 SOP,变成 Claude Code 可以自动调用的能力,并验证它会被正确触发,而且确实比不用它做得更好。
预计用时 60–90 分钟。你需要:
- 安装好 Claude Code,并能在终端运行
claude。 - 本机有 Python 3(示例脚本只用标准库)。
- 一个测试项目目录,里面放 2–3 个 HTML 页面。用本地文件而不是线上网址,结果更容易复现。
整个实操分成四个阶段、十个步骤:
第 1 步:写下人工 SOP
动手写 Skill 之前,先把专家现在怎么做写清楚。假设增长团队的 SEO 专员是这样检查页面的:
| 步骤 | 人工怎么做 | 容易出错的地方 |
|---|---|---|
| 1 | 打开页面源码,找 title 和 meta description | 漏看、长度靠目测 |
| 2 | 检查 H1 是否有且只有一个 | 模板里藏着第二个 H1 |
| 3 | 检查 canonical 是否指向自己或正确的主版本 | 跨域 canonical 被误判为错误 |
| 4 | 检查 robots 是否误设了 noindex | 预发布环境的配置被带上线 |
| 5 | 检查图片 alt | 装饰性图片的空 alt 被误报 |
| 6 | 按团队模板写报告,按严重程度排序 | 格式不统一,缺少证据 |
右边这一列最重要,它们会变成 Skill 里的踩坑清单。
第 2 步:先不用 Skill 跑一遍,记录缺口
在测试项目里启动 claude,直接说:
帮我检查 pages/about.html 和 pages/pricing.html 的 SEO 问题,给出报告。
记下 Agent 的实际表现:报告格式是否统一?有没有用团队的判定标准?有没有把 alt="" 误报成问题?每个问题有没有给出证据?这就是基线。Skill 要弥补的,正是这里暴露出来的缺口,而不是你想象中的缺口。
第 3 步:创建目录
项目级 Skill 放在 .claude/skills/,提交到 Git 后团队成员都能用;只想自己在所有项目里用,就放在 ~/.claude/skills/。本练习用项目级:
mkdir -p .claude/skills/seo-audit/scripts .claude/skills/seo-audit/references .claude/skills/seo-audit/assets
最终结构:
.claude/skills/seo-audit/
├── SKILL.md
├── scripts/
│ └── check_page.py
├── references/
│ └── rules.md
└── assets/
└── report-template.md
第 4 步:写 SKILL.md
---
name: seo-audit
description: 对网页做站内 SEO 诊断:提取并检查 title、meta description、H1、canonical、robots 与图片 alt,按团队口径判定问题等级,输出带证据的问题清单和修复建议。当用户要求检查页面 SEO、排查页面为什么没被收录或排名差、或在落地页上线前做审查时使用,即使用户没有明确提到 SEO。不负责关键词研究和外链分析。
---
# SEO 站内诊断
## 工作流程
复制以下清单并随进度更新:
```text
- [ ] 1. 对每个页面运行检查脚本
- [ ] 2. 按团队口径判定每一项
- [ ] 3. 汇总问题并排序
- [ ] 4. 按模板输出报告
- [ ] 5. 核对证据
```
1. 对每个页面(本地文件路径或 URL)运行:
`python3 ${CLAUDE_SKILL_DIR}/scripts/check_page.py <页面>`
脚本输出 JSON。多个页面逐个运行。
2. 读取 `references/rules.md`,按其中的口径把每一项判定为“错误 / 警告 / 通过”。
3. 汇总:错误在前,警告在后;同级别内按影响页面数从多到少排列。
4. 按 `assets/report-template.md` 的结构输出报告。
5. 输出前自查:报告中的每个问题都必须引用脚本输出里的具体字段和值;做不到的问题删除或标为“待人工确认”。
## 注意事项
- 图片的 `alt=""` 表示装饰性图片,是合法写法。脚本只统计完全没有 alt 属性的图片。
- canonical 指向另一个 URL 不一定是错误,可能是有意合并重复页面。先确认目标页面是否是主版本,无法确认时标为“警告”并说明原因。
- 脚本返回 `error` 字段时,在报告中标记该页面“未能检查”,并附上错误信息,不要推测结果。
- robots 含 `noindex` 时一律判为“错误”,并提醒检查是否是预发布配置被带上线。
几点说明:
${CLAUDE_SKILL_DIR}是 Claude Code 提供的变量,会被替换成 Skill 所在目录。Claude Code 通常在项目根目录执行命令,所以脚本要用这个变量定位。在其他支持该标准的产品里,按规范写相对 Skill 根目录的路径(scripts/check_page.py),并以该产品文档说明的执行方式为准。- 注意事项里的每一条,都来自第 1 步表格右列的真实易错点。
第 5 步:写脚本
先看清楚这个 Skill 里脚本和模型各管哪一段:
脚本负责确定性的提取工作,模型负责判断。把下面的代码保存为 scripts/check_page.py:
#!/usr/bin/env python3
"""提取网页的站内 SEO 信号,输出 JSON。用法:check_page.py <文件路径或 URL>"""
import json
import sys
import urllib.request
from html.parser import HTMLParser
class SEOParser(HTMLParser):
def __init__(self):
super().__init__()
self.title = ""
self.meta_description = None
self.robots = None
self.canonical = None
self.h1 = []
self.images_missing_alt = 0
self._in_title = False
self._in_h1 = False
def handle_starttag(self, tag, attrs):
a = dict(attrs)
name = (a.get("name") or "").lower()
if tag == "title":
self._in_title = True
elif tag == "h1":
self._in_h1 = True
self.h1.append("")
elif tag == "meta" and name == "description":
self.meta_description = a.get("content") or ""
elif tag == "meta" and name == "robots":
self.robots = a.get("content") or ""
elif tag == "link" and "canonical" in (a.get("rel") or "").lower().split():
self.canonical = a.get("href")
elif tag == "img" and "alt" not in a:
self.images_missing_alt += 1
def handle_endtag(self, tag):
if tag == "title":
self._in_title = False
elif tag == "h1":
self._in_h1 = False
def handle_data(self, data):
if self._in_title:
self.title += data
if self._in_h1 and self.h1:
self.h1[-1] += data
def load(target):
if target.startswith(("http://", "https://")):
req = urllib.request.Request(target, headers={"User-Agent": "seo-audit-skill"})
with urllib.request.urlopen(req, timeout=15) as resp:
return resp.read().decode("utf-8", errors="replace")
with open(target, encoding="utf-8", errors="replace") as f:
return f.read()
def main():
if len(sys.argv) != 2:
print("用法:check_page.py <文件路径或 URL>", file=sys.stderr)
sys.exit(2)
target = sys.argv[1]
try:
html = load(target)
except Exception as exc:
# 把失败原因作为结构化结果交给 Agent,而不是抛出一长串堆栈
print(json.dumps({"target": target, "error": str(exc)}, ensure_ascii=False))
sys.exit(1)
parser = SEOParser()
parser.feed(html)
title = " ".join(parser.title.split())
description = parser.meta_description
print(json.dumps({
"target": target,
"title": title,
"title_length": len(title),
"meta_description": description,
"meta_description_length": len(description) if description is not None else 0,
"h1": [" ".join(h.split()) for h in parser.h1],
"canonical": parser.canonical,
"robots": parser.robots,
"images_missing_alt": parser.images_missing_alt,
}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
先手动运行一次,确认输出正常:
python3 .claude/skills/seo-audit/scripts/check_page.py pages/about.html
第 6 步:补上参考规则与报告模板
references/rules.md 写团队自己的判定口径。下面的数值只是示例,请换成你们团队实际采用的标准:
# SEO 判定口径(团队内部标准)
| 检查项 | 错误 | 警告 | 通过 |
| --- | --- | --- | --- |
| title | 缺失或为空 | 长度少于 15 或多于 60 个字符 | 其他情况 |
| meta description | 缺失 | 长度少于 50 或多于 160 个字符 | 其他情况 |
| H1 | 没有 H1 | 多于 1 个 | 恰好 1 个 |
| canonical | 缺失 | 指向其他 URL | 指向自身 |
| robots | 含 noindex | 含 nofollow | 未设置或 index,follow |
| 图片 alt | — | 存在缺少 alt 属性的图片 | 全部具备 alt 属性 |
assets/report-template.md 固定报告结构:
# SEO 诊断报告:{站点或页面范围}
## 结论摘要
{一句话:共检查 N 个页面,发现 X 个错误、Y 个警告,最优先处理的是……}
## 问题清单
| 等级 | 页面 | 检查项 | 证据(脚本输出) | 修复建议 |
| --- | --- | --- | --- | --- |
## 未能检查的页面
{页面与错误信息;没有则写“无”}
第 7 步:检查格式
在项目根目录运行:
claude plugin validate .claude/skills
Claude Code 文档说明,这条命令可以找出元数据无法解析的 SKILL.md(需要较新版本的 Claude Code,旧版本没有该命令时跳过即可)。如果元数据格式有误,Claude Code 仍会加载正文,但元数据是空的,此时 /seo-audit 可以手动调用,Claude 却无法根据描述自动触发。启动时加 --debug 可以看到解析错误。
第 8 步:验证触发
Claude Code 会监测 Skill 目录的变化,新建或修改的 Skill 通常在当前会话中就能生效;如果 .claude/skills/ 是会话启动后才新建的,需要重启 Claude Code。
1. 确认被识别。 在 claude 里问:
What skills are available?
列表里应该出现 seo-audit。
2. 手动调用。 输入 /seo-audit pages/about.html,确认流程能完整跑通。手动调用成功,说明 Skill 本身可用。
3. 测自动触发。 每条提问都在新会话里运行,观察 Agent 是否加载了 seo-audit:
| 提问 | 预期 |
|---|---|
| 上线前帮我看看 pages/pricing.html 有没有什么会影响搜索排名的问题 | 触发 |
| 为什么 about 页面在 Google 上搜不到?帮我查查页面本身 | 触发 |
| 检查一下这两个落地页的标题和描述写得合不合规 | 触发 |
| 帮我给 pricing 页面写一段更吸引人的产品介绍 | 不触发 |
| 帮我修一下 about.html 里图片错位的 CSS 问题 | 不触发 |
| 帮我调研一下竞品主要在投哪些关键词 | 不触发(关键词研究不在范围内) |
4. 批量测试(可选)。 提问多了,可以用非交互模式批量运行,从输出中查找 Skill 调用记录:
claude -p "上线前帮我看看 pages/pricing.html 有没有影响搜索排名的问题" \
--output-format stream-json --verbose \
| jq -c 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use" and .name == "Skill") | .input'
输出里出现 seo-audit,说明本次触发了。输出的字段结构可能随版本变化,第一次使用时先去掉 jq 部分,人工看一眼原始输出,确认 Skill 调用长什么样。每条提问至少跑 3 次,计算触发率。
5. 调整。 应该触发却没触发,就在描述里补充用户的真实说法;不该触发却触发了,就把描述写得更具体,或写明不负责的范围。改完重新测试所有提问,而不只是失败的那几条。
第 9 步:验证效果
触发只说明 Agent 找到了 Skill,不说明它做得更好。用第 2 步的同一个任务做对比:
- 在新会话里运行,装着 Skill。
- 暂时关闭 Skill 再运行一次。可以在
/skills菜单里把它设为关闭,也可以暂时把文件夹移出.claude/skills/。 - 按下表对比:
| 维度 | 无 Skill | 有 Skill |
|---|---|---|
| 报告结构是否符合模板 | ||
| 判定是否符合团队口径 | ||
是否误报 alt="" |
||
| 每个问题是否都有证据 | ||
| 耗时与 Token 消耗 |
第 8、9 步合起来,就是 Skill 验证的两道关:
想要更系统的评测,可以安装官方的 skill-creator 插件(/plugin install skill-creator@claude-plugins-official),让它管理测试用例、隔离运行有无 Skill 的对比,并给出通过率报告。
第 10 步:迭代与分享
- 在真实使用中每纠正一次 Agent,就把这条纠正补进“注意事项”。
- 规则口径变了,只改
references/rules.md,不必动流程。 - 验证通过后,把
.claude/skills/seo-audit/提交到仓库,团队成员拉取代码就能用。 - 同一个文件夹也可以用在其他支持 Agent Skills 标准的产品里,但要调整脚本路径的写法,并确认目标环境允许运行 Python、能访问需要检查的页面。
常见误区
- 只测手动调用。
/seo-audit能用,只说明 Skill 本身没有问题,不代表用户自然提问时会触发。 - 在写 Skill 的同一个会话里测试。 之前的对话已经告诉了 Agent 很多背景,会掩盖 Skill 本身的缺口。
- 只看有没有触发,不看效果。 一个被触发却没带来改进的 Skill,只是在白白消耗上下文。
验收清单
- 手动调用
/seo-audit可以完整跑通 - 3 条应触发的提问,触发率都高于 50%
- 3 条近似负例都没有误触发
- 与无 Skill 相比,报告格式、判定口径和证据完整性都有明显改善
- 注意事项至少有一条来自测试中发现的新问题
- Skill 已提交到仓库,并写明负责人
练习
用同样的方法,把「客服工单排查」SOP 做成 Skill(约 60 分钟):
- 写出人工排查的 5–6 个步骤和各自的易错点。
- 没有真实系统时,用几个 JSON 文件模拟工单、历史回复和订单数据,写一个计算各环节响应与停留时长的脚本。
- 完成 SKILL.md,设计 6 条触发测试提问,完成有无 Skill 的效果对比。
延伸阅读
- Extend Claude with skills(Claude Code 文档):创建第一个 Skill、变量替换、故障排查与评测方法。
- Run Claude Code programmatically:
claude -p非交互模式与输出格式。 - Optimizing skill descriptions:触发测试集设计、触发率计算与训练集 / 验证集划分。
下一步
Skills 模块到此结束。下一个模块从「Workflow 与 Agent」开始:什么时候该让模型决定下一步,什么时候应该把路径预先写死。