
最近几个月我几乎每天都在跟 AI Agent 相关的项目打交道。如果你也混在这个圈子里应该能明显感受到一个趋势大家讨论的重点正在从“怎么写出一段惊艳的 Prompt”慢慢转向“怎么给 Agent 建一个真正能用的 Skills 库”。我最初对 Skills 的理解也很浅觉得不就是给模型几个示例、几个步骤嘛直到自己在实际项目里把一个流程拆成三四个 Skill 并跑通之后才意识到这东西的设计好坏直接决定了一个 Agent 项目是玩具还是生产力工具。这篇文章我想把这段时间的实践经验完整梳理一遍从 Skills 的定位、结构设计到手写一个 Skill 的完整流程再到那些不踩一次坑根本不会知道的细节。如果你正准备在自己的项目里引入 Skills或者已经在用但总觉得效果不够稳这篇文章应该能给你一些可以直接落地的参考。1. Skills 到底是什么为什么大家都在聊先说一个最朴素的观察过去我们跟模型对话本质上是在“求”模型理解我们。你把需求写得越详细模型发挥得越稳定。但这种方式的上限很低因为每一次交互都是一次全新的推理模型没有真正的“记忆”也没有一个固定的执行流程。遇到复杂任务它可能这次想到了这一步下次就忘了。Skills 解决的是这个问题。它把一组提示词、脚本、流程说明和示例打包成一个可以被 Agent 动态调用的小单元。你可以把它理解为“给模型配了一个随身携带的插件工具箱”它不是告诉模型“你要怎么做人”而是直接递给它“这是扳手、这是螺丝刀、这是操作手册”。很多刚接触的朋友会问我Skills 和 Prompt 到底有什么区别我用一个比喻回答Prompt 是教练在耳边喊“注意姿势、注意呼吸”而 Skill 是给你一本训练手册加一套辅助器械。前者依赖临场发挥后者把经验固化成流程。再直白一点Prompt 是“建议”Skill 里除了“建议”还有“强制执行的脚本”和“明确的结构化输出”。还有一个容易混淆的概念是 MCPModel Context Protocol。我自己的理解是MCP 解决的是“Agent 如何跟外部工具连接”的问题它是一套通信协议而 Skills 解决的是“Agent 怎么知道该用什么方式完成什么任务”的问题它是一套能力封装。两者可以配合使用但定位完全不同。你可以有 MCP 服务器提供数据接口再用 Skills 来定义何时调用、如何组合这些接口。很多项目一开始只接 MCP结果 Agent 虽然能访问工具了但不知道该在什么场景下用、怎么编排步骤这就是缺了 Skills 这层“行为规范”。在实战中我发现Skills 最大的价值不是让模型“变聪明”而是让项目“变确定”。模型依然有随机性但 Skill 通过固定流程和脚本输出把很多不可控的部分挡在了外面。这个认知是我在连续调整了三个月 Prompt 之后才彻底想通的。2. Skills 的结构拆解与设计思路2.1 一个标准 Skills 目录长什么样先直接给一个我目前正在用的标准目录结构你可以把它当成脚手架来参考。skills/ └── pdf-invoice-extractor/ ├── SKILL.md ├── scripts/ │ ├── extract.py │ └── validate.py ├── assets/ │ ├── example_input.pdf │ └── example_output.json └── requirements.txt这个结构是经过几次重构才定下来的每个部分都有它存在的理由。SKILL.md是整个 Skill 的入口也是模型最先读取的文件。它负责告诉模型这个 Skill 是干什么的、在什么条件下应该被触发、输入输出格式是什么样的、有哪些步骤必须遵守。注意SKILL.md 不是写给开发者看的文档而是写给模型看的“使用说明书”这个认知转变很重要。scripts/目录放着实际执行任务的代码。为什么要放脚本因为模型在处理结构化任务时哪怕是简单的字段提取也可能出现格式漂移。脚本则完全没有这个问题只要输入正确输出永远是确定性的。我在设计 Skills 时给自己定了一条规矩凡是能用代码固定下来的逻辑绝不交给模型自由发挥。assets/目录用来放示例文件。对于需要解析 PDF、处理图片这类任务示例文件比文字描述有用得多。模型可以对照示例和实际输入更准确地理解任务边界。2.2 SKILL.md 的核心字段与写法一个合格的 SKILL.md 通常包含这些关键字段name技能名称建议用英文短横线连接比如pdf-invoice-extractor方便在日志里检索。description这一段极其关键它直接决定了模型在什么场景下会调用这个 Skill。写得太窄模型想不到用它写得太泛模型什么任务都往这里套。我的经验是description 里至少包含三个要素适用场景、输入条件、预期输出。我通常会写成“当用户提供 PDF 格式的发票文件并希望提取关键字段时使用输出为结构化的 JSON”。执行步骤用有序列表把任务拆成 3 到 7 个步骤每个步骤尽量是可直接执行的命令或明确的操作。模型对你的 Skill 越熟悉步骤可以越精简。输入输出格式在这里明确输入参数的定义和输出结构。如果是 JSON 输出我强烈建议附上一个最小示例模型对示例的遵循度远高于对字段描述的遵循度。还有一个细节容易被忽略SKILL.md 里不要写太多“背景知识”和“设计理念”类的内容。模型读取这个文件是有上下文长度开销的写得越冗长留给实际任务的空间就越小。我在团队里经常强调一个原则SKILL.md 要像 Unix 的 man page短小精悍每一个词都有信息量。2.3 脚本层把模糊需求翻译成确定性流程脚本是 Skills 的灵魂。写脚本之前先搞清楚一个问题这个任务里哪些环节适合交给模型哪些环节必须用代码我的拆分原则是语义理解、意图判断、内容总结、文本生成——交给模型。文件解析、格式转换、字段校验、数据清洗、网络请求——交给脚本。既包含理解又包含执行的混合任务——先让模型输出中间结果再由脚本做后续处理。举一个典型的混合场景用户上传一份扫描版合同需求是提取其中的金额和日期。这个任务如果全交给模型模型需要一边读图一边做 OCR 一边做结构化输出最终的格式大概率不稳定。但如果拆成两步先由脚本调用 OCR 服务把 PDF 转成文本再把文本交给模型做字段提取最后脚本负责按 JSON Schema 校验并输出整条链路的稳定性就会高很多。脚本设计还有一个原则脚本内部不要写死业务规则而是把规则作为参数传入。还是以发票提取为例每个客户的发票字段规则不同如果规则写在 SKILL.md 里模型每次执行都要重新理解如果规则写成一个 JSON 配置文件脚本读取配置执行那么换一个客户只需要换一份配置完全不用动脚本。3. 手写一个 Skill从零到可用的完整流程很多教程会直接给你一个 Skill 的最终代码但看完之后你还是不知道它是怎么一步一步做出来的。下面我以一个最常用的场景——PDF 发票关键信息提取——作为案例完整走一遍从需求分析到测试迭代的流程。3.1 第一步定义输入输出边界任何 Skill 设计的第一步都不是写代码而是把问题边界划清楚。我在白板上列了一份问题清单这里只列核心几个这个 Skill 只处理 PDF 格式的发票还是也支持图片和 Excel提取的字段是固定的比如发票号码、开票日期、金额、税额、价税合计还是允许用户自定义字段输出是直接打印在对话里还是保存成 JSON 文件供下游使用我最终的定版是这样的输入为单张 PDF 发票文件路径输出为一个 JSON 对象包含发票号码、开票日期、购买方名称、销售方名称、合计金额、税额、价税合计这七个标准字段。如果解析失败输出一个包含错误码和错误信息的 JSON而不是直接抛异常。边界划清楚之后后面所有步骤都会有明确的指向性。我自己见过太多项目做着做着就“顺便支持一下图片吧”结果 Scope 无限膨胀最后连基础场景都处理不稳。3.2 第二步编写脚本骨架脚本我选用 Python一是因为生态成熟PDF 解析相关的库很多二是因为团队里其他成员对 Python 最熟悉后续维护成本最低。核心脚本的思路是先用 pdfplumber 提取文本层如果文本为空则判定为扫描版触发 OCR 兜底逻辑然后用正则和规则从文本中定位关键字段最后组装 JSON 并做字段完整性校验。这里贴一个高度简化但足以说明思路的示例#!/usr/bin/env python3 import json import re import sys from pathlib import Path def extract_invoice_fields(text: str) - dict: fields { invoice_no: None, invoice_date: None, buyer_name: None, seller_name: None, total_amount: None, tax_amount: None, total_with_tax: None, } # 发票号码常见格式为发票号码: 12345678 match re.search(r发票号码[:]\s*(\S), text) if match: fields[invoice_no] match.group(1) # 开票日期常见格式2025年03月14日 match re.search(r开票日期[:]\s*(\d{4}年\d{2}月\d{2}日), text) if match: fields[invoice_date] match.group(1) # 合计金额 match re.search(r合计[:]\s*¥?\s*([\d,]\.\d{2}), text) if match: fields[total_amount] match.group(1) return fields def main(): if len(sys.argv) ! 2: print(json.dumps({error: usage: extract.py pdf_path})) sys.exit(1) pdf_path Path(sys.argv[1]) if not pdf_path.exists(): print(json.dumps({error: file_not_found})) sys.exit(1) # 真实实现中这里会用 pdfplumber 提取文本 sample_text 发票号码: 12345678\n开票日期: 2025年03月14日\n合计: ¥100.00 fields extract_invoice_fields(sample_text) missing [k for k, v in fields.items() if v is None] if missing: fields[warnings] fmissing fields: {,.join(missing)} print(json.dumps(fields, ensure_asciiFalse)) if __name__ __main__: main()这个脚本的核心设计是不是所有字段都能通过正则稳定匹配所以脚本在最后会输出warnings字段提示哪些字段是缺失的。这个信息会回传给模型模型可以基于它决定是否追加询问用户。比起直接让脚本报错中断这种设计让 Agent 有了“下一步行动”的依据。3.3 第三步编写 SKILL.md 并串联执行流程脚本写完之后SKILL.md 就成了模型和脚本之间的桥梁。我习惯用 YAML front-matter 作为元信息区块正文部分用 Markdown 写步骤结构清晰也方便脚本做自动校验。下面是我这个案例对应的 SKILL.md 核心内容--- name: pdf-invoice-extractor description: 当用户提供 PDF 格式的发票文件并希望提取发票号码、日期、金额等关键信息时使用。输入是 PDF 文件路径输出是结构化 JSON。 --- # PDF 发票信息提取 ## 执行步骤 1. 确认用户提供的 PDF 文件路径如果路径不存在反馈给用户并要求重新提供。 2. 调用 python3 scripts/extract.py pdf_path 执行提取。 3. 解析 stdout 中的 JSON 结果。 4. 如果结果中包含 warnings 字段检查缺失字段向用户确认是否接受不完整数据。 5. 将最终 JSON 输出给用户。 ## 注意事项 - 只支持 PDF 格式其他格式请直接拒绝。 - 扫描版 PDF 的 OCR 处理耗时较长需要提示用户耐心等待。 - 输出金额字段保留两位小数不做四舍五入之外的其他处理。写 SKILL.md 最忌讳的是事无巨细地把所有情况都写进去模型读起来反而抓不住重点。我通常只写最小必要信息触发条件、步骤、输入输出、边界规则。更多细节放进脚本里交给确定性逻辑处理。3.4 第四步测试与迭代测试阶段我会准备三种样本标准样本、边缘样本、错误样本。标准样本是格式规整的 PDF 电子发票用来验证主流程边缘样本包括扫描版 PDF、包含多个表格的 PDF、金额特别大的 PDF用来检查脚本的容错能力错误样本包括加密 PDF、空文件、损坏文件用来确认脚本能输出合理的错误信息而不是崩溃。第一次跑测试时我通常会在 SKILL.md 的步骤里故意写得模糊一点看模型会怎么理解。比如我在步骤里只写“调用 extract.py 执行提取”模型有时会自作主张修改参数有时会跳过脚本直接根据文件名猜字段。这些跑偏现象其实都是 SKILL.md 描述不够精确的信号需要针对性优化而不是一味怪模型笨。一个很实用的测试技巧把模型的完整执行轨迹包括它读了什么、调了哪些工具、每一步的输出是什么记录下来像看日志一样复盘。很多时候问题根本不在脚本而在模型的执行路径和你的预设有偏差。看到偏差再去改 SKILL.md一次就能改准。4. 常见问题与排查技巧实录4.1 模型死活不调用我的 Skill这是最让人头疼的问题。你辛辛苦苦写好了 Skills结果模型在对话里就像完全不知道这东西存在一样。我排查这类问题的顺序是固定的先看 description 写的是什么再看模型实际把任务理解成了什么。description 写得太泛的典型表现是任何任务都有可能“沾边”但模型不会优先选择你的 Skill。举例来说如果你写“处理 PDF 文件”那模型面对一个市场调研报告 PDF 时也可能调用它结果自然是驴唇不对马嘴。写得太窄的典型表现则是用户换了个说法比如“帮我看看这张发票”模型根本没有把“发票”和“pdf-invoice-extractor”关联起来。我的补救办法是在 description 里加 3 到 5 个典型的用户提问示例比如“提取这张发票里的金额”“帮我解析这张 PDF 发票”。模型对具体问题的联想能力远强于对抽象描述的联想能力这一点在多轮实测里差异非常明显。还有一个小技巧在 SKILL.md 正文的开头写一句“当且仅当用户明确提出需要提取发票字段时才使用本技能”这句话我试下来能明显减少误触发。4.2 输出不稳定模型偶尔会擅自修改字段有一种情况特别隐蔽脚本输出的 JSON 明明是正确的但模型在把结果包装给用户时会“好心”地把某些字段改成它自己理解的值。比如脚本输出的金额是100.00模型看到上下文里有一句话提到“优惠后 90”就自动把金额改成了90.00。这类问题的根源是模型把你脚本的输出当成了“参考消息”而不是事实。解决办法有两个方向一是在 SKILL.md 里明确声明“脚本输出为最终结果任何情况下不得修改字段值”二是在脚本输出的 JSON 上做一层标记比如增加source: script字段或者干脆让脚本把结果写到一个独立的.json文件你再让模型原样读取文件内容并展示。我实测下来第二个办法更稳。当脚本输出被固化成文件之后模型在心理上会把它当成一个客观对象而不是一条可以随意改写的对话文本。这也是“确定性优先”原则的具体体现——能通过工程手段解决的问题就不要期望模型自觉。4.3 多个 Skill 互相抢任务当你的 skills 目录里积累了十几个 Skill 之后一定会遇到误触发问题。我之前做过的两个 Skill一个是report-summarizer负责总结周报并提取行动项一个是meeting-minutes负责把会议纪要整理成结构化记录。结果有一次用户说“帮我总结一下昨天会议纪要里的行动项”两个 Skill 都被触发了输出互相打架。解决这个问题我用了两个手段。第一在命名上做隔离为所有 Skill 加上业务域前缀比如hr-、finance-、devops-这样模型在选择时会自然按域过滤。第二在 SKILL.md 的 description 里增加“排除声明”比如meeting-minutes的 description 里明确写“本技能不处理会议纪事之后的行动项总结这类任务请使用 report-summarizer”。还有一种情况是同一个任务被两个 Skill 的部分功能覆盖。这时候不要急着拆 Skill先看看是不是可以把公共部分抽成一个 shared 函数两个 Skill 分别调用。我是到第三个项目才意识到Skills 之间也可以有依赖关系你的体系设计得越接近真实团队的职责划分模型的调度准确率就越高。4.4 问题排查速查表我把最常见的几类问题整理成了一张表每次调试新 Skill 都会先对着它过一遍现象常见原因解决方向Skill 完全不触发description 与用户语言不匹配在 description 中加入典型提问示例Skill 频繁误触发description 范围太宽增加排除声明收窄适用场景脚本输出被模型篡改模型将脚本结果当成参考信息结果先固化到文件再让模型读取展示执行到一半停止步骤描述缺乏兜底指令为每一步增加异常处理分支输出字段结构漂移缺少 JSON 格式示例在 SKILL.md 中附最小示例多个 Skill 同时触发职责边界重叠加业务域前缀写排除声明这张表不是一次性写出来的是踩坑之后慢慢积累的。每遇到一个新问题我会先对照表格看有没有现成解没有的话就补一条时间长了调试成本明显降下来了。5. 把 Skills 当工程资产来维护写一个能用的 Skill 不难难的是让整个 Skills 库在项目演进过程中保持健康。我见过很多项目Skill 数量一多开始出现僵尸 Skill——就是那种已经在 SKILL.md 里写了名字但没有任何 description 能匹配到触发场景、或者脚本依赖的第三方库已经升级到不可用的状态。我把维护 Skills 库的经验压缩成三个词命名规范、版本管控、定期评审。命名规范不需要复杂我目前用的是[业务域]-[动作]-[对象]三段式比如devops-analyze-log、finance-extract-invoice。这样在日志里看到触发记录时一眼就能判断责任方。版本管控方面我建议每个 Skill 目录里放一个 CHANGELOG.md记录每次修改的原因和时间。很多人会忽略这一步但 Skill 的调试往往是隔几周才做一次没有记录的话你根本想不起来上次为什么把那个正则从\d改成了[\d,]。定期评审我是在每月底做一次主要检查三件事哪些 Skill 在过去一个周期内从未被触发哪些 Skill 的输出被用户二次修正的频率过高哪些脚本的依赖已经过时。从未被触发的 Skill要么立即删除要么改写 description 重新激活。二次修正率高的 Skill说明它在准确性上还存在短板需要安排专项优化。还有一点是关于组织协作的。如果你的项目不止一个人维护强烈建议在仓库里加一个skills/CONTRIBUTING.md里面写明新增 Skill 的流程、目录规范、测试要求。我自己就经历过同事随手往 skills 目录里塞了个 Python 脚本但没有 SKILL.md导致模型完全无法使用这个“半个 Skill”的尴尬局面。规范写下来这种事就能从源头上避免。我在实际项目中的体会是Skills 的设计工作表面上是在写文件和脚本本质上是在重构你对 AI 能力的认知。以前我们总想着让模型更聪明后来慢慢明白真正聪明的做法是给模型一套稳定的脚手架让它把精力集中在最擅长的事情上。如果你正在尝试构建自己的 Skills 体系我建议从一个小而美的场景开始把一个 Skill 打磨到“稳定处理 80% 的标准输入”再谈扩展和复用。这个路径看起来慢但每一步都在为后面的复杂度打基础。