如何让自己的Agent支持skills功能(纯干货)
上一篇写了自己的原生agent如何实现的mcp,这篇文章说说怎么实现的skills。其实skills比mcp实现起来要简单很多,因为他本质是渐进式披露加载文件系统,然后让大模型去执行skill,也是需要初始化skills获取元数据,
·
前言
上一篇写了自己的原生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个工作日到账。
小王:好的,谢谢李经理!以后我会更注意报销流程。
直接抓一下提示词,我们可以看到元数据已经被加载进去了




说实话报告质量还不错
更多推荐

所有评论(0)