动手写个agent(五:加餐详解):实现接入skill能力
这是这一章最关键的概念。你可以把Skill理解成:一份按主题组织好的“专业操作说明书”。如何做代码审查如何写日报如何执行某种固定脚本如何处理某类任务它和普通工具不一样。
文章目录
- Chapter5 代码超详细新手导读
-
- 1. 这一章比上一章多了什么
- 2. 什么是 Skill
- 3. main.go: 程序如何把 Skill 装进去
- 4. Agent 有什么变化
- 5. loop.go 最大的变化: System Prompt 变聪明了
- 6. types/skill.go: Skill 的数据结构长什么样
- 7. skill/loader.go: 技能是怎么被发现和读取的
- 8. skill/registry.go: 技能管理员怎么工作
- 9. tool/skill_tools.go: 给 LLM 用的技能工具
- 10. Agent Loop 在 chapter5 中是怎么跑的
- 11. 这一章的设计精髓: 两级上下文
- 12. 和 chapter4 的关系
- 13. 这一章里你最该学会的源码阅读点
- 14. 用生活比喻把整章再讲一遍
- 15. 建议新手按什么顺序读这章源码
- 16. 一句话总复盘
- 17. 核心模块关系图
- 18. 一句超短版
本系列将从零开始,用 Go 语言实现一个具备基本功能(工具调用、循环思考、MCP、Skill)的 Agent
代码仓库:https://gitee.com/lymgoforIT/learn-agent(chapter1对应第一部分的代码,以此类推)
Chapter5 代码超详细新手导读
这份文档会继续沿用 chapter4 那种“把源码翻译成人话”的方式。
如果先用一句话总结 chapter5:
chapter5是在chapter4的 Agent 基础上,新增了一套Skill(技能)机制,让 Agent 不只是会用工具,还会在需要时“激活专业技能说明书”。
你可以把这章理解成:
chapter4教会 Agent “怎么干活”chapter5教会 Agent “先选对专业知识包,再干活”
1. 这一章比上一章多了什么
先看目录结构:
chapter5/
├── main.go
├── go.mod
├── agent/
│ ├── agent.go
│ └── loop.go
├── llm/
│ ├── client.go
│ └── openai.go
├── tool/
│ ├── tool.go
│ ├── registry.go
│ ├── shell.go
│ ├── mcp.go
│ └── skill_tools.go
├── skill/
│ ├── loader.go
│ └── registry.go
├── mcp/
│ ├── manager.go
│ ├── client.go
│ └── transport/
│ ├── transport.go
│ ├── types.go
│ ├── stdio.go
│ └── http.go
└── types/
├── config.go
├── llm.go
└── skill.go
和 chapter4 相比,最大的新增就是这两块:
skill/
新增技能发现、加载、注册机制。tool/skill_tools.go
新增给 LLM 使用的技能工具。
可以先看一张总图:
这张图想表达的是:
chapter4已经有 LLM、Tool、MCPchapter5额外接入了 Skill 体系- Skill 既会影响系统提示词,也会通过工具供 LLM 主动调用
2. 什么是 Skill
这是这一章最关键的概念。
你可以把 Skill 理解成:
一份按主题组织好的“专业操作说明书”。
比如某个技能可能专门教 Agent:
- 如何做代码审查
- 如何写日报
- 如何执行某种固定脚本
- 如何处理某类任务
它和普通工具不一样。
2.1 Tool 和 Skill 的区别
用生活比喻最容易理解:
Tool像螺丝刀、扳手、锤子Skill像“木工手册”、“电工手册”、“油漆工手册”
也就是说:
- 工具负责“动手”
- 技能负责“指导怎么更专业地动手”
2.2 为什么要有 Skill
如果什么知识都直接塞进系统提示词,会有几个问题:
- 提示词会越来越长
- 很多技能平时根本用不到
- 每次请求都带全部技能,浪费 token
所以作者设计成:
- 启动时只发现技能摘要
- 真正需要时再完整激活
这就像:
书架上先摆目录卡片,真要看哪本书时再把整本书拿下来。
3. main.go: 程序如何把 Skill 装进去
chapter5/main.go 的前半段和 chapter4 很像:
- 初始化日志
- 读取配置
- 创建 LLM Client
- 创建 Tool Registry
- 注册 shell
- 连接 MCP
但后半段多了关键几步:
skillRegistry := skill.NewRegistry(config.SkillsDir)
if err := skillRegistry.Discover(); err != nil {
log.Fatal().Err(err).Msg("发现Skill失败")
}
toolRegistry.Register(tool.NewActivateSkillTool(skillRegistry))
toolRegistry.Register(tool.NewListSkillsTool(skillRegistry))
myAgent := agent.NewAgent("MyAgent", "", 10, client, toolRegistry, skillRegistry)
这里的重点是:
- 创建一个
Skill Registry - 扫描
SkillsDir目录,发现技能 - 注册两个和技能相关的工具
- 创建 Agent 时,把
skillRegistry也注入进去
3.1 main.go 的新流程图
3.2 新增的配置项
在 types/config.go 里多了:
SkillsDir string `json:"skills_dir" yaml:"skills_dir"`
这表示程序现在还需要知道:
- 你的技能目录在哪
所以这章的配置不仅要告诉程序怎么连模型、怎么连 MCP,还要告诉它去哪里找技能文件。
4. Agent 有什么变化
先看 agent/agent.go:
type Agent struct {
name string
llmClient llm.Client
toolRegistry *tool.Registry
skillRegistry *skill.Registry
systemPrompt string
maxIterations int
}
与 chapter4 相比,多出来一个:
skillRegistry *skill.Registry
这意味着:
Agent 现在不仅知道自己有哪些工具,还知道有哪些技能可用。
4.1 NewAgent 的新职责
NewAgent(...) 也变成要同时接收:
toolRegskillReg
说明技能系统已经不是外挂,而是 Agent 的正式组成部分。
5. loop.go 最大的变化: System Prompt 变聪明了
这一章里,loop 的主体逻辑其实和 chapter4 基本一致。
真正的关键变化,在 buildSystemPrompt():
func (a *Agent) buildSystemPrompt() string {
prompt := a.systemPrompt
if prompt == "" {
prompt = `你是一个专业的AI Agent助手。你可以使用提供的工具来完成用户的任务。`
}
skillPromt := a.skillRegistry.BuildSkillsPrompt()
if skillPromt != "" {
prompt += "\n\n" + skillPromt
}
return prompt
}
这段代码的意思是:
- 先生成普通系统提示词
- 再把“当前可用技能摘要”拼接进去
换句话说,大模型每轮开始时就会先知道:
- 现在有哪些技能名字
- 每个技能大概是干什么的
- 如果任务匹配,应该调用
activate_skill
5.1 这像什么
这很像给新员工的入职页再加一份:
“公司内部可用专家手册目录”
目录先给你看,但不会一次把所有手册全文塞给你。
如果你发现任务适合某本手册,再主动去申请打开。
5.2 技能提示词大概长什么样
BuildSkillsPrompt() 会构造类似:
最后关键语句:当任务匹配某个技能时,使用 activate_skill工具来激活它。
## 可用技能
以下是可以激活的专业技能:
- **daily-script**: 执行每日脚本的规范说明
- **code-review**: 进行代码审查的流程与标准
当任务匹配某个技能时,使用 `activate_skill` 工具来激活它。
这相当于把“技能目录卡片”直接放进了模型上下文。
6. types/skill.go: Skill 的数据结构长什么样
这个文件很短,但很重要。
6.1 SkillMeta
type SkillMeta struct {
Name string `yaml:"name"`
Description string `yaml:"description"`
}
它可以理解为“技能名片”:
- 名字
- 简介
6.2 SkillInfo
type SkillInfo struct {
SkillMeta
Path string
}
这是“带路径的技能摘要”。
除了名字和描述,还知道:
- 这个技能文件夹在哪
6.3 Skill
type Skill struct {
Info SkillInfo
Instructions string
}
这是完整技能对象。
它不仅有元数据,还有:
Instructions
也就是技能正文内容
可以画成一个小 UML:
7. skill/loader.go: 技能是怎么被发现和读取的
如果说 Registry 是技能管理员,那么 Loader 就是档案管理员。
7.1 Loader 的职责
Loader 主要负责两件事:
- 扫描技能目录,发现有哪些技能
- 在需要时,把某个技能完整加载出来
7.2 DiscoverSkills: 先只读目录,不读全文
func (l *Loader) DiscoverSkills() ([]types.SkillInfo, error)
它的逻辑可以概括成:
注意这里的一个关键设计:
- 发现阶段只读元数据
- 不把技能正文全文一股脑全读进来
这是典型的“懒加载”思想。
7.3 LoadSkill: 真正用时再完整加载
func (l *Loader) LoadSkill(skillPath string) (*types.Skill, error)
这一步会:
- 打开对应目录下的
SKILL.md - 解析 frontmatter
- 读取正文 body
- 组装成完整
Skill
也就是说,只有当 Agent 真要激活某个技能时,正文才会进内存。
7.4 SKILL.md 的格式要求
这章代码要求技能文件使用类似 frontmatter 的格式:
---
name: daily-script
description: 执行每日脚本的步骤说明
---
这里是技能正文...
extractFrontmatter() 会强制要求:
- 文件必须以
---开头 - frontmatter 必须闭合
否则就报错。
7.5 为什么要 frontmatter
因为这样同一个文件里可以同时放:
- 结构化元数据
- 人类可读的详细说明
这非常适合技能文档这种场景。
7.6 validateSkillName: 为什么管得这么严
名字校验规则包括:
- 最长 64 字符
- 不能以
-开头或结尾 - 不能有连续
-- - 只能包含小写字母、数字、连字符
原因其实很务实:
- 技能名要给模型看
- 也要给工具参数传
- 还要作为目录名
如果命名随意,很容易出歧义或产生奇怪错误。
所以这里其实是在给系统“立规矩”。
8. skill/registry.go: 技能管理员怎么工作
这个文件是 Skill 系统的核心。
8.1 Registry 里有什么
type Registry struct {
loader *Loader
skillInfos []types.SkillInfo
loadedSkills map[string]*types.Skill
mu sync.RWMutex
}
这几个字段可以这样理解:
loader
负责读文件skillInfos
技能摘要缓存loadedSkills
已经激活过的完整技能缓存mu
并发锁
8.2 Discover: 启动时建立“技能目录卡”
func (r *Registry) Discover() error
它会调用 loader.DiscoverSkills(),然后把结果缓存到 skillInfos。
你可以把这一步理解成:
图书管理员先把馆里所有书的目录卡整理好。
8.3 GetSkillInfos: 给系统提示词用
func (r *Registry) GetSkillInfos() []types.SkillInfo
这一步返回的是技能摘要,不是全文。
用途通常是:
- 给
list_skills工具展示 - 给
BuildSkillsPrompt()拼接目录
8.4 BuildSkillsPrompt: 把技能目录翻译给模型看
这是本章最关键的方法之一。
它会把 skillInfos 拼成一段 prompt 文本,让模型知道:
- 哪些技能存在
- 技能分别擅长什么
- 想用时该调用
activate_skill
这里的设计非常巧妙,因为它实现了:
“先给模型看技能目录,再让模型按需拉取技能全文”
8.5 ActivateSkill: 真正激活某个技能
这个方法的流程可以画成:
这里的亮点有两个:
- 按需加载
- 加载后缓存
这很像应用第一次打开某个大文件会慢一点,但后面再打开就快了。
9. tool/skill_tools.go: 给 LLM 用的技能工具
这一章里最有意思的点是:
Skill 不是直接自动激活,而是被包装成工具,让 LLM 自己决定什么时候激活。
这体现了一个 Agent 设计思想:
- 框架提供能力
- 模型决定策略
9.1 activate_skill 工具
ActivateSkillTool 对外暴露的工具名是:
"activate_skill"
参数 schema 很简单:
{
"skill_name": "xxx"
}
它的作用是:
- 解析参数
- 调用
registry.ActivateSkill(name) - 把技能正文作为字符串返回给模型
返回内容大概像:
技能 'daily-script', 文件目录: /path/to/skill, 已激活。
这里是完整技能说明...
这意味着什么?
意味着:
一旦模型调用了
activate_skill,技能正文就会像工具结果一样,被放回消息上下文。
从下一轮开始,模型就真正“学会了”这份技能说明。
9.2 list_skills 工具
ListSkillsTool 的职责很简单:
- 列出当前所有可用技能及描述
虽然系统提示词里已经有技能列表,但这个工具依然有价值,因为:
- 模型可能想再次确认
- 某些场景下需要工具返回格式化列表
- 它给模型一个显式查询通道
9.3 这两个工具和 Skill Registry 的关系
10. Agent Loop 在 chapter5 中是怎么跑的
虽然 loop.go 的大体逻辑没变,但在 chapter5 中,它具备了一个新的策略能力:
10.1 以前
模型只能在这些对象里选:
- 直接回答
- 调 shell
- 调 MCP 工具
10.2 现在
模型多了两个新选择:
list_skillsactivate_skill
所以在 chapter5 里,Agent 的一轮循环可能变成:
- 用户说“执行我的每日脚本”
- 模型发现这像某个技能
- 调用
activate_skill - 技能正文回到上下文
- 模型根据技能说明决定执行哪些工具
- 调 shell / MCP 工具
- 最终完成任务
10.3 新版流程图
11. 这一章的设计精髓: 两级上下文
如果你是纯新手,我特别建议记住这章最有价值的思想:
chapter5把上下文分成了“轻量目录层”和“详细正文层”。
也就是:
11.1 第一层: 技能目录层
通过 BuildSkillsPrompt() 注入
特点:
- 轻量
- 便宜
- 每次都能带
- 告诉模型“有哪些能力”
11.2 第二层: 技能正文层
通过 activate_skill 注入
特点:
- 详细
- 更长
- 只在需要时加载
- 告诉模型“具体怎么做”
这是一种非常实用的上下文优化思路。
很多真实 Agent 系统都会类似这样做:
- 先检索目录
- 再拉正文
- 而不是一开始就把全量知识塞进去
12. 和 chapter4 的关系
你可以把 chapter5 视作 chapter4 + Skill 插件层。
下面这张对比图很适合记忆:
一句话概括变化:
chapter4是“工具型 Agent”chapter5是“工具型 Agent + 可按需加载技能的 Agent”
13. 这一章里你最该学会的源码阅读点
如果你想真的把这章吃透,建议重点看这几处:
13.1 buildSystemPrompt()
这是 Skill 系统真正影响模型行为的入口。
13.2 BuildSkillsPrompt()
这是“技能目录层”的生成器。
13.3 ActivateSkill()
这是“技能正文层”的入口。
13.4 ActivateSkillTool.Execute()
这是 Skill 如何被包装成 Tool 的关键桥梁。
14. 用生活比喻把整章再讲一遍
把 chapter5 想成一家升级后的智能事务所:
chapter4时,事务所里只有通用员工和工具柜chapter5时,又新增了一个“专家知识库”
工作流程就变成:
- 用户来办事
- 总调度员先看任务
- 如果觉得需要专家知识,就去查“技能目录”
- 找到对应技能后,把那本说明书拿出来
- 再决定调用哪些工具执行
- 完成任务
所以 Skill 就像:
- 不是手脚
- 而是脑中的“行业手册”
15. 建议新手按什么顺序读这章源码
推荐顺序:
main.go
看 Skill 是怎么接进系统的。agent/agent.go
看 Agent 多了什么字段。agent/loop.go
看系统提示词怎么注入技能目录。types/skill.go
看 Skill 的数据结构。skill/loader.go
看技能文件怎么发现、怎么解析。skill/registry.go
看技能怎么缓存和激活。tool/skill_tools.go
看技能为什么会变成 LLM 可调用工具。- 其余
tool/llm/mcp
这些大部分沿用chapter4的思路。
16. 一句话总复盘
如果你忘了这章的细节,只要记住这句话:
chapter5的核心,是让 Agent 先知道“有哪些技能可用”,再在需要时通过activate_skill按需加载完整技能说明,从而让模型做事更专业、更节省上下文。
17. 核心模块关系图
18. 一句超短版
chapter5 = chapter4 + 技能目录 + 按需激活技能正文
更多推荐




所有评论(0)