设计 Skill 系统,这 3 个坑我替你踩过了
设计 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 | 执行时是否开辟独立子上下文 |
其中
name和description决定了 Agent 什么时候调用它。allowed-tools可能很多人会理解为只允许使用什么工具,实际上是赋予 Agent 执行这些工具的权限,无需你再盯着点同意。disallowed-tools则是字面意思,硬性禁止某些工具/命令,即使模型想调也会被系统拦截。这里有个坑:此次禁用的工具,需要在下一个回合(turn)恢复,否则这些工具会在这个会话里一直被禁用,影响后续操作。。
(turn 是什么,后面文章会专门讲,这里先按这个理解:turn 就是一次会话回合。)
[第 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的头部元信息
---name:frontend-ui-engineeringdescription:构建生产级品质的用户界面。在构建或修改面向用户的界面时使用。在创建组件、实现布局、管理状态,或需要输出看起来达到生产级品质而非“AI生成感”时使用。---复杂一点的:
---name:code-reviewdescription:当用户要求审查代码、检查PR、或查找潜在bug时使用。allowed-tools:-Read-Grep-Bash(gitdiff:*)model:claude-sonnet-4-5disable-model-invocation:falselicense:MITversion:1.2.0metadata:scope:projectagents: [backend-bot, reviewer-bot]---(示例里的 license、disable-model-invocation、version 属于官方标准里的其他字段,基础版可以先不处理。)
结论:如果你实现的是基础 Skill 系统,只处理 name 和 description 就够了;后续要扩展,再基于上面这些字段往上加。
二、深入了解 Skill 与 Agent 的交互
1. Agent 如何 调用 Skill
Agent是怎么调用 Skill的 ? 目前有两种主流的做法:
- 单独为Skill设计一个工具,Agent 通过
skill_name加载 Skill
SKILL_TOOL = { "name": "skill", "description": "按 name 加载一个已注册的 Skill,返回它的完整指令。", "parameters": { "skill_name": { "type": "string", "description": "要加载的 Skill 名称", "required": True, }, },}defexecute_skill_tool(skill_name: str) ->str: skill = find_skill_by_name(skill_name) # 在注册表里按 name 找ifnot skill: returnf"未找到 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 正文
READ_FILE_TOOL = { "name": "read_file", "description": "按路径读取文件内容。", "parameters": { "path": { "type": "string", "description": "文件路径", "required": True, }, },}defexecute_read_file_tool(path: str) ->str: return Path(path).read_text(encoding="utf-8") 顺带补充一个小知识:Skill 的组成不只有 SKILL.md 这一个文件。除了 SKILL.md,还可以带知识文档、可执行脚本、静态资源等:
{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,收进注册表中
defregister_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 在上下文里的显示大概长这样:
<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,上下文里还多了份重复内容。
有激活标记 + 去重,系统就能判断:
一句话总结就是:去重 = 防止同一个 Skill 的正文被反复塞进上下文。
状态追踪:记住"现在哪些 Skill 是激活的"
系统需要维护一个"当前激活中的 Skill 列表",因为好几件事都靠它:
- 权限:Skill 激活期间,它的 allowed/disallowed-tools 生效;一旦退出激活,就要把权限收回来。系统得知道"现在该收谁的权限"。
- 前端展示:界面上显示"当前正在执行 code-review",也是从这个列表读的。
- 生命周期:记录 Skill 什么时候进 active、什么时候退出。
一句话总结就是:状态追踪 = 系统维护一张"谁正在激活"的表,用来管权限生效和恢复。
放到你的 Skill 系统实现里,大概就是:
active_skills = set() # 当前激活的 Skill 集合defactivate(skill_name): if skill_name in active_skills: return"已激活,跳过"# 去重 active_skills.add(skill_name) # 状态追踪 apply_permissions(skill)defdeactivate(skill_name): active_skills.discard(skill_name) restore_permissions()active_skills 这个集合,同时干了"去重"和"状态追踪"两件事——判断重复靠它,知道该恢复谁也靠它。
-
08.22
利用AI技术轻松制作高效专业的表格解决方案
-
08.22
掌握PPT制作实用技巧,助你在演示中脱颖而出
-
08.22
提升PPT制作技能的做法与技巧,助你打动观众
-
08.22
OpenClaw版本升级与本地模型Qwen对接操作步骤
-
08.22
Claude Code 六种扩展能力整理小结
-
08.22
DeepSeek Harness 快速上手指南之安装、多模型接入与「自带黑匣子」的会话日志
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏