设计 Skill 系统,这 3 个坑我替你踩过了

前言

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 执行时是否开辟独立子上下文
  • 其中 namedescription 决定了 Agent 什么时候调用它。

  • allowed-tools 可能很多人会理解为只允许使用什么工具,实际上是赋予 Agent 执行这些工具的权限,无需你再盯着点同意。

  • disallowed-tools 则是字面意思,硬性禁止某些工具/命令,即使模型想调也会被系统拦截

    这里有个坑:此次禁用的工具,需要在下一个回合(turn)恢复,否则这些工具会在这个会话里一直被禁用,影响后续操作。。

(turn 是什么,后面文章会专门讲,这里先按这个理解:turn 就是一次会话回合。)

css 复制代码
[第 1 回合] 你发消息 → 模型调用某 Skill → Skill 进入 active
            → disallowed-tools 里的工具被从可用工具池里移除
            → 这一回合里,模型根本"看不到" Write / Edit,想调都调不了

[第 2 回合] 你发下一条消息 → 限制清除 → Write / Edit 恢复可用
  • disallowed-toolsallowed-tools 一样,都是临时作用域。只在调用 Skill 的那个回合预批准

  • model 则是可以指定这个 Skill 只在特定模型下启用,别的模型加载不到。典型用法有两种:某个 Skill 依赖长上下文或复杂推理,小模型扛不住,就限定它只用大模型,免得跑崩;反过来,简单的 Skill 也可以限定用小模型,省钱。

  • contextagent 则是配合用的:context 决定这个 Skill 是在主对话里跑,还是开辟一个独立子上下文单独跑agent 决定用哪个子 Agent 来执行 。典型场景是:某个 Skill 过程很脏------读几十个文件、跑一堆命令------但你只关心最终结果。这时设 context: fork,让它跑在独立上下文里、不污染主对话,再指定一个子 Agent 去干这票重活。

  • metadata 则是 用来塞任意自定义键值 的地方,模型一般不读它。通常是给团队的管理、审计、成本核算用的------比如记 ownertagscost-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]
---

(示例里的 licensedisable-model-invocationversion 属于官方标准里的其他字段,基础版可以先不处理。)

结论:如果你实现的是基础 Skill 系统,只处理 namedescription 就够了;后续要扩展,再基于上面这些字段往上加。

二、深入了解 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 文件里面的内容,而是三样东西:

  1. activation(激活标记)<command-name>{skill_name}</command-name>。这是给系统看的------表示"这个 Skill 已被调用",用来做去重和状态追踪(下文作解释),也方便前端展示调用事件。
  2. base_dir(根目录) :Skill 所在的目录。
  3. 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 这个集合,同时干了"去重"和"状态追踪"两件事------判断重复靠它,知道该恢复谁也靠它。

相关推荐
这个DBA有点耶1 小时前
当数据库从“存储”走向“决策”:金仓数据库的融合架构之路
数据库·架构·aigc
爱勇宝2 小时前
客户只想看个页面,我却做了一个静态演示发布系统
前端·javascript·后端
wangruofeng2 小时前
新 Mac 到手先装什么:AI Builder 的 44 款工具,基础层照抄、场景层按需
github·aigc·ai编程
一个有理想的摸鱼选手2 小时前
(四)路书Agnet-综合天气距离交通节奏等多因素来编排旅行路线
前端·后端·gis
神奇的程序员2 小时前
在这个独属于ai的时代,我终究是被裁员了
前端·后端·面试
云边有个稻草人2 小时前
从单机工具到“云+端+服务”:KDMS数据库迁移工具如何支撑大型信创项目协同作战
后端
李广坤2 小时前
AgentScope Tool 热更新技术方案:不重启服务,实时管控 Agent 工具
后端·架构
ttwuai2 小时前
Go 后台接入 SSO 后菜单正常但接口 403,怎么排查权限链路?
开发语言·后端·golang
65岁退休Coder3 小时前
LangChain v1.3.4 笔记 - 08 MCP & 相关概念
后端·python·langchain