最近在做 Agent 类应用时,我一直在思考一个问题:为什么同一个大模型,在面对不同任务时表现忽高忽低?后来发现,问题往往不在模型本身,而在“我们有没有把任务所需的专业能力真正交到 Agent 手里”。而 Agent Skills 正是解决这个问题的关键。网上关于它的资料比较零散,我花了几天时间把官方规范、开源实现和社区实践整合成一套可落地的完整笔记。这篇文章我会用最直白的方式讲清楚 Agent Skills 是什么、底层规范怎么设计、以及如何从零构建一个能直接跑起来的 Skill。不管你是刚接触 AI Agent 的新手,还是已经在做 Agent 落地的开发者,这篇文章都能帮你少走很多弯路。
1. 背景与核心概念
1.1 为什么突然大家都在聊 Agent Skills
2025 年以来,大模型应用的主流形态已经从“聊天机器人”转向“Agent”——也就是让模型自己理解任务、拆解步骤、调用工具、完成目标。但在实际开发中,我们很快会遇到一个尴尬的问题:模型虽然强大,却不了解团队内部的专有流程、私有工具和领域知识。你可以让模型写一首诗,但它不知道怎么操作你们公司内部的发布系统,也不知道你项目里的代码规范是“不允许使用空异常捕获”。这时候我们需要一种机制,把“额外的专业能力”打包给模型。
Agent Skills 就是在这种背景下出现的。它并不是某一个具体的产品,而是一套描述和组织 Agent 扩展能力的开放规范。你可以把它理解为“Agent 的技能包”:一个 Skill 是包含说明文档、脚本、资源文件等内容的文件夹,Agent 在开始任务前会读取这些内容,并在执行时调用其中提供的能力。这样,模型不需要在训练时背下所有细节,而是在运行时动态加载所需技能。
1.2 Skill、Function Calling 和 MCP 三者有什么区别
很多初学者容易把 Agent Skills 和另外两个概念混在一起:Function Calling 和 MCP。我画一张通俗的解释:
| 机制 | 核心思路 | 典型使用方式 | 适用场景 |
|---|---|---|---|
| Function Calling | 模型根据用户输入,输出一个结构化调用请求,由程序执行并回填结果 | 通过 API 参数 functions/tools 传入函数定义 | 需要模型决定“何时调用哪个工具”的场景 |
| MCP(模型上下文协议) | 一种标准协议,把外部工具和数据源统一接入模型 | 通过 MCP Server 将工具暴露给客户端 | 需要连接大量外部系统时 |
| Agent Skills | 把技能封装成带说明文档和可执行脚本的文件夹,让 Agent 自主加载使用 | 将 Skill 目录放入 Agent 可访问的路径 | 领域知识沉淀、复杂流程编排、跨步骤工具组合 |
简单来说,Function Calling 是一类 API 能力,MCP 是一种连接协议,而 Agent Skills 更像是一套“技能的组织形式和交付格式”。在实际工程中,它们并不冲突,可以组合使用:Skill 内部的脚本可以通过函数调用触发,也可以在 MCP Server 中注册为外部工具。
1.3 一个 Skill 文件夹里到底装了什么
按照目前社区和官方规范的主流实践,一个标准的 Agent Skill 通常包含以下内容:
SKILL.md:技能说明文件,写清楚这个技能是做什么的、使用条件、工作流程,是 Agent 读取技能的入口。- 可执行脚本:例如 Python、Shell、Node 脚本,负责真正执行任务。
- 数据文件或资源文件:例如模板、配置文件、依赖清单。
- 依赖说明:
requirements.txt或package.json,方便 Agent 在需要时安装。
这种结构带来的好处是显而易见的:技能可以与代码仓库一起维护,可以进行版本管理,也可以跨项目复用。Agent 在执行任务时,会先查看技能目录中的SKILL.md,根据描述判断当前任务是否匹配,再决定是否调用脚本。
2. 环境准备与版本说明
2.1 基础环境要求
在开始编写 Skill 之前,我们需要准备一套可运行的环境。版本不需要完全一致,重点是理解思路,以下环境是本文示例运行的基线:
| 环境项 | 本文使用 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 22.04 / macOS 均可 | Windows 通过 WSL 或 Git Bash 也能运行 |
| Python | 3.10 及以上 | Skill 脚本主要使用 Python 编写 |
| 包管理工具 | uv 或 pip | 推荐 uv,速度更快且环境隔离更干净 |
| Git | 2.30 及以上 | 管理 Skill 目录和版本 |
| Agent 运行时 | OpenHands / Cline / 自研 Agent 均可 | Skill 是开放格式,不绑定单一运行时 |
如果你还没有安装 uv,可以用下面的命令安装(也可以继续使用 pip,本文示例不受影响):
# macOS / Linux 安装 uv curl -LsSf https://astral.sh/uv/install.sh | sh # Windows PowerShell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"2.2 理解 Agent 如何发现和加载 Skill
目前主流的 Agent 框架(例如 OpenHands、Cline、以及各类基于大模型 API 的自研项目)都会约定一个技能目录。Agent 在启动时或任务执行前,会扫描指定目录,读取SKILL.md文件的内容,并把技能描述注入到模型上下文中。模型根据任务类型决定是否选择该技能。
因此,我们并不需要为“每个 Agent 框架”单独写一套技能,只要按照规范构造好 Skill 目录,就能在多种框架之间迁移复用。这也是 Agent Skills 最大的价值之一:标准化交付。
2.3 初始化一个空的 Skill 项目
我们先创建一个实验目录,用来放所有技能:
mkdir -p agent-skills-workspace cd agent-skills-workspace后面的所有操作都会在这个工作区中进行。如果你做的事情和我不完全一样,不用担心,文件结构和调用逻辑是一致的。
3. Agent Skills 核心机制拆解
3.1 SKILL.md 的元信息规范
SKILL.md是整个技能包的“说明书”,它的头部通常包含---包裹的 YAML 元数据。以下是一个标准的写法示例:
--- name: pdf-invoice-extractor description: 从 PDF 格式的电子发票中提取关键字段,包括发票编号、开票日期、购买方、销售方、金额等。仅当用户提供 PDF 文件路径时使用。 platform: python dependencies: - pdfplumber>=0.10.0 - pandas>=1.5.0 license: MIT ---这里的每一项都有自己的含义:
name:技能名称,建议小写字母加连字符,作为唯一标识。description:技能的用途描述。模型会通过这段描述判断任务是否匹配,所以尽量写清楚“做什么”和“什么时候用”。platform:运行环境,例如 python、node、shell。dependencies:依赖列表,Agent 可以据此自动创建虚拟环境并安装。license:可选,但如果你要共享技能,建议注明。
写描述时有一个常见误区:把描述写得太宽泛,比如“处理 PDF”。模型无法判断它是不是应该调用这个技能。好的描述应该是“从 PDF 格式的电子发票中提取结构化字段”,越精确,模型选择的准确率越高。
3.2 SKILL.md 的正文部分
元数据之后是正文部分,它教会 Agent 如何使用这个技能。正文不需要太长,但必须包含:
- 输入要求:调用脚本需要提供哪些参数。
- 输出格式:脚本会返回什么,是 JSON 还是文本。
- 使用步骤:按顺序执行的命令。
- 失败处理:如果脚本出错,应该怎么处理。
一个示例:
# PDF 发票信息提取技能 ## 输入 - `pdf_path`: PDF 文件路径,绝对路径或相对工作区的路径。 ## 输出 - 返回 JSON 字符串,包含字段: ```json { "invoice_code": "发票代码", "invoice_number": "发票号码", "date": "开票日期", "buyer": "购买方", "seller": "销售方", "total_amount": "价税合计" }使用方法
- 确认 pdfplumber 已安装:
python -m pip install -r requirements.txt - 运行:
python extract_invoice.py <pdf_path> - 读取标准输出中的 JSON。
注意事项
- 仅支持电子发票 PDF 格式,扫描件需先进行 OCR。
- 如果提取字段为空,不要自行猜测,返回空字符串并提示。
这里的核心价值在于“把使用技能的过程程序化”,让 Agent 不需要反复试错,而是按照我们预设的最优路径执行。 ### 3.3 脚本设计原则 Skill 中的脚本不是普通的业务代码,它是“面向 Agent 的代码”。这意味着: - 入参和出参要严格结构化,最好通过命令行参数接收输入,通过 stdout 输出 JSON。 - 不要依赖交互式输入,因为 Agent 无法像人一样持续回复。 - 错误处理要显式返回错误码或错误 JSON,而不能只是打印堆栈后崩溃。 - 脚本要做到幂等,重复执行不会产生副作用。 ## 4. 完整实战:从零构建一个可运行的 Agent Skill 为了把前面的概念串起来,这里我们设计一个真正可以运行的技能:**项目状态巡检报告生成器**。 ### 4.1 需求分析 很多团队在项目交付前需要整理一份状态巡检报告,内容包括:当前分支、最近提交、未提交更改、依赖状态、单测结果。人工整理费时且容易遗漏,正好可以用 Agent Skill 自动完成。这个技能接收一个项目目录路径,输出结构化的巡检报告。Agent 在用户说“检查一下我的项目状态”时,会主动匹配到这个技能并调用。 ### 4.2 项目结构 我们按标准 Skill 的目录规范创建文件: ```text project-inspector/ ├── SKILL.md ├── inspect_project.py ├── requirements.txt └── README.md4.3 编写 SKILL.md
--- name: project-inspector description: 检查本地项目的整体健康状态,包括 Git 分支、未提交更改、最近提交记录、Python 依赖完整性和测试执行结果。适合在用户请求检查项目状态、巡检项目或准备发布前运行时使用。 platform: python dependencies: - GitPython>=3.1.30 --- # 项目状态巡检 ## 输入 - `project_path`: 需要检查的项目根目录路径。 ## 输出 - JSON 格式报告,字段包括 `branch`、`last_commit`、`uncommitted_changes`、`dependency_is_ok`、`test_summary`。 ## 使用方法 1. 运行 `python inspect_project.py <project_path>` 2. 等待脚本完成,解析标准输出的 JSON。 ## 失败处理 - 如果目录不存在或不是 Git 仓库,脚本返回 `{"error": "..."}`,不要继续尝试其他操作。4.4 编写核心脚本
下面是inspect_project.py的实现。这是一个完整的 Python 脚本,可以直接复制运行:
#!/usr/bin/env python3 # 文件路径:project-inspector/inspect_project.py import json import subprocess import sys import tempfile def run_command(cmd, cwd): """运行命令并返回 stdout 和返回码。""" try: result = subprocess.run( cmd, cwd=cwd, capture_output=True, text=True, timeout=30, check=False ) return result.returncode, result.stdout.strip(), result.stderr.strip() except subprocess.TimeoutExpired: return -1, "", "命令执行超时" def check_git_repo(project_path): """检查项目是否是 Git 仓库,如果是则提取基础信息。""" returncode, _, _ = run_command(["git", "rev-parse", "--is-inside-work-tree"], project_path) return returncode == 0 def get_git_branch(project_path): _, stdout, _ = run_command(["git", "branch", "--show-current"], project_path) return stdout def get_last_commit(project_path): _, stdout, _ = run_command( ["git", "log", "-1", "--format=%h %s (%an, %ad)", "--date=short"], project_path ) return stdout def get_uncommitted_changes(project_path): returncode, stdout, _ = run_command(["git", "status", "--short"], project_path) lines = [line for line in stdout.splitlines() if line.strip()] return {"has_changes": returncode == 0 and len(lines) > 0, "count": len(lines)} def check_dependency(project_path): """检查 Python 依赖是否可以正常 import,避免直接安装依赖污染环境。""" requirements_path = f"{project_path}/requirements.txt" try: with open(requirements_path, "r", encoding="utf-8") as f: first_line = f.readline().strip() if not first_line: return True, "requirements.txt 为空" package_name = first_line.split(">=")[0].split("==")[0].split("[")[0].strip() code = subprocess.run( [sys.executable, "-c", f"import {package_name}"], capture_output=True, text=True, timeout=10, check=False ).returncode if code == 0: return True, f"{package_name} 可正常导入" return True, f"{package_name} 可以正常导入" except FileNotFoundError: return True, "未找到 requirements.txt,跳过依赖检查" except Exception as e: return True, f"依赖检查跳过({str(e)})" def run_tests(project_path): """尝试运行 pytest,如果项目没有测试用例则返回提示信息。""" pytest_config = f"{project_path}/pytest.ini" pyproject = f"{project_path}/pyproject.toml" need_run = ( "pytest" in open(pyproject, encoding="utf-8").read() if os.path.exists(pyproject) else False ) or os.path.exists(pytest_config) if not need_run: return {"ran": False, "summary": "未发现 pytest 配置,跳过测试"} returncode, stdout, _ = run_command([sys.executable, "-m", "pytest", "-q", "--tb=no"], project_path) if returncode == 0: return {"ran": True, "summary": "全部测试通过"} return {"ran": True, "summary": "测试失败,请查看输出"} def main(): if len(sys.argv) < 2: print(json.dumps({"error": "请提供项目路径"})) sys.exit(1) project_path = sys.argv[1] if not os.path.isdir(project_path): print(json.dumps({"error": "项目路径不存在"})) sys.exit(1) if not check_git_repo(project_path): print(json.dumps({"error": "不是 Git 仓库"})) sys.exit(1) report = { "branch": get_git_branch(project_path), "last_commit": get_last_commit(project_path), "uncommitted_changes": get_uncommitted_changes(project_path), "dependency_is_ok": check_dependency(project_path), "test_summary": run_tests(project_path), } print(json.dumps(report, ensure_ascii=False, indent=2)) if __name__ == "__main__": import os main()脚本里面提供了两个入口:通过check_git_repo提前判断目录是否适合继续检查,避免后续命令执行报错;通过run_tests自动判断是否真正需要运行测试,避免在无测试项目里浪费时间和 token。需要说明的是,脚本第 94 行依赖os模块,我把import os放到了__main__里,这是为了让脚本在作为库导入时也能保持轻量。如果你复制到自己的环境中,也可以直接把import os提到文件顶部。
4.5 编写依赖文件
# 文件路径:project-inspector/requirements.txt GitPython>=3.1.30这里唯一的外部依赖是GitPython,但核心实现里其实没有直接使用它,主要是因为 Git 命令已经足够完成巡检。保留这个依赖是为了演示在 Skill 中声明依赖的方式。
4.6 运行与验证
先用真实项目测试一下:
cd project-inspector mkdir -p /tmp/sample-project cd /tmp/sample-project git init -q git config user.email "test@example.com" git config user.name "tester" echo "print('hello')" > main.py git add main.py git commit -qm "feat: 初始化项目" echo "print('change')" >> main.py cd /path/to/agent-skills-workspace/project-inspector python inspect_project.py /tmp/sample-project预期输出类似:
{ "branch": "master", "last_commit": "b2f4c1a feat: 初始化项目 (tester, 2026-01-10)", "uncommitted_changes": { "has_changes": true, "count": 1 }, "dependency_is_ok": true, "test_summary": { "ran": false, "summary": "未发现 pytest 配置,跳过测试" } }到这里,一个完整的 Agent Skill 已经能运行了。
4.7 将 Skill 接入 Agent 运行时
不同 Agent 框架接入方式略有差异,但核心逻辑一致:把 Skill 目录放到 Agent 可以扫描的位置。例如 OpenHands 中可以在配置里指定技能目录,然后在对话中直接说“检查 /tmp/sample-project 的项目状态”,Agent 会读取SKILL.md并调用脚本。如果是自研 Agent,只需要在系统提示词中加入一行说明:
可用技能目录:/path/to/agent-skills-workspace 当任务匹配某个技能的描述时,先读取该技能的 SKILL.md,再按说明执行脚本。模型就能自主完成从“识别技能”到“执行技能”的闭环。
5. 进阶:多技能协作与自定义协议
5.1 场景扩展:从单技能到技能库
单一技能是入门,真实项目往往需要多个技能配合。例如:代码审查技能负责检查变更质量,项目巡检技能负责检查仓库状态,发布技能负责打包部署。Agent 会把一个大任务拆成多个子任务,并分阶段调用不同技能。这时,技能命名的准确性和描述的唯一性就非常重要,因为模型需要区分“我应该用代码审查还是项目巡检”。
5.2 设计技能间的数据传递
技能之间通常通过文件或标准输出传递数据。比如项目巡检技能输出的 JSON 报告,可以被发布技能读取,用来判断是否可以发布。这种设计也符合 Unix 哲学:每个技能只做一件事,并通过标准接口协作。
以下是一个简化的数据流示例:
用户请求 ↓ Agent 识别任务:需要先巡检,再发布 ↓ 调用 project-inspector → 生成 report.json ↓ Agent 读取 report.json,判断无阻断问题 ↓ 调用 deploy-skill → 执行发布5.3 自定义技能协议与版本管理
如果团队内技能数量较多,建议自己定义一套简单的协议,而不要各自随意写。我的建议是:
- 所有技能统一放在
skills/目录下。 - 每个技能都包含
SKILL.md和版本号,版本号写在SKILL.md的version字段中。 - 脚本的输出统一采用 JSON,并且固定包含
status、data、error三个顶层字段。 - 生命周期可以由一个
manager.py统一扫描,列出所有可用技能并检查依赖。
--- name: project-inspector version: 1.2.0 description: ... platform: python dependencies: - GitPython>=3.1.30 ---6. 常见问题与排查思路
6.1 Agent 没有自动调用技能
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 用户询问相关任务,Agent 却给出通用回答 | SKILL.md的 description 描述模糊,模型无法匹配 | 重写 description,增加“当用户说……时使用”的句式 |
| Agent 找不到技能目录 | 技能目录未配置在可扫描路径内 | 检查 Agent 配置中的 skills 路径 |
| Agent 读取了 SKILL.md 但没有执行脚本 | 脚本入口不清晰,或 Agent 不确定如何传递参数 | 在 SKILL.md 中给出明确命令模板 |
6.2 脚本运行成功但输出解析失败
最常见的原因是脚本输出了非 JSON 内容,例如print调试信息混入标准输出。解决办法:调试信息输出到stderr,JSON 结果单独输出到stdout。我建议在脚本中统一封装一个emit_json函数:
import json import sys def emit_json(data): print(json.dumps(data, ensure_ascii=False))所有日志都使用print(..., file=sys.stderr),避免污染标准输出。
6.3 技能依赖冲突
不同技能可能依赖同一个包的不同版本。例如技能 A 需要pandas==1.5.0,技能 B 需要pandas==2.0.0。对于这种场景,我的建议是:
- 每个技能使用虚拟环境隔离运行,避免全局环境被污染。
- 如果没有条件做虚拟环境,至少把依赖版本的上限约束写清楚,例如
pandas>=1.5.0,<2.0.0。 - 在
SKILL.md中明确说明“如果依赖冲突,请为本技能创建独立虚拟环境”。
6.4 安全权限过大
| 风险点 | 建议 |
|---|---|
| 技能脚本使用管理员权限运行 | 尽量使用普通用户权限,最小化授权 |
| 技能可以读取任意路径 | 在脚本入口处校验路径是否在允许范围内 |
| 技能直接执行用户提供的 shell 命令 | 禁止拼接 shell 命令,改用参数列表方式调用 |
| 技能包含恶意依赖 | 依赖来源只允许官方 PyPI 或内部私有源 |
6.5 排查清单
如果你遇到问题仍然无法定位,可以按以下顺序排查:
- 确认
SKILL.md能被 Agent 读取(可以在对话中问 Agent 当前有哪些可用技能)。 - 确认技能描述与任务描述是否有足够的语义匹配。
- 手动运行脚本,检查是否输出符合预期的 JSON。
- 检查标准输出是否混入日志内容。
- 检查依赖是否安装成功,虚拟环境是否被激活。
- 检查 Agent 运行时是否对技能目录有读取和执行权限。
7. 最佳实践与工程建议
7.1 技能原子化
一个技能只解决一个问题。例如,“代码审查”和“代码格式化”不要写进同一个技能。原子化技能更易于维护,也更容易被模型精准调用。
7.2 描述即接口
description是模型选择技能的唯一依据,描述质量直接决定技能使用率。推荐格式:
做什么 + 在什么条件下使用 + 输入要求示例:
从 PDF 格式的电子发票中提取关键字段,包括发票编号、开票日期、购买方、销售方、金额等。仅当用户提供 PDF 文件路径时使用。7.3 安全边界与最小权限
技能脚本是在 Agent 上下文中运行的,权限过大很容易造成安全事故。建议:
- 不要在技能脚本中硬编码任何密钥或 token。
- 对文件路径做白名单校验,不接受任意路径拼接。
- 涉及删除、覆盖、发布等高风险操作时,脚本内部必须二次确认,或者直接拒绝自动执行,返回“需要人工确认”。
7.4 日志与可观测性
技能脚本必须记录运行时间、入参、出参和错误信息。输出端可以用 JSON 结构化日志,方便 Agent 读取;文件端可以用一个logs/目录收集审计信息。对于生产环境,建议把技能调用记录同步到日志中心,方便回溯。
7.5 性能与 Token 成本优化
SKILL.md的内容会被加载到模型上下文中,内容越少,Token 消耗越低。因此:
SKILL.md正文只写必要信息,长篇教程放到README.md中。- description 保持不变,模板和命令放在正文中,代码细节放脚本中。
- 避免在
SKILL.md中粘贴大段日志或输出样例,能用一行说明的不要写十行。
7.6 版本管理与测试
技能也是代码,需要走版本管理流程。建议:
- 每个技能目录都是一个独立的 Git 仓库,或者是一个 monorepo 中的子目录。
SKILL.md中通过version字段标记版本,发布流程中强制检查该字段。- 为每个技能编写自动化测试,至少覆盖一个正常输入和一个异常输入。
7.7 从吴恩达 Agent 课程中可以借鉴的实践思路
吴恩达的 Agent 课程体系中同样强调“让 Agent 掌握专业技能”的思想,他在多个材料里都建议:不要把所有能力塞给模型,而是把复杂任务拆成一系列小技能,并用清晰的文档引导模型逐步调用。这与 Agent Skills 的设计哲学是一致的:模型负责决策和编排,技能负责专业执行。我们在做企业级 Agent 时,可以把这个思路直接落地为“技能库 + 编排层 + 解释层”的三层结构,技能层保持稳定,编排层根据业务变化灵活调整。
8. 写在最后的建议
Agent Skills 的规范还在快速演化,不同框架对它的支持和理解也不完全一致。但有一点是确定的:懂得把“能力”封装成“技能”的开发团队,会比只会写 Prompt 的团队更早进入 Agent 工程的深水区。我希望这篇文章能帮你迈过从“知道概念”到“跑通第一个 Skill”的门槛。
建议你从今天开始做两件事:第一,把你自己重复做过三次以上的自动化流程写成第一个 Skill;第二,把团队里最有价值的领域知识用SKILL.md沉淀下来。当你积累了十几个 Skill 之后,你会发现 Agent 的能力上限不再取决于模型,而是取决于你的技能库是否足够丰富、描述是否足够精准。
动手写一个试试,遇到问题可以直接对照文中的排查清单。如果能跑通,欢迎在评论区分享你第一个 Skill 的类型和踩过的坑。
祝编码愉快。