前言
Skill 是什么?说白了就是一个技能包 ,核心是一个 SKILL.md 文件。Agent 干活的时候,会根据任务需要按需加载对应的 Skill,比如要做 UI 设计、代码审查,就加载相应的那一个。
听起来是不是挺简单?你可能已经打开 AI 编程工具,准备直接丢一句"帮我实现 Skill 机制"过去。
等一下 ,先别急。 Skill 听着简单,但你真的想清楚怎么在 Agent 系统里实现它了吗?先看这几个问题你能不能答上来:
- SKILL.md 里除了 name 和 description,还有什么头部元信息?
- Agent 要用某个 Skill 的时候,如何定位 以及如何调用 Skill 的?
- Skill 去重是否有考虑过?
如果这几个问题你还没想明白,那接下来我就一个一个拆,把整套 Skill 机制的设计讲清楚。
一、认识 SKILL.md:头部元信息
请先查收 Claude Code 官方给的标准Skill头字段:code.claude.com/docs/zh-CN/...
这里我挑几个典型的讲:
| 字段 | 作用 |
|---|---|
name |
展现名称 |
description |
给模型看,决定何时用 |
allowed-tools |
工具白名单 |
disallowed-tools |
工具黑名单 |
model |
限定模型 |
metadata |
自定义键值 |
agent |
指定执行用的 subagent |
context |
执行时是否开辟独立子上下文 |
-
其中
name和description决定了 Agent 什么时候调用它。 -
allowed-tools可能很多人会理解为只允许使用什么工具,实际上是赋予 Agent 执行这些工具的权限,无需你再盯着点同意。 -
disallowed-tools则是字面意思,硬性禁止某些工具/命令,即使模型想调也会被系统拦截 。这里有个坑:此次禁用的工具,需要在下一个回合(turn)恢复,否则这些工具会在这个会话里一直被禁用,影响后续操作。。
(turn 是什么,后面文章会专门讲,这里先按这个理解:turn 就是一次会话回合。)
css
[第 1 回合] 你发消息 → 模型调用某 Skill → Skill 进入 active
→ disallowed-tools 里的工具被从可用工具池里移除
→ 这一回合里,模型根本"看不到" Write / Edit,想调都调不了
[第 2 回合] 你发下一条消息 → 限制清除 → Write / Edit 恢复可用
-
disallowed-tools和allowed-tools一样,都是临时作用域。只在调用 Skill 的那个回合预批准 -
model则是可以指定这个 Skill 只在特定模型下启用,别的模型加载不到。典型用法有两种:某个 Skill 依赖长上下文或复杂推理,小模型扛不住,就限定它只用大模型,免得跑崩;反过来,简单的 Skill 也可以限定用小模型,省钱。 -
context和agent则是配合用的:context决定这个 Skill 是在主对话里跑,还是开辟一个独立子上下文单独跑 ,agent决定用哪个子 Agent 来执行 。典型场景是:某个 Skill 过程很脏------读几十个文件、跑一堆命令------但你只关心最终结果。这时设context: fork,让它跑在独立上下文里、不污染主对话,再指定一个子 Agent 去干这票重活。 -
metadata则是 用来塞任意自定义键值 的地方,模型一般不读它。通常是给团队的管理、审计、成本核算用的------比如记owner、tags、cost-center,Skill 管理平台扫目录时可以按这些字段分类统计。它不影响 Agent 怎么跑,只影响你怎么管。
Skill案例
简单场景,同时也是大多数Skill的头部元信息
yaml
---
name: frontend-ui-engineering
description: 构建生产级品质的用户界面。在构建或修改面向用户的界面时使用。在创建组件、实现布局、管理状态,或需要输出看起来达到生产级品质而非"AI 生成感"时使用。
---
复杂一点的:
yaml
---
name: code-review
description: 当用户要求审查代码、检查 PR、或查找潜在 bug 时使用。
allowed-tools:
- Read
- Grep
- Bash(git diff:*)
model: claude-sonnet-4-5
disable-model-invocation: false
license: MIT
version: 1.2.0
metadata:
scope: project
agents: [backend-bot, reviewer-bot]
---
(示例里的 license、disable-model-invocation、version 属于官方标准里的其他字段,基础版可以先不处理。)
结论:如果你实现的是基础 Skill 系统,只处理 name 和 description 就够了;后续要扩展,再基于上面这些字段往上加。
二、深入了解 Skill 与 Agent 的交互
1. Agent 如何 调用 Skill
Agent是怎么调用 Skill的 ? 目前有两种主流的做法:
- 单独为Skill设计一个工具,Agent 通过
skill_name加载 Skill
python
SKILL_TOOL = {
"name": "skill",
"description": "按 name 加载一个已注册的 Skill,返回它的完整指令。",
"parameters": {
"skill_name": {
"type": "string",
"description": "要加载的 Skill 名称",
"required": True,
},
},
}
def execute_skill_tool(skill_name: str) -> str:
skill = find_skill_by_name(skill_name) # 在注册表里按 name 找
if not skill:
return f"未找到 Skill: {skill_name}"
body = load_body(skill["path"]) # 正文
apply_permissions(skill["meta"]) # 应用 allowed/disallowed-tools
# 返回三件套,正文作为 tool result 注入上下文
return {
"activation": f"<command-name>{skill_name}</command-name>", # 激活标记
"base_dir": skill["base_dir"], # 根目录,正文里的相对路径靠它
"body": body, # 正文指令
}
注意这个返回值不只是 Skill.md 文件里面的内容,而是三样东西:
- activation(激活标记) :
<command-name>{skill_name}</command-name>。这是给系统看的------表示"这个 Skill 已被调用",用来做去重和状态追踪(下文作解释),也方便前端展示调用事件。 - base_dir(根目录) :Skill 所在的目录。
- body(正文) :SKILL.md 正文内容。
- 通过 ReadFile 工具根据路径读取 SKILL.md 正文
python
READ_FILE_TOOL = {
"name": "read_file",
"description": "按路径读取文件内容。",
"parameters": {
"path": {
"type": "string",
"description": "文件路径",
"required": True,
},
},
}
def execute_read_file_tool(path: str) -> str:
return Path(path).read_text(encoding="utf-8")
SKILL_TOOL能把元信息字段都利用起来------细粒度权限、执行上下文、激活标记,适合复杂 Skill 系统;ReadFile只能读全文,简单,够前期用
顺带补充一个小知识:Skill 的组成不只有 SKILL.md 这一个文件。除了 SKILL.md,还可以带知识文档、可执行脚本、静态资源等:
bash
{skill_name}/
├── SKILL.md # 必填:入口(YAML frontmatter + Markdown 指令)
├── scripts/ # 可选:可执行脚本(Python / Bash)
├── references/ # 可选:长文档、规范、示例
└── assets/ # 可选:模板、图标、字体等静态资源
所以调用Skill的本质其实是工具调用,使用 读工具 读SKILL.md 或者其他知识文件,使用 Bash 工具执行脚本。
不过在Codex中我倒是发现其内部使用 PowerShell 的 cat 命令获取SKILL.md。

Claude Code 就偏向使用 SKILL_TOOL 完成Skill的加载
2. Agent 如何 定位 Skill
第一步系统首先扫一遍指定目录,找到所有 skill文件夹下的SKILL.md,只读头部的 name 和 description,收进注册表中
python
def register_skills(skill_dir: str) -> list[dict]:
skills = []
for path in Path(skill_dir).rglob("SKILL.md"):
meta = parse_frontmatter(path) # 只读头部 name 和 description
skills.append({
"name": meta["name"],
"description": meta["description"],
"path": str(path),
})
return skills
接着把这些信息注入系统提示词。两种工具对应的注入内容不一样:
- 用 SKILL_TOOL 的方式:只注入 name 和 description
- 用 ReadFile 的方式:除了 name 和 description,还要把 SKILL.md 的路径也注入,让模型知道去哪读
Skill 在上下文里的显示大概长这样:
xml
<available_skills>
<skill>
<name>pdf</name>
<description>Comprehensive PDF manipulation toolkit for extracting text and tables, merging/splitting documents, and handling forms.</description>
<path>/absolute/path/to/pdf/SKILL.md</path>
</skill>
</available_skills>
(<path> 是给 ReadFile 方式定位文件用的;如果是 Skill 工具方式,路径留在注册表里,不暴露给模型。)
注意,这里只放了 name 和 description,没放正文。这是整个 Skill 系统里最关键的一个设计:注册要轻。 如果这一步就把正文全塞进去,后面的"匹配"和"加载"就没意义了,上下文也会被无关内容占满。
三、Skill 的激活标记:去重与状态追踪
去重:别把同一个 Skill 重复加载
一次任务里,模型可能反复想用同一个 Skill。比如用户连续问两轮"再帮我审查一下代码",模型可能两次都想调 code-review。
如果没有去重,code-review 的正文会被注入两次,白白浪费 token,上下文里还多了份重复内容。
有激活标记 + 去重,系统就能判断:
code-review已经激活过了,这次不重复注入,直接复用。
一句话总结就是:去重 = 防止同一个 Skill 的正文被反复塞进上下文。
状态追踪:记住"现在哪些 Skill 是激活的"
系统需要维护一个"当前激活中的 Skill 列表",因为好几件事都靠它:
- 权限:Skill 激活期间,它的 allowed/disallowed-tools 生效;一旦退出激活,就要把权限收回来。系统得知道"现在该收谁的权限"。
- 前端展示:界面上显示"当前正在执行 code-review",也是从这个列表读的。
- 生命周期:记录 Skill 什么时候进 active、什么时候退出。
一句话总结就是:状态追踪 = 系统维护一张"谁正在激活"的表,用来管权限生效和恢复。
放到你的 Skill 系统实现里,大概就是:
python
active_skills = set() # 当前激活的 Skill 集合
def activate(skill_name):
if skill_name in active_skills:
return "已激活,跳过" # 去重
active_skills.add(skill_name) # 状态追踪
apply_permissions(skill)
def deactivate(skill_name):
active_skills.discard(skill_name)
restore_permissions()
active_skills 这个集合,同时干了"去重"和"状态追踪"两件事------判断重复靠它,知道该恢复谁也靠它。