Skill:Agent 的可插拔能力单元

什么是 Skill

在 Agent 体系中,LLM 通过 Function Call / Tool Use 调用外部工具来弥补自身"只能处理文本"的局限。但随着系统复杂度提升,裸 Tool 暴露出一个根本问题:

确定性的业务编排逻辑,不应该交给概率模型去推理。

裸 Tool 模式下,"查订单 → 查物流 → 匹配退款政策 → 计算金额"这样的确定性流程,全靠 LLM 自己编排------它可能漏步骤、搞错顺序、SQL 写错、甚至不知道要查退款政策。Tool 越多,LLM 选错的概率越高,token 消耗也越大。

Skill 就是为了解决这个问题而产生的中间抽象层------把"怎么做"固化在代码和指令中,只让 LLM 决定"做不做"

复制代码
── 裸 Tool:LLM 自己编排 4 步 ──────────────────────
用户:"订单 X123 能退款吗?"
LLM → query_database(orders)
    → query_database(logistics)       ← 可能忘记这步
    → http_request(refund-policy)     ← 可能不知道要查
    → 自己综合判断                     ← 可能算错

── Skill:LLM 只做一次决策 ─────────────────────────
用户:"订单 X123 能退款吗?"
LLM → skill("check_refund")
      Skill 内部指令引导 Agent:查订单 → 查物流 → 匹配政策 → 返回结构化结果

Skill 带来的架构优势

优势 说明 举例
可复用 一个 Skill 注册一次,多个 Agent 共享 订单查询 Skill 同时服务客服 Agent、退款 Agent
可插拔 Agent 按需声明所需 Skill,运行时动态组合 灰度上线"智能推荐"Skill,只挂载到部分 Agent
收敛 Tool 数量 几十个原子 Tool 聚合为少量 Skill,LLM 选择更准 10 个数据库/API 操作 → 1 个退款检查 Skill
开发效率 业务开发者写一个 SKILL.md + 脚本即可被 Agent 调用 不需要理解 LLM 调用协议
提示词即逻辑 流程用自然语言写在 SKILL.md 中,改流程改提示词即可,无需编译部署 调整退款审核流程只需编辑 Markdown

Skill 的目录结构

一个 Skill 本质上是一个文件夹,包含指令、脚本和资源,Agent 可以按需发现和使用:

复制代码
my-skill/
├── SKILL.md          # 必需:核心指令文件(元数据 + 执行说明)
├── scripts/          # 可选:可执行脚本(Python、Shell 等)
├── references/       # 可选:参考文档(API 文档、规范等)
└── assets/           # 可选:模板、配置、静态资源

SKILL.md --- 核心指令文件

SKILL.md 是 Skill 的唯一必需文件,由两部分组成:

1. YAML Frontmatter(元数据)

yaml 复制代码
---
name: log-analyzer
description: 分析日志文件,提取错误和警告信息,生成结构化报告。当用户需要分析日志时使用。
---
  • name:Skill 的唯一标识,Agent 通过此名称调用
  • description:功能描述,这是 Agent 判断是否使用该 Skill 的关键依据,应说明"何时用"而非仅"做什么"

2. Markdown 正文(执行指令)

Frontmatter 之后的正文是 Agent 激活 Skill 后读取的详细指令,包含工作流程、步骤说明、示例等。Agent 会严格按照这些指令执行任务。

scripts/ --- 可执行脚本

Skill 可以捆绑脚本(Python、Shell 等),Agent 在执行 Skill 时可以调用这些脚本完成具体工作。例如一个日志分析 Skill 可以包含 scripts/analyze.py,由 Agent 通过绝对路径调用执行。

references/ 和 assets/

references/ 存放参考文档(如 API 规范、协议说明),assets/ 存放模板、配置等静态资源。Agent 在需要时按需读取。


Skill 的执行流程 --- 渐进式展示

Skill 采用**渐进式展示(Progressive Disclosure)**策略来高效管理上下文,而不是一次性把所有 Skill 内容塞进 System Prompt:

为什么不直接全量加载? 假设系统中有 20 个 Skill,每个 SKILL.md 平均 2000 token,全量加载就是 40000 token 的 System Prompt。渐进式展示下,发现阶段可能只需 20 × 50 = 1000 token(name + description),激活时才加载当前需要的那一个。


在 Eino 中使用 Skill

Eino 通过 Skill Middleware 为 ADK Agent 提供 Skill 支持。下面以一个日志分析 Skill 为例,展示完整的接入流程。

1. 准备 Skill 目录

复制代码
workdir/
├── skills/
│   └── log_analyzer/
│        ├── scripts/
│        │   └── analyze.py
│        └── SKILL.md
└── other files

2. 创建 Backend 和 Middleware

go 复制代码
import (
    "github.com/cloudwego/eino/adk/middlewares/skill"
    "github.com/cloudwego/eino/adk/middlewares/filesystem"
    "github.com/cloudwego/eino-ext/adk/backend/local"
)

ctx := context.Background()

// 1. 创建本地文件系统 Backend
be, err := local.NewBackend(ctx, &local.Config{})

// 2. 基于文件系统创建 Skill Backend
skillBackend, err := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{
    Backend: be,
    BaseDir: "workdir/skills",  // 扫描此目录下所有一级子目录中的 SKILL.md
})

// 3. 创建 Skill Middleware
sm, err := skill.NewMiddleware(ctx, &skill.Config{
    Backend: skillBackend,
})

// 4. 创建 Filesystem Middleware(让 Agent 能读取文件、执行脚本)
fsm, err := filesystem.New(ctx, &filesystem.MiddlewareConfig{
    Backend:        be,
    StreamingShell: be,
})

3. 挂载到 Agent

go 复制代码
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
    Name:        "LogAnalysisAgent",
    Description: "An agent that can analyze logs",
    Instruction: "You are a helpful assistant.",
    Model:       cm,
    Handlers:    []adk.ChatModelAgentMiddleware{fsm, sm},  // 两个 Middleware 都需要
})

注意:Skill Middleware 只负责加载 SKILL.md 内容。如果 Skill 需要 Agent 读取文件或执行脚本,必须另外配置 Filesystem Middleware。


Backend 接口与自定义实现

Backend 接口

Backend 接口定义了 Skill 的检索方式,将 Skill 的存储使用解耦:

go 复制代码
type Backend interface {
    List(ctx context.Context) ([]FrontMatter, error)  // 发现阶段:列出所有 Skill 的元数据
    Get(ctx context.Context, name string) (Skill, error)  // 激活阶段:获取完整 Skill 内容
}

这意味着 Skill 可以存储在任何地方------本地文件系统、数据库、远程服务、Git 仓库------只要实现这两个方法。

内置实现:NewBackendFromFilesystem

Eino 提供了基于文件系统的 Backend 实现:

go 复制代码
skillBackend, err := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{
    Backend: be,           // filesystem.Backend 实现(如 local.Backend)
    BaseDir: "skills/",    // 扫描此目录下的一级子目录,查找 SKILL.md
})

工作方式:扫描 BaseDir 一级子目录 → 查找 SKILL.md → 解析 Frontmatter 获取元数据。深层嵌套的 SKILL.md 会被忽略。

自定义 Backend:按需过滤和聚合

在实际项目中,不同的 Agent 往往只需要一部分 Skill。通过自定义 Backend,可以实现白名单过滤多目录聚合

FilteredBackend --- 白名单过滤

不同 Agent 只加载自己需要的 Skill,避免无关 Skill 干扰 LLM 决策:

go 复制代码
// FilteredBackend 通过白名单过滤 Skill
type FilteredBackend struct {
    inner      skill.Backend
    allowNames map[string]bool
}

func NewFilteredBackend(inner skill.Backend, names []string) skill.Backend {
    if len(names) == 0 {
        return inner // 无白名单,直接透传
    }
    allow := make(map[string]bool, len(names))
    for _, n := range names {
        allow[n] = true
    }
    return &FilteredBackend{inner: inner, allowNames: allow}
}

func (f *FilteredBackend) List(ctx context.Context) ([]skill.FrontMatter, error) {
    all, err := f.inner.List(ctx)
    if err != nil {
        return nil, err
    }
    filtered := make([]skill.FrontMatter, 0, len(f.allowNames))
    for _, fm := range all {
        if f.allowNames[fm.Name] {
            filtered = append(filtered, fm)
        }
    }
    return filtered, nil
}

func (f *FilteredBackend) Get(ctx context.Context, name string) (skill.Skill, error) {
    if !f.allowNames[name] {
        return skill.Skill{}, fmt.Errorf("skill %q not in allow list", name)
    }
    return f.inner.Get(ctx, name)
}
CompositeBackend --- 多目录聚合

Skill 可能分散在多个目录(公共 Skill、业务 Skill、团队 Skill),通过聚合 Backend 统一管理:

go 复制代码
// CompositeBackend 将多个 Backend 合并为一个
type CompositeBackend struct {
    backends []skill.Backend
}

func NewCompositeBackend(backends ...skill.Backend) *CompositeBackend {
    return &CompositeBackend{backends: backends}
}

func (c *CompositeBackend) List(ctx context.Context) ([]skill.FrontMatter, error) {
    var all []skill.FrontMatter
    for _, b := range c.backends {
        items, err := b.List(ctx)
        if err != nil {
            return nil, fmt.Errorf("composite backend list error: %w", err)
        }
        all = append(all, items...)
    }
    return all, nil
}

func (c *CompositeBackend) Get(ctx context.Context, name string) (skill.Skill, error) {
    for _, b := range c.backends {
        s, err := b.Get(ctx, name)
        if err == nil {
            // 将模板变量替换为实际路径
            s.Content = strings.ReplaceAll(s.Content, "{{.BaseDirectory}}", s.BaseDirectory)
            return s, nil
        }
    }
    return skill.Skill{}, fmt.Errorf("skill not found in any backend: %s", name)
}
组合使用:为不同 Agent 配置不同 Skill 集

将 FilteredBackend 和 CompositeBackend 组合起来,实现"多目录 + 按 Agent 过滤":

go 复制代码
func CreateSkillMiddlewares(ctx context.Context, config AgentSkillConfig) ([]adk.ChatModelAgentMiddleware, error) {
    localBackend, _ := local.NewBackend(ctx, &local.Config{})

    // 为每个 Skill 来源创建 Backend(可带过滤)
    var skillBackends []skill.Backend
    for _, src := range config.Sources {
        fsBackend, _ := skill.NewBackendFromFilesystem(ctx, &skill.BackendFromFilesystemConfig{
            Backend: localBackend,
            BaseDir: src.Dir,
        })
        // 每个来源可以单独设置白名单
        skillBackends = append(skillBackends, NewFilteredBackend(fsBackend, src.Names))
    }

    // 聚合为一个 Backend
    compositeBackend := NewCompositeBackend(skillBackends...)

    // 创建 Middleware
    skillMW, _ := skill.NewMiddleware(ctx, &skill.Config{Backend: compositeBackend})
    fsMW, _ := filesystem.New(ctx, &filesystem.MiddlewareConfig{
        Backend: localBackend, StreamingShell: localBackend,
    })

    return []adk.ChatModelAgentMiddleware{fsMW, skillMW}, nil
}

使用时,不同 Agent 只需声明自己需要的 Skill:

go 复制代码
// 客服 Agent:只需要退款和物流相关 Skill
handlers, _ := CreateSkillMiddlewares(ctx, AgentSkillConfig{
    Sources: []SkillSource{
        {Dir: "skills/common", Names: []string{"refund-check", "logistics-query"}},
        {Dir: "skills/customer-service", Names: nil},  // nil = 加载目录下所有
    },
})

// 数据分析 Agent:只需要日志和报表 Skill
handlers, _ := CreateSkillMiddlewares(ctx, AgentSkillConfig{
    Sources: []SkillSource{
        {Dir: "skills/common", Names: []string{"log-analyzer"}},
        {Dir: "skills/analytics"},
    },
})

Eino Skill Middleware 的实现原理

Skill Middleware 的核心工作是向 Agent 注入 System Prompt注册 Skill Tool,实现渐进式展示。

1. 注入 System Prompt

Middleware 初始化时向 Agent 的 System Prompt 追加以下内容,教会 Agent 如何使用 Skill:

复制代码
# Skills System

Skills follow a **progressive disclosure** pattern:

1. **Recognize when a skill applies**: Check if the user's task matches a skill's description
2. **Read the skill's full instructions**: Use the '{tool_name}' tool to load skill
3. **Follow the skill's instructions**: tool result contains workflows, best practices, and examples
4. **Access supporting files**: Skills may include helper scripts, configs, or reference docs

When to Use Skills:
- User's request matches a skill's domain
- You need specialized knowledge or structured workflows
- A skill provides proven patterns for complex tasks

Executing Skill Scripts:
Skills may contain Python scripts or other executable files. Always use absolute paths.

2. 注册 Skill Tool

Middleware 调用 Backend.List() 获取所有 Skill 的元数据,然后注册一个名为 skill(可配置)的 Tool。这个 Tool 的 description 中列出所有可用 Skill 的 name 和 description:

复制代码
Execute a skill within the main conversation

<available_skills>
<skill>
<name>log-analyzer</name>
<description>分析日志文件,提取错误和警告信息</description>
</skill>
<skill>
<name>refund-check</name>
<description>检查订单退款资格</description>
</skill>
</available_skills>

3. 运行时流程

当 Agent 判断需要使用某个 Skill 时,整个调用链如下:

关键点:Skill Middleware 只负责加载 SKILL.md。Agent 执行脚本、读取文件等能力由 Filesystem Middleware 提供,两者各司其职。

ContextMode:Skill 的执行上下文

除了默认的内联模式,Skill 还支持在独立上下文中执行(需配置 AgentHub):

模式 Frontmatter 配置 行为
内联(默认) 不填 context Skill 内容作为 Tool Response 返回,当前 Agent 继续处理
fork_with_context context: fork_with_context 创建新 Agent,复制当前对话历史,独立执行后返回结果
fork context: fork 创建新 Agent,隔离上下文(仅包含 Skill 内容),独立执行后返回结果

适用场景:当 Skill 任务较重(如长文档分析)、需要不同模型、或不希望污染主对话上下文时,使用 fork 模式。

相关推荐
羊小猪~~2 小时前
【QT】-- QMainWindow简介
开发语言·数据库·c++·后端·qt·前端框架·求职招聘
Boop_wu2 小时前
[MyBatis] MyBatis 快速入门(1)
数据库·oracle
l1t2 小时前
QWen 3.5plus总结的总结基准测试结果的正确方法
前端·数据库
额1292 小时前
CentOS 7 安装apache部署discuz导入数据库表
数据库·centos·apache
大阿明2 小时前
Spring.factories
java·数据库·spring
2401_873544922 小时前
使用Fabric自动化你的部署流程
jvm·数据库·python
wenlonglanying2 小时前
MYSQL的第一次作业
数据库·mysql
万岳科技系统开发2 小时前
外卖系统选型与源码与 SaaS 实践的思考
数据库·小程序
欧洵.2 小时前
MySQL:事务详细
数据库·mysql·oracle