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 是最关键的文件,通常由两部分组成:
- 元信息(frontmatter):名称、描述、版本、依赖等,供系统检索与路由。
- 正文说明:触发条件、步骤、注意事项、示例。
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 | |
| 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。