很多团队构建 Agent 的第一步,是不断扩充系统提示:提交代码前要跑哪些命令、故障排查先看什么日志、报告必须采用什么格式、哪些目录不能修改,都写进同一段文本。几十条规则时还能维护,增长到几百条以后,模型每次处理简单问题也要携带全部流程。指令互相覆盖,修改一条发布规范可能影响数据分析任务,出了问题又很难回答"这次到底加载了哪版操作说明"。
Agent Skills 试图解决的不是模型缺少某个事实,而是团队如何把可复用的工作方法做成可以发现、按需加载、测试和版本化的制品。一个 Skill 最小只是一座目录和其中的 SKILL.md:YAML Front Matter 描述它叫什么、做什么、何时使用,正文说明执行步骤;复杂任务再按需读取 references/,运行 scripts/,或者使用 assets/ 中的模板。它没有神奇地让模型变得确定,也不会自动获得工具权限,但能把散落在系统提示、Wiki 和个人经验中的过程收拢到一个可审计边界。
2026 年 Agent Skills 已形成开放格式并被多个 Agent 产品采用,这是值得关注的工程信号,却不是质量证明。仓库 Star、安装次数和传播速度只能说明有人尝试,不能说明某个 Skill 安全、准确或适合你的环境。真正决定能否上线的,是触发是否稳定、流程是否完成、脚本是否受控、升级能否回滚,以及与不使用 Skill 的基线相比是否确实改善任务结果。本文从一个"发布前检查"Skill 出发,把这些问题逐一落到文件、代码与验收指标上。
1. 先分清 Skill、Prompt、Tool 与 MCP
这四个概念经常同时出现,但职责不同。Prompt 是本轮或某段会话中的输入与约束,适合临时目标,例如"把这段说明改成面向新人的版本"。Tool 是 Agent 可以调用的确定性能力接口,例如读取文件、查询工单或执行测试;它解决"能做什么"。MCP 是连接 Host、Client 与 Server 的协议层,可以标准化暴露 Tool、Resource 和 Prompt;它解决能力如何被发现、调用和返回。Skill 则打包一项任务的操作知识,告诉 Agent 在什么场景下如何组合已有能力、检查什么结果、遇到异常何时停止。
假设团队需要发布 Python 服务。run_tests 是工具,负责真正执行命令;MCP Server 可以把这个工具从远端 CI 平台暴露给不同 Agent;"先检查工作树,再跑目标测试,失败时不得发布,输出固定验收摘要"属于 Skill;"今天只验证支付模块"是本次 Prompt。Skill 可以引用工具,却不等于工具;它可以在没有 MCP 时使用本地命令,也可以通过 MCP 调用远端能力。反过来,连接了一百个工具的 Agent 若不知道顺序、停止条件与组织规则,仍然可能把事情做错。
还要区分 Skill 与微调。Skill 是运行时可读、可替换的过程制品,适合经常变化且需要审计的团队规范;微调改变模型参数,更适合稳定的行为分布和大量训练样本。把每天变化的部署命令写进训练集既慢又难回滚,而把基本语言能力全塞进 Skill 又会让指令臃肿。好的边界是:模型负责理解与推理,Skill 提供任务方法,Tool 执行可验证动作,策略层决定授权。
| 机制 | 核心作用 | 典型内容 | 是否直接提供权限 |
|---|---|---|---|
| Prompt | 表达当前目标与上下文 | 本轮要求、输入材料 | 否 |
| Skill | 封装可复用工作方法 | 步骤、检查、模板、脚本 | 否 |
| Tool | 执行或查询具体能力 | API、文件、命令、数据库 | 由宿主决定 |
| MCP | 标准化连接能力提供方 | Tool、Resource、Prompt | 仍由宿主授权 |
2. 渐进式加载为什么是核心设计
如果把所有团队手册放进系统提示,Skill 只是换了文件名,并没有解决上下文污染。开放规范采用渐进式披露:发现阶段只暴露 name 与 description 等少量元数据;任务匹配后才读取完整 SKILL.md;正文引用的详细资料、脚本和资产,又只在当前步骤确实需要时加载。这样,一个 Agent 可以知道许多能力的存在,却不必在每次对话中携带全部内容。
这种加载方式带来三个直接收益。第一,触发前的上下文成本主要由元数据决定,所以 description 必须既说明"能做什么",又说明"什么时候用"。第二,主体应该是导航和决策流程,而不是把两百页产品文档原样粘进去。第三,脚本可以直接执行并只把必要结果返回上下文,适合哈希、格式校验、压缩和数据转换等确定性步骤。真正的大文件仍然占磁盘和审计成本,并非"免费",只是不在未使用时占模型上下文。
#mermaid-svg-67asvrTeTGWAQS1z{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-67asvrTeTGWAQS1z .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-67asvrTeTGWAQS1z .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-67asvrTeTGWAQS1z .error-icon{fill:#552222;}#mermaid-svg-67asvrTeTGWAQS1z .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-67asvrTeTGWAQS1z .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-67asvrTeTGWAQS1z .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-67asvrTeTGWAQS1z .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-67asvrTeTGWAQS1z .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-67asvrTeTGWAQS1z .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-67asvrTeTGWAQS1z .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-67asvrTeTGWAQS1z .marker{fill:#333333;stroke:#333333;}#mermaid-svg-67asvrTeTGWAQS1z .marker.cross{stroke:#333333;}#mermaid-svg-67asvrTeTGWAQS1z svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-67asvrTeTGWAQS1z p{margin:0;}#mermaid-svg-67asvrTeTGWAQS1z .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-67asvrTeTGWAQS1z .cluster-label text{fill:#333;}#mermaid-svg-67asvrTeTGWAQS1z .cluster-label span{color:#333;}#mermaid-svg-67asvrTeTGWAQS1z .cluster-label span p{background-color:transparent;}#mermaid-svg-67asvrTeTGWAQS1z .label text,#mermaid-svg-67asvrTeTGWAQS1z span{fill:#333;color:#333;}#mermaid-svg-67asvrTeTGWAQS1z .node rect,#mermaid-svg-67asvrTeTGWAQS1z .node circle,#mermaid-svg-67asvrTeTGWAQS1z .node ellipse,#mermaid-svg-67asvrTeTGWAQS1z .node polygon,#mermaid-svg-67asvrTeTGWAQS1z .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-67asvrTeTGWAQS1z .rough-node .label text,#mermaid-svg-67asvrTeTGWAQS1z .node .label text,#mermaid-svg-67asvrTeTGWAQS1z .image-shape .label,#mermaid-svg-67asvrTeTGWAQS1z .icon-shape .label{text-anchor:middle;}#mermaid-svg-67asvrTeTGWAQS1z .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-67asvrTeTGWAQS1z .rough-node .label,#mermaid-svg-67asvrTeTGWAQS1z .node .label,#mermaid-svg-67asvrTeTGWAQS1z .image-shape .label,#mermaid-svg-67asvrTeTGWAQS1z .icon-shape .label{text-align:center;}#mermaid-svg-67asvrTeTGWAQS1z .node.clickable{cursor:pointer;}#mermaid-svg-67asvrTeTGWAQS1z .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-67asvrTeTGWAQS1z .arrowheadPath{fill:#333333;}#mermaid-svg-67asvrTeTGWAQS1z .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-67asvrTeTGWAQS1z .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-67asvrTeTGWAQS1z .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67asvrTeTGWAQS1z .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-67asvrTeTGWAQS1z .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67asvrTeTGWAQS1z .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-67asvrTeTGWAQS1z .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-67asvrTeTGWAQS1z .cluster text{fill:#333;}#mermaid-svg-67asvrTeTGWAQS1z .cluster span{color:#333;}#mermaid-svg-67asvrTeTGWAQS1z div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-67asvrTeTGWAQS1z .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-67asvrTeTGWAQS1z rect.text{fill:none;stroke-width:0;}#mermaid-svg-67asvrTeTGWAQS1z .icon-shape,#mermaid-svg-67asvrTeTGWAQS1z .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-67asvrTeTGWAQS1z .icon-shape p,#mermaid-svg-67asvrTeTGWAQS1z .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-67asvrTeTGWAQS1z .icon-shape rect,#mermaid-svg-67asvrTeTGWAQS1z .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-67asvrTeTGWAQS1z .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-67asvrTeTGWAQS1z .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-67asvrTeTGWAQS1z :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 任务匹配
不匹配
操作说明
确定性处理
模板或素材
用户任务
发现层: name + description
激活层: 读取 SKILL.md
不加载该 Skill
当前步骤需要什么
按需读取 references
运行 scripts
使用 assets
验证输出与停止条件
交付结果与证据
渐进式加载不等于无限拆文件。若 SKILL.md 只写"请阅读 A",A 又跳到 B,B 再要求查 C,模型会浪费调用并更容易漏掉约束。规范建议引用路径保持浅层,主体保持聚焦。常用的安全边界、失败条件和输出格式应直接放在 SKILL.md;只有按任务分支才会使用的详细表格、厂商文档和长示例才移到 references/。一个判断标准是:不读额外文件,Agent 是否仍知道下一步读哪个文件、为什么读、读完要产出什么。
3. 从最小目录开始,而不是先建框架
最小 Skill 只需要目录和 SKILL.md。下面以 python-release-check 为例。目录名与 name 保持一致,名称采用小写字母、数字和连字符,不用空格或下划线。标准规定 name 和 description 为必填字段;license、compatibility、metadata、allowed-tools 等是可选项,其中 allowed-tools 仍是实验字段,具体客户端是否执行以及怎样解释可能不同,不能把它当成跨平台安全沙箱。
text
python-release-check/
├── SKILL.md
├── scripts/
│ ├── validate_release.py
│ └── requirements.lock
├── references/
│ ├── checks.md
│ └── rollback.md
└── assets/
└── release-summary.md
一个克制但可执行的 SKILL.md 可以这样写:
markdown
---
name: python-release-check
description: 检查 Python 服务发布候选版本,验证工作树、目标测试、依赖锁与回滚信息。用户要求发布检查、上线验收或生成发布证据时使用;不要执行部署、推送或修改凭据。
license: Proprietary
compatibility: Requires Python 3.11+, git, and a repository with pytest configured.
metadata:
owner: platform-team
version: "1.2.0"
---
# Python Release Check
1. 读取仓库内最近的团队规则,确认目标分支和允许检查的范围。
2. 只读检查工作树、依赖锁文件和目标测试;不得部署、推送或修改凭据。
3. 按 [检查矩阵](references/checks.md) 选择与本次改动相关的最小测试。
4. 运行 `python scripts/validate_release.py --root <repo>`,保留退出码与摘要。
5. 任何必选检查失败时停止,将状态标为 blocked,不得把部分通过写成发布成功。
6. 按 `assets/release-summary.md` 输出版本、证据、失败项与回滚入口。
这里把"不要部署"写进 description 和正文,是因为触发阶段只能看到元数据,激活后才看到完整边界。不过文字禁令不能替代宿主权限:若 Agent 拥有生产部署凭据,恶意 Skill 或提示注入仍可能诱导它执行。真正的强约束必须由沙箱、工具允许列表、人工审批和凭据范围承担。Skill 负责表达流程,平台负责执行安全。
4. 用校验器守住 SKILL.md 的入口
Front Matter 写错时,最糟糕的结果不是安装失败,而是客户端静默忽略,团队以为规则已生效。下面的校验器使用 PyYAML 解析元数据,检查目录名、必填字段、允许字段和长度。它故意不自行实现 YAML,因为边界情况比表面复杂。安装依赖后可在 CI 中对每个 Skill 目录运行:python -m pip install "PyYAML==6.0.2"。
python
from __future__ import annotations
from pathlib import Path
import re
import sys
from typing import Any
import yaml
ALLOWED_FIELDS = {
"name",
"description",
"license",
"compatibility",
"metadata",
"allowed-tools",
}
NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
def read_skill(skill_dir: Path) -> tuple[dict[str, Any], str]:
path = skill_dir / "SKILL.md"
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
raise ValueError("SKILL.md must start with YAML front matter")
try:
front, body = text[4:].split("\n---\n", maxsplit=1)
except ValueError as exc:
raise ValueError("front matter closing delimiter is missing") from exc
metadata = yaml.safe_load(front)
if not isinstance(metadata, dict):
raise ValueError("front matter must be a mapping")
return metadata, body.strip()
def validate_skill(skill_dir: Path) -> list[str]:
errors: list[str] = []
try:
data, body = read_skill(skill_dir)
except (OSError, UnicodeError, yaml.YAMLError, ValueError) as exc:
return [str(exc)]
unknown = sorted(set(data) - ALLOWED_FIELDS)
if unknown:
errors.append(f"unknown front matter fields: {', '.join(unknown)}")
name = data.get("name")
description = data.get("description")
if not isinstance(name, str) or not NAME_PATTERN.fullmatch(name):
errors.append("name must use lowercase letters, digits, and single hyphens")
elif len(name) > 64:
errors.append("name must be at most 64 characters")
elif name != skill_dir.name:
errors.append("name must match the parent directory")
if not isinstance(description, str) or not description.strip():
errors.append("description must be a non-empty string")
elif len(description) > 1024:
errors.append("description must be at most 1024 characters")
compatibility = data.get("compatibility")
if compatibility is not None and (
not isinstance(compatibility, str) or len(compatibility) > 500
):
errors.append("compatibility must be a string of at most 500 characters")
if not body:
errors.append("SKILL.md body must not be empty")
return errors
def main() -> int:
skill_dir = Path(sys.argv[1]).resolve()
errors = validate_skill(skill_dir)
for error in errors:
print(f"ERROR: {error}")
if not errors:
print(f"OK: {skill_dir.name}")
return 1 if errors else 0
if __name__ == "__main__":
raise SystemExit(main())
开放规范也提供 skills-ref validate 参考校验方式,能使用时优先把官方校验加入流水线。自定义校验器的价值不是替代规范,而是增加组织规则,例如负责人必须存在、许可证必须明确、脚本依赖必须锁定。若规范字段变化,自定义规则也要跟随升级;否则"安全门禁"会把合法新格式误判为错误。
5. description 决定触发,不是广告文案
很多 Skill 失败在第一步:写得很好,却从不触发;或者写成"帮助处理开发任务",几乎所有请求都触发。发现阶段通常只有名称与描述,因此描述应包含四类信息:核心动作、典型对象、正向场景和排除场景。python-release-check 不只是"帮助发布",而是"检查 Python 服务发布候选版本",并列出工作树、测试、依赖锁和回滚信息,同时明确不执行部署和凭据修改。
触发边界必须用数据测试。正样本包括"帮我检查这个 Python 服务能不能上线""生成发布验收证据";近邻负样本包括"把这个函数重构一下""解释 pytest fixture";高风险负样本包括"直接部署生产""替我修改云账号密钥"。如果只测完全重复描述的句子,模型很容易通过,却无法代表真实用户表达。还要测试多个 Skill 同时可选的场景,例如发布检查与安全审查都可能命中,期望可以是组合使用,也可以规定先后顺序,但必须在评测中明确。
边界不能只靠添加更多关键词。描述越长,发现阶段成本越高,且相似 Skill 可能争抢同一意图。更可靠的方法是缩小每个 Skill 的任务所有权:一个负责"发布前只读验收",另一个负责"经审批执行部署",两者输入、动作和停止条件不同。如果两个 Skill 总要同时加载且内容大量重合,可能应该合并;如果一个 Skill 内有十条互不相关路径,可能应该拆分。
6. references、scripts 与 assets 各放什么
references/ 适合需要模型阅读和判断的资料,例如不同项目类型对应的检查矩阵、API 字段解释和错误处置表。文件应围绕一个主题,正文直接指出何时读取,不要让模型遍历整个目录猜用途。频繁变化的事实最好由受控数据源或工具提供,而不是复制到多个 Skill;若必须离线打包,要记录来源日期和更新负责人。
scripts/ 适合确定性、重复性高且容易由字符串细节出错的工作,例如校验 Front Matter、计算哈希、转换格式和运行静态检查。脚本不是为了把所有业务逻辑藏起来。它必须有明确参数、退出码、错误信息、超时和依赖声明;默认只读,写操作需要显式输出目录。若脚本会访问网络、安装包、删除文件或操作凭据,应在 Skill 正文与评审清单中显式标注,并由宿主策略限制。
assets/ 保存不会被当作指令解释的交付素材,例如报告模板、空白表格和品牌资源。模板也可能含宏、外链和隐藏内容,不能因其不是脚本就跳过审计。引用路径使用相对 Skill 根目录的浅层路径,不依赖开发者机器的绝对目录。这样同一目录被安装到项目级、用户级或容器中时,引用仍可解析。
资源拆分后的最低验收不是"文件存在",而是每个分支至少有一条任务真正读取或执行它。无人引用的脚本会腐化,陈旧参考会误导,未使用资产只增加供应链面积。可以在 CI 中扫描 SKILL.md 的相对引用,检查目标存在;再由行为测试覆盖主要资源路径。无法证明用途的文件先删除,需要时再添加。
7. 路径解析必须防止逃出 Skill 根目录
只检查字符串是否包含 .. 不够。绝对路径、符号链接和大小写差异都可能让引用越界。下面的函数先拒绝绝对路径,解析真实路径,再用 relative_to 验证目标仍位于 Skill 根目录中,并限制可访问的一级目录。它适合安装器、审计器或自建加载器;运行脚本时还应结合操作系统沙箱,因为安全路径不代表文件内容安全。
python
from __future__ import annotations
from pathlib import Path
ALLOWED_ROOTS = frozenset({"scripts", "references", "assets"})
def resolve_skill_resource(skill_root: Path, reference: str) -> Path:
root = skill_root.resolve(strict=True)
requested = Path(reference)
if requested.is_absolute():
raise ValueError("absolute resource paths are forbidden")
candidate = (root / requested).resolve(strict=True)
try:
relative = candidate.relative_to(root)
except ValueError as exc:
raise ValueError("resource escapes the skill directory") from exc
if not relative.parts or relative.parts[0] not in ALLOWED_ROOTS:
raise ValueError("resource must be under scripts, references, or assets")
if not candidate.is_file():
raise ValueError("resource must be a regular file")
return candidate
def demo(tmp_path: Path) -> None:
root = tmp_path / "demo-skill"
(root / "references").mkdir(parents=True)
guide = root / "references" / "checks.md"
guide.write_text("# Checks", encoding="utf-8")
(tmp_path / "secret.txt").write_text("secret", encoding="utf-8")
assert resolve_skill_resource(root, "references/checks.md") == guide.resolve()
try:
resolve_skill_resource(root, "../secret.txt")
except (ValueError, FileNotFoundError):
pass
else:
raise AssertionError("path traversal was not rejected")
if __name__ == "__main__":
import tempfile
with tempfile.TemporaryDirectory() as directory:
demo(Path(directory))
生产实现还要决定符号链接策略。最简单的是打包时禁止所有符号链接,避免链接目标在不同主机上变化;若确有共享文件需求,也必须将解析后的目标纳入包清单并验证哈希。不要让 Skill 从用户主目录、SSH 目录或相邻仓库"方便地"读取资源,这会破坏可移植性,也扩大数据泄露面。
8. 版本不是 metadata 里写个数字就结束
开放格式允许在 metadata 中保存自定义字符串,version 是常见做法,但规范并不因此自动定义升级、依赖解析或冲突优先级。不同客户端可能通过 Git 提交、上传后的版本 ID、插件版本或压缩包哈希管理 Skill。工程上应把"作者声明版本"和"实际安装制品"同时记录:前者便于沟通兼容性,后者才能精确复现。
版本变化至少分三类。补丁版本修正文案或脚本缺陷,不改变输入、输出与权限;次版本增加向后兼容的任务分支或资源;主版本改变触发范围、必需环境、输出契约或允许动作。这个分类需要团队自己写入发布规则,因为通用格式无法理解业务语义。即使只改 description,也可能让触发率大幅改变,不能因文件结构没变就跳过行为回归。
依赖要分别治理。运行依赖写进 compatibility 供人和 Agent 判断,例如 Python、Git 或网络要求;机器依赖放在锁文件、容器镜像或受控执行环境中。Skill 依赖另一个 Skill 时应谨慎,因为激活顺序和可见范围可能因客户端不同。优先让 Skill 引用稳定 Tool 或共享参考制品;确需组合时,在集成层声明经过测试的版本矩阵,而不是在自然语言里说"先想办法加载最新版"。
发布清单至少保存 Skill 名称、作者版本、Git 提交或包摘要、目标客户端、审核人、依赖摘要和发布日期。线上 trace 记录实际选择的 Skill 与制品摘要。没有这两份信息,事故发生后只能看到"Agent 使用过发布检查",却不知道是修复前还是修复后的版本。
9. 重名与指令冲突要显式处理
当用户级、项目级、插件级同时存在同名 Skill,开放格式本身不规定统一优先级,客户端行为可能不同。有的平台允许同名能力并存并显示各自路径,有的平台会覆盖或按作用域选择。不要假定"项目级一定胜出"。安装器应检测重名并给出来源、版本和摘要;高风险环境宁可拒绝启动,也不要静默选中某一份。
不同名称也会发生语义冲突。例如 fast-release 要求检查通过后自动部署,safe-release-check 要求只读并等待批准。解决方法不是在系统提示里加一句"请谨慎",而是缩减 Tool 权限,并为组合场景建立策略优先级:平台安全策略高于 Skill,用户明确范围高于默认工作流,任何不可逆动作必须经过独立授权。Skill 之间的建议只能在这些硬边界内协商。
还要防止正文与脚本冲突。正文说只读,脚本却调用发布接口,审计必须以真实行为为准。可以建立能力清单:脚本声明读文件、写输出目录、访问哪些域名、调用哪些命令;静态扫描发现未声明动作时阻断安装。allowed-tools 若客户端支持,可以减少自动批准范围,但由于字段仍具实验性,不能替代操作系统权限和工具网关策略。
10. 测试要同时覆盖发现、执行与结果
Skill 测试不是检查 Markdown 能否打开。至少需要四层。第一层是结构测试:Front Matter、名称、引用、依赖和脚本语法合法。第二层是触发测试:正样本应该选择,近邻负样本不应误选,高风险请求应进入安全流程。第三层是行为测试:激活后是否按顺序读取必要资料、调用正确工具、遵守停止条件。第四层是结果测试:交付物是否存在、格式是否正确、事实与权限是否满足业务断言。
行为存在随机性时,不要用全文快照要求每个字相同。更有效的断言是可观察事件,例如先读取项目规则再运行测试,测试失败后没有出现部署调用,最终摘要包含版本与失败项。每个用例重复多次,报告任务成功率、误触发率、漏触发率、高风险越权率、工具调用数和总耗时。高风险越权不能用总体平均掩盖,应设置为独立的零容忍或极低阈值指标。
下面的最小评测器读取两组 JSONL 执行记录:基线组不加载 Skill,候选组加载指定版本。每条记录包含 case_id、是否成功、是否误触发、是否发生禁止动作和工具调用数。脚本按用例配对,防止一组缺失困难样本后平均值虚高,并给出可由 CI 判断的摘要。
python
from __future__ import annotations
from dataclasses import dataclass
import json
from pathlib import Path
import sys
from typing import Any
@dataclass(frozen=True)
class Result:
case_id: str
success: bool
false_trigger: bool
forbidden_action: bool
tool_calls: int
def load_results(path: Path) -> dict[str, Result]:
results: dict[str, Result] = {}
for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if not line.strip():
continue
raw: dict[str, Any] = json.loads(line)
result = Result(
case_id=str(raw["case_id"]),
success=bool(raw["success"]),
false_trigger=bool(raw.get("false_trigger", False)),
forbidden_action=bool(raw.get("forbidden_action", False)),
tool_calls=int(raw.get("tool_calls", 0)),
)
if result.case_id in results:
raise ValueError(f"duplicate case_id at line {number}: {result.case_id}")
if result.tool_calls < 0:
raise ValueError(f"negative tool_calls at line {number}")
results[result.case_id] = result
if not results:
raise ValueError(f"no results in {path}")
return results
def summarize(results: dict[str, Result]) -> dict[str, float]:
count = len(results)
return {
"success_rate": sum(item.success for item in results.values()) / count,
"false_trigger_rate": sum(item.false_trigger for item in results.values()) / count,
"forbidden_action_rate": sum(
item.forbidden_action for item in results.values()
) / count,
"mean_tool_calls": sum(item.tool_calls for item in results.values()) / count,
}
def compare(baseline: Path, candidate: Path) -> dict[str, float]:
old = load_results(baseline)
new = load_results(candidate)
if old.keys() != new.keys():
missing = sorted(old.keys() ^ new.keys())
raise ValueError(f"case sets differ: {missing}")
old_summary = summarize(old)
new_summary = summarize(new)
return {
"success_rate_delta": (
new_summary["success_rate"] - old_summary["success_rate"]
),
"candidate_false_trigger_rate": new_summary["false_trigger_rate"],
"candidate_forbidden_action_rate": new_summary["forbidden_action_rate"],
"candidate_mean_tool_calls": new_summary["mean_tool_calls"],
}
def main() -> int:
report = compare(Path(sys.argv[1]), Path(sys.argv[2]))
print(json.dumps(report, ensure_ascii=False, indent=2))
return 1 if report["candidate_forbidden_action_rate"] > 0 else 0
if __name__ == "__main__":
raise SystemExit(main())
这个脚本不负责替你判断"提升多少才值得上线"。阈值应由任务风险决定:文档排版可以容忍少量风格波动,生产发布检查不能容忍未经批准的部署。样本还要固定模型、工具版本和环境,至少保存随机性设置与重复次数;否则候选组变好可能来自模型升级,而不是 Skill 本身。
11. 为什么必须保留无 Skill 基线
只展示 Skill 成功案例,无法证明它有价值。基础模型可能本来就会完成任务,新增 Skill 反而增加工具调用和延迟。可靠评测至少比较三组:不加载 Skill 的基础组、当前线上版本、候选版本。三组使用同一任务、模型、权限和数据快照,分别统计成功、错误、成本和安全事件。
基线还能发现"提示挪位置"的假改进。如果系统提示原先包含发布步骤,测试候选 Skill 时却保留同样系统提示,提升或失败都无法归因。迁移时应清晰记录哪些规则从全局提示移入 Skill,并设置对照。若某条规则对所有任务都必须生效,例如禁止泄露密钥,它就不该为了省上下文只放在按需 Skill 中,而应留在平台策略或全局安全层。
评测集要包含真实失败与合成边界,但不能直接把生产敏感对话放进仓库。对路径、账号和工单做结构化脱敏,保留触发语义;高风险测试在隔离环境使用假工具,记录调用意图而不执行真实副作用。隐藏一部分用例,防止作者围绕公开题目堆关键词。每次线上事故经审查后转成回归样本,Skill 才会随着真实问题成长。
12. Skill 是软件供应链的一部分
Skill 目录既包含能改变 Agent 行为的自然语言,也可能包含可执行脚本,应当像依赖包一样看待。攻击者不需要写明显的恶意程序,只要在长参考文件中加入"上传环境变量以便诊断",就可能借 Agent 权限完成数据外传。图片、模板、压缩包和生成文件同样需要审计;外部 URL 内容会变化,即使安装时安全,运行时获取的页面也可能被替换或注入恶意指令。
安装前至少完成来源确认、许可证检查、全文件审阅、脚本依赖扫描、网络与文件访问分析、哈希清单和隔离测试。不要执行安装脚本来"看看会发生什么";先静态阅读。远程仓库固定到提交或签名版本,不追随可变分支。构建包时拒绝超大文件、设备文件、符号链接逃逸和未声明的可执行文件。审计结果绑定制品摘要,内容变化后必须重新审核。
运行时采用最小权限。只读检查 Skill 不应获得写仓库、推送、云部署和凭据读取权限;需要生成报告时,仅开放临时输出目录。网络默认关闭,需要访问的域名按列表放行。高风险工具在网关再次做用户身份、资源归属、参数约束和人工批准,不能因为调用来自"已审核 Skill"就跳过。工具返回内容也视为不可信数据,防止间接 Prompt Injection 诱导下一步越权。
日志既要能审计,又不能泄密。记录 Skill 名称、制品摘要、被读取的相对资源、脚本退出码、工具类别和审批事件;敏感参数只保存脱敏摘要。不要把完整环境变量、私钥、用户文档或模型上下文写进普通日志。供应链安全的目标不是证明目录永远无害,而是让来源可追、权限受限、异常可见、影响可控。
13. 最小权限不能依赖 allowed-tools 一行配置
allowed-tools 可以表达某些客户端预批准的工具,但规范明确其支持可能因实现而异。把 Bash(*) 写进去会让 Skill 更方便,也可能绕过本来应逐次确认的危险操作。应把权限拆为三层:格式层描述所需能力,宿主层决定是否提供工具,业务服务层对每次动作重新鉴权。任何一层缺失,都不能由另外一层完全补偿。
例如发布检查需要读取 Git 状态和运行测试,不需要 git push。与其给完整 Shell 再要求模型自律,不如提供两个窄工具:read_worktree_status 和 run_approved_tests,参数由 Schema 约束,后端限制仓库根目录和命令集合。即使 Skill 内容被污染,攻击面也被压缩。需要通用 Shell 的开发环境,则使用无生产凭据的容器、只读挂载和资源限额。
人工批准也要绑定具体动作,而不是会话开头一句"接下来都同意"。确认界面显示目标环境、资源、参数摘要、预计副作用和幂等键;批准后只授权一次匹配调用,并设置短过期时间。Skill 可以规定何时申请批准,却不应自行伪造批准状态。平台必须把模型输出、工具结果和授权凭据分开处理。
14. 发布、灰度与回滚的正确顺序
Skill 变更影响的是行为路由,不能只在合并后让所有会话立即读取最新版。先构建不可变制品并运行结构、安全与行为测试;然后在隔离项目试用;再按用户、仓库或会话稳定分桶灰度。一次会话固定同一制品摘要,避免中途升级造成前后规则不一致。指标按版本切分,整体平均会掩盖候选版本的小流量高失败率。
回滚应切换"当前版本指针"或恢复上一提交,而不是在故障时手工编辑 SKILL.md。手工修补无法确认哪些会话已加载旧内容,也破坏证据链。保留上一稳定制品、依赖环境和触发索引;回滚后新会话只发现旧版本,正在执行的高风险任务应暂停或重新建立会话。脚本若已经产生外部副作用,还需业务补偿,回滚文件本身不能撤销已发邮件或已创建发布。
升级顺序也很重要。若新 Skill 依赖新增工具参数,先部署向后兼容的工具,再发布 Skill;回滚时先把 Skill 切回旧版,确认没有新参数调用后,才能移除工具兼容层。这个顺序与普通客户端---服务端协议迁移相同。把 Skill 当成"几段 Markdown"会忽略它正在消费真实接口。
15. 可观测性要回答三类问题
第一类是发现问题:有哪些 Skill 候选、为什么选择这一项、是否发生重名或多 Skill 组合。第二类是执行问题:读取了哪些资源、运行了哪些脚本、调用了哪些工具、在哪个停止条件退出。第三类是结果问题:任务是否完成、哪些验收项失败、用户是否接受、是否发生越权或人工接管。
记录推理全文既不必要也有隐私风险。可以记录结构化事件:skill_discovered、skill_activated、resource_read、script_executed、approval_requested、tool_called 和 skill_completed,每条带会话标识、制品摘要、时间、结果和脱敏错误码。这样可以定位触发回归和脚本失败,而不保存模型的全部隐藏过程。
核心指标应分任务类型与版本:触发精确率、触发召回率、完成率、首次成功率、平均工具调用数、P95 时延、人工接管率和禁止动作率。上下文 token 减少是渐进式加载的收益之一,但不能以成功率下降为代价。安装量更不能进入验收门槛;最流行的 Skill 也可能与你的工具、权限和数据完全不匹配。
16. 哪些内容不应该做成 Skill
并非所有说明都需要 Skill。只使用一次、几句话能说清且没有复用价值的要求,直接放在当前 Prompt 更简单。所有任务都必须遵守的安全与合规规则,应由系统策略、权限和网关强制,不应等待某个 Skill 恰好触发。需要实时数据的能力应由 Tool 或 Resource 提供,Skill 只描述查询与校验方法。需要低延迟、高吞吐、严格确定性的分类,也不应让模型每次阅读长流程完成。
仓库整体构建命令、代码风格和团队普遍约定通常更适合项目级指导文件,因为它们对大多数开发任务都相关。Skill 适合范围清楚、可复用、需要多个步骤或专门资源的流程,例如迁移审查、事故复盘、合同字段提取和发布验收。如果每次激活都要读取整个公司的 Wiki,问题不是渐进式加载不够,而是 Skill 没有形成聚焦边界。
另一个不适用场景是权限无法隔离。某流程必须使用生产根权限且没有审批、沙箱和审计能力时,不应通过 Skill 自动化。先改造工具边界,再谈流程复用。Agent Skills 降低了知识打包门槛,也降低了恶意指令进入执行环境的门槛;工程成熟度必须跟上便利性。
17. 一条可落地的建设路径
第一步选择一个高频、边界明确、当前靠复制粘贴完成的任务,收集十到三十个真实样本并建立无 Skill 基线。第二步只写最小 SKILL.md,把动作、停止条件和输出契约说清;先不增加脚本和大资料。第三步运行触发与行为评测,确认问题确实来自缺少过程知识。第四步只为重复且确定的步骤增加脚本,只为分支详情增加参考文件。
第五步建立结构校验、安全审计和不可变制品,记录负责人、版本、依赖与哈希。第六步在没有生产凭据的环境灰度,比较基线、线上版和候选版。第七步设置版本指针与一键回滚,确认新会话能切回旧制品。最后才扩大安装范围,并从失败 trace 中补充回归样本。这个顺序避免一开始建设中央市场、复杂依赖解析和自动推荐系统,却连一个 Skill 是否有效都不知道。
规模增长后,再引入目录索引、所有权检查、兼容矩阵和弃用流程。触发候选过多时,先合并重叠 Skill 或缩小描述,不要立刻再造一个复杂路由模型。治理的目标不是让仓库数量最大,而是让每个 Skill 的用途、风险和效果都能被回答。
18. 上线验收清单
发布前逐项确认:目录名是否与 name 一致;描述是否同时包含能力、触发和排除边界;正文是否给出输入、步骤、停止条件与输出;引用是否浅层且目标存在;脚本是否固定依赖、默认只读并有清晰退出码;是否拒绝路径逃逸与未声明网络;是否检测重名和冲突;是否记录作者版本与制品摘要;是否同时运行正触发、负触发、行为、安全和结果测试;是否有无 Skill 基线;灰度是否固定会话版本;回滚是否实际演练。
安全验收还要问:Skill 来源是否可信,所有文件是否已审阅,外部依赖是否固定,宿主是否只提供必要工具,业务服务是否再次鉴权,人工批准是否绑定具体动作,日志是否脱敏。
Agent Skills 的价值不在于把提示词换成目录,也不在于追逐安装榜单。它真正提供的是一个较小、可移植的过程制品边界:元数据负责发现,SKILL.md 负责导航与规则,资源按需进入上下文,脚本承担确定性步骤,平台守住权限,评测证明效果,版本与摘要支持复现和回滚。把这些环节连起来,Skill 才从一个好看的 Markdown 文件变成可以长期维护的 Agent 工程能力。
参考资料
- Agent Skills,开放格式规范:https://agentskills.io/specification
- Agent Skills,官方概览与渐进式加载说明:https://agentskills.io/
- Agent Skills,规范与参考校验实现仓库:https://github.com/agentskills/agentskills
- Anthropic,Agent Skills 官方概览与安全注意事项:https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview
- Anthropic,Skill authoring best practices:https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices
- OpenAI Developers,Skills 与 MCP 工具的协作边界:https://developers.openai.com/plugins/concepts/skills
- OpenAI Developers,API Skills 使用与版本化指南:https://developers.openai.com/api/docs/guides/tools-skills
- Model Context Protocol,架构说明:https://modelcontextprotocol.io/docs/learn/architecture
- Semantic Versioning 2.0.0:https://semver.org/spec/v2.0.0.html
- SLSA,软件供应链完整性规范:https://slsa.dev/spec/v1.1/