Skill 详解:从概念到用 AI 写好一个 Skill

1. 什么是 Skill

Skill(技能)是让 AI 助手在特定场景下"知道该怎么做"的一套结构化能力描述。它把触发条件、执行步骤、所需工具、输出规范、边界与示例打包在一起,让模型不必每次从零推理,而是按既定套路稳定完成任务。

一句话概括:Prompt 是"说一次",Skill 是"沉淀下来反复用"。

一个 Skill 通常包含以下要素:

要素 作用 示例
名称与描述 让模型判断"什么时候该用我" pdf-form-filler:填写 PDF 表单
触发条件 明确适用与不适用场景 用户上传 PDF 并要求填表时
执行步骤 可复现的操作流程 读取字段 → 匹配数据 → 写入 → 校验
工具与资源 需要调用的脚本、模板、API fill_form.py、字段映射表
输出规范 结果格式与质量标准 输出填写后的文件 + 变更清单
示例 正例与反例 一次完整调用示例

2. Skill 与 Prompt、Agent、MCP 的区别

很多人把 Skill 和 Prompt 混为一谈,其实它们的抽象层级不同:

  • Prompt:一次性的自然语言指令,灵活但不可复用、不稳定。
  • Skill:可复用的能力包,强调"流程 + 资源 + 规范",追求稳定与可维护。
  • Agent:具备自主规划与多步执行能力的智能体,Skill 是它可调用的"技能库"。
  • MCP:模型与外部工具/数据源之间的标准化连接协议,解决"能不能连上";Skill 解决"连上之后怎么干"。

可以用一个类比理解:MCP 是插座标准,Agent 是电工,Skill 是电工手里的作业指导书。

下表从六个维度做横向对比,方便快速定位它们的差异:

维度 Prompt Skill Agent MCP
抽象层级 单次指令 能力封装 自主智能体 连接协议
可复用性 低,需重写 高,可沉淀 中,依赖配置 高,标准接口
是否含资源文件 否 是,脚本模板 视实现而定 否,仅协议
是否自主规划 否 否,按流程走 是,多步规划 否
解决的核心问题 表达一次意图 稳定复用经验 自主完成任务 工具能否连上
典型使用场景 临时问答 固定业务流程 复杂多步任务 接入外部工具

如何快速判断该用哪个

真实场景里往往来不及逐条对照表格,可以用下面几条判定式问句快速定位------从上往下问,第一个回答"是"的就是你该用的东西:

  • 是否只需一次性表达意图、说完就结束?→ Prompt
  • 是否需要把固定流程稳定复用、还要带上脚本和模板?→ Skill
  • 是否需要多步自主规划、根据中间结果动态调整?→ Agent
  • 是否只是想让模型连上外部工具或数据源?→ MCP
  • 如果同时命中多条:MCP 打底、Skill 沉淀流程、Agent 负责调度、Prompt 做临时补充,四者可以叠加使用。

选型对照清单(可直接照着勾):

  • 只问一次、不打算复用 → 写 Prompt,别急着建 Skill
  • 流程固定、步骤可枚举、有脚本/模板 → 做成 Skill
  • 任务目标明确但路径不确定、需要边做边判断 → 交给 Agent
  • 缺的是"连不上"而不是"不会干" → 先接 MCP
  • 已有 Skill 但触发不准 → 回去改 description,而不是换 Agent

#mermaid-svg-NAwxrlSv8cGKGjeo{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-NAwxrlSv8cGKGjeo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NAwxrlSv8cGKGjeo .error-icon{fill:#552222;}#mermaid-svg-NAwxrlSv8cGKGjeo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NAwxrlSv8cGKGjeo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NAwxrlSv8cGKGjeo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NAwxrlSv8cGKGjeo .marker.cross{stroke:#333333;}#mermaid-svg-NAwxrlSv8cGKGjeo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NAwxrlSv8cGKGjeo p{margin:0;}#mermaid-svg-NAwxrlSv8cGKGjeo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster-label text{fill:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster-label span{color:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster-label span p{background-color:transparent;}#mermaid-svg-NAwxrlSv8cGKGjeo .label text,#mermaid-svg-NAwxrlSv8cGKGjeo span{fill:#333;color:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo .node rect,#mermaid-svg-NAwxrlSv8cGKGjeo .node circle,#mermaid-svg-NAwxrlSv8cGKGjeo .node ellipse,#mermaid-svg-NAwxrlSv8cGKGjeo .node polygon,#mermaid-svg-NAwxrlSv8cGKGjeo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NAwxrlSv8cGKGjeo .rough-node .label text,#mermaid-svg-NAwxrlSv8cGKGjeo .node .label text,#mermaid-svg-NAwxrlSv8cGKGjeo .image-shape .label,#mermaid-svg-NAwxrlSv8cGKGjeo .icon-shape .label{text-anchor:middle;}#mermaid-svg-NAwxrlSv8cGKGjeo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NAwxrlSv8cGKGjeo .rough-node .label,#mermaid-svg-NAwxrlSv8cGKGjeo .node .label,#mermaid-svg-NAwxrlSv8cGKGjeo .image-shape .label,#mermaid-svg-NAwxrlSv8cGKGjeo .icon-shape .label{text-align:center;}#mermaid-svg-NAwxrlSv8cGKGjeo .node.clickable{cursor:pointer;}#mermaid-svg-NAwxrlSv8cGKGjeo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NAwxrlSv8cGKGjeo .arrowheadPath{fill:#333333;}#mermaid-svg-NAwxrlSv8cGKGjeo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NAwxrlSv8cGKGjeo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NAwxrlSv8cGKGjeo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NAwxrlSv8cGKGjeo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NAwxrlSv8cGKGjeo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NAwxrlSv8cGKGjeo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster text{fill:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo .cluster span{color:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo 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-NAwxrlSv8cGKGjeo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NAwxrlSv8cGKGjeo rect.text{fill:none;stroke-width:0;}#mermaid-svg-NAwxrlSv8cGKGjeo .icon-shape,#mermaid-svg-NAwxrlSv8cGKGjeo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NAwxrlSv8cGKGjeo .icon-shape p,#mermaid-svg-NAwxrlSv8cGKGjeo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NAwxrlSv8cGKGjeo .icon-shape .label rect,#mermaid-svg-NAwxrlSv8cGKGjeo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NAwxrlSv8cGKGjeo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NAwxrlSv8cGKGjeo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NAwxrlSv8cGKGjeo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 有
无
用户需求
Agent 规划
是否有匹配 Skill
加载 Skill 流程
通用推理
调用工具 / MCP
按输出规范交付

3. 一个 Skill 的标准结构

以常见的目录式 Skill 为例,典型结构如下:

text 复制代码
skills/
└── pdf-form-filler/
    ├── SKILL.md          # 核心说明:何时用、怎么用
    ├── scripts/
    │   └── fill_form.py  # 可执行脚本
    ├── references/
    │   └── field_map.md  # 字段映射参考
    └── assets/
        └── template.pdf  # 模板资源

其中 SKILL.md 是最关键的文件,通常由两部分组成:

  1. 元信息(frontmatter):名称、描述、版本、依赖等,供系统检索与路由。
  2. 正文说明:触发条件、步骤、注意事项、示例。
markdown 复制代码
---
name: pdf-form-filler
description: 当用户需要向 PDF 表单批量填写结构化数据时使用;不适用于扫描件 OCR 识别。
version: 1.0.0
---

# PDF 表单填写

## 何时使用
- 用户提供可编辑 PDF 表单与结构化数据(JSON/CSV)
- 需要保留原始版式并输出填写后的文件

## 执行步骤
1. 解析 PDF 表单字段,输出字段清单
2. 将输入数据按 `references/field_map.md` 映射到字段
3. 调用 `scripts/fill_form.py` 写入
4. 校验必填项,输出变更清单

## 注意事项
- 字段缺失时不要猜测,向用户确认
- 不修改原始模板文件,输出到新路径

4. 如何用 AI 写好一个 Skill

写 Skill 本质上是"把隐性经验显性化"。借助 AI 可以显著提速,但关键在于你提供的信息质量。推荐按下面五步走。

4.1 第一步:先写清楚"能力边界"

不要一上来就让 AI 写全文。先自己用三句话回答:

  • 这个 Skill 解决什么问题?
  • 什么情况下不该用它?
  • 成功的输出长什么样?

把这三句话作为输入交给 AI,能避免它写出"什么都想干、什么都干不好"的泛化技能。

4.2 第二步:让 AI 生成初稿骨架

可以直接用这样的提示词:

text 复制代码
你是 Skill 设计专家。请为「PDF 表单填写」设计一个 Skill 的 SKILL.md 骨架,
要求:
1. frontmatter 包含 name、description(含适用与不适用场景)、version
2. 正文包含:何时使用、前置条件、执行步骤、输出规范、注意事项、示例
3. 步骤必须是可执行的动作,不要写"处理好数据"这类空话
4. 用中文输出,代码块标注语言

AI 给出的骨架通常结构完整,但步骤往往偏抽象,需要你补充真实细节。

4.3 第三步:用真实案例"喂"细节

这是决定 Skill 质量的关键一步。把你过去手工做这件事的过程拆解出来:

  • 遇到过哪些坑?(比如字段名不一致、日期格式不同)
  • 哪些判断依赖经验?(比如什么时候该停下来问用户)
  • 有哪些固定模板或脚本可以复用?

把这些作为补充材料交给 AI,让它改写"执行步骤"和"注意事项"。经验越具体,Skill 越可靠。

4.3.1 实战:从零写一个 pdf-form-filler Skill

下面把上面的方法落到一个具体例子上:一个把结构化数据填进可编辑 PDF 表单的 Skill。先看完整的 SKILL.md,再看核心脚本。

SKILL.md 完整示例

markdown 复制代码
---
name: pdf-form-filler
description: 当用户提供可编辑 PDF 表单(AcroForm)与结构化数据(JSON/CSV),要求批量填写并输出填写后的文件时使用;不适用于扫描件、图片型 PDF 或需要 OCR 识别的场景。
version: 1.0.0
dependencies:
  - pypdf>=4.0.0
---

# PDF 表单填写

## 何时使用
- 用户上传的是**可编辑** PDF 表单,且字段可被解析
- 用户同时提供了结构化数据(JSON 或 CSV)
- 用户要求保留原始版式,输出填写后的新文件

## 何时不使用
- 扫描件 / 图片型 PDF(无表单字段)→ 提示用户先做 OCR 或改用其它方案
- 用户要求修改 PDF 正文排版、合并拆分页面 → 不属于本 Skill 职责
- 表单字段无法解析时,不要猜测字段含义,直接向用户确认

## 前置条件
1. 已安装依赖:`pip install pypdf`
2. 已确认输入数据字段与 PDF 表单字段的对应关系(见 `references/field_map.md`)
3. 输出目录可写,且**不覆盖**原始模板文件

## 执行步骤
1. 调用 `scripts/fill_form.py --list` 解析 PDF,输出全部表单字段清单(字段名、类型、是否必填)
2. 将字段清单与用户数据字段做比对,生成映射表;无法自动匹配的字段列出来向用户确认
3. 按 `references/field_map.md` 的规则做值转换(日期统一为 `YYYY-MM-DD`,布尔值转 `Yes/No`)
4. 调用 `scripts/fill_form.py --input data.json --map field_map.json --output out.pdf` 写入
5. 校验必填字段是否全部有值,输出变更清单(字段名、原值、新值)
6. 把输出文件路径与变更清单一起返回给用户

## 输出规范
- 输出文件:`<原文件名>_filled.pdf`,与模板同目录或用户指定路径
- 变更清单:Markdown 表格,列为 `字段名 | 原值 | 新值 | 状态`
- 若存在未填写的必填字段,状态标记为 `MISSING` 并在末尾汇总数量

## 注意事项
- 字段缺失时**不要猜测**,向用户确认
- 不修改原始模板文件,一律输出到新路径
- 日期、金额等格式不一致时,先统一格式再写入
- 单次处理超过 100 个字段时,先输出字段清单让用户确认再写入

## 示例

输入:

```json
{
  "full_name": "张三",
  "birth_date": "1990/05/12",
  "subscribe": true
}

输出变更清单:

字段名 原值 新值 状态
full_name 张三 OK
birth_date 1990-05-12 OK
subscribe Yes OK
email MISSING
复制代码
**scripts/fill_form.py 核心代码**

```python
"""pdf-form-filler 核心脚本:读取 PDF 表单字段并按映射写入。"""
import argparse
import json
from pathlib import Path

from pypdf import PdfReader, PdfWriter
from pypdf.generic import NameObject


def list_fields(pdf_path: str) -> dict:
    """解析 PDF,返回 {字段名: 当前值} 的字典。"""
    reader = PdfReader(pdf_path)
    fields = reader.get_fields() or {}  # 无表单字段时返回空字典
    return {name: (f.get("/V") or "") for name, f in fields.items()}


def normalize(value, field_type: str):
    """按字段类型做值转换,避免写入非法类型。"""
    if isinstance(value, bool):
        return "Yes" if value else "Off"  # 复选框只接受 Yes/Off
    if field_type == "/Tx" and isinstance(value, str):
        return value.replace("/", "-")  # 日期统一分隔符,示例:1990/05/12 -> 1990-05-12
    return str(value)


def fill(pdf_path: str, data: dict, mapping: dict, output: str) -> list:
    """按 mapping 把 data 写入 PDF,返回变更清单。"""
    reader = PdfReader(pdf_path)
    writer = PdfWriter()
    writer.append(reader)  # 保留原始页面与版式

    fields = reader.get_fields() or {}
    changes = []

    for data_key, field_name in mapping.items():
        if field_name not in fields:
            changes.append((field_name, "", "", "FIELD_NOT_FOUND"))
            continue  # 字段不存在时跳过,不猜测

        old_value = fields[field_name].get("/V") or ""
        if data_key not in data:
            changes.append((field_name, old_value, "", "MISSING"))
            continue  # 数据缺失时标记,交由上层向用户确认

        new_value = normalize(data[data_key], fields[field_name].get("/FT", ""))
        writer.update_page_form_field_values(
            writer.pages[0], {field_name: new_value}, auto_regenerate=False
        )
        changes.append((field_name, old_value, new_value, "OK"))

    # 关闭自动重生成,避免部分阅读器打开后表单被清空
    writer.set_need_appearances_writer(True)
    with open(output, "wb") as f:
        writer.write(f)

    return changes


def main():
    parser = argparse.ArgumentParser(description="填写 PDF 表单")
    parser.add_argument("--list", metavar="PDF", help="仅列出表单字段")
    parser.add_argument("--input", help="结构化数据 JSON 文件")
    parser.add_argument("--map", dest="mapping", help="字段映射 JSON 文件")
    parser.add_argument("--output", help="输出 PDF 路径")
    args = parser.parse_args()

    if args.list:
        for name, value in list_fields(args.list).items():
            print(f"{name}\t{value}")
        return

    data = json.loads(Path(args.input).read_text(encoding="utf-8"))
    mapping = json.loads(Path(args.mapping).read_text(encoding="utf-8"))
    changes = fill(args.list or args.input, data, mapping, args.output)

    print("| 字段名 | 原值 | 新值 | 状态 |")
    print("|---|---|---|---|")
    for name, old, new, status in changes:
        print(f"| {name} | {old} | {new} | {status} |")


if __name__ == "__main__":
    main()

示例运行输出

text 复制代码
$ python scripts/fill_form.py --list template.pdf
full_name	/张三
birth_date	
subscribe	/Off
email	

$ python scripts/fill_form.py --input data.json --map field_map.json --output template_filled.pdf
| 字段名 | 原值 | 新值 | 状态 |
|---|---|---|---|
| full_name | 张三 | 张三 | OK |
| birth_date |  | 1990-05-12 | OK |
| subscribe | Off | Yes | OK |
| email |  |  | MISSING |

注意几个设计取舍:脚本只负责"按映射写入",字段匹配与确认交给上层对话完成;遇到字段不存在或数据缺失时只标记不猜测,把决策权交回用户。这正是 4.3 节强调的"把经验写进 Skill"的具体体现。

4.4 第四步:让 AI 做"红队测试"

初稿完成后,让 AI 扮演刁钻的用户来挑毛病:

text 复制代码
请以严格的评审者身份审查这个 Skill,重点检查:
1. 描述是否能让模型准确判断触发时机
2. 步骤是否存在歧义或缺失分支
3. 异常与边界情况是否覆盖
4. 是否存在与其它 Skill 职责重叠
输出问题清单,按严重程度排序。

常见问题包括:触发描述太宽导致误触发、缺少失败处理、输出格式未定义、示例与步骤不一致。

4.5 第五步:小样本验证并迭代

把 Skill 放进真实对话里跑几轮,记录失败案例,再回到第 3、4 步修改。建议维护一个"失败样例集",每次改动后回归测试,避免改一处坏一处。

5. 写好 Skill 的实用技巧

  • 描述要"可判定":写"当用户上传 CSV 并要求生成图表时使用",而不是"用于数据处理"。
  • 步骤要"可执行":每个步骤以动词开头,明确输入与输出。
  • 显式写出禁止项:模型对"不要做什么"同样敏感,边界越清晰越稳定。
  • 控制篇幅 :核心流程放正文,长参考资料放 references/,按需加载。
  • 版本化:给 Skill 打版本号,记录每次变更原因,便于回滚。
  • 单一职责:一个 Skill 只做一件事,复杂任务拆成多个 Skill 组合。

6. 常见误区

误区 后果 改进方式
把 Skill 写成一篇教程 模型抓不住重点 用清单式步骤,删掉背景铺垫
触发描述过于宽泛 频繁误触发 补充"不适用场景"
只写正常流程 异常时胡编 增加失败分支与确认机制
依赖模型"自己理解" 结果不稳定 明确输出格式与校验规则
一次写太大 难维护难调试 拆分职责,逐步扩展

7. 总结

Skill 的价值在于把一次性的提示词经验,沉淀为可复用、可验证、可维护的能力资产。用 AI 写 Skill 的正确姿势不是"让 AI 全权代写",而是你负责定义边界与经验,AI 负责结构化表达与查漏补缺。按"定边界 → 出骨架 → 喂细节 → 红队测试 → 小样本迭代"的流程走一遍,通常就能得到一个质量不错的 Skill。

相关推荐
万化冥合1 小时前
如何批量把 PPT 转成视频?
python·1024程序员节
茉莉玫瑰花茶1 小时前
线性代数 [ 序言 ]
线性代数·math·1024程序员节
陈葛杰1 小时前
Adobe Premiere Pro2025安装教程
windows·adobe·开源软件·1024程序员节
杜哥无敌1 小时前
连接服务器干活的终端 / SSH 客户端工具大 PK
ssh·1024程序员节·云主机
apple_692658851 小时前
B站音乐播放器:鸿蒙与全平台可用的开源方案
1024程序员节
池央1 小时前
不同电脑和浏览器里的收藏怎么合到一起?Linkding提供了一套自托管书签方案
cpolar·1024程序员节
CHHH_HHH1 小时前
【Linux网络篇】初识网络与Socket编程基础
linux·服务器·c语言·开发语言·网络·后端·1024程序员节
zero_70321 小时前
linux复习作业
1024程序员节
xiangjiaodalishi1 小时前
武汉玖叁鹿科技:我们把 GEO 服务做成了可以审计的交付工程
python·1024程序员节