首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >给 Agent 装技能:一套可落地的 Skill 机制设计

给 Agent 装技能:一套可落地的 Skill 机制设计

原创
作者头像
Archive
发布2026-08-27 14:02:07
发布2026-08-27 14:02:07
110
举报
文章被收录于专栏:随笔随笔

从目录结构、工具封装到加载流程,把"一段能力"变成可挂载、可发现、可组合的单元

一、写在前面

做 Agent 应用的人大多会走到同一个岔路口:模型本身越来越强,但"能力"却越来越难管。

一开始,你会在 system prompt 里塞一段指令,告诉模型"遇到用户要查订单,就调用这个函数"。这能跑通。等业务长起来,prompt 里堆了十几段这样的指令,函数散落在各个模块里,有的要权限、有的要联网、有的只在某个环境里可用。你想复用某段能力到另一个 Agent,发现它和业务代码耦合在一起,拔不出来。

Skill 这个思路,就是为了解决这件事:把"一段能力"封装成一个独立的、可挂载、可发现、可组合的单元。它自带元信息、自带指令、自带可选的脚本或工具,Agent 运行时按需加载,不用改核心代码。

这篇文章不讨论某个具体框架的实现细节,而是从架构层拆解一套可以自己实现的 Skill 机制。文末会给一个最小可运行的 Python 骨架,你可以拿去改。

二、Skill 是什么,边界在哪

先给一个工作定义,方便后面对齐:

Skill 是一个自带元信息(manifest)、指令(instruction)和可选脚本/工具的、可独立分发与按需加载的能力单元。

拆开看三个部分:

元信息(manifest):描述这个 Skill 是谁、干什么、依赖什么、需要什么权限。通常是一个声明式文件,比如 SKILL.mdskill.json

指令(instruction):告诉 Agent"什么时候用我、怎么用我、注意什么"。这部分会注入到模型的上下文里。

脚本/工具:可选的执行体。可以是 Python 脚本、一段 API 调用封装,甚至是一个外部命令。

这里有一个容易混淆的点,值得单独说清楚。Skill 和下面这几个概念不是一回事:

Function / Tool,本质是单个可调用函数,输入输出明确。它与 Skill 的关系是:Skill 内部可以包含多个 Tool。

Plugin,本质是强调"接入外部系统"的能力。它与 Skill 的关系是:Skill 更偏"内部能力封装",两者可以重叠。

Agent,本质是有自主决策和任务拆解能力的执行体。它与 Skill 的关系是:Agent 是 Skill 的宿主,Skill 是 Agent 的能力来源。

再明确一下 Skill 不做什么,这个边界很重要:

  • 不承载状态。Skill 本身应该是无状态的,状态交给宿主或外部存储。
  • 不承载模型。Skill 不绑定某个模型,它描述的是"能力",不是"推理"。

把边界划清楚,后面做架构才不会越做越重。

三、整体架构

我把它分成四层。这个分层不是唯一解,但它足够清晰,也足够通用,能覆盖大部分自研场景。

第一层,Skill 定义层。 开发者在这里写 SKILL.md 和脚本。这一层要解决的问题是"如何用最少的约定,描述清楚一个能力"。

第二层,注册与发现层。 启动时扫描目录、校验 manifest、建立索引。这一层决定了"能力能不能被快速找到和加载"。

第三层,执行层。 把脚本或 API 封装成可调用单元,做权限控制和参数校验。这一层决定了"能力能不能安全地跑起来"。

第四层,编排层。 Agent 运行时根据任务,按需加载 Skill,注入指令,调用工具,回传结果。这一层决定了"能力在什么时机、以什么方式被用上"。

数据流的方向是:定义层产出 Skill 目录,注册与发现层扫描并建立索引,执行层封装成可调用工具,编排层按需加载并回传结果。

两个关键设计取舍,直接影响后续实现:

第一,声明式还是命令式。 manifest 用声明式(描述"有什么"),执行体用命令式(描述"怎么做")。声明式负责发现和校验,命令式负责干活,两者分离,互不干扰。

第二,同步还是异步。 工具调用默认同步、短耗时;如果 Skill 内部有长任务(比如跑一个批处理),让 Skill 自己返回一个任务句柄,由宿主去轮询,而不是让调用方一直阻塞。

四、核心设计

(一)目录结构与 manifest

一个 Skill 就是一个目录。目录名即 Skill 的唯一标识,目录内至少有一个 manifest 文件。我用的约定是这样:

skills 目录下,每个 Skill 一个子目录。以 order-query 为例,目录里放三个东西:SKILL.md(元信息加指令)、main.py(可选的执行脚本)、requirements.txt(可选的依赖声明)。

SKILL.md 用 YAML 前置信息放元信息,正文放指令。这样人可读、机器可解析,一举两得。元信息里包含这几个字段:

  • name:与目录名一致,避免索引错位。
  • version:必须有,否则能力升级后没法判断兼容性。
  • description:一句话说明这个 Skill 干什么。
  • permissions:用"动作:资源"的格式声明权限,粒度够细又不至于爆炸。
  • inputs:声明输入 schema,让执行层能做前置校验,而不是等脚本报错。

正文是指令,直接注入模型上下文,所以它的措辞要面向模型写清楚"何时用、怎么用、注意什么"。

(二)加载与发现

加载器做三件事:扫描、校验、索引。核心逻辑是:遍历 skills 目录下的每个子目录,解析 SKILL.md,把 YAML 前置信息和正文拆开,校验 name 与目录名是否一致,通过后构造成一个 Skill 对象放进索引。

这里有个值得注意的细节:单个 Skill 解析失败,不应该让整个注册过程失败。一个坏的 Skill 只影响它自己,日志里记一条,跳过即可。否则一个格式错误就能让所有能力都加载不出来。

(三)工具封装

脚本是 Skill 的执行体,但 Agent 不能直接"运行脚本",它需要的是一个有明确输入输出的调用接口。工具封装就是在这中间加一层适配。

实现上,用子进程隔离执行,而不是直接 import。这样做有两个好处:一是环境隔离,脚本崩了不会拖垮宿主;二是可以配合系统级沙箱做权限收敛。代价是每次调用有进程启动开销,对于低频、短耗时的工具调用,这个开销可以接受。

对应地,脚本侧约定一个统一的入口:用命令行参数传入一个 JSON 载荷,里面包含要调用的函数名和参数,脚本解析后调用对应函数,把结果以 JSON 打印到标准输出。

(四)生命周期

一个 Skill 从进来到退场,状态机很简单:未安装、已安装、已启用、运行中、已禁用、已卸载。

安装:把目录放到 skills 下,触发一次扫描。

启用或禁用:控制这个 Skill 是否参与加载。禁用不等于删除,只是不注入指令、不可调用。

卸载:从索引移除,删除目录或只移除引用。

这套状态机看着简单,但它是"按需加载"的基础——只有"已启用"的 Skill 才会进入索引,从而控制注入到模型上下文里的指令总量。

五、一个最小可运行实现

把上面的片段拼起来,就是一个能跑的骨架。完整流程:注册一个 skill,扫描发现,构造工具调用,拿到结果。核心代码大致如下(Python,可直接运行验证):

代码语言:python
复制
import json
import subprocess
from pathlib import Path

import yaml


class Skill:
    def __init__(self, name, meta, instruction, root):
        self.name = name
        self.meta = meta
        self.instruction = instruction
        self.root = root


def parse_skill(dir_path: Path) -> Skill:
    manifest = dir_path / "SKILL.md"
    if not manifest.exists():
        raise ValueError(f"{dir_path} 缺少 SKILL.md")

    text = manifest.read_text(encoding="utf-8")
    meta, instruction = split_frontmatter(text)

    if meta.get("name") != dir_path.name:
        raise ValueError(f"skill name 与目录名不一致: {dir_path.name}")

    return Skill(dir_path.name, meta, instruction, dir_path)


def split_frontmatter(text: str):
    if not text.startswith("---"):
        return {}, text
    parts = text.split("---", 2)
    if len(parts) < 3:
        return {}, text
    meta = yaml.safe_load(parts[1]) or {}
    return meta, parts[2].strip()


class SkillRegistry:
    def __init__(self, root: Path):
        self.root = root
        self.index = {}

    def discover(self):
        self.index = {}
        for d in self.root.iterdir():
            if not d.is_dir():
                continue
            try:
                skill = parse_skill(d)
                self.index[skill.name] = skill
            except ValueError as e:
                print(f"[skip] {d.name}: {e}")

    def get(self, name: str):
        return self.index.get(name)


class Tool:
    def __init__(self, skill: Skill, entry: str, function: str):
        self.skill = skill
        self.entry = entry
        self.function = function

    def run(self, args: dict) -> dict:
        payload = json.dumps({"function": self.function, "args": args})
        proc = subprocess.run(
            ["python", self.entry, "--invoke", payload],
            capture_output=True,
            text=True,
            timeout=30,
        )
        if proc.returncode != 0:
            return {"ok": False, "error": proc.stderr.strip()}
        return json.loads(proc.stdout)


def main():
    registry = SkillRegistry(Path("./skills"))
    registry.discover()
    print("已加载 skills:", sorted(registry.index.keys()))

    skill = registry.get("order-query")
    if skill is None:
        return

    tool = Tool(skill, entry=str(skill.root / "main.py"), function="query")
    result = tool.run({"order_id": "10086"})
    print(result)


if __name__ == "__main__":
    main()

脚本侧的入口约定如下(以 main.py 为例):

代码语言:python
复制
import sys
import json


def query(order_id: str) -> dict:
    return {"ok": True, "status": "shipped", "amount": 128.00}


if __name__ == "__main__":
    payload = json.loads(sys.argv[2])
    fn = globals()[payload["function"]]
    result = fn(**payload["args"])
    print(json.dumps(result, ensure_ascii=False))

跑起来之后,输出大致是:

代码语言:shell
复制
已加载 skills: ['log-analysis', 'order-query']

{'ok': True, 'status': 'shipped', 'amount': 128.0}

到这里,你已经有了一条从"目录里的一个文件夹"到"Agent 可调用的一个能力"的完整链路。剩下的工作,是在这条链路上加约束。

六、落地时要注意的事

这几个坑,都是实际做的时候会踩到的,提前想清楚能省不少返工。

安全,安全,还是安全。 Skill 本质是"让模型去触发代码执行",这是最大的风险面。至少要做四件事:子进程或容器隔离;权限声明与实际执行做比对;输入严格校验(类型、长度、格式);敏感字段脱敏后再回传。如果 Skill 要联网或访问文件,权限粒度要细到"哪个资源、什么动作"。

版本与依赖。 manifest 里的 version 不是摆设。能力升级后,旧的 Agent 可能依赖旧行为,所以要么做语义化版本加兼容声明,要么让 Agent 显式声明它要哪个版本的 Skill。依赖(比如 requirements.txt)要能声明、能校验、能隔离,否则两个 Skill 依赖冲突会非常难查。

可观测性。 Skill 调用要留痕:哪个 Agent、在什么任务里、调了哪个 Skill、入参出参、耗时、是否报错。没有这层,出问题时你只能靠猜。

上下文预算。 每个启用的 Skill 都会往模型上下文里注入指令。Skill 一多,上下文会被挤爆。所以指令要短,而且要支持"按需注入"——不是把所有 Skill 的指令一次性全塞进去,而是让 Agent 先看到一份"能力清单加一句话描述",确定要用哪个,再加载那个 Skill 的完整指令。

七、收尾

Skill 机制的要点,一句话概括:用声明式的 manifest 描述能力,用独立的目录承载能力,用统一的工具封装暴露能力,用按需加载控制能力的注入时机。

这套东西不难,难的是把边界划清楚、把安全做扎实。真正落地的 Agent 应用,核心竞争力往往不在模型本身,而在你能不能把"能力"沉淀成可复用、可治理、可组合的资产。Skill 是往这个方向走的第一步。

如果你想继续往下做,两个自然的方向:一是给 Skill 加"组合"能力,让一个 Skill 能调用另一个 Skill;二是引入 Skill 的发布与版本管理,让团队之间能共享和复用。前者解决能力复用,后者解决能力治理。


原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 一、写在前面
  • 二、Skill 是什么,边界在哪
  • 三、整体架构
  • 四、核心设计
    • (一)目录结构与 manifest
    • (二)加载与发现
    • (三)工具封装
    • (四)生命周期
  • 五、一个最小可运行实现
  • 六、落地时要注意的事
  • 七、收尾
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档