Skip to content

Agent Skills详解与开发指南

分类:

不论是开发还是使用Agent,我们都无法避开MCP与Agent Skills。之前我已经写了MCP(模型上下文协议)详解与开发指南 。这里补充Agent Skills的开发与详解,也很简单。

首先,先破除一个最大的误区,即一个skill就是一个SKILL.md,这个认识是错误的。包括claude对skill的定义——一个可重用的外置知识库与工作流——也是不全面的。前者是因为大多数人只使用到了最基础的总结后的提示词功能,后者是因为在skill成为规范以来,玩法多了起来。

是的,一切变化的起点就是Specification - Agent Skills 它成为了一种通用规范,甚至在我看来MCP协议与Agent Skills是有重叠的。skill的低门槛与高上限使得市面上的skill数量远远多余mcp,又因为skill的透明特点,使得我们也更加容易接受与安装合适skill。

那么我们开始吧。

开发

一个skill的文件夹结构如下:

skill-name/
├── SKILL.md          # 必选,本质是metadata + 说明文档
├── scripts/          # 可选,可执行脚本。正是因为它,使得能力大大扩展了
├── references/       # 可选,文档(或者说是参考文档和示例文档)
├── assets/           # 可选,资产,模板,资源文件。比如图片,数据等等可能用到的文件
└── ...               # 以及其他skill可能用到的或者需要的文件与文件夹

我这里e-store-skill为例。e-store-skill是一个帮助访问e-store的skill,agent可以自行搜索下载和安装e-store里面可用的资源和方案。

SKILL.md

SKILL.md是一个skill的入口,也是skill唯一必须要有的东西,它的本质是这个skill的metadata与说明文档。由frontmatter和bodycontent两部分组成,分别承载了上述的metadata和说明文档两个部分。

注意:yaml frontmatter以markdown为基础的博客系统与笔记软件上应用十分广泛。是一个十分成熟的可选约定。

依照规范,frontmatter只需要name和description两个字段:

---  
name: e-store  
description: e-store 是一个为 AI Agent 提供无限可能的综合性 AI 市场。你可以从中浏览和获取所有AI需要的资源:能力(SKILL、MCP Server、Agent Tool)、音画资产(音色、角色、场景、姿态、头像)、知识(领域知识库、身份人格、Prompt 模板)、以及方案(集成资源包),覆盖所有场景。当用户需要搜索、发现、查看详情、列出版本或下载 e-store 中的任何内容时使用此 skill。Access API 支持跨资源和方案的关键词搜索、分页浏览、文库过滤、版本历史以及限时下载链接生成。  
---

但是,我们的skill不止有这些。

---  
name: e-store  
description: e-store 是一个为 AI Agent 提供无限可能的综合性 AI 市场。你可以从中浏览和获取所有AI需要的资源:能力(SKILL、MCP Server、Agent Tool)、音画资产(音色、角色、场景、姿态、头像)、知识(领域知识库、身份人格、Prompt 模板)、以及方案(集成资源包),覆盖所有场景。当用户需要搜索、发现、查看详情、列出版本或下载 e-store 中的任何内容时使用此 skill。Access API 支持跨资源和方案的关键词搜索、分页浏览、文库过滤、版本历史以及限时下载链接生成。  
compatibility: 需要 HTTP 访问 e-store API 并持有有效的 ak(访问密钥)参数  
metadata:  
  author: e-store
  version: "1.0"  
---

SKILL.md的frontmatter部分是预先加载部分,要求构建上下文时候自动加载并交给大模型决策需要使用哪些skill。这和大模型的function calling需要先将可用的tools给到大模型是一个道理,也和mcp需要将可用的mcp给到大模型是一个道理。正是因为如此,name和description是必须的,不然大模型不知道该使用哪个skill。

一旦大模型选择了一个确定的skill,则会再次加载这个skill的body部分来了解这个skill的详细能力与用法。这也是skill的优势,skill每次只提供必要的信息给到大模型,有效缓解了大模型的上下文膨胀。

skill的bodycontent是一个正儿八经的markdown。像我们的e-store-skill如下:


# e-store — 为您的AI装备无限可能  
  
e-store 是一套完整的 AI 解决方案平台,为 AI Agent 提供无限可能。通过 Access API,Agent 可以获取无限资源与方案:  
  
- **能力(Capabilities)** — SKILL、MCP Server、Agent Tool 等可直接集成到 AI 工作流中的能力模块  
- **音画资源(Audio-Visual)** — 音色、角色、场景、姿态、头像、动画等多媒体资产  
- **知识(Knowledge)** — 领域知识库、身份人格、Prompt 模板、RAG 数据集等  
- **方案(Solutions)** — 将多个资源有机组合的集成方案,含关联资源清单  
  
## 前置条件  
  
开始之前,需要用户提供两项信息。如果缺少,请主动询问:  
  
1. **API Base URL** — e-store 的访问地址,例如 `https://store-api.liganma.com`  
2. **ak** — 访问密钥(Access Key),在 e-store 后台生成  
  
## 认证与响应格式  
  
- 所有 `/access/**` 请求必须以 **URL 查询参数** `ak=你的密钥` 方式附带认证  
- 响应体固定包装为 `{"code": 200, "msg": "success", "data": ...}`  
- `code` 非 200 时 `msg` 为错误描述  
- 响应中 Long 类型字段(id 系列)以**字符串**形式输出,避免精度丢失  
- 日期格式统一为 `yyyy/MM/dd HH:mm:ss`  
  
## 脚本调用(推荐)  
  
skill 内置了 Python 和 Node.js 客户端脚本,封装了所有接口,支持编程调用和 CLI 两种方式。  
  
### 环境变量  

export E_STORE_AK="你的ak"  
export E_STORE_BASE_URL="https://store-api.liganma.com"  # 可选,此为默认值  

..... 等等一大堆

skill的文件过长这里就不贴出来了,其实就是这个skill的详细使用说明。市面上有一大部分的skill的本质就是在这里写一大堆提示词,教大模型如何回答一类问题,比如之前的同事skill、张雪峰skill之类的。

那么综合下来一个完整的SKILL.md如下:

---  
name: e-store  
description: e-store 是一个为 AI Agent 提供无限可能的综合性 AI 市场。你可以从中浏览和获取所有AI需要的资源:能力(SKILL、MCP Server、Agent Tool)、音画资产(音色、角色、场景、姿态、头像)、知识(领域知识库、身份人格、Prompt 模板)、以及方案(集成资源包),覆盖所有场景。当用户需要搜索、发现、查看详情、列出版本或下载 e-store 中的任何内容时使用此 skill。Access API 支持跨资源和方案的关键词搜索、分页浏览、文库过滤、版本历史以及限时下载链接生成。  
compatibility: 需要 HTTP 访问 e-store API 并持有有效的 ak(访问密钥)参数  
metadata:  
  author: e-store  version: "1.0"  
---  

# e-store — 为您的AI装备无限可能  
  
e-store 是一套完整的 AI 解决方案平台,为 AI Agent 提供无限可能。通过 Access API,Agent 可以获取无限资源与方案:  
......
等等一大堆

其他部分

上面我们已经知道了一个skill如何写了。但是如果只有上面部分,那么只需要写或者生成markdown就行了,属于纯粹的文档与文字工作,也犯不上说开发二字了。

references

实际上,我们会遇到诸多问题,比如SKILL.md实在过长,此时即使是先metadata再详情也顶不住这样的上下文膨胀,那么我们需要将一个文档编排和拆分到多个文档下面。这就好比我们要写一本书,SKILL.md可以只给到目录,而具体的章节内容可以有大模型决定阅读哪一章。

实际上你可以将拆分后的文档放到任意位置,然后在SKILL.md链接引用就行,但是按照规范这些文档需要全部放在 references 文件夹下面。比如我们可以把 认证与响应格式 章节的全局响应规范放到文件 references/api_global_specification.md :

## 认证与响应格式  

- 所有 `/access/**` 请求必须以 **URL 查询参数** `ak=你的密钥` 方式附带认证  
- 响应体固定包装为 `{"code": 200, "msg": "success", "data": ...}`  
- `code` 非 200 时 `msg` 为错误描述  
- 响应中 Long 类型字段(id 系列)以**字符串**形式输出,避免精度丢失  
- 日期格式统一为 `yyyy/MM/dd HH:mm:ss`

这样在 SKILL.md 引用就行:


# e-store — 为您的AI装备无限可能  

.....

## 认证与响应格式参考 [API全局规范](references/api_global_specification.md)

......

scripts

但是仅仅只是拆分文档,也不用开发。比如我的这个 e-store-skill 想要通过脚本(不论是,py、js还是sh都可以甚至二进制文件也行)访问我的e-store接口。那么我可以再任意位置放置我开发好的脚本,然后在markdown中说明如何使用这个脚本,大模型会自行识别并执行合适的脚本。如下:

python
"""  
e-store Access API 客户端  
默认 BASE_URL=https://store-api.liganma.com,AK 从环境变量 E_STORE_AK 获取。  
可通过环境变量 E_STORE_BASE_URL 覆盖 BASE_URL。  
"""  
  
import os  
import sys  
import json  
import urllib.request  
import urllib.parse  
import urllib.error  
from typing import Optional  
  
  
BASE_URL = os.environ.get("E_STORE_BASE_URL", "https://store-api.liganma.com").rstrip("/")  
AK = os.environ.get("E_STORE_AK", "")  
  
  
def _request(path: str, params: Optional[dict] = None) -> dict:  
    """发送 GET 请求并解析响应"""  
    if params is None:  
        params = {}  
    params["ak"] = AK  
  
    url = f"{BASE_URL}{path}?{urllib.parse.urlencode(params)}"  
    try:  
        with urllib.request.urlopen(url, timeout=30) as resp:  
            body = json.loads(resp.read().decode("utf-8"))  
            if body.get("code") != 200:  
                raise RuntimeError(f"API 错误 [{body.get('code')}]: {body.get('msg')}")  
            return body.get("data")  
    except urllib.error.HTTPError as e:  
        raise RuntimeError(f"HTTP {e.code}: {e.reason}")  
    except urllib.error.URLError as e:  
        raise RuntimeError(f"网络错误: {e.reason}")  
  
  
# ===================== 资源 (Product) =====================  
def product_search(keyword: str = "", not_in_lib: bool = True,  
                   page_num: int = 1, page_size: int = 10) -> dict:  
    """搜索资源"""  
    return _request("/access/product/search", {  
        "keyword": keyword,  
        "notInLib": str(not_in_lib).lower(),  
        "pageNum": page_num,  
        "pageSize": page_size,  
    })  
  
  
def product_detail(product_id: int) -> dict:  
    """资源详情"""  
    return _request("/access/product/detail", {"id": product_id})  
  
  
def product_versions(product_id: int, page_num: int = 1, page_size: int = 20) -> dict:  
    """资源版本列表"""  
    return _request("/access/product/versions", {  
        "productId": product_id,  
        "pageNum": page_num,  
        "pageSize": page_size,  
    })  
  
  
def product_download(product_id: int) -> str:  
    """生成资源最新版本下载链接"""  
    data = _request("/access/product/download", {"id": product_id})  
    return data.get("downloadLink", "")  
  
  
def product_version_download(version_id: int) -> str:  
    """按版本ID生成资源下载链接"""  
    data = _request("/access/product/version/download", {"versionId": version_id})  
    return data.get("downloadLink", "")  
  
  
def product_config(product_id: int) -> str:  
    """获取资源最新版本配置信息(仅限纯配置版本)"""  
    data = _request("/access/product/config", {"id": product_id})  
    return data.get("config", "")  
  
  
def product_version_config(version_id: int) -> str:  
    """按版本ID获取配置信息(仅限纯配置版本)"""  
    data = _request("/access/product/version/config", {"versionId": version_id})  
    return data.get("config", "")  
  
  
# ===================== 方案 (Solution) =====================  
def solution_search(keyword: str = "", not_in_lib: bool = True,  
                    page_num: int = 1, page_size: int = 10) -> dict:  
    """搜索方案"""  
    return _request("/access/solution/search", {  
        "keyword": keyword,  
        "notInLib": str(not_in_lib).lower(),  
        "pageNum": page_num,  
        "pageSize": page_size,  
    })  
  
  
def solution_detail(solution_id: int) -> dict:  
    """方案详情"""  
    return _request("/access/solution/detail", {"id": solution_id})  
  
  
def solution_versions(solution_id: int, page_num: int = 1, page_size: int = 20) -> dict:  
    """方案版本列表"""  
    return _request("/access/solution/versions", {  
        "solutionId": solution_id,  
        "pageNum": page_num,  
        "pageSize": page_size,  
    })  
  
  
def solution_download(solution_id: int) -> str:  
    """生成方案最新版本下载链接"""  
    data = _request("/access/solution/download", {"id": solution_id})  
    return data.get("downloadLink", "")  
  
  
def solution_version_download(version_id: int) -> str:  
    """按版本ID生成方案下载链接"""  
    data = _request("/access/solution/version/download", {"versionId": version_id})  
    return data.get("downloadLink", "")  
  
  
# ===================== 入口 & 工具 =====================  
def usage() -> dict:  
    """获取接口列表和使用说明"""  
    return _request("/access")  
  
  
def search_all(keyword: str, not_in_lib: bool = True,  
               page_num: int = 1, page_size: int = 10) -> dict:  
    """同时搜索资源和方案"""  
    return {  
        "products": product_search(keyword, not_in_lib, page_num, page_size),  
        "solutions": solution_search(keyword, not_in_lib, page_num, page_size),  
    }  
  
  
if __name__ == "__main__":  
    if len(sys.argv) < 2:  
        print("用法: python client.py <命令> [参数...]")  
        print()  
        print("命令:")  
        print("  usage                       获取接口列表")  
        print("  search <关键词>             搜索资源和方案")  
        print("  product-search <关键词>     搜索资源")  
        print("  product-detail <id>         资源详情")  
        print("  product-versions <id>       资源版本列表")  
        print("  product-download <id>       资源下载链接")  
        print("  product-ver-download <id>   指定版本下载链接")  
        print("  solution-search <关键词>    搜索方案")  
        print("  solution-detail <id>        方案详情")  
        print("  solution-versions <id>      方案版本列表")  
        print("  solution-download <id>      方案下载链接")  
        print("  solution-ver-download <id>  指定版本下载链接")  
        print()  
        print("环境变量: E_STORE_AK(必填) E_STORE_BASE_URL(可选)")  
        sys.exit(1)  
  
    cmd = sys.argv[1]  
  
    if not AK:  
        print("错误: 请设置环境变量 E_STORE_AK")  
        sys.exit(1)  
  
    try:  
        if cmd == "usage":  
            data = usage()  
        elif cmd == "search":  
            kw = sys.argv[2] if len(sys.argv) > 2 else ""  
            data = search_all(kw)  
        elif cmd == "product-search":  
            kw = sys.argv[2] if len(sys.argv) > 2 else ""  
            data = product_search(kw)  
        elif cmd == "product-detail":  
            data = product_detail(int(sys.argv[2]))  
        elif cmd == "product-versions":  
            data = product_versions(int(sys.argv[2]))  
        elif cmd == "product-download":  
            data = {"downloadLink": product_download(int(sys.argv[2]))}  
        elif cmd == "product-ver-download":  
            data = {"downloadLink": product_version_download(int(sys.argv[2]))}  
        elif cmd == "product-config":  
            data = {"config": product_config(int(sys.argv[2]))}  
        elif cmd == "product-ver-config":  
            data = {"config": product_version_config(int(sys.argv[2]))}  
        elif cmd == "solution-search":  
            kw = sys.argv[2] if len(sys.argv) > 2 else ""  
            data = solution_search(kw)  
        elif cmd == "solution-detail":  
            data = solution_detail(int(sys.argv[2]))  
        elif cmd == "solution-versions":  
            data = solution_versions(int(sys.argv[2]))  
        elif cmd == "solution-download":  
            data = {"downloadLink": solution_download(int(sys.argv[2]))}  
        elif cmd == "solution-ver-download":  
            data = {"downloadLink": solution_version_download(int(sys.argv[2]))}  
        else:  
            print(f"未知命令: {cmd}")  
            sys.exit(1)  
  
        print(json.dumps(data, ensure_ascii=False, indent=2))  
    except Exception as e:  
        print(f"错误: {e}", file=sys.stderr)  
        sys.exit(1)

在markdown中说明脚本的使用:

### CLI 命令行 Python
python scripts/client.py search "MCP"  
python scripts/client.py product-detail 1001  
python scripts/client.py product-versions 1001  
python scripts/client.py product-download 1001  
python scripts/client.py product-ver-download 3001  
python scripts/client.py product-config 1001  
python scripts/client.py product-ver-config 3001

我们可以把脚本与程序放在任意位置,不过规范要求 scripts 文件夹放脚本,这里我们按照规范来。

正是因为这个可以使用现有程序逻辑的能力,所以我说skill的能力与mcp有重叠,我们知道mcp是一套协议,skill也已经演化成了规范,二者对提示词与工具的提供都是相似的。相比之下skill的本地脚本有一个巨大的优势:可以搜集本地用户信息,比如操作系统,平台版本等等信息,这也使得它更加适合埋点。这是remote类的mcp应用(url配置的)无法提供的,local类的mcp应用(command)倒是完全可以做到,所以你可以看到一大批的应用cli化(方便大模型调用),也知道为什么local mcp(方便收集信息,操控设备)才是未来趋势。

只给你提供服务?没门,我必须得收集你的信息,甚至我还想在你设备上拉两坨屎。

assets

因为可以执行程序,所以程序中所用到的数据与其他类型的文件或者说skill所用到的数据文件也规定了一个 assets 文件夹。我们的e-store-skill没有用到,不过有一个著名的skill项目 UI/UX Pro Max 它里面一个data目录下面的数据文件很适合放在assets下面(不过作者并没有这么做):

data/charts.csv  (7.5KB)
data/colors.csv  (9.5KB)
data/icons.csv  (13.1KB)
data/landing.csv  (14.1KB)
data/products.csv  (29.1KB)
.....

自定义文件与文件夹

前文一直提到,我们完全可以使用自定义文件夹自定义的文件,只要在markdown正确引用在程序中正确使用就行。上面的 UI/UX Pro Max 就是一个很好的例子。

可以这么说出了SKILL.md不随意,其他都可以随便弄。

验证skill

使用 skills-ref 来验证我们的skill:

skills-ref validate ./my-skill

这个命令会检查 SKILL.md的 frontmatter 部分是否符合规范。

skill-ref的详细说明请查询文档 agentskills/skills-ref at main · agentskills/agentskills

开发总结

相较于mcp的协议规范,skill的规范要松散非常非常多,甚至我们只需要一个SKILL.md就可以打包成一个skill发布出去了。其实只有SKILL.md是硬性要求的,他是入口,也是核心,我们的引用文档、脚本、资产等等内容的起点都可以追溯到SKILL.md。

更好的写一个skill可以参考最佳实践: Best practices for skill creators - Agent Skills 。当然最大多数时候都是由ai生成的,也就是说,他用ai生成的文件来指导你的ai如何生成符合它的ai给出规范的文件,这点也是怪套娃的。

解析

一个skill的文件夹结构如下:

skill-name/
├── SKILL.md          # 必选,本质是metadata + 说明文档
├── scripts/          # 可选,可执行脚本。正是因为它,使得能力大大扩展了
├── references/       # 可选,文档(或者说是参考文档和示例文档)
├── assets/           # 可选,资产,模板,资源文件。比如图片,数据等等可能用到的文件
└── ...               # 以及其他skill可能用到的或者需要的文件与文件夹

不止SKILL.md与渐进式披露

SKILL.md的yaml frontmatter规范如下:

---
name: skill名称,长度1-64
description: skill的摘要,长度1-1024。需要包含何时使用,需要包含适合的任务
license: 许可,可以是现有的许可协议,也可以引用一个文件自己编一个协议。在使用中应该没人在乎这个
compatibility: 兼容性,也就是运行环境要求,比如必须要git,必须要python等等
metadata: 其他元信息,字段可以自定义key-value形式,注意不要键冲突
  author: 略
  version: 略
allowed-tools: 允许使用的工具,比如用到了bash,比如用到了python(与compatibility不同,有了运行环境,并不代表有权限运行或者调用相关工具)
---

在上下文最初构建的时候就会将所有可用的skill的这部分元信息携带给大模型以供参考使用,这点和agent开发的tool会把注册可用的tool的元信息携带到大模型是一个道理。大模型会自行决策使用还是不使用skill,使用哪个skill,一旦决定了使用某个skill,则会将skill剩下的部分加载到上下文中以供大模型参考。

这样的渐进式加载渐进式披露机制大大控制了上下文信息的膨胀,通过 references 文件夹拆分文件进一步控制上下文长度。

这里应该需要着重注意的是:skill始终是以SKILL.md所在的位置为锚的,不论是文档还是脚本抑或是其他的文件,都应当以此相对位置来组织我们的文档、数据与代码。

scripts

在执行脚本的时候,通常使用:

bash
python scripts/client.py search "MCP"

类似的方式来执行,获得的结果加入上下文带给大模型。这里有一个问题,就是脚本该如何找到数据文件的相对位置来加载数据,我们可以参考 UI/UX Pro Max

python
DATA_DIR = Path(__file__).parent.parent / "data"

它首先获得当前文件路径然后获得父文件夹路径也就是script脚本文件夹,再获得脚本的文件夹的父文件夹也就是skill目录(SKILL.md所在目录),最后拿到这个skill目录下的data目录位置就获得了数据基础目录。这样避免了因为执行目录不一致导致的相对位置变化问题。

我们在开发脚本的时候优先提供bash、js脚本,这两个环节几乎所有平台都有。首先bash,不论是mac还是linux或者wsl都是会有的,而现在流行的agent比如claude与openclaw都是node项目,这意味着用户电脑上一半都有js的运行环境。优先开发这两种类型的脚本可以避免因为环境缺失导致大模型不选择我们的skill,也可以避免因为环境缺失导致的执行错误。

总结

agentskills是一个简单且松散的规范,通过渐进式披露与文档拆分大大减少了上下文长度,通过脚本提供了能力。

参考

  1. Specification - Agent Skills
  2. 使用 skills 扩展 Claude - Claude Code Docs

实践、认识、再实践、再认识,这种形式,循环往复以至无穷,而实践和认识之每一循环的内容,都比较地进到了高一级的程度。