第 4 章(上):Skill——把重复操作封装成技能
我遇到了什么问题
第 3 章解决了"上下文从哪来"的问题——记忆、检索、规范,三者构成 Agent 的知识体系。
但知识体系建好之后,新的问题出现了。
问题 1:创建需求文档,每次都要从头教
我的项目有文档先行的规范——新功能开发前,必须先写需求分析文档。文档有固定的格式:文件名用 MMDD-HHMMSS-需求名.md,放在 Documents/需求分析记录 目录下,内容要包含业务背景、流程分析、架构设计、修改清单。
但每次让 Agent 写需求文档,它都要我重新说一遍:放哪个目录?文件名什么格式?文档结构是什么?模块架构有几层?说了第一遍,第二篇需求文档来的时候,又从头问起。
问题 2:执行测试,每次都像第一次
我有一套完整的组件测试框架——支持工作流、节点、CLI、LLM 等十几种组件类型的测试。测试命令是 dotnet run --project Components.Test -- -w WorkflowName,参数不同测试类型不同。
Agent 每次执行测试,都要先"理解"一遍测试框架结构:这个目录是工作流测试还是节点测试?该用 -w 还是 -n?canvas.json 和 input.json 哪个是必需的?这些知识我已经说过无数遍了,但它每次都像第一次接触这个项目。
问题 3:Git 推送,细节总是忘
提交代码看起来简单,但我的项目有提交规范——commit message 要用 type(scope): subject 格式,scope 要从固定模块列表里选,多行提交信息要用编辑器模式而不是 -m 参数。
Agent 每次提交,要么忘了 scope,要么在 PowerShell 里用 -m 写多行结果报错,要么提交前不审查变更。都是小问题,但每次都烦一遍。
本质:重复操作没有标准化
回过头看,这三个问题的本质是一样的:重复的操作没有被标准化。
创建需求文档、执行测试、Git 推送——这些操作每天都在做,步骤几乎一样,但每次都靠口头教。这不就是带新人时的感觉吗——你教过他一次,但他下次还是从头来。
换句话说,Agent 缺的不是记忆力,缺的是一套"操作手册"。
我做了什么决策
决策 1:Skill——把反复说的操作手册写成文件
一开始我的做法是:每次开始干活前,先花几分钟把规范再说一遍。"文档放这个目录,文件名用时间戳,格式是 MMDD-HHMMSS"。说了几次之后意识到,这不就是带新人时该写操作手册的事吗?
于是我把这些反复说的操作手册写成了文件——这就是 Skill。
一个 Skill 就是一份标准化的操作手册——输入是什么、输出是什么、放在哪里、遵循什么规范。比如我实际用的创建需求文档的 Skill(节选核心部分):
markdown
---
name: create-proposal
description: 创建需求分析设计文档,遵循"文档先行、架构清晰、
自顶向下设计、自底向上实现"的核心思想。
当用户提出新功能需求、模块修改、Bug修复等需要编写
需求分析文档时使用。通过 /create-proposal 快捷命令触发。
---
# 需求分析设计 Skill
> **需求文档路径**: `Documents\需求分析记录`
## 核心思想
**文档先行,架构清晰,自顶向下设计,自底向上实现。**
## 文件名格式
`MMDD-HHMMSS-具体需求名字.md`
## 工作流程
1. 获取当前时间,生成时间戳文件名
2. 在 `Documents/需求分析记录` 目录创建文档
3. 按模板填写:需求分析 → 业务流程 → 架构设计 → 修改清单
4. 确认方案后再编码
## 编码顺序(自底向上)
Entity → Repository → Engine → Implementation → Interface → Module注册这个 Skill 不到 30 行(完整版有 200 多行,包含模块架构约束、命名规范、完整示例等),但核心信息已经够了:放哪个目录、文件名什么格式、工作流程是什么、编码顺序是什么。
有了这个 Skill,Agent 每次写需求文档都会自动遵循规范——文件放对位置、命名格式正确、内容结构完整。
同样的思路,我把执行测试的操作手册也写成了 Skill——测试框架有哪些参数、哪种测试用哪个命令、canvas.json 和 input.json 分别是什么,全写进去了。Git 推送也是——commit 格式、scope 列表、PowerShell 兼容性注意事项,一次写清,永久生效。
同样的事情不用再说第二遍。
决策 2:Skill 的系统化管理
当 Skill 数量超过 10 个之后,新的问题出现了:怎么让 Agent 知道有哪些 Skill 可以用?
最直觉的做法:把所有 Skill 的路径硬编码到代码里。
csharp
// 硬编码方式
var skillPaths = new List<string>
{
"/path/to/skill1",
"/path/to/skill2",
// 每加一个 Skill 就要改代码
};这种方式的问题是:每加一个 Skill 就要改代码、重新编译。
更好的做法:让各模块自己报告 Skill 路径。
csharp
// 接口定义:各模块实现此接口,报告自己的 Skill 路径
public interface IAgentSkillsContributor
{
IReadOnlyList<string> GetSkillPaths();
}
// 管理器:从 DI 收集所有贡献者的路径,统一构建 Provider
internal class AgentSkillsManager
{
private readonly IEnumerable<IAgentSkillsContributor> _contributors;
public AgentSkillsProvider BuildProvider()
{
var allPaths = _contributors
.SelectMany(c => c.GetSkillPaths())
.Distinct()
.ToList();
return new AgentSkillsProvider(allPaths, ...);
}
}这个设计的好处:
- 新增 Skill 不需要改代码:只需要在对应模块的
GetSkillPaths()里加一个路径 - 模块解耦:每个模块管自己的 Skill,不需要知道其他模块有什么
- 延迟加载:首次 LLM 调用时才扫描 Skill 文件,启动时不花时间
然后在 AiAgentModule 中注册:
csharp
// 核心层注册默认空实现(无任何 Skill)
services.TryAddSingleton<IAgentSkillsContributor, EmptySkillsContributor>();
// Integration 层可以注册自己的实现,覆盖默认
// services.AddSingleton<IAgentSkillsContributor, MyIntegrationSkillsContributor>();这里用了 Provider 可覆盖模式——核心层注册默认空实现,业务层通过提前注册来覆盖。这是整个 Components 项目的核心设计模式之一。

决策 3:Skill 的渐进式加载——解决“撑爆上下文”
Skill 系统化管理之后,新的问题来了:当 Skill 数量超过 20 个,如果把所有 Skill 的全文都塞进 System Prompt,上下文窗口直接爆了。
这就是扁平化加载的问题——每次 LLM 调用,所有 Skill 全文一次性拼进去:
【扁平式:所有 Skill 全文一次性塞入】
System Prompt = 角色定义 + 记忆 + ▼全部 Skill 全文▼ + 工具指引
↑ 20 个 Skill × 平均 200 行 = 4000 行
↑ 上下文窗口直接爆了解法是渐进式加载(Progressive Disclosure)——分三层,按需加载:
第 1 层:广告(Advertise)
→ System Prompt 中只注入 Skill 的 name + description(几行)
→ LLM 知道"有哪些 Skill 可用",但还没看全文
第 2 层:加载(Load)
→ LLM 判断当前任务需要某个 Skill → 调用 load_skill("create-csharp-module")
→ 框架返回该 Skill 的 SKILL.md 全文
第 3 层:读取/执行(Read/Execute)
→ LLM 需要 Skill 的附属资源 → 调用 read_skill_resource(...)
→ 需要执行脚本 → 调用 run_skill_script(...)关键思路:LLM 自己决定什么时候加载哪个 Skill。 不是我们帮它全塞进去,而是它看到目录之后自己判断。
这就像你去图书馆——不是把所有书搬到你面前,而是先看目录索引,需要哪本再取。
框架怎么实现的
微软的 Microsoft.Agents.AI 框架提供了 AgentSkillsProvider,它就是实现这个三层加载的核心类:
csharp
// AgentSkillsProvider 作为 AIContextProvider 注入到 Agent 管道
// 每次 LLM 调用前,框架自动执行 ProvideAIContextAsync()
//
// 它做两件事:
// 1. Instructions → 生成 <available_skills> 列表(只有 name + description)
// 2. Tools → 注册 load_skill / read_skill_resource / run_skill_script 三个工具
//
// LLM 看到 available_skills 列表后,如果需要某个 Skill,就调用 load_skill 工具在我们的产品中,AgentSkillsManager 负责从 DI 收集所有 Skill 路径,构建 AgentSkillsProvider:
各业务模块(Integration、Workflow...)
│ 各自实现 IAgentSkillsContributor.GetSkillPaths()
│ 返回各自的 Skills/ 目录
▼
AgentSkillsManager(核心层)
│ 汇总所有路径 → 构建 AgentSkillsProvider
│ 作为 AIContextProvider 注册到管道
▼
Agent 每次调 LLM 前,框架自动执行:
├── Instructions: "可用 Skill 列表(仅 name + desc)"
└── Tools: [load_skill, read_skill_resource, run_skill_script]这就是为什么 Skill 文件必须有 YAML Frontmatter——name 和 description 字段就是给第 1 层"广告"用的。没有 Frontmatter,LLM 连 Skill 的名字都看不到,更别说按需加载了。
上下文还是太多怎么办?
渐进式加载解决了"Skill 太多塞不下"的问题。但 Skill 加载完之后,长对话本身也会撑爆上下文——工具调用结果越积越多、历史消息越来越长。这个问题在第 3 章讲过,这里不展开,核心思路是上下文压缩(Context Compaction)——在消息到达窗口上限之前,主动压缩历史。
代码里长成了什么样
Skill 的目录结构
.qoder/skills/
├── create-proposal/ ← 创建需求分析文档(最常用)
│ └── SKILL.md
├── test-component/ ← 执行组件测试(最常用)
│ └── SKILL.md
├── git-push/ ← Git 提交推送(最常用)
│ └── SKILL.md
├── create-csharp-module/ ← 创建 C# 模块
│ └── SKILL.md
├── create-web-module/ ← 创建前端模块
│ ├── SKILL.md
│ └── web-module-standard/ ← 子规范
│ ├── SKILL.md
│ └── templates/
├── design-wpf-ui/ ← 设计 WPF 界面
│ └── SKILL.md
├── framework/ ← 框架参考文档(作为 Skill 注入)
│ ├── dictionary-extensions.md
│ ├── enum-extensions.md
│ └── ...(12 份基础类库文档)
└── ...(还有十几个 Skill)Skill 在 Agent 调用链中的位置
用户发消息
↓
Agent 准备调用 LLM
↓
框架自动执行所有 AIContextProvider:
├── MemoryFactsContextProvider → 注入记忆事实
├── CompactionProvider → 按需压缩历史上下文
├── MonitoringContextProvider → 记录监控数据
├── TitleContextProvider → 更新会话标题
└── AgentSkillsProvider → 注入 Skill 目录(仅 name+desc)
+ 注册 load_skill 等工具
↓
所有上下文合并 → 调用 LLM
↓
LLM 判断需要某个 Skill → 调用 load_skill("xxx") → 获取全文
↓
LLM 输出 → 返回结果业界怎么做的:Skill 加载机制对比
LangGraph 没有 Skill 概念——所有知识编码到图节点的系统提示词里。每个节点有自己的 prompt,节点之间不共享。好处是简单直接,坏处是知识不能复用——两个节点需要相同知识时,得写两遍。
微软 Agent Framework (MAF) 通过 AgentSkillsProvider 实现渐进式加载——先注入 Skill 目录(仅 name + desc),LLM 按需调用 load_skill 工具获取全文。这个设计最接近我的思路,也是我在产品中实际使用的方案。
Hermes 用 Skill 文件 + 插件系统来定义 Agent 的能力边界。每个 Skill 是一份 Markdown 文档,定义了 Agent 在特定场景下应该怎么做。Hermes 还有一个"Curator"机制——自动维护 Skill 的有效性,过时的 Skill 会被标记或移除。这个思路很好,但我还没实现。
AgentScope Java 的做法是"知识注入"——通过 AIContextProvider 在 Agent 执行前注入上下文。没有独立的 Skill 概念,所有知识通过 ContextProvider 统一注入。
对比总结
| 框架 | Skill 加载方式 | 特点 |
|---|---|---|
| LangGraph | 无 Skill,知识写进节点 prompt | 简单直接,但知识不能复用 |
| MAF | AgentSkillsProvider 渐进式加载 | 最接近我的设计,按需加载 |
| Hermes | Skill 文件 + Curator 自动维护 | 自动化高,但还没实现类似机制 |
| AgentScope | AIContextProvider 统一注入 | 解耦彻底,但没有独立 Skill 概念 |
关键认知
这个阶段做完,我有了两个关键认知:
Skill 是"肌肉记忆"——解决"怎么做"的问题,让 Agent 不用每次重新教。
Skill 的价值不在于省时间,在于让输出质量稳定。 不管是我口头教还是 Agent 自己摸索,每次做出来的结果都可能不一样。但有了 Skill,第 1 次和第 100 次的输出结构是一样的。
但光有 Skill 还不够。Agent 还需要知道"不能怎么做"——编码约束,防止它犯已知的错误。
比如"异常不能吞掉,必须上抛"——这不是"怎么做一件事",而是"做这件事时必须遵守什么"。这类东西不适合写成 Skill,因为它是每条代码都要遵守的约束,不是按需加载的操作手册。
这就是 Rule 要解决的问题,下一章来讲。