为你的 Agent 集成 Skill 系统

20 min

为自己开发的 Agent 添加 Skill 支持时,开发的核心步骤:发现、解析、使用、管理

  1. 发现:你的 Skill 存储在哪里,本地还是云端,项目级和用户级的优先级是什么,如何确定该文件夹是一个 Skill
  2. 解析:将 SKILL.md 的元信息解析出来
  3. 使用:Agent 如何使用解析出来的元信息,是系统提示词还是工具描述,后续的渐进式披露策略的执行,是使用读取工具还是使用内部的激活工具
  4. 管理:如何维持加载进入上下文的 Skill 的有效性,上下文压缩的时候 Skill 的内容是否需要保护

🌟 开发的核心原则也是 Skill 的核心特点:渐进式披露

Skill 的渐进式加载
Skill 的渐进式加载
  • 第一层披露:在会话启动的时候,将**元信息(名称 + 描述)**加载到上下文中
  • 第二层披露:在 Skill 激活的时候,也就是 Agent 根据用户输入和元信息匹配到相应的 Skill,将完整的 SKILL.md 内容加载到上下文中
  • 第三层披露:当 SKILL.md 的正文内容加载到上下文之后,Agent 根据任务的复杂情况来选择加载更详细的指导说明,也就是脚本、参考资料、静态资源这三种可按需加载的资源

一、发现

Agent 是需要从相应的文件目录中发现运行环境有哪些 Skill,大部分 Agent 是运行在本地环境的,所以我们重点说一下本地环境的 Skill 发现

对于 Skill 的文件目录的范围是分为两种的:用户全局范围和项目局部范围

  • 项目局部范围:只对当前项目生效的 Skill,例如:前端设计 Skill、React 最佳实践 Skill 等
  • 用户全局范围:对用户所有的项目生效的 Skill,例如:find-skills(发现 Skill)、pptx(生成 PPT 的 Skill)
Skill 的发现
Skill 的发现

具体的 Skill 的目录模块是这样的:

  1. <project>/.<agent-client>/skills/:项目范围的可用 Skill
  2. <project>/.agents/skills/
  3. ~/.<agent-client>/skills/:全局范围的可用 Skill
  4. ~/.agents/skills/

从上面的路径中可以发现存在 /.agents/ 的文件路径,这是因为该路径已经成为跨各种不同的客户端技能共享的广泛采用的约定,例如:

别人可能开发了 a-agent-client,它的 Skill 的目录是 ~/.a-agent-client/skills/,你这个时候开发的 b-agent-client 想要使用 a 的 Skill 目录,你就要做兼容,读取目录的时候特定读取~/.a-agent-client,一个当然不要紧,那假如有很多个呢?并且路径都不一样,那岂不是每次都要更新这个东西, 所以大家就采用一种约定俗成的规范,无论什么客户端,都可以给安装的 Skill 提供 ~/.agents/skills/ 这个目录,那么后续开发的时候就都可以默认读取一下这个目录,达到兼容各种客户端统一规范的目的

🌴 所以读取/.agents/的文件路径意味着其他符合规范的客户端安装的 Skill 会自动对你的客户端可见,相反别人也可以自动可见你的

有时候会在用户全局范围和项目局部范围出现相同的 Skill,这个时候优先级判断是:

  • 项目级优先级大于用户级
  • 相同范围内,按照发现顺序排优先级

设计信任检查的考虑是因为,有一些项目拉取下来会存在一些 skill,可能这些 skill 是恶意不安全的,可能会泄漏你的密钥等隐私性的东西。

所以在 Agent 的配置文件中可以考虑设计一层 skill 信任检查,在项目范围内,只有受信任的 Skill 才可以加载使用,这个是在业务逻辑层面进行判断的

🎃 一点开发技巧的小补充:

在读取 skills 文件夹的时候,要**查找包含名为 SKILL.md 的子目录,**这个才是有效的 Skill,是符合 Skill 规范的,一些开发读取 Skill 路径技巧:

  • 要跳过不包含 skills 的目录,例如:node_modules/这种依赖项文件
  • 可以考虑遵守项目的.gitgnore 文件,避免扫描一些构建产物,类似于 dist/文件夹
  • 不要一直嵌套循环的深度搜索,要设置最大搜索层数 (5-6 层),同时设置最大文件数量

二、解析

在 Agent 会话开始的阶段,是需要将 Skill 的元信息加载到上下文中的,所以我们在获取到 Skill 的文件路径之后,下一步就是需要解析 SKILL.md 的元信息啦

SKILL.md 的文件包含两部分:

  1. --- 分隔符分开的 YAML 前置元数据
  2. 以结束分隔符分开的 MD 格式的内容

这些元信息的字段和约束如下:

  • name(必需):表示技能的名称,最多 64 字符,仅允许小写字母、数字、连字符。不得以连字符开头或结尾
  • description(必需):对于该技能的描述,最多 1024 字符,不能为空
  • license(可选):对于许可证名称或者许可证文件的引用
  • compatibility(可选):表示环境的要求(目标产品、系统包、网络访问)
  • metadata(可选):附加元数据
  • allowed-tools(可选):技能允许执行的工具列表

那么详细的开发步骤为:

  1. 找到 SKILL.md 文件分隔符的开始和结尾部分
  2. 解析中间的 YAML 代码块,提取出来 name 和 description 字段,还有其他的可选字段
  3. 而结尾分隔符后面的 MD 语法格式的内容就是 SKILL.md 的正文内容

在对于 YAML 解析的时候,错误处理不要太严格,小的错误不能影响 Skill 的解析,可以将错误信息返回给用户,以此进行警告提示

🤔 那么解析之后的 SKILL.md 的元信息,是需要加载进入 Agent 的上下文中的,如何存储是一个值得考虑的问题

我们可以使用 Map 这种键值对的格式,将这些元信息存储在内存中,name 作为 key,value 值至少需要下面三个字段:

  • name:名称
  • description:描述
  • location:SKILL.md 文件的绝对路径

关于是否需要将 md 格式的正文内容也加载进来,有两种考虑,如果直接加载进来的话,内容存储会变大一些,但是在渐进式披露第二层的时候,读取速度会很快,如果不加载进来,那内存会小一些,等进入第二层上下文加载的时候,就要再读取这个 SKILL.md 文件,IO 读取会影响整个 Agent 的响应时间,这里需要自己根据实际业务进行权衡

三、使用

3.1、将元信息放入到系统提示词中

系统提示词放入元信息
系统提示词放入元信息
  1. 在 Agent 会话初始的时候,将元信息放入系统提示词中,放入的时候要提供一个简短的 Skill 使用说明,告诉模型如何使用以及什么时候使用,这个是第一层披露
  2. 当用户输入或任务触发了相应的 Skill 的时候,这个时候 Agent 会调用读取工具读取完整的 SKILL.md,这个是第二层披露
  3. 完整的 SKILL.md 加载进入 Agent 上下文中之后,遇到任务复杂度很高,Agent 需要得到更加详细的”说明文档”,这个时候会继续调用读取工具,从 SKILL.md 获取的引用路径读取相应的 references、assets、scripts
  4. 如果读取到 scripts 的时候,有需要执行脚本的需求,那么 Agent 就会调用 Bash 工具执行脚本文件

在将元信息放入到系统提示词中的时候,采用一些特定的格式,XML、JSON 等,后续在上下文管理的时候会非常有效

<available_skills>
  <skill>
    <name>code-review</name>
    <description>Review code for bugs, style issues, and best practices. Use when the user wants feedback on their code.</description>
    <location>/home/user/.agents/skills/code-review/SKILL.md</location>
  </skill>
  <skill>
    <name>git-commit</name>
    <description>Generate clear and conventional git commit messages from diffs or change descriptions.</description>
    <location>/home/user/project/.agents/skills/git-commit/SKILL.md</location>
  </skill>
</available_skills>

location 字段表示的是 SKILL.md 的完整绝对路径,它的用途有两个:

  1. 给读取工具提供正确的路径参数
  2. 为模型提供一个基本的路径参考,用于正确读取 SKILL.md 内容中的引用资源(reference、asset、scripts/xxx.js)

🎃一点开发小建议:可以再提供一个文件列出的工具(listFiles),这样在第三层披露的时候,模型有工具可以”自修复”路径导致的读取错误

3.2、将元信息放入到专有的工具中

专有工具使用 skill 元信息
专有工具使用 skill 元信息
  1. 在会话开始的时候,Skill 的元信息会以工具的描述的格式提供给 Agent,这个是第一层渐进式披露
  2. 当 Agent 需要更完整的 SKILL.md 内容的说明,传入给工具的参数是 name,工具返回的是 SKILL.md 的内容,重点是对于引用资源的路径展示更为准确和具体了,这个是第二层渐进式披露
  3. 在第三层披露中,Agent 可以使用读取工具去获取完整的引用资源的内容,当然这里你也可以单独在创建一个工具获取引用资源的内容,也不一定仅依靠读取工具
  4. Bash 工具依旧有效,可以执行 skill 中的 scripts 脚本文件

使用专用工具和系统提示词这两种方式都是有效的,但是专用工具比系统提示词在开发控制上面更具有优势:

  • 控制返回的内容 - 只返回 SKILL.md 中的正文内容,而不是重复返回元信息
  • 在上下文管理过程中,工具的返回结果可以单独做一些特殊的标记,在上下文压缩的时候是可以跳过筛选出来
  • 引用资源是以结构化的列表形式展现,在模型理解上更加友好
  • 可以做一些独特的控制流,工具是否执行等操作
  • 可以做一些统计分析的功能
<skill_content name="code-review">
# Code Review

## 何时使用
当用户需要对代码进行审查时使用此技能,包括 Bug 排查、
代码风格检查、最佳实践建议等。

## 审查步骤
1. 阅读代码,理解其意图
2. 对照 references/style-guide.md 检查风格问题
3. 运行 scripts/lint-check.sh 进行静态分析
4. 输出结构化的审查报告

Skill directory: /home/user/.agents/skills/code-review
此技能中的相对路径均相对于上述目录。

<skill_resources>
  <file>scripts/lint-check.sh</file>
  <file>references/style-guide.md</file>
  <file>references/common-bugs.md</file>
</skill_resources>
</skill_content>

关于 skill_resources 的内容,之前是需要依靠 SKILL.md 中书写相应的引用介绍,但是如果是工具结果返回,我们可以单独将相应的 skill 下面的 reference、scripts、asset 文件夹都读一遍,然后将文件路径放入到 skill_resources 标签中,要注意限制文件列表大小,不要过大了,同时要向模型释放信号「当前的列表可能不完整,具体情况具体分析」,这样不会让模型在自主运行的过程中被工具返回内容限制

四、管理

Skill 的管理
Skill 的管理

渐进式加载的意义是避免预加载所有 Skill,因为有些 Skill 刚开始可能不会使用到,加载进去是上下文的浪费

但是已经加载进来的 Skill 内容,是值得在会话中持续保留的,因为 skill 已经成为了当前会话中 Agent 的一种任务行为指导,如果盲目的压缩,是会降低 Agent 的性能,保留 skill 的内容有两种方式

  1. 系统提示词的使用:在读取工具的结果中,识别相应的结构化标签,来保留 skill 的内容
  2. 专用工具的使用:将技能激活工具的输出标记为保护状态,在压缩的时候进行标记判断

但是这也要根据实际情况来看,如果我们的对话很长,skill 数量很多,上下文窗口小,不压缩的话,根本没有新的上下文空间给剩下的任务执行链,那么这个时候压缩是更优解

但是压缩之后,要显示的提示模型”skill 也被压缩了,如有需要请重新加载相应的 skill”

不然模型会陷入压缩幻觉,“觉得自己里面已经有了相应的 skill,不用在加载了”

还有一种更高级的用法:使用 subAgent(子智能体)运行 Skill

子智能体的整个执行过程(技能指令 + 引用文件 + 中间推理)都发生在它的上下文窗口中,主智能体的上下文完全不受污染,只看最终结果

在开发的时候,我们可以将”执行 Skill”这个行为创建一个子智能体,将用户输入+Skill 元信息注入给子智能体,最终子智能体返回结果给主智能体消费

具体是否使用子智能体来执行 Skill,要交给主智能体自己来判断,例如:任务超过阈值的时候就自动委托

五、快速构建

上面提到的方式,是你从头给 Agent 构建一个支持 Skill 的功能模块,里面涉及到的元信息加载,正文内容读取,资源文件读取的功能,由你来创建一套读取工具或者读取 Skill 内容的工具

目前的生态比较成熟,很多 Agent SDK 是开箱即用这个 Skills 功能模块的,只要引入 SDK 下的 Agent 方法,那么 Agent 就自带 skills 加载的功能,接下来我给大家使用 Claude Agent SDK 构建一下看看,开发起来非常方便快速

1、Agent 运行文件

"""简化版,主要展示核心流程的步骤,尤其是调用和输出结果解析"""
async def _run_repl_async() -> None:
    config = load_runtime_config()
    apply_runtime_env(config)
    client = build_client(config)

    # 输入
    try:
        user_input = input("\n> ").strip()
    except (EOFError, KeyboardInterrupt):
        return print("\n再见!")

    # 请求 + 接收
    try:
        print("[发送请求...]")
        await client.query(user_input)

        print("[接收响应...]")
        events = [e async for e in _iter_events(client)]
    except Exception as exc:
        print(f"[错误] {exc}")

2、Claude Agent SDK 核心运行配置文件

"""SDK 封装 - 简化版"""
from __future__ import annotations

from pathlib import Path
from app.config import RuntimeConfig

# 允许 Agent 使用的工具列表
ALLOWED_TOOLS = ["Skill", "Read", "Write", "Edit", "Bash", "Grep", "Glob"]

def build_client(config: RuntimeConfig):
    """创建 Claude SDK 客户端"""
    try:
        from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions
    except ImportError:
        raise RuntimeError(
            "请先安装 Claude Agent SDK: pip install claude-agent-sdk"
        )

    options = ClaudeAgentOptions(
        #设置 project 会默认加载项目内的./claude/skills 下的 skill
        setting_sources=["project", "user"],
        allowed_tools=ALLOWED_TOOLS,
        model="deepseek-chat",
        env={
            "ANTHROPIC_AUTH_TOKEN": config.api_key,
            "ANTHROPIC_BASE_URL": config.base_url,
        },
        # 单独指定 skills 文件加载的位置,配合 user 值使用
        add_dirs=["/xxx/xxxx/.agents/skills"]
    )
    return ClaudeSDKClient(options=options)

核心就是这两个文件,运行 Agent 之后,就会自动加载 skill 文件,同时也会自动使用渐进式披露的策略来进一步加载正文内容和资源文件

TIP

关于 Claude Agent SDK 模型最好是使用 Claude 的,但是因为一些因素无法使用的话,也可以考虑支持 ClaudeCode 和 Anthropic API 格式的模型供应商

类似的 SDK 还有:

  • pi-mono 中的 pi-coding-agent 核心包
  • kimi Agent SDK

🪐 如果追求快速构建是完全可以使用这些成熟的 Agent SDK 构建 Skill 支持功能的,相应的你的 Agent 的整体构建设计思路也要去贴合这些 Agent SDK,我觉得是有利有弊的,需要开发者们按照场景去选择自己从头构建还是借助 SDK 集成,

  • 自己构建自由度更高,可操作性更强,
  • 使用 SDK 构建,速度更快,迭代方便,开发难度较小