前言

上一篇写了自己的原生agent如何实现的mcp,这篇文章说说怎么实现的skills。

其实skills比mcp实现起来要简单很多,因为他本质是渐进式披露加载文件系统,然后让大模型去执行skill,也是需要初始化skills获取元数据,

构建SkillsManager

代码主要功能点:

  • initialize 初始化管理器并加载现有的技能元数据(name、desc、path)

  • discoverSkills 从 .schoober/skills 目录发现技能。

  • loadSkillMetadata 从特定的技能目录加载技能元数据。

  • getAllSkills 获取所有已加载的技能。

  • getSkillContent 获取特定技能的完整内容(instructions)。

  • 其实应该还有skills优先级和文件监听的处理,原理和mcp的处理差不多这里没做。

import * as fs from "fs/promises"
import * as path from "path"
import matter from "gray-matter"

export interface SkillMetadata {
    name: string
    description: string
    path: string
}

export interface SkillContent extends SkillMetadata {
    instructions: string
}

export class SkillsManager {
    private skills: Map<string, SkillMetadata> = new Map()
    // 项目根目录,其中包含 .schooberAi/skills
    private workspaceRoot: string

    constructor(workspaceRoot: string) {
        this.workspaceRoot = workspaceRoot
    }

    /**
     * 初始化管理器并加载现有的技能。
     */
    async initialize(): Promise<void> {
        await this.discoverSkills()
    }

    /**
     * 从 .schoober/skills 目录发现技能。
     */
    async discoverSkills(): Promise<void> {
        this.skills.clear()
        const skillsDir = path.join(this.workspaceRoot, ".schoober", "skills")

        try {
            // 检查目录是否存在
            await fs.access(skillsDir)
        } catch {
            // 目录不存在,直接返回空
            return
        }

        try {
            const entries = await fs.readdir(skillsDir, { withFileTypes: true })

            for (const entry of entries) {
                if (entry.isDirectory()) {
                    await this.loadSkillMetadata(path.join(skillsDir, entry.name))
                }
            }
        } catch (error) {
            console.error("Failed to discover skills:", error)
        }
    }

    /**
     * 从特定的技能目录加载技能元数据。
     */
    private async loadSkillMetadata(skillDir: string): Promise<void> {
        const skillMdPath = path.join(skillDir, "SKILL.md")

        try {
            // 检查 SKILL.md 是否存在
            await fs.access(skillMdPath)

            const fileContent = await fs.readFile(skillMdPath, "utf-8")
            // 解析 frontmatter
            const { data: frontmatter } = matter(fileContent)

            // 基本验证
            if (!frontmatter.name || typeof frontmatter.name !== "string") {
                console.warn(`Skill at ${skillDir} is missing required 'name' field`)
                return
            }
            if (!frontmatter.description || typeof frontmatter.description !== "string") {
                console.warn(`Skill at ${skillDir} is missing required 'description' field`)
                return
            }

            // 存储元数据
            this.skills.set(frontmatter.name, {
                name: frontmatter.name,
                description: frontmatter.description,
                path: skillMdPath,
            })
        } catch (error) {
            // SKILL.md 缺失或不可读
        }
    }

    /**
     * 获取所有已加载的技能。
     */
    getAllSkills(): SkillMetadata[] {
        return Array.from(this.skills.values())
    }

    /**
     * 获取特定技能的完整内容(说明)。
     */
    async getSkillContent(name: string): Promise<SkillContent | null> {
        const skill = this.skills.get(name)
        if (!skill) return null

        try {
            const fileContent = await fs.readFile(skill.path, "utf-8")
            const { content } = matter(fileContent)

            return {
                ...skill,
                instructions: content.trim(), // markdown 文件的正文
            }
        } catch (error) {
            console.error(`Failed to read skill content for ${name}:`, error)
            return null
        }
    }
}

agent提示词注入

原理是一样的,在vscode插件初始化的时候,创建实例,并在agent提示词初始化之前把相关信息放入system prompt中,这里首先需要放入的是元数据部分(name、desc、path)skills部分可以看我之前的文章,本文内容讲的是实现。

实操案例

直接看效果吧~

输入提示词

根据下面这段会议,生成一下会议报告:李经理:小王,你这次去北京出差的费用报销单我看了,有几个问题需要确认。
小王:好的,您说。
李经理:餐饮费这块,7月15号晚餐花了850元,超出了标准。能解释一下吗?
小王:那天是和客户商务宴请,有发票和签约合同可以证明。
李经理:嗯,那就没问题。但这个打车费298元没有发票,只能按50%报销。
小王:明白了,下次我会注意索要发票。
李经理:住宿费符合标准,机票也都是经济舱。总体来说,除了那个打车费,其他都可以全额报销。大概3个工作日到账。
小王:好的,谢谢李经理!以后我会更注意报销流程。

直接抓一下提示词,我们可以看到元数据已经被加载进去了

说实话报告质量还不错

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐