也许你会有这样的经历:本周你已经用同样的说明四次解释了 AI 模型。
你反复谈到你们团队是如何组织一份演示文稿的;部署前会运行哪些检查;为什么暂存数据库不是 README 中提到的那个。
每次你都要重新输入一次;每次,代理都能做得不错;但每次,下一次会话都要从零开始。
这就是 Agent Skills 要解决的问题。
一个 skill 是一个仅包含单个 Skill.md 文件的文件夹。代理在启动时读取一行摘要,仅在任务真正需要时才打开完整说明。只需编写一次解释,将其提交到代码旁边,团队中的每个代理即可随时访问并自动化任何乏味的任务。
该格式起源于 Anthropic,随后作为开放标准发布,如今它支持超过 45 种可与之配合使用的工具,包括 Claude Code、VS Code、GitHub Copilot、Cursor、Gemini CLI、Codex、Goose 和 JetBrains Junie。Google Antigravity 也支持它。一个文件夹,适用于所有工具。
使用 Agent Skills 自动化可重复任务
大多数指南会把十几个半成品示例散落在十几个小节里。这里,我们将从零开始构建一个真正的 agent skill,使其能够在所有 AI 工具中兼容。
我们将构建 恰好一个 skill,命名为 deck-builder,它是本文唯一的示例。它教会代理如何将模糊的请求如 “为我制作一个关于 Q3 迁移的演示文稿” 转变为真实的演示大纲,方法是 先头脑风暴,后编写幻灯片。
这种顺序正是重点。让任何模型生成演示文稿,它会立即开始制作第一张幻灯片。结果是十二张内容整齐但泛泛而谈的要点幻灯片,却从未明确演示文稿的目的 是什么。
擅长此事的人会采取不同的做法。他们会先询问在场的人是谁,想向观众传达的核心信息是什么,以及他们想使用的大纲或框架。随后,他们会基于心理模型开始制作演示文稿。
该 skill 最初只有 28 行 Markdown,十分钟即可写完。到最后,它将具备经过调校的描述、误报列表、捆绑的验证器、按需参考文件、评估套件以及干净的安全扫描。本指南中的每个想法都会在这个随之增长的同一文件夹中得到演示。
这里不需要任何账户、API 密钥或 PowerPoint。你只需把 skill 放入 skills 文件夹,它就会根据你创建的 skill 生成演示文稿。
目录
Prerequisites
你不需要深入了解 AI 代理就能跟随本指南,但掌握一些基础知识会让示例更易于操作。
你应该能够熟悉使用 Markdown 文件、在项目目录中导航以及在终端运行简单命令。示例使用 Python 作为捆绑验证器,因此你的机器上还需要安装 Python 3.9 或更高版本。
在演练过程中,你将使用支持 Agent Skills 的 AI 编程客户端,例如 Claude Code、带有 GitHub Copilot 的 VS Code 或 Google Antigravity。该技能本身不需要账户、API 密钥或 PowerPoint。
这就是你所需的全部。我们将从一个单一的 SKILL.md 文件开始,然后随着工作流变得更有用且更可靠,逐步构建它。
代理技能到底是什么
一个技能是一个目录。其中有一个必需的文件,名为 SKILL.md。
该文件由两部分组成:顶部是简短的 YAML 块,下面是 Markdown 说明;其余内容均可选。
注意:在此文件夹中添加更多文件会消耗更多 token,因为上下文窗口会因无关或废弃文件而增大。
本指南结束时,deck-builder 将位于此处:
deck-builder/
├── SKILL.md
├── scripts/
│ └── validate_deck.py
├── references/
│ └── narrative-patterns.md
└── evals/
└── evals.json
您需要安装 Python 来执行脚本。技能是一份包含特定目标的元数据和说明的 Markdown 文件。
使用这种格式的价值在于它所捕捉的内容:
代理无法猜测的专业知识:您的评审门槛、演示风格、幻灯片惯例、框架等等。
重复的工作流程:多步骤任务变成一致的程序,而非即兴发挥。
跨工具复用:一次构建,在任何兼容的客户端(Claude Code、VS Code 等)中运行。
效果最好的心智模型是一个 可执行的运行手册。把一个好的技能想象成你会在第一天交给一个聪明新手的文档:它解释了确切的工作流程,指出了常见的陷阱,为繁琐的部分提供了脚本,并展示了如何验证一切是否正常运行。
Why Your Giant System Prompt Stops Working
大多数团队一开始会把所有内容堆进一个始终开启的文件中:AGENTS.md、CLAUDE.md,或者代码中的系统提示。幻灯片惯例被塞进去,位于部署运行手册和风格指南之间。
当只有两个约定时,这没问题。但会因三个原因而失效。
1. It costs you on every single request
系统提示会在每次 API 调用时加载。在用户输入任何内容之前,二十份运行手册可能会达到数万个标记。
您在第一轮就要付出这个成本。在第四十轮时又要再付一次。即便对话与演示无关,您仍需承担这一开销。
2. Long context doesn't mean even attention
大的上下文窗口并不等同于在其上均匀分配注意力。
当重要的那行被埋在数千行无关内容中时,遵循指令的能力会下降。"在写幻灯片之前先进行头脑风暴" 在 2000 个标记的提示中会被可靠遵循。在 80000 个标记的提示中,主要观点会被跳过。加载更多上下文并不等同于被理解。
3. Prose can't enforce a procedure
让模型“检查大纲”,每次得到的检查结果都不同。有时很彻底,有时只是一句话的赞美。
散文会让代理可能做对的事情。只有脚本才能使其可验证。
渐进式披露可以解决前两个问题。我们将在版本 4 中添加的验证器可以解决第三个问题。
Token Optimisation for Agent Skills
代理分三个阶段加载技能。了解每个阶段的结束位置是保持技能高效的关键。

规范对此给出了具体数字。发现阶段每个技能的元数据大约为 100 个 token。激活时,正文建议上限为 500 行或 5,000 个 token。超过此范围的内容应放在 references/ 目录中。
激活不是一次性费用
这一细节改变了你编写技能的方式。激活不是在代理完成任务后就会消失的一次性成本。 一旦技能被激活,其说明将保持在对话中,并在会话剩余时间继续消耗上下文。
Claude Code 精确地记录了这一点:渲染后的 SKILL.md 以单条消息的形式进入对话,并且会一直保留到会话结束。该文件不会在后续轮次被重新读取。
这里有三个后果:
每一行都是持续性成本: 膨胀的正文会在第二十轮产生费用,而不仅仅是在触发它的那轮。
编写常驻指令,而非一次性步骤: 代理在整个会话期间看到的都是相同的文本。
压缩可能会驱逐你的技能: 当 Claude Code 压缩长对话时,它会重新附加每个技能的最近一次调用,在总共 25,000 token 的预算内保留每个技能的前 5,000 token,优先保留最近的。如果调用许多技能,最早的那些将被完全驱逐。
最后一点解释了人们常见的误解。当技能在会话中途似乎停止工作时,其说明未必已经消失。模型可能只是更倾向于另一种方法。在这种情况下,应加强技能的说明,或在上下文压缩后重新调用它。
数学概要
设 N 为你已安装的技能数量,T(下降)为单个技能的元数据成本,T(完整)为单个技能正文的成本,k 为任务实际打开的技能数量。
一次性加载全部内容:
$$C_{ ext{static}} = N imes T_{ ext{full}}$$
逐步加载:
$$C_{ ext{progressive}} = N imes T_{ ext{desc}} + k imes T_{ ext{full}}$$
$$Use\\ the\\ spec's\\ own\\ budgets: T_{desc} = 100\\ and\\ T_{full} = 5,000,\\ for\\ 50\\ skills\\ where\\ one\\ activates.$$
$$C_{ ext{static}} = 50 imes 5{,}000 = 250{,}000 ext{ tokens}$$
$$C_{ ext{progressive}} = (50 imes 100) + 5{,}000 = 10{,}000 ext{ tokens}$$
$$\\rho = 1 - \\frac{10{,}000}{250{,}000} = 0.96$$
可实现 96% 的减少,在任务相同且可用的五十项能力相同的情况下。
该公式也表明了它失效的地方。当 k 远小于 N 时,渐进式披露更有效,也就是说大多数技能保持关闭状态。如果每个请求都打开你目录的一半,那么你的技能范围过宽,你实际上重建了单体技能文件。保持 k 较小是一个设计目标。
技能的结构
前置信息
顶部的 YAML 块包含两个必需字段和四个可选字段。
| 字段 | 必需 | 规则 |
|---|---|---|
name |
是 | 1–64 个字符。小写字母、数字、连字符。不能有首尾或连续的连字符。必须与文件夹名称匹配。 |
description |
是 | 1–1024 个字符。说明技能的作用 以及 何时使用它。 |
license |
否 | 许可证名称,或一个捆绑的许可证文件。 |
compatibility |
否 | 最多 500 个字符。环境需求:产品、软件包和网络访问。 |
metadata |
否 | 用于自定义工具的自由格式字符串映射。 |
allowed-tools |
否 | 由空格分隔的预批准工具列表。该特性在规范中被标记为实验性,Claude Code 完全实现了它。请参见下方注释。 |
以下是 deck-builder 最终得到的内容:
---
name: deck-builder
description: >-
Turn a request for a presentation into a slide outline. Brainstorm the
audience, core message, and narrative arc first, then write slides. Use
when someone asks for a deck, slides, a presentation, a readout, a board
update, or a talk, including when they only say "put something together
for Thursday" without naming a format.
license: Apache-2.0
compatibility: Requires Python 3.9+. No network access needed.
---
有两条规则容易被忽视,尤其是在你开始让技能在不同客户端之间可移植时。第一条是 name 字段:它不仅仅是一个标签。根据规范,它必须完全匹配父文件夹。因此,如果你的技能位于 deck-builder/SKILL.md,前置信息必须使用 name: deck-builder,而不是 name: deckBuilder。
第二条是 description 字段的限制。规范允许最多 1024 个字符,但一旦你为了在 Version 2 中获得更好的触发效果而调整描述,这个限制就很容易被超出。一些客户端更宽松,接受这些规则的变体,但这种灵活性可能导致可移植性问题。先按照严格的规范编写,你的技能在各处表现一致的机会会大大提升。
客户端如何扩展这六个字段
该表格即是 规范。各个客户端会对其中的部分内容进行宽松处理,并添加自己的字段。在你计划共享的前置信息编写之前,了解这一点很有价值。
Claude Code 是扩展最多的实现。它将 name 视为可选项,并默认使用文件夹名称。在个人或项目技能中,name 仅设置显示标签,而文件夹名称决定命令。它将 description 视为推荐而非必需,若未提供则回退到正文的第一段。在其技能列表中,它会将描述截断至 1,536 个字符。它会接受 license 和 compatibility 字段,但不会对它们进行任何处理。
在这六个字段之上,它又额外添加了大约十几个字段,包括 when_to_use、argument-hint、arguments、disable-model-invocation、user-invocable、disallowed-tools、model、effort、context、agent、hooks、paths 和 shell。
这里有个陷阱。这些扩展既不可移植,也不会被简单地忽略。
例如,Claude Code 支持诸如 argument-hint 和 when_to_use 之类的额外前置信息字段。带有这些字段的技能在 Claude Code 中可以完美运行。但如果你将同样的 SKILL.md 移动到更严格的验证路径,比如 Claude Desktop 或 Claude API,这些额外字段可能导致验证失败。
请看下面这个前置信息示例:
---
name: deck-builder
description: Build presentation outlines. argument-hint: Give me a topic for the deck.
---
Claude Code 可以接受 argument-hint 作为客户端特定的扩展。更严格的验证器可能会因 argument-hint 不属于规范中定义的六个字段而拒绝该文件。
这就是错误的表现形式:
Unexpected key(s) in SKILL.md frontmatter: argument-hint.
Allowed properties are: allowed-tools, compatibility, description, license, metadata, name
实际规则很简单:如果某个技能只打算在单个客户端内使用,则使用客户端特定的扩展;否则,保持前置元数据仅限于规范定义的六个字段。这种区别很重要,因为可移植性不仅仅取决于另一个客户端是否能读取该文件,而在于是否能在没有客户端特定字段导致故障的情况下发现并验证相同的技能。
对于 deck builder,没有必要冒这个风险。它仅使用规范定义的六个字段,Claude Code 会直接加载而无需修改。这样可以使技能与格式的可移植子集保持一致,并且相同的 SKILL.md 在兼容的客户端之间移动会更容易。
常规文件夹
除了SKILL.md,规范定义了三种约定:
scripts/:代理运行的代码。自包含,错误明确,不会挂起。我们的包含validate_deck.py。references/:代理按需打开的文档。保持每个文件内容简洁。我们的包含narrative-patterns.md。assets/:模板、模式和图像。deck-builder不需要此目录。
不同的客户端可能会识别额外的目录。例如,Antigravity 还记录了 examples/ 和 resources/。但这些目录只是约定,而不是技能格式的固定要求。
实际上重要的是 SKILL.md 如何指向这些文件。代理会遵循你定义的相对路径,因此你可以根据技能的工作方式来组织支持材料。只要合适,就可以保留 scripts/、references/、assets/ 或客户端特定的目录,然后在 SKILL.md 中显式引用它们。
这些约定仍然具有人性化的重要好处。当有人打开他们未编写的技能时,熟悉的目录会立即告诉他们在哪里可以找到可执行代码、参考材料、示例或其他支持资源。
指向您自己的文件
始终使用相对于技能根目录的路径,永不使用绝对路径。
See [the narrative patterns guide](references/narrative-patterns.md) when the instructions are unclear.
Check the outline before writing slides:
python3 scripts/validate_deck.py --file outline.md
保持引用只有一层深度。指向指向更多文件的文件会让代理在导航上消耗轮次,而不是去工作。而 /Users/you/dev/skills/... 在队友克隆仓库的瞬间就会失效。
客户特定的改进:
Claude Code 会将 ${CLAUDE_SKILL_DIR}(即存放 SKILL.md 的文件夹)替换到 allowed-tools 中的正文和 Bash 规则两处。在两处都使用它,可以让技能在不弹出权限提示的情况下运行其自身捆绑的脚本。这是 Claude Code 的扩展,因此在便携技能中请保持使用纯粹的相对路径。
在提交前进行验证
随标准一起提供的参考库会检查前置元数据和命名:
skills-ref validate ./deck-builder
在 CI 中运行验证器,以便在技能进入运行时之前捕获格式错误的技能。
前置元数据错误对机器来说易于检测,但在代理运行后却难以诊断。令人沮丧的是,客户端并不总是以相同方式暴露这些失败。严格的验证器可能会直接以明确的错误拒绝该技能,而另一个客户端可能根本不列出该技能,导致你没有明显的迹象表明问题出在前置元数据上。
十分钟内构建版本 1
首先,选择正确的文件夹
规范说明了技能 内部 应包含什么内容。它 不 说文件夹应放置的位置,且各客户端有所不同。若在此处出错,是导致首个技能永不触发的最常见原因,因此在运行 mkdir 之前请先确认。
| 客户端 | 项目范围 | 个人范围 |
|---|---|---|
| Claude Code | .claude/skills/ |
~/.claude/skills/ |
| VS Code / Copilot | .github/skills/, .claude/skills/, .agents/skills/ |
~/.copilot/skills/, ~/.claude/skills/, ~/.agents/skills/ |
| Google Antigravity | .agents/skills/ |
~/.gemini/config/skills/ |
.agents/skills/ 是新兴的跨客户端约定。Claude Code 是个大例外:其文档命名为 .claude/skills/ 和 ~/.claude/skills/,并且根本不列出 .agents/skills/。
重叠部分是有用的部分。VS Code 也会扫描 .claude/skills/ 同样,因此一个文件夹能够满足这两个最典型的客户端。这就是本指南默认使用它的原因。
# Claude Code, and VS Code / Copilot, which both scan this location
SKILLS_DIR=.claude/skills
# Antigravity, or VS Code if you prefer the cross-client convention
# SKILLS_DIR=.agents/skills
mkdir -p "$SKILLS_DIR/deck-builder"
然后编写文件
创建 deck-builder/SKILL.md:
---
name: deck-builder
description: Helps make presentations.
---
# Deck Builder
Never start writing slides immediately. Decide what the deck is for first.
## Workflow
### Step 1: Brainstorm
Before any slide exists, write a `## Brainstorm` block answering three questions:
- **Audience.** Who is in the room, how long do you have, what do they
already know, and what decision do they need to make?
- **Core message.** One sentence. If the audience remembers nothing else,
what is it?
- **Arc.** How the deck moves from opening to ask.
If the request does not give you enough to answer these, ask the user
before continuing. Do not guess the audience.
### Step 2: Write slides
One idea per slide. Six bullets maximum. End on the ask, never on a
body slide.
这是一个完整可用的技能,单文件仅二十八行,无任何依赖。
它的以下三点适用于你将要构建的一切:
跳过背景说明: 不解释什么是幻灯片,代理已经知道。
规则可检验: “最多六个要点”可以通过目视确认。“遵循演示最佳实践”则不行。
描述故意写得不好:
Helps make presentations.正是那种模糊的概述,导致无法触发。修复它就是版本 2。
在 VS Code 或 Claude Code 中运行
文件已存在。现在确认你的客户端能看到它。
这比你想象的更重要。如果客户端永远发现不了某个技能,它就不会被加载到上下文中,表现出来的症状可能与描述薄弱或调校不当的技能完全一样。
在修改触发逻辑之前,先确认客户端确实能看到该技能。这个简单的检查能告诉你是在调试发现问题还是触发问题。
两个客户端均提供两种运行技能的方式,它们测试的内容不同:
按名称调用: 这会跳过描述,直接测试 主体。
提出应触发它的问题,不命名它。这会测试 描述。
两种路径都要测试。显式调用 能确认技能本身有效且其指令被遵循。自动触发 用来检验 description 是否足够具体,以便代理能够识别何时应用该技能。
这一区分让调试变得简单得多。如果显式调用成功但自动触发失败,说明技能主体正常工作,问题出在描述上。这就是进入 版本 2、调整触发逻辑的信号。
在 Claude Code 中
重启 Claude Code 以便发现新文件夹。它会实时加载对已有 skills 文件夹的修改,但若该文件夹在会话开始时不存在,则需要重启。
输入
/skills并确认列出deck-builder。直接用
/deck-builder调用,或者即使你提到“创建演示文稿”,它也会自动检测到/deck-builder并根据技能指令开始执行工作流。在全新会话中测试触发。提出问题,但不要命名技能:
你能为周四的董事会会议准备一些关于 Q3 迁移的材料吗?
命令名称来源于 文件夹 名称,而不是前置信息中的 name。重命名文件夹后,命令也会随之改变。Claude Code 还将自定义命令合并到了 skills 中,所以 .claude/commands/deploy.md 和 .claude/skills/deploy/SKILL.md 都会生成 /deploy。
在 VS Code 中
打开已安装 GitHub Copilot 的项目。
打开 Copilot Chat 面板。Agent Skills 快速入门建议在模式下拉菜单中选择 Agent 模式,这是因为代理可以在此模式下运行终端命令。VS Code 自身的技能文档未说明模式要求,因此如果在其他模式下看不到技能,请先切换到 Agent 模式,再假设文件有问题。
输入
/以列出可用的技能和提示。技能会以斜杠命令的形式出现在提示文件旁边。/skills会打开 Configure Skills 菜单,您可以在此确认deck-builder已被识别。从
/列表中选择它以运行。您可以追加上下文,例如/deck-builder for the board meeting。再次提出同样的未指定目标问题。
因为 VS Code 也会扫描.claude/skills/,您可以在那里重复使用相同的 deck builder 目录,而无需更改其结构。当同一个技能需要在 Claude Code 和 VS Code 之间共享时,这提供了一个便捷的共享位置。
如果您更倾向于遵循 VS Code 自身的约定,请将技能放置在 .github/skills/ 而不是。重要的不是选择一个单一的通用文件夹,而是将技能放置在目标客户端实际查找的位置,并在调试技能本身之前验证其是否被发现。
成功是什么样子
没有该技能时,模型通常会从第一张幻灯片打开:
Slide 1: Q3 Migration Overview
Slide 2: Goals
Slide 3: Timeline
Slide 4: Challenges
Slide 5: Results
Slide 6: Thank You
没有该技能,代理可以生成看似完全合理的内容:一系列熟悉章节的幻灯片,但缺乏对该幻灯片实际服务对象或需要完成目标的明确决定。
加载该技能后,在写出第一张幻灯片之前,行为就会发生变化。代理会先创建一个 Brainstorm 块,覆盖受众、核心信息和叙事弧线。它甚至可能停下来提出澄清性问题,因为诸如 “为周四的董事会会议准备一些材料” 这样的请求仍然留下一个关键细节未得到解答:董事会需要决定什么?
这种停顿正是 Version 1 的真正价值所在。代理不会急于生成幻灯片,而是花一点时间建立应指导幻灯片制作的思路。
当它不起作用时
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
技能在 /skills 中缺失 |
该文件夹位于客户端未扫描的位置,或者会话早于该文件夹的存在 | 重新检查路径表,然后重启 |
已列出,但未找到 /deck-builder |
命令来源于文件夹名称 | 将文件夹重命名为 deck-builder |
| 调用有效,但触发失败 | 描述过于狭窄。这是版本 1 的预期行为 | 继续使用版本 2 |
| 技能已加载,但代理仍然生成幻灯片 | 对指令的遵循程度因模型而异 | 在编辑技能之前先尝试其他模型 |
| 前置元数据被视为意外键而被拒绝 | 在更严格的路径上出现了客户端特定字段 | 仅限于规范规定的六个字段 |
第四行值得停下来思考。当输出令人失望时,本能是去修改技能。先更换模型。否则你会同时调试两个变量,而只有其中一个是你的文件。
版本 2:让其每次都被触发
一个永远不会被触发的技能毫无价值。description 承担了全部责任。这是代理在决策前看到的你的技能的唯一部分。
版本 1 的描述是 Helps make presentations. 它在出现 “presentation” 时触发,而在 “把东西准备好以便周四使用” 时保持沉默,而这正是人们实际提出的请求方式。
四项原则
以指令的形式编写:“在何时使用此技能……” 胜过 “此技能的作用……”。代理正在做出决策,因此应直接针对决策。
描述意图:代理匹配用户所请求的内容,而非你的架构。
强势一点: 列出适用的情形,包括用户回避你的词汇的情况。
注意上限: 1024 个字符是硬性上限,调优过程中描述会增长。
调整技能描述时,一个容易被忽略的细节是:并非每个请求都需要技能。当任务涉及多个步骤、领域特定判断,或难以自行可靠重复的流程时,代理倾向于使用技能。
比如 “幻灯片的合适字体大小是多少?” 通常不需要 deck builder。代理可以直接回答。当请求需要可重复的工作流时,描述才变得有价值,例如确定受众、定义核心信息、选择合适的叙事弧线,然后制作幻灯片。这正是一段多步骤工作,编写良好的技能在此能发挥作用。
测试触发而非猜测
你可以这样衡量。构建约 20 个带有标签的真实提示,以表明它们 应该 触发技能:8 到 10 个正例,8 到 10 个负例。将它们存储在 evals/trigger_queries.json 中。
[
{
"query": "can you put something together for thursday's board meeting on the q3 migration",
"should_trigger": true
},
{
"query": "I need to walk the new hires through how our deploy pipeline works, 20 mins",
"should_trigger": true
},
{
"query": "make the font bigger on slide 4 of this pptx",
"should_trigger": false
},
{
"query": "write me a one-page summary of the q3 migration for the wiki",
"should_trigger": false
}
]
有价值的正例是指技能有帮助,但措辞并未明说的情况。第一个例子从未提到“deck”、“slides”或“presentation”。如果提示已经明确要求技能所做的事情,任何描述都能通过,你也学不到任何东西。
最有价值的负面测试是近似失误:提示包含与技能相同的词汇,但实际上需要不同的任务。类似“Write a Fibonacci function”的提示几乎毫无用处,因为它与演示工作没有重叠。“Make the font bigger on slide 4”则更有用,因为它明确提到了幻灯片,但任务是编辑现有幻灯片而不是设计整套幻灯片。
维基摘要也是一个强烈的边界情况。它可能涉及相同的迁移,但用户要求的是书面文档,而不是演示。这些近似失误能告诉你描述是否理解了请求的意图,而不仅仅是匹配熟悉的词汇。
模型行为在每次运行之间会有差异,因此对每个查询运行三次并计算触发率。正例的触发率应高于0.5,负例应低于0.5。
不要过拟合
针对你编写的每个查询进行调优会产生一个在对应表述上有效但在真实用户面前失效的描述。
将数据集划分为大约60%的训练集和40%的验证集,并在每个集合中保持正负例的比例。利用训练集的失误来指导编辑。仅使用验证集来检查编辑是否具备泛化能力。
在两个集合上进行评估。
找出训练集的失误。遗漏触发意味着太窄;误触发意味着太宽。
朝着失误所代表的一般类别进行修订。永远不要直接粘贴失败查询中的关键词,这属于过拟合。
重复此过程,通常不超过五次。
选择验证率最高的迭代。这通常不是最后一次迭代。
结果
# Version 1 — fires on the word "presentation" and little else
description: Helps make presentations.
# Version 2 — tuned against 20 labelled queries
description: >-
Turn a request for a presentation into a slide outline. Brainstorm the
audience, core message, and narrative arc first, then write slides. Use
when someone asks for a deck, slides, a presentation, a readout, a board
update, or a talk, including when they only say "put something together
for Thursday" without naming a format. Not for editing existing slide
files or for writing prose documents.
两项改动起到了作用。它通过命名‘头脑风暴-然后构建’流程,使其对所做之事的描述更具体;同时,通过涵盖从未提及‘幻灯片’的请求,使其适用范围更广。
这句话的结尾部分是大多数人会跳过的。通过说明技能不适用于什么,可以防止误触发的近失。
版本 3:编写能赢得其 token 的主体
技能激活后,其完整主体会与对话、系统上下文以及其他所有激活的技能争夺注意力。请将其视为一种预算。
从真实专业知识出发
技能编写中最常见的失败是让 LLM 在没有领域输入的情况下编写技能。返回的结果虽然流畅但毫无用处:'考虑你的受众','保持幻灯片清晰'。
有效的技能来源于已有的事物。拿起你团队实际称赞的牌组,写下它为什么有效。拿出有人评论说:"这是三副牌组,而不是一副"的审阅意见。拿出因第二张幻灯片失去观众而被截断的汇报。
对于 deck-builder,原始材料是你已经对草稿给予人们的反馈。
删除代理已知的内容
对每一行自问:如果没有它,代理会弄错吗?如果不会,则删除。
## Slide design
A slide is a single screen in a presentation. Slides should be visually
clear and not too crowded. Audiences find it hard to read a lot of text
on a screen, so you should use bullet points to summarize your ideas.
## Slide design
One idea per slide. Six bullets maximum, each under 120 characters.
Anything longer belongs in speaker notes, not on the slide.
把它定义为一个连贯的工作
范围太窄会让一项任务牵涉四项技能;范围太宽则描述无法精准命中。
构思大纲和制作幻灯片是一项工作,因为后者依赖于前者。添加图表设计、演讲指导以及编辑现有 .pptx 文件相当于一人戴四顶帽子的四项工作。这就是我们经过调整的描述明确排除它们的原因。
将指令的力度与任务的脆弱程度相匹配
留出空间 以容纳多种可行的方法。解释为什么 胜过僵化的命令,因为理解目的的代理能够更好地适应。
## Choosing an arc
Pick the arc that matches what the audience needs to do:
- They must decide something: Situation, Complication, Resolution
- They are skeptical: lead with the objection, then dismantle it
- They need to learn: chronological, simplest case first
- They already agreed: skip persuasion, go straight to the plan
要明确规定顺序重要的地方:
## Order of operations
Do these in order. Do not write slides before the brainstorm exists.
1. Write the `## Brainstorm` block
2. Run `python3 scripts/validate_deck.py --file outline.md`
3. Write slides only after it exits 0
大多数技能都需要两者。按章节逐段校准。
提供默认选项
列出五个选项会引发深思:
You could use SCR, PAS, AIDA, the pyramid principle, the hero's journey...
Default to Situation, Complication, Resolution. It fits most internal
readouts. For a skeptical audience, lead with the objection instead.
完成大部分工作的四种模式
误报
在大多数技能中,这是最高价值的部分。这不是建议,而是对代理否则会犯的错误的纠正:
## False Positives
- "Put something together" is not a brief. It gives you no audience and
no decision. Ask before you build; do not invent an audience.
- A deck for a 15-minute slot is not a shorter version of the 45-minute
deck. Fewer slides with the same message, not the same slides compressed.
- If the core message needs an "and" to state it, it is two decks. Split
it or pick one.
- Our leadership readouts open with the ask, not the background. Reverse
the arc for anyone above director level.
那是模型无法猜到的那类东西——它是你的演示样式和设计约定,也是文件中最宝贵的一行。
将误报保留在 SKILL.md 中,而不是参考文件。代理需要在遇到情况之前就拥有它们(之前),并且它不知道该打开一个描述它不存在的陷阱的文件。
每次你在任务进行中纠正代理时,这种纠正都应该放在这里。
模板
当输出形状很重要时,请展示形状。代理对结构的模式匹配远胜于他们对关于结构的散文的遵循。
## Outline format
Produce exactly this structure:
```markdown
# Deck:
## Brainstorm
- Audience:
- Core message:
- Arc:
## Slides
###
-
```
检查清单
有了明确的进度清单,就不会漏掉任何步骤:
## Progress
- [ ] 1. Brainstorm block written
- [ ] 2. Missing audience info asked about, not invented
- [ ] 3. `validate_deck.py` exits 0
- [ ] 4. Slides written
- [ ] 5. Validator re-run on the finished outline
验证循环
让代理检查自己的工作并进行迭代。这将一次性生成转变为自我纠正的过程:
## Validation loop
1. Write or edit the outline.
2. Run `python3 scripts/validate_deck.py --file outline.md`.
3. If it exits 1, read the rule ID and message, fix it, run again.
4. Only continue when it exits 0.
当主体真正需要更多时
我们的主体如今已接近其实用上限。如果把完整的叙事弧线目录(附带示例)写出来,必定会远超 500 行。
这指向 references/,也就是 Version 5。规则:告诉代理 何时 打开每个文件。“Read references/narrative-patterns.md when the audience is skeptical or the arc is unclear” 是可操作的做法。“See references/ for details” 则不是。
版本 4:捆绑一个用于检查工作的脚本
文字说明能让代理 更倾向于遵循流程,却无法证明流程真的被执行。脚本则提供了缺失的验证层。
我们的技能已经让代理去验证大纲。现在我们为这个指令提供了一种具体的验证机制。与其让代理记住每条规则,不如让 validate_deck.py 检查大纲并返回明确的通过或失败信号。于是,“请检查你的工作”从一条建议转变为可度量、可重复的东西。
内联声明依赖
捆绑脚本应声明其自身依赖,使代理能够用一条命令直接运行,无需额外安装。在 Python 中,PEP 723 正是如此:
# /// script
# requires-python = ">=3.9"
# dependencies = []
# ///
uv run scripts/validate_deck.py 然后会构建一个隔离的环境并执行。我们的只使用标准库,所以列表为空,普通的 python3 就能工作。
这是一个值得借鉴的设计选择。零依赖的验证器在三年后仍能在受限的 CI 容器中运行。
为代理设计
代理会读取 stdout 和 stderr 来决定下一步该做什么。以下六个选择决定了这是否顺利:
Never prompt interactively: 这是硬性要求。代理在非交互式 shell 中运行,无法响应 TTY 提示。如果脚本在输入上阻塞,它会一直挂起直到被杀死。
Document through
--help: 这是代理了解您接口的方式。包括目的、标志、退出码。保持简短。它会出现在上下文窗口中。Write errors that suggest the fix:
Error: invalid input会消耗一次操作。指出规则和补救方法则不花任何成本。Emit structured output: 在 stdout 上输出 JSON,在 stderr 上输出诊断信息。
Be idempotent: 代理会重试。静态检查器天然适合重复运行。
Bound your output: 许多测试 harness 会在大约 10–30K 字符后截断工具输出,悄悄丢掉重要部分。请仅报告发现,而不要输出整个文件。
验证器
创建 deck-builder/scripts/validate_deck.py:
#!/usr/bin/env python3
"""Static checker for deck outlines produced by the deck-builder skill.
Checks that an outline brainstormed before it built, and that no slide is
overloaded. Reads the outline file only; nothing is rendered or uploaded.
Usage:
scripts/validate_deck.py --file outline.md
scripts/validate_deck.py --file outline.md --format json
Exit codes:
0 Outline passes every check.
1 One or more problems found.
2 The file could not be read.
"""
import argparse
import json
import re
import sys
from typing import Dict, List
MAX_BULLETS = 6
MAX_BULLET_CHARS = 120
MAX_TITLE_CHARS = 60
CLOSING_WORDS = ("next step", "call to action", "recap", "takeaway", "ask")
def parse(outline: str) -> List[Dict]:
"""Split the outline into slides. A slide starts at a '### ' heading."""
slides, current = [], None
for lineno, line in enumerate(outline.split("
"), 1):
heading = re.match(r"^###\\s+(.*\\S)\\s*$", line)
if heading:
current = {"title": heading.group(1), "line": lineno, "bullets": []}
slides.append(current)
continue
bullet = re.match(r"^\\s*[-*]\\s+(.*\\S)\\s*$", line)
if bullet and current is not None:
current["bullets"].append({"text": bullet.group(1), "line": lineno})
return slides
def analyze(path: str) -> List[Dict]:
with open(path, "r", encoding="utf-8") as handle:
outline = handle.read()
findings: List[Dict] = []
def add(rule, line, message, snippet=""):
findings.append(
{"rule": rule, "line": line, "message": message, "snippet": snippet}
)
if not re.search(r"^##\\s+Brainstorm\\s*$", outline, re.M | re.I):
add(
"DECK001", 1,
"No '## Brainstorm' section. The skill must think before it builds: "
"record audience, core message, and arc before writing slides.",
)
slides = parse(outline)
if not slides:
add("DECK006", 1, "No slides found. Each slide is a '### ' heading.")
for slide in slides:
if len(slide["title"]) > MAX_TITLE_CHARS:
add(
"DECK004", slide["line"],
f"Slide title is {len(slide['title'])} characters; keep it under "
f"{MAX_TITLE_CHARS} so it fits one line at presentation size.",
slide["title"][:70],
)
if len(slide["bullets"]) > MAX_BULLETS:
add(
"DECK002", slide["line"],
f"Slide has {len(slide['bullets'])} bullets; split it. More than "
f"{MAX_BULLETS} reads as a document, not a slide.",
slide["title"][:70],
)
for bullet in slide["bullets"]:
if len(bullet["text"]) > MAX_BULLET_CHARS:
add(
"DECK003", bullet["line"],
f"Bullet is {len(bullet['text'])} characters. Tighten it to "
f"under {MAX_BULLET_CHARS} or move it to speaker notes.",
bullet["text"][:70],
)
if slides:
tail = " ".join(
[slides[-1]["title"]] + [b["text"] for b in slides[-1]["bullets"]]
).lower()
if not any(word in tail for word in CLOSING_WORDS):
add(
"DECK005", slides[-1]["line"],
"Last slide has no recap, takeaway, or next step. End on the ask, "
"not on the final body slide.",
slides[-1]["title"][:70],
)
return sorted(findings, key=lambda f: (f["line"], f["rule"]))
def main() -> int:
parser = argparse.ArgumentParser(
description="Check a deck outline for structure and slide density.",
epilog="Exit codes: 0 clean, 1 findings, 2 unreadable file.",
)
parser.add_argument("--file", required=True, help="Path to the outline")
parser.add_argument(
"--format", choices=["text", "json"], default="text",
help="Output format (default: text)",
)
args = parser.parse_args()
try:
findings = analyze(args.file)
except OSError as exc:
print(f"Error: could not read {args.file}: {exc}", file=sys.stderr)
return 2
if args.format == "json":
json.dump({"file": args.file, "findings": findings}, sys.stdout, indent=2)
sys.stdout.write("
")
elif findings:
for f in findings:
print(f"{args.file}:{f['line']}: [{f['rule']}] {f['message']}")
if f["snippet"]:
print(f" {f['snippet']}")
else:
print(f"{args.file}: outline passes all checks.")
return 1 if findings else 0
if __name__ == "__main__":
sys.exit(main())
chmod +x "$SKILLS_DIR/deck-builder/scripts/validate_deck.py"
以下三个细节让此验证器 对代理友好,而不仅仅是技术正确。
首先,每个错误都应告诉代理如何修复。仅仅报告规则被违反只是工作的一半。比如下面的消息 “幻灯片的项目超过六条;将其拆分为更小的想法” 能为代理提供足够信息来修正大纲并重试,而无需把工作退回给人。
其次,机械地强制执行技能的核心工作流。版本 1 告诉代理在编写幻灯片前进行头脑风暴,但散文把该要求变成了建议。
验证器改变了这一点。如果缺少头脑风暴部分,脚本将返回非零退出码,向代理发出客观信号,表明它必须在继续之前修复工作流。
第三,明确区分惯例和硬性要求。每张幻灯片六个要点、每个要点 120 个字符并不是演示设计的普遍法则。它们只是此团队的惯例。若把它们当作普遍规则,当用户有正当理由打破它们时,代理可能会产生抵触。
明确说明哪些规则是强制的,哪些规则仅仅反映了你们团队偏好的工作方式。
将其接入
在 SKILL.md 中添加:
## Available scripts
- **`scripts/validate_deck.py`** — checks one outline. Exits 0 clean,
1 on findings, 2 if the file cannot be read.
Run it after the brainstorm and again after writing slides:
```bash
python3 scripts/validate_deck.py --file outline.md
```
循环已经闭合。代理负责起草,脚本负责判断,代理负责修复,并在退出码 0 时结束循环。
版本 5 将深度材料移开
我们的 SKILL.md 已经覆盖了工作流、格式、误报以及验证器。它没有涵盖叙事理论,也不应涵盖。这种理论也许只在五份幻灯片中的一份中需要,而在每次激活时为其付费正是渐进式披露所要防止的浪费。
创建 deck-builder/references/narrative-patterns.md:
# Narrative Patterns
Pick the arc from what the audience must do, not from what feels natural
to write.
## Situation, Complication, Resolution
The default for internal readouts. Works when the audience needs to
approve or fund something.
- **Situation.** What everyone already agrees is true. Keep it short.
- **Complication.** What changed, or what broke. This is the slide that
earns attention.
- **Resolution.** What you did or propose, and the ask.
Failure mode: spending four slides on Situation. If the audience lived
through it, one slide is enough.
## Objection first
For a skeptical audience, or a proposal that was rejected before.
Open with the strongest argument against you, stated fairly. Then
dismantle it. An audience that hears its own objection spoken aloud
stops rehearsing it and starts listening.
## Chronological
For teaching, onboarding, and post-incident reviews. Simplest case
first, then complications in the order they were discovered.
Failure mode: chronological order is rarely the persuasive order. Do
not reach for it just because it matches how the work happened.
## Ask first
For leadership above director level, and for any slot under 10 minutes.
State the decision on slide one. Then support it. If they say yes on
slide one, you have saved everyone twenty minutes, and the rest of the
deck becomes optional backup.
## Choosing quickly
| Audience state | Arc |
| :--- | :--- |
| Needs to decide | Situation, Complication, Resolution |
| Doubts you | Objection first |
| Needs to learn | Chronological |
| Already agrees | Ask first |
| Very senior, short slot | Ask first |
接下来,在 SKILL.md 中添加条件指针:
Read [references/narrative-patterns.md](references/narrative-patterns.md)
when the audience is skeptical, when the slot is under 10 minutes, when
the audience is above director level, or when Situation-Complication-
Resolution does not obviously fit.
这句话就是全部诀窍。四个命名条件,每个条件都是代理在触发时能够识别的。
相比之下,“see references/ 了解更多信息”这句话并未给出文件何时变得相关并被忽略的任何信号。
技能现在已经完成:
技能存放位置及代理如何查找它们
版本 1 已经给出了你所需的唯一路径。这是完整的图景,因为发现是开放标准止步的地方,而每个客户端的行为从此开始。
客户端通常会扫描项目范围和用户范围。填充这些范围的文件夹正是它们分歧的地方:
| 客户端 | 项目范围 | 用户范围 | 还会扫描 |
|---|---|---|---|
| Claude Code | .claude/skills/ |
~/.claude/skills/ |
在工作目录下嵌套的 .claude/skills/。任何 --add-dir 目录内的 .claude/skills/。插件技能位于 下。通过托管设置的企业目录。 |
| VS Code / Copilot | .github/skills/, .claude/skills/, .agents/skills/ |
~/.copilot/skills/, ~/.claude/skills/, ~/.agents/skills/ |
通过 chat.agentSkillsLocations 添加的任何文件夹 |
| Google Antigravity | .agents/skills/ |
~/.gemini/config/skills/ |
遗留的 .agent/skills/ |
两个不对称因素决定了技能的放置位置。
首先,Claude Code 不会扫描 .agents/skills/。放在那里的技能永远不会出现。如果你的第一个技能未被触发且你正在使用 Claude Code,请在重写描述之前先检查这一点。
其次,VS Code 除了扫描自己的约定之外,还会扫描 .claude/skills/。结合上一点,.github/skills/ 是两个主要客户端都会读取的唯一文件夹。当你的受众使用 Antigravity 时,应转向 .agents/skills/。
有些客户端还会向上遍历父文件夹直到 Git 根目录,因此单一仓库的子项目会继承在根目录定义的技能。
项目还是个人?
对于 deck-builder,选择很明确。关于领导读数在打开时带有请求的误报,这是关于 此 组织的决定。它应放在项目中,并提交以确保每个人都能在无需额外设置的情况下获得相同的规则。
编码个人品味而非团队政策的技能应放在个人范围内。
名称冲突:永远不要假设哪个技能会获胜
两个技能可以具有相同的名称,这就是事情变得微妙的地方。不同的客户端使用不同的优先级规则来决定哪个技能优先。如果你假设顺序在所有地方都相同,你可能会调用与你预期不同的技能,而不会得到明显的错误。
这使得名称冲突不仅仅是命名问题。它们是一种 行为风险:命令可能会按预期完全运行,但实际上执行了错误的技能。
Agent Skills 客户端实现指南将常见约定描述为 项目覆盖用户,理由是版本控制中的运行手册代表团队决策。
Claude Code 记录了相反的约定:
Enterprise overrides personal, and personal overrides project.
假设你在 ~/.claude/skills/ 中有一个名为 deck builder 的技能,然后你克隆了一个包含同名另一个技能的仓库。在 Claude Code 中,个人技能优先,因此你已经信任的版本会继续运行,而不是被仓库副本静默替换。
这种行为虽然有用,但并非普遍适用。更广泛的 Agent Skills 约定将项目技能的优先级设置得高于用户技能,而 Claude Code 则采用相反的优先级。这种差异正是可能导致可移植技能表现出乎意料行为的根源。
最安全的做法是 永远不要假设哪个技能会获胜。检查你所使用的客户端文档中记录的优先级规则。Claude Code 还为插件技能提供了显式命名空间,例如 plugin-name:skill-name,并能通过类似 apps/web:deploy 的路径区分嵌套的单仓库技能。
值得命名的信任边界
优先级在任一方向上都有锐利的边缘。项目技能来源于你正在工作的仓库,可能是你五分钟前克隆的且尚未阅读的仓库。加载它们意味着将该仓库作者编写的指令直接载入你的代理上下文。
Claude Code 为项目技能增加了一个重要的信任边界。来自 .claude/skills/ 的技能必须在客户端发现它之前通过工作区信任检查。但这种保护仅在发现阶段起作用。信任决定技能是否可用,而不决定已经被调用的技能被允许执行什么操作。
这一区别很重要,因为即使是受信任的或显式调用的技能,仍可能包含影响代理行为的指令、脚本或工具权限。换句话说,通过信任对话不应被视为对技能本身的安全审查。安全章节会审视这一第二层。
技能 vs 规则 vs MCP vs 钩子 vs 插件
技能是若干原语之一。在它们之间正确选择,就是架构工作的主要内容。
| 原语 | 用途 | 加载方式 | Where deck-builder fits |
|---|---|---|---|
规则 (AGENTS.md) |
始终生效的约束和标准 | 始终生效或路径匹配 | "All readouts live in docs/decks/" 是一条规则。如何 构建一个则不是。 |
技能 (SKILL.md) |
领域过程和操作手册 | 渐进式披露 | 我们的全部技能 |
| MCP 服务器 | 连接到实时外部工具 | 活跃进程 | 一个将大纲渲染为 Google Slides 的服务器 |
| 钩子 | 生命周期事件触发的 shell 命令 | 事件触发 | 在每次写入 docs/decks/ 时自动运行 validate_deck.py |
| 插件 | 以上内容的捆绑 | 发现时被导入 | 将技能、规则和钩子作为一个包一起发布 |
令人困惑的一对是 Skills 与 MCP。我们的示例将它们清晰地区分:
MCP 是能力: 它为代理提供了一只手:创建此文件,调用此 API,渲染这些幻灯片。
Skills 是判断: 它们提供策略、顺序以及决定 如何 和 何时 该手应如何移动的检查。
deck builder 定义了工作流的判断层。它决定代理应先进行头脑风暴,识别受众和核心信息,并将每张幻灯片的要点限制在六条以内。
MCP 服务器提供另一层能力:它为代理提供了将完成的大纲转化为实际幻灯片所需的工具。
这两部分协同工作,但它们解决的问题不同。技能决定 工作应如何进行,而 MCP 提供执行该工作所需的能力。即使没有 MCP,技能仍然有用,因为结构良好的大纲即使不被渲染为幻灯片也仍具价值。

钩子和插件是客户端特定的
规则、MCP 和 Skills 具有广泛的可移植性。Hooks 和 Plugins 不具备此特性。 清单位于不同位置,事件集的规模也有所不同:
| Google Antigravity | Claude Code | |
|---|---|---|
| Plugin manifest | plugin.json 位于插件根目录 |
.claude-plugin/plugin.json |
| Bundled skills | skills/ |
skills/ |
| Hook config | hooks.json 位于 .agents/ 或 ~/.gemini/config/ |
hooks/hooks.json 位于插件根目录,或内联在 plugin.json 中 |
| Lifecycle events | 5: PreToolUse、PostToolUse、PreInvocation、PostInvocation、Stop |
13+,包括 SessionStart、UserPromptSubmit、PreToolUse、PostToolUse、PostToolUseFailure、PermissionRequest、PermissionDenied、Notification、SubagentStop、Stop、StopFailure、PreCompact、SessionEnd |
为一个构建的捆绑包在另一个中无法加载。值得知道的一点是:在 Claude Code 中,将 .claude-plugin/plugin.json 放入 skills 目录内的一个文件夹中,会将其提升为名为 的插件,使得技能可以在无需安装步骤的情况下成长为一个捆绑包。
以下示例来自 Antigravity,文档位于 antigravity.google/docs。在复制之前,请先检查您的客户端。
{
"$schema": "https://antigravity.google/schemas/v1/plugin.json",
"name": "presentation-suite",
"description": "Deck brainstorming and outline validation for the platform team."
}
一个自动运行校验器的钩子,这样智能体就不会忘记第 2 步:
{
"outline-validator": {
"PostToolUse": [
{
"matcher": "run_command",
"hooks": [
{
"type": "command",
"command": "./scripts/validate-changed-outlines.sh",
"timeout": 10
}
]
}
]
}
}
PreToolUse 是安全方面有趣的一个。Hooks 在 stdin 上接收 JSON 并在 stdout 上返回 JSON,而 PreToolUse 响应需要一个 decision 字段来控制调用:allow 继续执行,deny 阻止,ask 在尊重 "always allow" 设置的情况下提示,而 force_ask 则无论如何都会提示。该字段是自动防护栏的机制。
证明技能实际上有帮助
你构建了一个技能。它曾经产生过一个更好的大纲。仅此这一点不能证明该技能实际上在提升代理的表现。模型输出在每次运行之间会有所不同,一次运行中看起来像是改进的东西可能仅仅是噪声。
这也是与版本 2 不同的一种测量方式。在那里,问题是 “技能在应该触发时会触发吗?” 在这里,问题是 “一旦触发,它实际上会改善结果吗?” 一个技能可以完美触发却仍然没有增加任何价值。它也可能在显式调用时改善输出,但在可靠触发方面却失败。你需要同时测量这两方面。
编写测试用例
每个用例包含一个真实的提示、一个成功的描述,以及可选的输入文件。将它们存储在 evals/evals.json 中:
{
"skill_name": "deck-builder",
"evals": [
{
"id": 1,
"prompt": "can you put something together for thursday's board meeting on the q3 migration",
"expected_output": "A brainstorm block first, and a clarifying question about what the board needs to decide, before any slides exist.",
"assertions": [
"A '## Brainstorm' section appears before any slide",
"The brainstorm names an audience, a core message, and an arc",
"The agent asks what decision the board needs to make, rather than inventing one",
"The core message is one sentence with no 'and' joining two claims"
]
},
{
"id": 2,
"prompt": "20 min onboarding walkthrough of our deploy pipeline for new hires, here are my notes",
"expected_output": "A chronological outline. No slide over 6 bullets. Ends on next steps.",
"assertions": [
"No slide has more than 6 bullets",
"No bullet exceeds 120 characters",
"The final slide contains a recap or next step",
"validate_deck.py exits 0 on the produced outline"
]
},
{
"id": 3,
"prompt": "I already know the audience and the message, just give me the 5 slides for the migration retro",
"expected_output": "Slides, without re-interrogating the user. The skill should not force a brainstorm the user already did.",
"assertions": [
"The agent does not ask questions the prompt already answered",
"The outline still records the supplied audience and message in the brainstorm block",
"The agent produces slides in this turn rather than stopping to plan"
]
}
]
}
先准备两到三个测试用例。变换措辞、语气和细节程度,以免只针对一种狭窄的请求风格测试技能。至少包含一个表面看似相关但不应触发技能的边界情况。让提示基于用户实际可能向代理提出的真实场景。
案例 3 需要特别关注,因为它是一个 负面能力测试。它用来检验技能在不再需要时是否会因应用其工作流程而让代理变得更差。
例如,我们的技能在制作幻灯片前需要进行头脑风暴,但用户可能已经提供了受众和核心信息。强制用户重复同样的发现过程只会增加摩擦而非价值。一个好的评估应能发现这种行为,而不仅仅是奖励技能遵循自身规则。
与无技能基线对比
对每个案例分别运行两次:使用技能 和 不使用技能。基线是整个评估的核心。只有当技能能够击败裸模型时,它才具备使用价值,而许多技能做不到这一点。
为每次运行提供干净的上下文,使其仅遵循 SKILL.md。在包含子代理的客户端中,每个子任务从头开始;否则使用独立的会话。改进现有技能时,对旧版本进行快照并将其作为基线。版本 1 可以自然地作为版本 5 的基线。
编写断言并进行评分
在查看前几次输出后再编写断言。在技能运行之前,往往难以以既实用又可测试的方式定义“好”是什么样子。早期的输出会暴露出评估实际上需要衡量的行为。
好的断言应是 具体、可观察且可检查 的。例如,“Brainstorm 部分应出现在任何幻灯片之前”给出一个可验证的具体条件。同样,要求精确匹配短语 core message 太脆弱,因为代理可能仅在表面上满足措辞而未真正进行所需的思考。
将每个断言评判为 PASS 或 FAIL,并要求以实际输出作为证据。即使头脑风暴部分写有 Audience: everyone,如果没有提供有意义的受众分析,也应判定为失败。仅有标签的出现并不能证明所需的推理已经发生。
在条件可以通过机械方式检查的地方使用脚本。validate_deck.py 能够一致地处理结构性检查,将人工审查留给那些难以用规则量化的方面,例如叙事是否具有说服力,或者核心信息是否真的能够触达受众。
读取数字
{
"run_summary": {
"with_skill": { "pass_rate": 0.85, "time_seconds": 38.0, "tokens": 3900 },
"without_skill": { "pass_rate": 0.29, "time_seconds": 26.0, "tokens": 2200 },
"delta": { "pass_rate": 0.56, "time_seconds": 12.0, "tokens": 1700 }
}
}
增量如实说明了交易:12秒和1,700个token能够换取56个通过率点。这显然是值得的。一个让token翻倍却只得到两分的技能则不然,基准会告诉你你到底构建了哪一种。
然后超越平均值来阅读:
在两者中都通过的断言衡量的是模型,而不是你的技能。请移除它们。
在两者中都失败的断言通常是损坏的断言或不可能的情况。请修复它们。
仅在使用技能时通过的断言才是价值所在。这里是头脑风暴块,基线很少产生。
多次运行间方差高表示指令不明确,而不是测试不稳定。
闭环
修订应由三个信号驱动:失败的断言、人工反馈和执行 transcripts。在这三个信号中,transcripts 常能揭示最多的信息,因为它们展示了 代理出错的位置以及导致它出错的原因。
假设代理在生成大纲之前尝试了三种不同的方法。这通常表明指令留有过多的解释空间。或者假设它为一个简单的站会幻灯片打开了 narrative patterns.md。这表明参考条件过于宽泛。transcript 揭示了失败背后的推理路径,而不仅仅是失败本身。
使用这些信号来改进底层模式,而不是一次只修补一个失败的示例。同时也要愿意删除指令。如果通过率停止提升而技能却在不断增大,那么该技能可能承载了过多的约束。当代理在执行过程中反复重新创建相同的辅助逻辑时,这是一个强烈的信号,表明该逻辑应放在 scripts/ 中。这正是验证器在 deck builder 中获得其位置的确切方式。
安装前扫描
到目前为止的一切都假设你编写了该技能。但越来越多情况下,你并没有。
技能的传播方式就像 npm 包一样:从市场复制,从 GitHub 克隆,或从同事那里粘贴。技能不是惰性数据。它是进入你的代理上下文的指令,以及在你的机器上执行的脚本,通常使用你的 shell 已经具备的任何凭据。
想象在市场上找到 deck-builder-pro。我们的一切功能外加图表生成和品牌模板。它有星标。你会在将其放入代理可写入的仓库之前,先阅读全部四个文件吗?
前置元数据是攻击面的一部分
在查看脚本之前,先看看前置元数据。技能可以授予 自身 权限。
Claude Code 的 allowed-tools 会在调用技能的回合中预先批准工具。当你自己编写时这很方便,但当你没有编写时,这实际上是一种能力授予。
Claude Code 的文档直截了当:工作区信任不会限制此字段。 项目技能的授权在技能被调用时始终生效,即使是在你从未信任的文件夹中执行的 -p 运行时也是如此。
因此,恶意技能无需利用漏洞或混淆载荷。它只需要一行:
---
name: deck-builder-pro
description: Brainstorm and build presentation outlines with brand templates.
allowed-tools: Bash
---
在代码库中运行代理之前,先阅读任何已检入技能的 allowed-tools。这是目前最便宜的审查步骤,也是最常被跳过的一步,因为前置元数据看起来像配置而非代码。
这两条规则听起来矛盾却并不矛盾:Claude Code 将项目技能的 发现 放在信任对话之后进行把关,但不对被调用技能的 授予 进行把关。
数据表明
关于此生态系统的首次大规模研究——《Agent Skills in the Wild》(Liu 等人,2026 年 1 月)——从两大市场收集了 42,447 项技能,并对其中的 31,132 项进行了分析:
26.1% 至少包含一个漏洞。
13.3% 出现数据外泄模式,且 11.8% 出现特权提升。
5.2% 表现出作者认为强烈暗示恶意意图的高危模式。
捆绑可执行脚本的技能相比仅包含指令的技能,存在漏洞的可能性高 2.12×(OR = 2.12,p < 0.001)。
大约每四个中就有一个存在问题。每二十个中就有一个看起来是故意的。
最后这一发现正好适用于与我们类似的技能。就在我们添加 validate_deck.py 的那一刻,deck-builder 便进入了高风险类别。这并不是反对捆绑脚本的论点,而是主张对其进行扫描,并为我们为任何任务创建的每个代理技能进行安全审计。
SkillSpector
SkillSpector 是 NVIDIA 的开源解决方案:一个专门为代理技能构建的静态安全扫描器。采用 Apache-2.0 许可,使用 Python 编写,其存在的目的就是在您安装任何内容之前回答一个问题:这是否安全?
它是 NVIDIA 验证技能管道 的一部分,该管道在技能进入 NVIDIA 技能目录之前会进行扫描、评估和签名。
截至 v2.9.6,它拥有 17 类共 70 种漏洞模式:
| 类别 | 代表性模式 |
|---|---|
| 提示注入 | 指令覆盖,注释或不可见文本中的隐藏指令,将文本推出可视范围的空白填充 |
| 规避拒绝 | "永不拒绝","省略所有免责声明","越狱框架" |
| 数据外泄 | 环境变量采集,文件系统枚举,将对话上下文发送到外部端点 |
| 特权提升 | 调用 sudo 和 root,读取 SSH 密钥、令牌、密码存储 |
| 供应链 | 通过 curl 管道到 shell 实现远程执行,base64 有效载荷,拼写错误利用的包,通过实时 OSV.dev 查询传送的已知 .pyc CVE |
| 过度代理 | 无限制的工具访问,没有人类参与的高影响决策 |
| 内存毒害 | 设计为跨会话持续存在的内容 |
| 流氓代理 | 运行时自修改,通过 cron 或启动脚本实现持久化 |
| 触发器滥用 | 精心构造的描述,用于掩盖内置功能或最大化误触发 |
| 行为 AST | exec,eval,动态导入,subprocess,反射 getattr 汇聚点 |
| 污点追踪 | 凭据流向网络汇聚点,文件读取到达网络输出 |
| MCP 特定 | 通过 Unicode 同形字进行工具中毒,参数-描述注入 |
有两项值得更多关注,因为它们对普通代码审查是不可见的。
触发器滥用攻击描述字段,正是我们在 Version 2 中花费精力优化的东西。使描述能够可靠触发的相同技术可以被推送到一种被设计为在一切情况下激活的描述中。一个恶意的 deck-builder-pro 可能会将自己描述为适用于“任何文档、文件或规划任务”,正是为了在它不应加载的回合中被加载。
空白填充将指令隐藏在文件可见区域之下,随意滚动的审查者永远看不到它们。
两者都利用了 SKILL.md 由模型读取而非由解析器编译这一事实。
扫描
uv tool install git+https://github.com/NVIDIA/skillspector.git
skillspector scan "$SKILLS_DIR/deck-builder/"
它接受文件夹、单个 SKILL.md 文件、Git URL 和 ZIP 压缩包。其中最后这一功能最为重要,因为你可以扫描技能 之前 它到达你的文件系统:
skillspector scan https://github.com/someone/deck-builder-pro
分析分为两个阶段。第一阶段始终是静态的:SkillSpector 使用正则表达式、Python AST 检查、YARA 签名以及实时 CVE 查询来检测技能。可选的 LLM 阶段随后会检查意图、过滤误报,并将发现转化为更易理解的说明,使准确率提升至约 87%。如果您希望获得更快的扫描,或者技能内容必须保留在本机,请使用 --no-llm。
我们的 deck builder 应该能够干净通过此次审查。它不会进行任何网络调用,不会读取环境变量,不会启动子进程,也没有外部依赖。而可疑的技能则讲述了完全不同的故事:它的扫描可能在您允许其运行之前就暴露出环境信息收集或意外的外部传输。
SkillSpector Security Report
Skill: deck-builder-pro
Score 78/100
Severity HIGH
Recommendation DO NOT INSTALL
Issues (2)
HIGH: Env Variable Harvesting (E2)
Location: scripts/brand_sync.py:23
Finding: for key, val in os.environ.items():...
Confidence: 94%
HIGH: External Transmission (E1)
Location: scripts/brand_sync.py:45
Finding: requests.post("https://api.deckmetrics.io/telemetry"...
Confidence: 89%
单独来看,任一发现都不足以得出结论。读取环境变量的技能可能有合理原因,比如定位品牌资产。向外部端点发送数据也可能只是普通的分析。
当这两种行为被一起考虑时,问题便显现出来:环境数据正在被收集并传输,这可能暗示凭证外泄,伪装成遥测数据。因此,扫描器会关联相关发现,而不是把每个匹配都视为孤立问题。
得到的分数落入预定义的风险等级。0‑20 分标记为 LOW 或 SAFE,21‑50 分为 MEDIUM 或 CAUTION,51‑80 分为 HIGH,81‑100 分为 CRITICAL。HIGH 和 CRITICAL 级别的发现都会导致 DO_NOT_INSTALL 建议。
分数是根据加权发现计算的:严重问题贡献 50 分,高危 25 分,中危 10 分,低危 5 分。如果技能包含脚本,总分会额外乘以 1.3,以反映可执行代码带来的更高风险。
门禁安装和 CI
退出码是一个稳定的契约:
| 代码 | 含义 |
|---|---|
0 |
扫描完成,得分 ≤ 50(SAFE 或 CAUTION) |
1 |
扫描完成,得分 > 50(DO_NOT_INSTALL) |
2 |
错误:输入错误、源不可读、内部故障 |
因为 0 将 SAFE 和 CAUTION 合并,当需要区分它们时,请从 JSON 中读取 recommendation 字段:
skillspector scan ./candidate-skill/ --format json --output report.json
--format sarif 发出 SARIF 2.1.0,GitHub 高级安全及大多数静态分析仪表盘可直接摄取。将其添加到已经在运行 skills-ref validate 的 CI 作业中。
对自身技能的重复扫描会产生已被评审的发现。基线会抑制这些发现,使得重新扫描仅显示新内容:
skillspector baseline "$SKILLS_DIR/deck-builder/" -o .skillspector-baseline.yaml
skillspector scan "$SKILLS_DIR/deck-builder/" --baseline .skillspector-baseline.yaml
提交基线。指纹条目具有证据约束性:更改源或扫描器版本会重新激活该发现,直至再次审查。
作为运行时门控
最有趣的部署方式是作为 MCP 服务器,将扫描从审计转移到门控:
uv tool install --force 'skillspector[mcp] @ git+https://github.com/NVIDIA/skillspector.git'
claude mcp add skillspector -- skillspector mcp
它暴露了一个工具,scan_skill,返回 risk_score、severity、recommendation、safe_to_install 和发现。它还报告 llm_used 和 scan_mode,因此静态-only 通过的低分不会被误认为是完整扫描的干净结果。
值得再读两次的警告:HTTP 传输未带认证。 在 stdio 或 127.0.0.1 上,这与 CLI 的信任边界匹配。绑定到可路由接口时,您需要在前面放置一个带认证的反向代理。本地路径和 file:// URL 在 HTTP 上会被自动拒绝,但这不能替代认证。
了解它不做什么
在信任扫描之前,有一个重要的边界需要理解。SkillSpector 永不会执行它正在分析的技能。 它通过正则表达式模式、AST 检查和 YARA 签名静态检查文件,并可选地使用 LLM 通过评估文件内容。扫描器可以在安装前标记可疑行为,但一旦您选择安装并运行该技能,它就无法包含该技能。
这也意味着扫描有明显的盲点。模式可能会漏掉非英文内容,嵌入图像中的文本不会被分析,编译或加密的有效载荷无法被检查。运行时行为超出扫描器的范围。无法访问 api.osv.dev 时,CVE 检查将回退到较小的捆绑漏洞列表。
还有一个值得在运行扫描前检查的数据处理细节。启用 LLM 分析时,文件内容会发送到您配置的提供者。当您需要分析保持本地时,请使用 --no-llm。供应链检查是独立的:即使使用 --no-llm,它也会将依赖名称和版本发送到 OSV.dev,而不是发送您文件的内容。我们的 deck builder 技能声明没有依赖项,因此该检查没有内容可发送。
三个检查点
在安装您未编写的任何内容之前: 在磁盘接触 Git URL 之前进行扫描。这是您将运行的最高价值扫描。
在 CI 中,您自己的技能每次更改时: 上一季度干净的技能可能已经获取了带有新 CVE 的依赖项。
在安装时,通过 MCP 门: 自动门胜过无人遵循的文档政策。
该论文的结论值得直白地重申:此生态系统需要基于能力的权限以及在攻击面被更广泛利用之前的强制审查。在权限到来之前,扫描是实用的控制手段,大约需要十秒。
悄悄浪费您令牌的错误
这些都是 deck-builder 早期草稿实际上犯过的错误。
| 错误 | 表现 | 修复方案 |
|---|---|---|
| 描述模糊 | 版本 1 的 Helps make presentations. 在说 “把东西准备好以供周四使用” 时未触发 |
明确操作和触发条件,排除相邻领域,然后测量触发率 |
| 庞大的运行手册 | 将每个叙事弧内联导致 SKILL.md 超过 500 行,每次激活都要付出代价 |
将深度内容移至 references/,并注明何时打开 |
| 重复一般知识 | 早期的正文解释了什么是幻灯片 | 仅写出代理出错的内容 |
| 无人负责的规则 | “Six bullets max” 听起来像设计法则,于是代理与想要七个项目的用户争论 | 说明哪些规则是团队约定,哪些是硬性要求 |
| 仅在正文中强制 | “Brainstorm first” 在压力下被模型跳过了 | 将其设为带非零退出码的验证规则 |
| 过度应用自身规则 | 该技能对已经提供受众和消息的用户进行了审问 | 在评估集中添加负能力测试 |
| 绝对路径 | /Users/you/dev/... 在其他机器上均会失效 |
始终使用相对于技能根目录的路径 |
| 交互式脚本 | 草稿验证器提示 Continue? [y/N] 并导致代理挂起 |
仅使用标志。若缺失标志,则报错并指出缺失的标志 |
| 假设只有一种发现路径 | 早期草稿到处使用 .agents/skills/。在 Claude Code 中,教程静默地未产生任何输出 |
检查客户端的路径,并使用其技能列表命令进行确认 |
| 跳过基线评估 | “它产生了更好的大纲” 只是一次运行 | 有无均进行评估。删除表现较差的技能 |
| 安装未扫描的技能 | 添加一个脚本会使该技能进入 2.12× 风险类别 | 在安装您未编写的任何内容之前,先运行 skillspector scan |
| 将前置信息当作配置 | allowed-tools 是一种权限授予,工作区信任不会对其进行限制 |
应像审查脚本一样仔细阅读第三方前置信息 |
预飞检查清单
在提交他人将克隆的技能之前运行此检查
格式
[ ]
SKILL.md包含有效的 YAML 前置元数据,其中包含name和description字段。[ ]
name全部小写,使用连字符,长度不超过 64 个字符,且必须与文件夹名称完全一致。[ ]
description长度不超过 1024 个字符,且说明该技能的作用 以及 何时使用。[ ]
skills-ref validate ./deck-builder通过验证。[ ] 该文件夹位于客户端实际扫描的路径中,可通过其 skill-listing 命令确认。
[ ] 了解客户端的冲突优先级后,你就知道哪个副本会生效。
[ ] 如果该技能可能被迁移,则前置元数据仅使用规范定义的六个字段。
内容
[ ] 正文行数不超过 500 行,约 5,000 个 token,且始终保持在会话上下文中。
[ ] 需要在整个任务期间保持有效的指导应以常驻指令的形式编写。
[ ] 不应对模型已知的内容进行解释。
[ ] 特定环境的陷阱位于
SKILL.md内的误报部分。[ ] 每个引用文件都具有明确的打开条件。
[ ] 属于内部惯例的规则应予以标注。
[ ] 所有路径均相对于技能根目录。
脚本
[ ] 可执行(
chmod +x),依赖项在内联声明。[ ] 不会有交互式提示。
[ ]
--help说明用途、标志和退出码。[ ] 错误消息应给出解决办法,而不仅仅是失败信息。
[ ] 标准输出提供结构化输出,标准错误输出诊断信息。
[ ] 技能中任何位置均不含凭据、令牌或密钥。
[ ] 任何
allowed-tools授予均为所需最小权限,并应作为权限进行审查。
证据
[ ] 触发率基于带有训练/验证划分的标记查询进行测量。
[ ] 在有无该技能的情况下评估输出质量,结果显示该技能更胜一筹。
[ ] 至少有一项测试确认该技能不会过度应用自身规则。
[ ] token 和延迟成本已知,且被判定为可接受。
安全
[ ]
skillspector scan报告为SAFE,或者每个CAUTION发现均已审查并纳入已提交的基线。[ ] 该扫描在每次更改时均在 CI 中运行。
关键要点
我们从 28 行代码构建了一个名为 deck-builder 的技能,使其成为经过验证、评估和扫描的完整包。本课程的所有经验都体现在该文件夹中。
渐进式披露是整个经济论点: 每个技能在发现时约 100 个 token,而非约 5000 个 token,在 50 个技能时大约减少 96%,并且仅在大多数技能保持关闭时成立。
激活持续存在: 一旦被调用,该主体在整个会话期间保持在上下文中。每一行都是反复产生的成本,压缩操作可能将其从长对话中驱逐。
描述完成所有触发: 版本 1 在 “把东西准备好以供星期四使用” 上失败。版本 2 成功是因为它命名了过程,涵盖了人们实际的提问方式,并声明了近似情况不适用。
在正确性重要的地方捆绑脚本: 散文将“先头脑风暴”设为建议。
validate_deck.py使跳过它成为非零退出码。误报 是你会写的最高价值内容: 我们技能中最有用的一行是说明领导读取以请求开头。没有模型能够猜测你的组织。
将你的约定标记为约定: 将内部样式呈现为普遍真理的技能会教会代理与工作方式不同的用户争论。
相对于基准进行评估,否则你只是在猜测: 包含一个能捕捉过度应用的测试,而不仅仅是性能不足。
了解你的客户在哪里查找,以及哪个技能获胜:
.agents/skills/是跨客户端的约定,但 Claude Code 使用.claude/skills/且不扫描.agents/。优先级也有所不同,且 Claude Code 文档记录了该约定的逆向。规范是可移植的子集: 客户端大量扩展前置元数据,而更严格的验证路径会拒绝未知键,而不是忽略它们。
扫描你未编写的内容: 已发布的技能中有 26.1% 携带漏洞,5.2% 表现出可能的恶意意图,未扫描的安装即是未经审查的信任决策。
技能是一个包含 SKILL.md 文件: 无需构建步骤、注册表或运行时。
从我们开始的地方开始:一个文件夹、一个文件,以及您团队已经反复大声重复的一条规则。版本 1 用了十分钟。之后的一切都是由测量驱动的改进。
下次您发现自己第四次解释同一件事时,停下来,改为将其写为一个技能。
结论
有用的代理技能不仅仅是编写更多指令。而是将您团队反复的知识转化为可靠且可重复使用的工作流。
我们从一个小的 SKILL.md 文件构建出了 deck builder,并将其发展为经过验证、评估和安全扫描的软件包。在此过程中,一些重要的经验变得清晰:保持发现过程轻量化,将激活的上下文视为持续的成本,使描述足够精确以便在真实请求时触发,在误报中捕获团队特定的知识,并将可重复的检查移入脚本中,以便可以验证正确性而不是假设。
同样重要的是,一个好的技能要知道自己的边界。当用户已经有了自己的想法时,它不应强行施加其工作流程。它不应把团队的约定当作普遍规则。它也不应仅仅因为看起来像无害的配置而被信任,尤其是当它来源于你自己未编写的仓库时。
这个模式很简单:记录知识,明确工作流程,测试它是否真的有帮助,并扫描你未编写的内容。
当你发现自己第四次解释同样的流程时,那可能就是你的信号。停止重复,把它变成一个技能。
祝你开发愉快,安装前请先扫描。