文章目录

本系列将从零开始,用 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 相比,最大的新增就是这两块:

  1. skill/
    新增技能发现、加载、注册机制。
  2. tool/skill_tools.go
    新增给 LLM 使用的技能工具。

可以先看一张总图:

用户任务

Agent

LLM

Tool Registry

Skill Registry

Skill Loader

skills 目录 / SKILL.md

shell

MCP tools

activate_skill

list_skills

这张图想表达的是:

  • chapter4 已经有 LLM、Tool、MCP
  • chapter5 额外接入了 Skill 体系
  • Skill 既会影响系统提示词,也会通过工具供 LLM 主动调用

2. 什么是 Skill

这是这一章最关键的概念。

你可以把 Skill 理解成:

一份按主题组织好的“专业操作说明书”。

比如某个技能可能专门教 Agent:

  • 如何做代码审查
  • 如何写日报
  • 如何执行某种固定脚本
  • 如何处理某类任务

它和普通工具不一样。

2.1 Tool 和 Skill 的区别

用生活比喻最容易理解:

  • Tool 像螺丝刀、扳手、锤子
  • Skill 像“木工手册”、“电工手册”、“油漆工手册”

也就是说:

  • 工具负责“动手”
  • 技能负责“指导怎么更专业地动手”

2.2 为什么要有 Skill

如果什么知识都直接塞进系统提示词,会有几个问题:

  1. 提示词会越来越长
  2. 很多技能平时根本用不到
  3. 每次请求都带全部技能,浪费 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)

这里的重点是:

  1. 创建一个 Skill Registry
  2. 扫描 SkillsDir 目录,发现技能
  3. 注册两个和技能相关的工具
  4. 创建 Agent 时,把 skillRegistry 也注入进去

3.1 main.go 的新流程图

启动程序

读取配置

创建 LLM Client

创建 Tool Registry

注册 shell 工具

连接 MCP 并注册 MCP 工具

创建 Skill Registry

Discover skills

注册 list_skills / activate_skill 工具

创建 Agent(含 skillRegistry)

运行 Agent

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(...) 也变成要同时接收:

  • toolReg
  • skillReg

说明技能系统已经不是外挂,而是 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
}

这段代码的意思是:

  1. 先生成普通系统提示词
  2. 再把“当前可用技能摘要”拼接进去

换句话说,大模型每轮开始时就会先知道:

  • 现在有哪些技能名字
  • 每个技能大概是干什么的
  • 如果任务匹配,应该调用 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:

SkillMeta

+string Name

+string Description

SkillInfo

+string Name

+string Description

+string Path

Skill

+SkillInfo Info

+string Instructions


7. skill/loader.go: 技能是怎么被发现和读取的

如果说 Registry 是技能管理员,那么 Loader 就是档案管理员。

7.1 Loader 的职责

Loader 主要负责两件事:

  1. 扫描技能目录,发现有哪些技能
  2. 在需要时,把某个技能完整加载出来

7.2 DiscoverSkills: 先只读目录,不读全文

func (l *Loader) DiscoverSkills() ([]types.SkillInfo, error)

它的逻辑可以概括成:

进入 skillsDir

遍历子目录

检查是否存在 SKILL.md

只解析 frontmatter 元数据

校验 name 与目录名一致

生成 SkillInfo

加入结果列表

注意这里的一个关键设计:

  • 发现阶段只读元数据
  • 不把技能正文全文一股脑全读进来

这是典型的“懒加载”思想。

7.3 LoadSkill: 真正用时再完整加载

func (l *Loader) LoadSkill(skillPath string) (*types.Skill, error)

这一步会:

  1. 打开对应目录下的 SKILL.md
  2. 解析 frontmatter
  3. 读取正文 body
  4. 组装成完整 Skill

也就是说,只有当 Agent 真要激活某个技能时,正文才会进内存。

7.4 SKILL.md 的格式要求

这章代码要求技能文件使用类似 frontmatter 的格式:

---
name: daily-script
description: 执行每日脚本的步骤说明
---

这里是技能正文...

extractFrontmatter() 会强制要求:

  • 文件必须以 --- 开头
  • frontmatter 必须闭合

否则就报错。

7.5 为什么要 frontmatter

因为这样同一个文件里可以同时放:

  1. 结构化元数据
  2. 人类可读的详细说明

这非常适合技能文档这种场景。

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: 真正激活某个技能

这个方法的流程可以画成:

没有

没有

找到了

收到 skill name

loadedSkills 里已有吗?

直接返回缓存中的 Skill

在 skillInfos 中查路径

找到了吗?

报错: Skill不存在

Loader.LoadSkill(path)

放入 loadedSkills 缓存

返回完整 Skill

这里的亮点有两个:

  1. 按需加载
  2. 加载后缓存

这很像应用第一次打开某个大文件会慢一点,但后面再打开就快了。


9. tool/skill_tools.go: 给 LLM 用的技能工具

这一章里最有意思的点是:

Skill 不是直接自动激活,而是被包装成工具,让 LLM 自己决定什么时候激活。

这体现了一个 Agent 设计思想:

  • 框架提供能力
  • 模型决定策略

9.1 activate_skill 工具

ActivateSkillTool 对外暴露的工具名是:

"activate_skill"

参数 schema 很简单:

{
  "skill_name": "xxx"
}

它的作用是:

  1. 解析参数
  2. 调用 registry.ActivateSkill(name)
  3. 把技能正文作为字符串返回给模型

返回内容大概像:

技能 'daily-script', 文件目录: /path/to/skill, 已激活。

这里是完整技能说明...

这意味着什么?

意味着:

一旦模型调用了 activate_skill,技能正文就会像工具结果一样,被放回消息上下文。

从下一轮开始,模型就真正“学会了”这份技能说明。

9.2 list_skills 工具

ListSkillsTool 的职责很简单:

  • 列出当前所有可用技能及描述

虽然系统提示词里已经有技能列表,但这个工具依然有价值,因为:

  1. 模型可能想再次确认
  2. 某些场景下需要工具返回格式化列表
  3. 它给模型一个显式查询通道

9.3 这两个工具和 Skill Registry 的关系

SKILL.md Loader SkillRegistry activate_skill ToolRegistry 大模型 SKILL.md Loader SkillRegistry activate_skill ToolRegistry 大模型 Execute("activate_skill", {"skill_name":"daily-script"}) Execute(params) ActivateSkill("daily-script") LoadSkill(path) 读取 SKILL.md 返回元数据+正文 Skill Skill "已激活 + 技能正文" tool result

10. Agent Loop 在 chapter5 中是怎么跑的

虽然 loop.go 的大体逻辑没变,但在 chapter5 中,它具备了一个新的策略能力:

10.1 以前

模型只能在这些对象里选:

  • 直接回答
  • 调 shell
  • 调 MCP 工具

10.2 现在

模型多了两个新选择:

  • list_skills
  • activate_skill

所以在 chapter5 里,Agent 的一轮循环可能变成:

  1. 用户说“执行我的每日脚本”
  2. 模型发现这像某个技能
  3. 调用 activate_skill
  4. 技能正文回到上下文
  5. 模型根据技能说明决定执行哪些工具
  6. 调 shell / MCP 工具
  7. 最终完成任务

10.3 新版流程图

用户输入

system prompt + 可用技能目录

LLM 第一次思考

需要技能吗?

调用 activate_skill

技能正文回到 messages

LLM 再次思考

需要工具吗?

执行 shell / MCP / 其他工具

工具结果回到 messages

输出最终答案


11. 这一章的设计精髓: 两级上下文

如果你是纯新手,我特别建议记住这章最有价值的思想:

chapter5 把上下文分成了“轻量目录层”和“详细正文层”。

也就是:

11.1 第一层: 技能目录层

通过 BuildSkillsPrompt() 注入

特点:

  • 轻量
  • 便宜
  • 每次都能带
  • 告诉模型“有哪些能力”

11.2 第二层: 技能正文层

通过 activate_skill 注入

特点:

  • 详细
  • 更长
  • 只在需要时加载
  • 告诉模型“具体怎么做”

这是一种非常实用的上下文优化思路。

很多真实 Agent 系统都会类似这样做:

  • 先检索目录
  • 再拉正文
  • 而不是一开始就把全量知识塞进去

12. 和 chapter4 的关系

你可以把 chapter5 视作 chapter4 + Skill 插件层

下面这张对比图很适合记忆:

chapter5

Agent

LLM

Tool Registry

Skill Registry

Skill Loader

shell / MCP

activate_skill / list_skills

chapter4

Agent

LLM

Tool Registry

shell / MCP

一句话概括变化:

  • 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 时,又新增了一个“专家知识库”

工作流程就变成:

  1. 用户来办事
  2. 总调度员先看任务
  3. 如果觉得需要专家知识,就去查“技能目录”
  4. 找到对应技能后,把那本说明书拿出来
  5. 再决定调用哪些工具执行
  6. 完成任务

所以 Skill 就像:

  • 不是手脚
  • 而是脑中的“行业手册”

15. 建议新手按什么顺序读这章源码

推荐顺序:

  1. main.go
    看 Skill 是怎么接进系统的。
  2. agent/agent.go
    看 Agent 多了什么字段。
  3. agent/loop.go
    看系统提示词怎么注入技能目录。
  4. types/skill.go
    看 Skill 的数据结构。
  5. skill/loader.go
    看技能文件怎么发现、怎么解析。
  6. skill/registry.go
    看技能怎么缓存和激活。
  7. tool/skill_tools.go
    看技能为什么会变成 LLM 可调用工具。
  8. 其余 tool/llm/mcp
    这些大部分沿用 chapter4 的思路。

16. 一句话总复盘

如果你忘了这章的细节,只要记住这句话:

chapter5 的核心,是让 Agent 先知道“有哪些技能可用”,再在需要时通过 activate_skill 按需加载完整技能说明,从而让模型做事更专业、更节省上下文。


17. 核心模块关系图

Agent

-llmClient Client

-toolRegistry Registry

-skillRegistry SkillRegistry

+Run(ctx, userInput)

-buildSystemPrompt()

SkillRegistry

-loader Loader

-skillInfos []SkillInfo

-loadedSkills map[string]Skill

+Discover()

+GetSkillInfos()

+BuildSkillsPrompt()

+ActivateSkill(name)

Loader

-skillsDir string

+DiscoverSkills()

+LoadSkill(skillPath)

ActivateSkillTool

-registry SkillRegistry

+Execute(ctx, params)

ListSkillsTool

-registry SkillRegistry

+Execute(ctx, params)

ToolRegistry

+Register(tool)

+Execute(ctx, name, params)

+ToLLMTools()

Skill

+Info SkillInfo

+Instructions string


18. 一句超短版

chapter5 = chapter4 + 技能目录 + 按需激活技能正文

Logo

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

更多推荐