动手写个agent(五):实现接入skill能力
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
文章目录
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent
代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
本节目标:引入 Skill 概念,将其作为“工具集+执行流程”的封装,解决工具过载和流程不固定的问题。
第五部分:封装固化流程:创建你的第一个 Skill
引入 MCP 协议后,我们的 Agent 拥有了强大的工具扩展能力。但“能力越大,烦恼越多”。当工具库膨胀到一定程度时,两个核心问题暴露出来:
上下文爆炸与选择困难:为了让 LLM 知道有哪些工具可用,我们需要将所有工具的描述都塞进 Prompt。当工具有成百上千个时,这会消耗海量的上下文额度,甚至超出模型的上限。更糟糕的是,在海量工具的描述中,LLM 做出正确选择的难度也大大增加,就像让你在几百页的菜单中点菜一样。流程不固定,效果不可靠:对于一些有固定步骤的复杂任务(例如“发布一个软件版本”),我们希望 Agent 能严格按照预设的流程来执行。但如果只提供一堆零散的原子工具,完全依赖 LLM 去“临场发挥”,其执行路径将充满不确定性,效果难以保障。
问题:如何既能让 Agent拥有丰富的潜在能力,又不必在每次对话时都背负所有工具的“认知包袱”?如何将一系列工具调用固化成一个可靠的“任务模板”?解决方案:Skill。
Skill 是一个更高层次的抽象。我们可以把它理解为一个预先打包好的“任务攻略”或“标准作业流程 (SOP)”。一个 Skill 内部封装了:
- 完成特定任务所需的工具子集。
- 指导 LLM 如何一步步使用这些工具的详细指令 (Instructions)。
而暴露给 LLM 的,不再是这个 Skill 内部所有工具的繁琐描述,而只是一个关于这个 Skill 本身的、高度浓缩的一句话简介。
这种机制,本质上是一种懒加载 (Lazy Loading):
初始状态:Agent 启动时,只告诉 LLM 它有哪些“技能包”可用,以及每个“技能包”的一句话功能描述(例如,“code-reviewer:一个用于评审代码合并请求的技能”)。按需激活:当用户的任务与某个 Skill 的描述相匹配时,LLM 会决定“激活”这个 Skill。加载攻略:Agent 在收到“激活”指令后,才会将这个 Skill 内部详细的“任务攻略”(即详细的执行步骤和所需的工具集)加载到上下文中。执行任务:LLM 根据刚刚加载的“攻略”,按部就班地调用工具,完成任务。
通过这种方式,Skill 巧妙地平衡了能力的广度与上下文的简洁性,让 Agent 变得更加通用和强大。
5.1 Skill 的文件结构
一个 Skill 通常以一个文件夹的形式存在,其核心是一个 SKILL.md 文件。这个文件由两部分组成:
元数据 (Frontmatter):文件开头,由 — 包裹的 YAML 块,定义了 Skill 的基本信息,其中最重要的是 name (唯一标识) 和 description (一句话功能简介)。指令 (Instructions):文件的正文部分,用 Markdown 格式详细描述了该如何使用此 Skill,包括它的作用、触发时机、思考步骤、需要用到的工具等。
这是一个 SKILL.md 的示例:
---
name: my-skill
description: 用于处理某类固定任务。当用户提出相关需求或需要按标准流程输出时,应优先使用本技能。
---
# My Skill
## 作用
用于处理:
- 任务 A
- 任务 B
## 触发时机
当用户:
- 提到相关任务
- 希望结构化输出
- 希望按固定流程处理
......
5.2 实现 Skill 的加载与管理
为了让 Agent 能够理解和使用 Skill,我们需要实现一个 skill.Registry (技能注册器)。它的职责是:
- Discover(): 在 Agent 启动时,扫描技能目录,只解析所有 SKILL.md 的元数据部分,建立一个可用的“技能摘要”列表。
- BuildSkillsPrompt(): 将上述“技能摘要”列表格式化成一段通顺的文本,用于注入到 LLM 的系统提示词中,告诉 LLM 有哪些 Skill 可用。
- ActivateSkill(name): 当 LLM 决定激活某个 Skill 时,完整地加载该 Skill 的 SKILL.md 文件,包括其详细的指令部分。
同时,我们还需要创建一个 activate_skill 工具。LLM 就是通过调用这个工具来通知 Agent 它想要激活哪个 Skill 的。
由于这部分代码主要是文件操作和状态管理,我们在此省略其具体实现,聚焦于它如何与 Agent 的主流程结合。
我们修改 Agent 的 buildSystemPrompt 方法,将技能列表的提示词自动追加到系统提示词的末尾。
// buildSystemPrompt 构建系统提示词
func (a *Agent) buildSystemPrompt() string {
prompt := a.systemPrompt
if prompt == "" {
prompt = `你是一个专业的AI Agent助手。你可以使用提供的工具来完成用户的任务。`
}
// 将可用技能列表追加到系统提示词中
skillPrompt := a.skillRegistry.BuildSkillsPrompt()
if skillPrompt != "" {
prompt += "\n\n" + skillPrompt
}
return prompt
}
5.3 实战:让 Agent 激活并执行 Skill
现在,我们把所有东西串联起来。我们创建一个my-script技能,该技能负责执行script/目录下的所有脚本。
---
name: my-script
description: 我的每日脚本
---
# My Script
每日都执行一下`script/`目录下的脚本
我们在 main 函数中初始化 skill.Registry,发现并注册所有技能,同时将 activate_skill 工具也加入到工具箱中。
func main() {
// ... 省略了 LLM Client, MCP Manager 的初始化代码 ...
// 初始化技能注册器
skillRegistry := skill.NewRegistry(config.SkillsDir) // config.SkillsDir 指向技能存放的目录
if err := skillRegistry.Discover(); err != nil {
log.Fatal().Err(err).Msg("发现Skill失败")
}
// 将与 Skill 相关的工具也注册到工具箱
// 激活skill的工具
toolRegistry.Register(tool.NewActivateSkillTool(skillRegistry))
// 列出所有skill的工具
toolRegistry.Register(tool.NewListSkillsTool(skillRegistry))
myAgent := agent.NewAgent("MyAgent", "", 10, client, toolRegistry, skillRegistry)
// 提出一个可以匹配到 "my-script" 技能的任务
answer, err := myAgent.Run(context.Background(), "执行我的每日脚本")
if err != nil {
log.Fatal().Err(err).Msg("运行Agent失败")
}
fmt.Println(answer)
}
当 Agent 运行时,它会经历以下步骤:
- 思考:用户的任务是“执行我的每日脚本”。我可用的技能中,有一个叫 my-script 的,它的描述是“我的每日脚本”。这看起来很匹配。
- 行动:调用 activate_skill 工具,参数为 {“skill_name”: “my-script”}。
- 观察:activate_skill 工具执行成功,返回了 my-script 技能的详细指令:“首先,使用 ls script/…”。
- 思考:好的,我收到了详细的攻略。第一步是列出 script/ 目录下的文件。
- 行动:调用 shell 工具,参数为 ls script/。
- … Agent 会继续按照 Skill 的指令执行下去,直到任务完成。



第五部分小结
通过引入 Skill 这一更高层次的抽象,我们有效地解决了工具过载和流程固化的难题。Agent 现在能够通过“懒加载”的方式,管理和使用海量的、封装了复杂流程的“技能包”。
至此,我们的 Agent 在“大脑”层面已经基本成型。但从用户体验上看,它还只是一个后台程序。最后一部分,我们将为它加上一点“交互的魔法”,让它成为一个真正可用的命令行应用。
更多推荐




所有评论(0)