一、什么是 Skill:从「硬编码 Prompt」到「可组合的 SOP 包」
在没有 Skill 之前,Agent 工程里最常见的「领域能力复用」就是把一段固定的 Prompt 拼到 System Message 末尾、或者封装成一个 Go/Python 工具函数。这种方式的问题很明显:
-
上下文爆炸:每个领域 SOP 几百行,10+ 领域一次性全部塞进 system prompt,首轮 token 就爆了。
-
模型选择刚性:文档解析、代码生成、数据清洗三种工作流可能各自需要不同的模型,但 System Prompt 级别的拼法无法做到"某段流程切模型"。
-
版本管理混乱:Prompt 写在 Go 字符串里,改一行 SOP = 重新编译发布;跨项目复制粘贴 = 分叉漂移。
Skill(以 SKILL.md 为载体)是 2025 年由 Anthropic 确立、并被 CloudWeGo/eino 等主流 Agent 框架实现的一种开放式 AI 代理构建标准。它本质上是 「一份标准化操作手册(SOP)」+「一组执行策略(元数据)」 的打包体,由框架在运行时按需加载。
一份典型的 Skill 目录结构:
text
skills/
├── pdf-batch/
│ ├── SKILL.md ← 技能定义(YAML FrontMatter + Markdown Body)
│ ├── scripts/
│ │ └── extract.py ← Skill 正文里引用的辅助脚本
│ └── examples/
│ └── config.yaml ← Skill 正文里提到的参考文件
└── web-research/
└── SKILL.md
SKILL.md 采用 YAML FrontMatter + Markdown Body 双段结构:
markdown
--- name: pdf-batch description: 批量 PDF 提取文本并重命名。当用户提到"PDF 提取、批量 PDF2txt、扫描目录 PDF"时使用;单个小 PDF 文件不要使用。 context: fork # 执行模式:inline | fork | fork_with_context agent: worker # 交给哪个子 Agent 执行(仅 fork 模式生效) model: gpt-4o # 执行时切换的模型(仅 fork / inline 模式生效) --- # PDF 批量处理工作流 ## 前置检查 先用 ls 确认输入目录存在;不存在先跟用户确认。 ## 步骤 1:枚举 ls {input_dir}/*.pdf → 拿到文件列表;>50 个建议先分组并发 ## 步骤 2:提取 调用 {BaseDir}/scripts/extract.py --input <abs_path> --output <out_dir> 失败回退:python + pypdf 直接实现 ## 步骤 3:命名 从首页提取 Author / Year / Title,格式 {Author}-{Year}-{Title}.txt 重命名冲突先 + (1),不要覆盖用户已有文件
Skill 的双重身份:
-
对框架来说 :它是
{Name, Description, Context, Agent, Model} + Content + BaseDir的结构化数据。 -
对模型来说 :它同时是 「名片」 (
Name+Description做召回)和 「操作手册」 (Content指导后续执行步骤)。
二、完整执行流程:5 个节点串联
抛开所有技术术语,Skill 的完整运行流程可以拆解为 5 个核心节点:
节点 1:启动时扫目录,但只看标题
Agent 启动时,扫描指定的 Skill 存放目录。只做一件事 :把每个 Skill 文件夹的 名字 和 一句话介绍 摘出来,记在一个列表里。绝对不看 那些几百行的详细操作步骤。结果:把这个"名字+介绍"列表放进 Agent 的初始系统提示里,同时在工具箱里只放 1 个名叫 skill 的工具。
节点 2:根据用户问题,选一个匹配的
用户发来一个问题(比如"帮我把这些 PDF 合并一下")。Agent 把这个问题和自己系统提示里的"名字+介绍"列表做比对。如果发现某个 Skill 的"介绍"和用户问题对得上,Agent 就调用那唯一的 skill 工具,并把选中的 Skill 名字作为参数传进去。
节点 3:真正去读操作手册,并决定谁来做
skill 工具被调用后:① 去硬盘里找到对应的 Skill 文件夹,把完整的 SKILL.md 操作手册读出来;② 检查手册头部标注的 context 类型:
-
inline:把读出来的完整操作手册原文,作为结果返回给当前 Agent。 -
fork:新建一个独立的子 Agent,把操作手册丢给它,让它在后台从头做到尾。等子 Agent 做完后,只把最终结果压缩成一段话返回。 -
fork_with_context:同上,但子 Agent 会携带父对话历史。
节点 4:照着操作手册,一步步执行(仅 inline 模式)
当前 Agent 拿到节点 3 返回的完整操作手册(这是它第一次 看到具体步骤)。于是它照着手册里的描述,一步步调用各种基础工具(运行脚本、读文件、写文件等),把手册上的每个步骤都执行完。
节点 5:整理结果,回复用户
Agent 把执行完的所有结果汇总,整理成一段通顺的自然语言,发送给用户。
三、四种注入位置:Skill 向 Agent 传递信息的 4 个通道
| 注入位置 | 注入内容 | 发生时机 | 作用 |
|---|---|---|---|
| ① System Prompt | <available_skills> 列表(所有 Skill 的 Name + Description) |
Agent 启动时(节点 1) | 让模型知道"有哪些技能可用",做召回决策 |
| ② Tool Schema | 1 个名为 skill 的工具,参数为 {"skill": string} |
Agent 启动时(节点 1) | 让模型能够通过调用工具来触发 Skill 加载 |
| ③ ToolMessage | SKILL.md 的完整正文(操作手册) |
skill 工具被调用后(节点 3,inline 模式) |
让模型第一次看到完整步骤,开始执行 |
| ④ 子 Agent 初始消息 | SKILL.md 的完整正文 + 父对话历史(仅 fork* 模式) |
skill 工具被调用后(节点 3,fork* 模式) |
让子 Agent 直接带着操作手册开始干活 |
注意一个反直觉事实 :不管你有 10 个还是 100 个 Skill,在 Tools 集合里 都只会出现一个工具 (即②中的 skill 工具)。这是「渐进式披露」的前提条件。
四、三种执行模式:context 字段的三种取值
SKILL.md 头部的 context 字段控制"拿到操作手册后,谁来执行、怎么执行":
| 模式 | 执行方式 | 上下文隔离? | 适用场景 |
|---|---|---|---|
inline(默认) |
当前 Agent 自己读手册,自己一步步执行 | 不隔离,所有中间步骤都在主对话里 | SOP 需要和用户交互、需要结合其他上下文 |
fork |
新建独立子 Agent,手册丢给它,子 Agent 从头做到尾,只返回最终结果 | 完全隔离,父 Agent 看不到中间过程 | SOP 很重、步骤多、会输出大量中间信息,避免撑爆主上下文 |
fork_with_context |
新建独立子 Agent,但会把父对话历史也一并复制给它 | 隔离但带上下文 | 子流程需要知道用户的历史输入(如之前给的路径) |
五、渐进式披露:解决 Context Bloat 的核心设计
5.1 为什么要「渐进」
如果不做渐进式披露,支持 100 个 Skill 的系统首轮 System Prompt 就是 100 × 几百行 = 几万 token 起步,同时「Description 太泛 → 模型误选」「Description 太细 → Token 爆炸」的 trade-off 永远无解。
Skill 体系的解法非常克制:只在合适的层级提供合适粒度的信息。
5.2 四层披露金字塔
| 层级 | 信息内容 | 展示时机 | 消费方 |
|---|---|---|---|
| L1 名片 | 仅 Name + 1-3 行 Description |
每个模型首轮推理都能看到(节点 1) | 模型做召回决策;Token 开销极小 |
| L2 阻塞语义 | 强制约束"命中时必须先调 skill 工具" |
首轮 System + ToolInfo 同时(节点 1) | 约束模型不要跳过 Skill 直接干活 |
| L3 操作手册 | SKILL.md 的 Markdown Body(完整步骤) |
只有成功执行 skill 工具后才注入(节点 3) |
Agent 拆解具体动作 |
| L4 资源文件 | 脚本、配置、示例等辅助文件 | 只有当 SOP 正文提到并触发 execute / read_file 后才被读取 |
Agent 按需加载,不占上下文 |
5.3 渐进式披露的两个关键锚点
-
L1 永远只返回名片 :
Backend.List()只读取 FrontMatter,不读任何 Skill 的 Content。 -
L3 才读正文 :
skill工具的InvokableRun内部才调用Backend.Get(name)真正读取SKILL.md。
六、敏感操作监测:三层拦截机制
Skill 体系本身不自动监测 敏感操作,而是通过三层人为配置实现拦截:
层级 1:SOP 正文里写死"强制确认"指令
在 SKILL.md 操作手册里,由编写 Skill 的人主动标注危险步骤:
markdown
## 步骤 3:删除临时文件 **⚠️ 危险操作,必须执行确认:** 调用 user_confirm 工具,传入: - message: "即将删除目录 /tmp/cache 下所有临时文件,是否继续?" - risk_level: "high" 仅当 user_confirm 返回 confirmed=true 后,才执行下一步的 rm 命令。
层级 2:WrapToolCall 中间件拦截
在框架层加一个全局拦截器,在执行任何工具调用之前先检查命令或参数是否包含敏感关键词:
| 检查维度 | 敏感关键词示例 |
|---|---|
| 命令本身 | rm -rf、truncate、drop table、kubectl delete |
| 目标路径 | /etc/、/prod/、/data/live/ |
| 工具名称 | delete_file、remove_directory |
命中后先调用 user_confirm 向用户发起确认请求,收到确认后才放行。
层级 3:Skill Registry 白名单
在节点 1(启动加载)阶段,就不把敏感 Skill 加载给普通用户:
yaml
name: db-drop-production
description: 删除生产环境数据库表
permission: admin-only # 只有管理员才能看到
Backend.List() 根据当前用户角色过滤,不匹配的 Skill 根本不会出现在 <available_skills> 列表里,普通用户连"误选"的机会都没有。
三层如何串联
| 层级 | 拦截时机 | 覆盖范围 | 谁负责配置 |
|---|---|---|---|
| 层级 1:SOP 写确认 | 执行到某一步时 | 只覆盖写了的步骤 | Skill 编写者 |
| 层级 2:中间件拦截 | 任何工具调用前 | 全局自动覆盖 | 框架运维者 |
| 层级 3:白名单过滤 | Agent 启动加载时 | 整个 Skill 不可见 | 权限管理员 |
生产组合方式:层级 3 让普通用户根本看不到高危 Skill;层级 2 在最后关头兜底拦截;层级 1 作为第一道防线,在 SOP 里明确告知 Agent 必须先确认。
七、生产级管控:8 条工程规范
7.1 元数据治理:Description 是召回率生命线
L1 名片是模型唯一的预触发信息。写得好 = 模型选得准;写得差 = 该用不用或乱用。
写 Description 的模板:
yaml
description: |
[触发条件正向]:当用户提到 <关键词> 时使用
[适配反例] :不要用于 <明确不适用的场景>(给出替代做法)
[上下文条件] :需要 <某个前置配置> 才能工作
[产物说明] :产出 <具体产物类型> 到 <约定路径>
7.2 路径治理:让 Agent 绝不写错路径
Agent 执行 execute 时经常忘记加 BaseDir,这是排名第一的 Skill 失败原因:
-
BuildContent 预渲染绝对路径(推荐) :将
{``{BaseDir}}/xxx在返回前全部替换为skill.BaseDir + "/xxx"。 -
SKILL.md顶部加硬约束 :ALL FILE REFERENCES MUST USE ABSOLUTE PATH: <skill.BaseDir>。 -
WrapToolCall + cwd 注入 :自动前置
cd <BaseDir> &&。
7.3 目录治理:一级子目录约定
Eino 自带的 FilesystemBackend 只扫描 <BaseDir>/*/SKILL.md 的一级子目录。需要分类层级时可:
-
方案 A :保留一级目录,
Name写 qualified name,如"office-suite/pdf-batch"。 -
方案 B :自行实现
Backend接口,递归扫描**/SKILL.md。
7.4 模式选择治理:决策表
| 场景 | 推荐模式 | 不推荐原因 |
|---|---|---|
| SOP 需要和用户交互、和其他工具混用 | inline |
fork 后父子隔离,做不到澄清 |
| SOP 很重,会输出大量中间信息 | fork |
inline 会把父 Context 撑爆 |
| 子流程需要知道用户历史输入 | fork_with_context |
fork 看不到父历史 |
| 需要切换专属模型且严格隔离 | fork + model |
inline 模型切换延迟一轮生效 |
7.5 依赖治理:自检 SOP + 失败回退链
markdown
## Step 0:环境自检(必须先跑) 1. 先执行:<BaseDir>/scripts/selfcheck.sh - 成功 → 进入 Step 1 - 失败 → 尝试 pip install xxx;仍失败走降级路径 2. 降级路径:不用脚本,用内联实现 3. 仍失败 → 通知用户,给出三条手动替代命令
7.6 权限治理:Skill 级白名单 + 破坏性操作强制审批
-
敏感 Skill 只在特定 RBAC 角色下才出现在
<available_skills>中。 -
高危命令在 SOP 里明确写"执行前先调用
user_confirm"。
7.7 版本治理:Skill Registry + 变更审计
-
所有 Skill 放独立 Git 仓库,加
version字段,发布走 MR/PR。 -
Backend按project + env拉取不同 tag 的 Skill。 -
开启 Callbacks 打点,记录
skill_name, skill_version, success/failure, latency,按版本号定位并一键回滚。
7.8 用户体验治理:区分「加载说明书」和「真正执行」
inline 模式下默认返回 Launching skill: xxx,让用户误以为已经在跑。应改写为 Loaded instructions for skill: xxx(下一步将按此步骤执行),并在前端附加轻提示。
八、总结:Skill = Prompt 工程 + 配置管理 + Agent 编排 的交集
| 维度 | Tool(本地函数) | MCP(远程函数) | SubAgent(task) |
Skill(SKILL.md) |
|---|---|---|---|---|
| 本质 | 可执行函数 | 远程可执行函数(协议) | 拉起一个独立 Agent 端到端跑 | 一份 Markdown SOP + 运行时策略 |
| 被模型看到的方式 | 每个工具一条 ToolInfo | 每个工具一条 ToolInfo | 每个 SubAgent 一条 desc | 所有 Skill 的 Name+Desc 拼进 1 个 skill 工具的描述 |
| 任务执行形态 | 一次调用 = 一次原子动作 | 同上 | 一次调用 = 子 Agent 全流程跑完 | inline = 先加载 SOP,后续 N 轮执行;fork* = 内部独立 Agent 跑完 |
| 模型切换 | 不支持 | 不支持 | SubAgent 级别可切 | 通过 model 字段切换 |
| 动态参数注入 | 通过函数参数 | 通过 JSON 参数 | 通过 inputs 传递 |
通过 CustomToolParams 钩子注入并渲染进 SOP |
| 上下文隔离 | 不隔离 | 不隔离 | 默认隔离 | inline 不隔离,fork* 隔离 |
| 改动成本 | 改代码 → 重新编译 | 改 MCP Server 部署 | 改配置 + 重新编译 | 改 SKILL.md → 发布目录即可 |
如果把 Tool 比作「螺丝刀」,MCP 是「外采的电动工具」,SubAgent 是「外包同事」,那么 Skill 更像一份放在抽屉里、随时取出、上面还贴着步骤图解和质检清单的 《标准作业指引》 ------它不改变底层工具有多少、同事能力有多强,但把 「怎么组合这些东西能稳定地、合规地、可审计地把一件事做完」 的知识,沉淀到了独立可复用的载体里。
这就是为什么生产级 Agent 体系无论底层框架怎么换,Skill(或类似 SKILL.md 格式的标准载体)一定会成为工程化落地的基础构件。
