随着 Microsoft Agent Framework(MAF)1.0.0-rc1 版本的发布,距离其 1.0 正式版亮相应该已经不远了。
目前 MAF 1.0.0-rc2 已支持 Agent Skills,虽然这一版主要针对静态内容,尚未开放脚本执行能力,但本系列文章将逐步分享如何进一步拓展其功能,敬请持续关注。
以下内容选自我精心打造的《.NET+AI | 智能体开发进阶》课程,如需系统学习,不妨阅读原文了解详情。
MAF 如何集成 Agent Skills — 让 Agent 拥有领域专长
MAF 1.0.0-rc2 已支持 Agent Skills,虽然这一版主要针对静态内容,尚未开放脚本执行能力,但已经允许我们为 Agent 注入模块化的领域知识,拓展 Agent的能力边界,接下来我们将带你一步一步的了解 Agent Skills的实现细节和集成路径。
📚 课程目标
本节课将学习 Agent Skills — 一种为 AI Agent 注入领域知识的模块化机制。你将掌握:
-
✅ 理解 Agent Skills 规范及其渐进式披露(Progressive Disclosure)设计模式 -
✅ 掌握 SKILL.md 文件的结构(YAML Frontmatter + Markdown Body + 资源引用) -
✅ 使用 FileAgentSkillsProvider从文件系统发现和加载 Skills -
✅ 理解 load_skill和read_skill_resource两个工具的工作原理 -
✅ 通过费用报销场景实战验证 Agent Skills 的完整工作流程
📋 前置知识
必须掌握 ✅
-
第4章第1-2课: MAF 基础(Agent 创建、多轮对话) -
第4章第7课: Agent Function Calling(函数调用/工具调用) -
第4章第15-16课: AIContextProvider 概念与实践
建议了解 🔵
-
第4章第8课: Agent Plugins(插件机制,与 Skills 互补) -
Markdown 语法: 理解 YAML Frontmatter 格式
预计学习时间: ⏱️ 35-45 分钟
难度级别: ⭐⭐⭐ 中级
💡 说明: Agent Skills 是基于 AIContextProvider 机制的实现,专注于静态领域知识的管理和按需注入。
🧩 第一部分:什么是 Agent Skills?
1.1 问题引入 — Agent 如何获取领域知识?
假设你正在开发一个企业级 AI 助手,需要它精通费用报销政策——包括各类费用限额、审批流程、收据要求等。你有几种选择:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1.2 Agent Skills 规范
Agent Skills 是一种开放规范,定义了如何将领域知识打包为模块化、可复用的知识包(Skill),供 AI Agent 按需使用。
核心设计理念 — 渐进式披露(Progressive Disclosure):
三阶段解释:
|
|
|
|
|
|---|---|---|---|
| 📢 摘要展示(Advertise) |
|
|
|
| 📖 加载(Load) |
|
|
|
| 📎 读取资源(Read) |
|
|
|
❝💡 为什么不一次性全部加载? 渐进式披露通过延迟加载,大幅降低 Token 消耗。一个 Agent 可能注册 20 个 Skills,但每次请求通常只需要 1-2 个。如果全部塞进 System Prompt,可能消耗数万 Token;而渐进式披露将初始开销控制在每个 Skill ~100 tokens。
1.3 Agent Skills vs Function Calling vs RAG
|
|
|
|
|
|---|---|---|---|
| 加载方式 |
|
|
|
| 知识类型 |
|
|
|
| 维护者 |
|
|
|
| 文件格式 |
|
|
|
| 典型场景 |
|
|
|
| 注册方式 | AIContextProviders |
ChatOptions.Tools |
AIContextProviders |
❝💡 Skills 与 Function Calling 是互补关系:Skills 提供"知道什么"(领域知识),Function Calling 提供"能做什么"(操作能力)。一个完善的 Agent 通常两者兼备。
📄 第二部分:SKILL.md 文件剖析
一个 Agent Skill 就是一个文件夹,核心是一个 SKILL.md 文件。让我们剖析它的结构:
2.1 目录结构
skills/
└── expense-report/ # 技能目录(名称与 frontmatter.name 一致)
├── SKILL.md # 技能定义文件(必需)
├── references/ # 参考资料目录
│ └── POLICY_FAQ.md # 费用政策 FAQ
└── assets/ # 资产目录
└── expense-report-template.md # 报销模板
2.2 SKILL.md 结构
SKILL.md 由两部分组成:YAML Frontmatter(元数据)和 Markdown Body(指令内容)。
---
name: expense-report
description: File and validate employee expense reports according to company policy.
---
# Expense Report
## Categories and Limits
| Category | Limit | Receipt | Approval |
|---|---|---|---|
| Meals — solo | $50/day | >$25 | No |
...
## Filing Process
1. Collect receipts...
2. Use template: [assets/expense-report-template.md](assets/expense-report-template.md)
...
## Policy Rules
- For policy questions, consult the FAQ: [references/POLICY_FAQ.md](references/POLICY_FAQ.md)
📌 Frontmatter 规则:
|
|
|
|
|
|---|---|---|---|
name |
|
|
expense-report |
description |
|
|
File and validate... |
📌 Body 中的资源引用:
Body 中可以使用 Markdown 链接引用本地文件。FileAgentSkillLoader 会自动解析这些链接,将其注册为可读取的资源:
<!-- ✅ 支持:相对路径引用 -->
[assets/expense-report-template.md](assets/expense-report-template.md)
[references/POLICY_FAQ.md](references/POLICY_FAQ.md)
<!-- ✅ 支持:带 ./ 前缀 -->
[./references/POLICY_FAQ.md](./references/POLICY_FAQ.md)
<!-- ❌ 不支持:URL 链接(会被忽略) -->
[外部链接](https://example.com/doc.md)
<!-- ❌ 不支持:路径穿越(安全防护) -->
[恶意路径](../../etc/passwd)
2.3 安全机制
FileAgentSkillLoader 内置了多重安全防护:
-
🛡️ 路径穿越防护:资源文件必须位于 Skill 目录内部 -
🛡️ 符号链接检查:防止通过 symlink 逃逸到外部目录 -
🛡️ XML 转义:技能元数据在嵌入提示词前会进行 XML 编码 -
🛡️ 名称验证:仅允许 [a-z0-9-]格式的技能名称
🔬 第三部分:源码架构深度解析
3.1 核心类关系
3.2 FileAgentSkillsProvider — 技能提供者
FileAgentSkillsProvider 是 AIContextProvider 的子类,实现了完整的渐进式披露机制。
构造函数中的初始化流程:
public FileAgentSkillsProvider(string skillPath, ...)
{
// 1️⃣ 创建 Loader,扫描目录发现所有 Skill
this._loader = new FileAgentSkillLoader(this._logger);
this._skills = this._loader.DiscoverAndLoadSkills(skillPaths);
// 2️⃣ 构建技能摘要提示词
this._skillsInstructionPrompt = BuildSkillsInstructionPrompt(options, this._skills);
// 3️⃣ 注册两个 AI 工具
this._tools =
[
AIFunctionFactory.Create(this.LoadSkill,
name: "load_skill",
description: "Loads the full instructions for a specific skill."),
AIFunctionFactory.Create(this.ReadSkillResourceAsync,
name: "read_skill_resource",
description: "Reads a file associated with a skill, such as references or assets."),
];
}
内置的系统提示词模板:
You have access to skills containing domain-specific knowledge and capabilities.
Each skill provides specialized instructions, reference documents, and assets.
<available_skills>
<skill>
<name>expense-report</name>
<description>File and validate employee expense reports...</description>
</skill>
</available_skills>
When a task aligns with a skill's domain:
1. Use `load_skill` to retrieve the skill's instructions
2. Follow the provided guidance
3. Use `read_skill_resource` to read any references or other files mentioned
Only load what is needed, when it is needed.
❝💡 注意:
FileAgentSkillsProvider重写的是ProvideAIContextAsync方法(而非InvokingAsync),返回的AIContext同时包含Instructions(系统提示)和Tools(两个工具函数)。
3.3 FileAgentSkillLoader — 技能发现与加载
FileAgentSkillLoader 负责从文件系统中发现和解析 SKILL.md 文件。
发现流程:
关键代码片段 — YAML 解析:
// 正则匹配 YAML frontmatter(--- 分隔符之间的内容)
privatestaticreadonly Regex s_frontmatterRegex =
new(@"\A\uFEFF?^---\s*$(.+?)^---\s*$",
RegexOptions.Multiline | RegexOptions.Singleline | RegexOptions.Compiled);
// 正则匹配 Markdown 中的本地资源链接
privatestaticreadonly Regex s_resourceLinkRegex =
new(@"\[.*?\]\((\.?\.?/?[\w][\w\-./]*\.\w+)\)", RegexOptions.Compiled);
// 技能名称验证:仅允许小写字母、数字和连字符
privatestaticreadonly Regex s_validNameRegex =
new(@"^[a-z0-9]([a-z0-9\-]*[a-z0-9])?$", RegexOptions.Compiled);
3.4 工具调用链路分析
当 Agent 调用 load_skill 或 read_skill_resource 时:
// load_skill:根据名称返回 SKILL.md 的 Body 部分
private string LoadSkill(string skillName)
{
if (!this._skills.TryGetValue(skillName, out FileAgentSkill? skill))
return$"Error: Skill '{skillName}' not found.";
return skill.Body; # 返回 Markdown 指令内容
}
// read_skill_resource:从磁盘读取资源文件内容
private async Task<string> ReadSkillResourceAsync(
string skillName, string resourceName, CancellationToken cancellationToken)
{
if (!this._skills.TryGetValue(skillName, out FileAgentSkill? skill))
return$"Error: Skill '{skillName}' not found.";
// 委托给 Loader,包含路径安全检查
returnawaitthis._loader.ReadSkillResourceAsync(skill, resourceName, cancellationToken);
}
🚀 第四部分:实战 — 费用报销 Agent
场景说明
我们将创建一个具备费用报销领域知识的 AI Agent,它能够:
-
📋 回答费用报销政策问题(小费能否报销?住宿限额多少?) -
📝 根据模板生成报销报告 -
📖 按需加载 FAQ 和模板等参考资料
步骤 1:环境准备
导入必要的依赖包和 Helper 类。
// 导入 AI Client Helper
#!import ../helper/AIClientHelper.cs
// 导入 MAF 核心包
#!import ../helper/MafHelper.cs
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
Console.WriteLine("✅ 依赖包加载完成");
Console.WriteLine("📦 核心命名空间:");
new {
Microsoft_Agents_AI = "Agent 核心功能 + Skills 支持",
Microsoft_Extensions_AI = "AI 抽象层(IChatClient、AIFunctionFactory)"
}.Display();
步骤 2:准备 Skill 文件
我们将在 Notebook 运行目录下动态创建 expense-report 技能文件。这包含三个部分:
-
SKILL.md — 技能定义(Frontmatter + 指令) -
references/POLICY_FAQ.md — 费用政策 FAQ -
assets/expense-report-template.md — 报销模板
using System.IO;
// 在 Notebook 运行目录下创建 skills 文件结构
var skillsRootPath = Path.Combine(Directory.GetCurrentDirectory(), "skills");
var expenseSkillPath = Path.Combine(skillsRootPath, "expense-report");
var referencesPath = Path.Combine(expenseSkillPath, "references");
var assetsPath = Path.Combine(expenseSkillPath, "assets");
// 确保目录存在
Directory.CreateDirectory(referencesPath);
Directory.CreateDirectory(assetsPath);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1️⃣ 创建 SKILL.md — 技能定义文件(已中文化)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var skillMd = """
---
name: expense-report
description: 按照 Contoso 公司政策填写和审核员工费用报销。适用于费用报销、报销规则、收据要求、支出限额或费用类别等相关问题。
---
# 费用报销(Expense Report)
## 费用类别与限额
| 类别 | 限额 | 收据要求 | 审批 |
|---|---|---|---|
| 单人用餐 | $50/天 | >$25 | 无需 |
| 团队/客户用餐 | $75/人 | 必须 | 总额>$200需经理 |
| 住宿 | $250/晚 | 必须 | 超过3晚需经理 |
| 地面交通 | $100/天 | >$15 | 无需 |
| 机票 | 经济舱 | 必须 | >$1,500需VP |
| 会议/培训 | $2,000/次 | 必须 | 经理+L&D |
| 办公用品 | $100 | 是 | 无需 |
| 软件/订阅 | $50/月 | 是 | >$200/年需经理 |
## 报销流程
1. 收集收据——需包含商家、日期、金额、支付方式。
2. 按上表分类。
3. 使用模板:[assets/expense-report-template.md](assets/expense-report-template.md)。
4. 团队/客户用餐需列出参与人及业务目的。
5. 提交——<$500自动审批;$500–$2,000需经理;>$2,000需VP。
6. 报销:10个工作日内通过银行转账。
## 政策规则
- 需在交易后30天内提交。
- 酒精类费用不予报销。
- 外币:按交易日汇率折算为美元,并注明原币种及金额。
- 混合个人/商务出行:仅报销商务部分,需提供对比报价。
- 丢失收据(>$25):需提交财务部的丢失收据声明,每季度最多2次。
- 如有未涵盖的问题,请查阅FAQ:[references/POLICY_FAQ.md](references/POLICY_FAQ.md)。答案应以本文件和FAQ为准。
""";
File.WriteAllText(Path.Combine(expenseSkillPath, "SKILL.md"), skillMd);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2️⃣ 创建 references/POLICY_FAQ.md — 费用政策 FAQ(已中文化)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var faqMd = """
# 费用政策常见问答(FAQ)
## 用餐
**问:工作日咖啡或零食可以报销吗?**
答:每日咖啡/零食低于$10不予报销(视为个人消费)。客户会议或团队协作期间购买的咖啡可作为团队用餐报销。
**问:团队晚餐超出人均限额怎么办?**
答:$75/人限额为指导标准。超出20%以内可书面说明(如“客户指定场地”),超出20%需VP预先批准。
**问:需要列出所有参与人吗?**
答:是。客户用餐需列明客户姓名及公司,团队用餐需列出所有员工姓名。10人以上可单独附名单。
## 差旅
**问:可以预订高端经济舱或商务舱吗?**
答:经济舱为标准。6小时以上可选高端经济舱,商务舱需VP预批,通常仅限10小时以上或医疗原因。
**问:打车(Uber/Lyft)和租车如何选择?**
答:往返30英里以内建议打车,多日或超$100/天建议租车。3人以上可选更大车型。
**问:小费可以报销吗?**
答:用餐、打车、酒店清洁小费20%以内可报销,超出需说明理由。
## 住宿
**问:部分城市$250/晚不够怎么办?**
答:纽约、旧金山、伦敦、东京、悉尼等高消费城市自动提升至$350/晚,无需额外审批。其他特殊情况请提前向经理申请。
**问:住亲友家能否领取补贴?**
答:不可以。公司仅报销实际住宿费用,不提供补贴。
## 订阅与软件
**问:个人效率工具可以报销吗?**
答:需与工作直接相关,如IDE、设计软件、项目管理工具可报销。一般效率类应用需经理书面确认。
## 收据与材料
**问:收据模糊/损坏怎么办?**
答:可向商家补打,如无法获取,需提交丢失收据声明(财务部网站下载),每季度限2次。
**问:停车/过路费需要收据吗?**
答:低于$15无需收据,注明日期、地点、金额即可;$15及以上需收据或银行流水。
## 审批与报销
**问:经理休假谁来审批?**
答:可由上级经理或系统指定的代理审批人审批。
**问:能否报销上季度的费用?**
答:标准为30天内,超期需书面说明并VP批准,超90天仅特殊情况经CFO批准。
""";
File.WriteAllText(Path.Combine(referencesPath, "POLICY_FAQ.md"), faqMd);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 3️⃣ 创建 assets/expense-report-template.md — 报销模板(无需翻译,表头已通用)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var templateMd = """
# 费用报销模板(Expense Report Template)
| 日期 | 费用类别 | 商家 | 描述 | 金额(USD) | 原币种 | 原币金额 | 参与人 | 业务目的 | 已附收据 |
|------|----------|------|------|-------------|--------|----------|--------|----------|----------|
| | | | | | | | | | 是 / 否 |
""";
File.WriteAllText(Path.Combine(assetsPath, "expense-report-template.md"), templateMd);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Console.WriteLine("✅ Skill 文件创建完成!");
Console.WriteLine();
Console.WriteLine($"📁 技能根目录: {skillsRootPath}");
Console.WriteLine();
// 显示文件结构
new {
文件结构 = new[] {
"skills/",
" └── expense-report/",
" ├── SKILL.md (技能定义:Frontmatter + 指令)",
" ├── references/",
" │ └── POLICY_FAQ.md (费用政策 FAQ)",
" └── assets/",
" └── expense-report-template.md (报销模板)"
}
}.Display();
步骤 3:加载 Skills 并创建 Agent
使用 FileAgentSkillsProvider 从文件系统发现和加载技能,然后将其注入到 Agent 中。
核心流程:
-
FileAgentSkillsProvider扫描skills/目录 -
发现 expense-report/SKILL.md,解析 Frontmatter 和 Body -
验证所有资源文件(FAQ、模板)存在且路径安全 -
自动注册 load_skill和read_skill_resource两个 AI 工具 -
将技能列表注入系统提示词
#pragma warning disable MAAI001
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 1️⃣ 创建 SkillsProvider — 从文件系统发现和加载 Skills
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var skillsProvider = new FileAgentSkillsProvider(
skillPath: Path.Combine(Directory.GetCurrentDirectory(), "skills")
);
Console.WriteLine("✅ FileAgentSkillsProvider 创建成功");
Console.WriteLine("📂 Skills 已从文件系统加载");
Console.WriteLine();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 2️⃣ 创建 Agent — 注入 SkillsProvider
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var chatClient = AIClientHelper.GetDefaultChatClient();
AIAgent agent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "SkillsAgent",
ChatOptions = new()
{
Instructions = "You are a helpful assistant.",
},
// 🔑 关键:通过 AIContextProviders 注入 SkillsProvider
AIContextProviders = [skillsProvider],
});
Console.WriteLine("✅ Agent 创建成功");
Console.WriteLine();
new {
Agent名称 = "SkillsAgent",
基础指令 = "You are a helpful assistant.",
上下文提供者 = "FileAgentSkillsProvider",
自动注册的工具 = new[] { "load_skill", "read_skill_resource" },
技能摘要开销 = "~100 tokens/skill(仅名称和描述)"
}.Display();
Console.WriteLine();
Console.WriteLine("💡 注意:此时 Agent 的系统提示中已包含技能摘要,但完整指令尚未加载。");
Console.WriteLine(" 只有当 Agent 判断用户问题与技能相关时,才会调用 load_skill 按需加载。");
步骤 4:测试 — 费用政策问答
第一个测试:询问关于小费报销的问题。
预期行为:
-
Agent 看到系统提示中的技能摘要 → 识别问题属于 expense-report领域 -
调用 load_skill("expense-report")→ 获取完整的费用报销规则 -
发现规则中提到 FAQ → 调用 read_skill_resource("expense-report", "references/POLICY_FAQ.md") -
根据 FAQ 中的小费政策回答用户问题
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 📋 测试 1:费用政策 FAQ 问答");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
// 提出关于小费报销的问题
string question = "小费可以报销吗?我在一次出租车行程中给了 25% 的小费,想知道这是否可以报销。";
Console.WriteLine($"👤 用户: {question}");
Console.WriteLine();
// Agent 会自动执行渐进式披露流程:
// 1. 识别属于 expense-report 领域
// 2. 调用 load_skill 获取完整规则
// 3. 调用 read_skill_resource 获取 FAQ
// 4. 根据 FAQ 内容回答
AgentResponse response = await agent.RunAsync(question);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"🤖 Agent: {response.Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine("🔍 观察要点:");
new {
渐进式披露 = "Agent 自动判断需要加载 expense-report 技能",
工具调用链 = "load_skill → read_skill_resource",
FAQ匹配 = "FAQ 中明确说明:小费 20% 以内可报销,超过 20% 需要说明理由",
预期回答 = "25% 的小费超过了 20% 的标准,需要额外提供理由说明"
}.Display();
步骤 5:测试 — 多轮对话生成报销报告
第二个测试:通过多轮对话生成费用报告。Agent 需要加载报销模板资产(assets/expense-report-template.md),并引导用户补充缺失信息。
预期行为:
-
Agent 加载 skill → 获取费用类别和限额规则 -
Agent 读取报销模板 → 按照模板格式生成报告 -
Agent 识别缺失信息 → 主动询问用户补充细节
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 📝 测试 2:多轮对话生成报销报告");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
// 使用 AgentSession 支持多轮对话
AgentSession session = await agent.CreateSessionAsync();
// 第 1 轮:提供费用信息,要求生成报告草稿
string request = "我上周有 3 笔客户晚餐费用和一张 $1,200 的机票。请先返回一份报销报告草稿,并询问我缺失的细节信息。";
Console.WriteLine($"👤 用户: {request}");
Console.WriteLine();
// Agent 会:
// 1. 加载 expense-report skill(如果尚未加载)
// 2. 读取报销模板 assets/expense-report-template.md
// 3. 根据用户提供的信息填写模板
// 4. 识别缺失字段并主动询问
AgentResponse response2 = await agent.RunAsync(request, session);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"🤖 Agent: {response2.Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine("🔍 观察要点:");
new {
模板加载 = "Agent 通过 read_skill_resource 获取报销模板",
智能分类 = "Agent 根据 Skill 中的限额表对费用进行分类",
缺失信息 = "Agent 会询问缺失信息(如晚餐日期、具体金额、参与人等)",
审批提示 = "$1,200 机票需要 Manager 审批(< $1,500)"
}.Display();
步骤 6:多轮跟进 — 补充缺失信息
根据 Agent 上一轮指出的缺失信息,我们补充细节,观察 Agent 如何更新报销草稿。
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 📝 测试 2(续):补充缺失信息");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
// 提供详细信息,让 Agent 完善报告
string details = """
以下是详细信息:
- 客户晚餐 1:周一在 Starlight Restaurant,$280,4 人(我、来自 Marketing 的 Alice、来自 Contoso Corp 的 Bob Chen、来自 Contoso Corp 的 Lisa Wang)。业务目的:Q4 合作复盘。
- 客户晚餐 2:周三在 Golden Dragon,$195,3 人(我、来自 ABC Inc 的 Tom Li、来自 ABC Inc 的 Sarah Kim)。业务目的:新项目启动会。
- 客户晚餐 3:周五在 Café Milano,$150,2 人(我、来自 XYZ Ltd 的 David Liu)。业务目的:合同续签讨论。
- 机票:Delta Airlines,经济舱,JFK 往返 SFO,通过公司差旅平台预订。
所有收据均已附上。
""";
Console.WriteLine($"👤 用户: {details}");
Console.WriteLine();
// Agent 利用 session 中的对话历史,更新报告草稿
AgentResponse response3 = await agent.RunAsync(details, session);
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine($"🤖 Agent: {response3.Text}");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine("🔍 观察要点:");
new {
模板填充 = "Agent 使用报销模板格式填充所有字段",
限额检查 = "Agent 根据 Skill 中的限额规则检查每笔费用",
Dinner1 = "$280/4人=$70/人 → 未超过 $75/人限额 ✅ 但总额 >$200 需 Manager 审批",
Flight = "$1,200 经济舱 → 需 Manager 审批(< $1,500,无需 VP)",
审批总额 = "总额可能超过 $2,000 → 需要 VP 审批"
}.Display();
🔧 第五部分:进阶用法
5.1 自定义技能摘要提示词
FileAgentSkillsProviderOptions 允许你自定义系统提示词模板。使用 {0} 作为技能列表的占位符:
var skillsProvider = new FileAgentSkillsProvider(
skillPath: "skills",
options: new FileAgentSkillsProviderOptions
{
SkillsInstructionPrompt = """
你是一名专业的企业咨询顾问。你具备以下专业技能:
{0}
当用户问题与某个技能相关时:
1. 使用 load_skill 加载该技能的详细指令
2. 严格按照指令中的规则回答
3. 需要时使用 read_skill_resource 读取参考资料
务必用中文回答所有问题。
"""
}
);
5.2 多技能注册
一个 Agent 可以同时注册多个 Skills:
// 方式 1:通过父目录自动发现(推荐)
// skills/ 下的每个子目录如果包含 SKILL.md,就会被注册为一个 Skill
var provider = new FileAgentSkillsProvider("skills");
// 自动发现:expense-report, travel-policy, it-support ...
// 方式 2:指定多个路径
var provider = new FileAgentSkillsProvider([
"skills/finance", // 财务相关技能
"skills/hr", // 人力资源技能
"skills/it-support" // IT 支持技能
]);
❝💡 性能提示:渐进式披露确保了即使注册 20+ 个 Skills,初始 Token 开销也仅为 ~2000 tokens(每个 ~100 tokens 的名称+描述摘要)。Agent 会智能判断并仅加载相关的 Skill。
5.3 Skills 文件夹结构最佳实践
skills/
├── expense-report/ # ✅ 好的:清晰的领域边界
│ ├── SKILL.md
│ ├── references/
│ └── assets/
├── travel-policy/ # ✅ 好的:独立的政策领域
│ ├── SKILL.md
│ └── references/
├── code-review/ # ✅ 好的:操作指南
│ ├── SKILL.md
│ └── assets/
└── general-hr/ # ⚠️ 避免:范围过大
├── SKILL.md # 应拆分为更细粒度的技能
└── ...
命名规范:
-
✅ 使用 kebab-case:expense-report、travel-policy -
✅ 名称简洁且具描述性 -
❌ 避免特殊字符:~~ expense_report、ExpenseReport~~ -
❌ 避免过长名称(最大 64 字符)
5.4 实战:创建第二个 Skill 并测试多技能 Agent
让我们创建一个 travel-policy 技能,然后测试 Agent 在同时拥有两个 Skills 时的智能路由能力。
#pragma warning disable MAAI001
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 创建第二个 Skill:travel-policy(差旅政策,已中文化)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var travelSkillPath = Path.Combine(skillsRootPath, "travel-policy");
Directory.CreateDirectory(travelSkillPath);
var travelSkillMd = """
---
name: travel-policy
description: 公司差旅预订与审批政策。适用于航班预订、酒店预订、差旅审批流程或差旅安全指引等相关问题。
---
# 差旅政策(Travel Policy)
## 预订规则
| 项目 | 政策 | 审批 |
|------|--------|----------|
| 国内航班 | 仅限经济舱 | <$800自动审批 |
| 国际航班 | 经济舱;6小时以上可选高端经济舱 | 均需经理审批 |
| 酒店 | 优先公司协议酒店 | ≤$250/晚自动审批 |
| 租车 | 紧凑/标准车型 | ≤3天自动审批 |
| 火车/高铁 | 标准座 | 自动审批 |
## 预订流程
1. 所有预订须通过公司差旅平台(TravelHub)完成
2. 国内提前14天,国际提前21天预订
3. 总费用超$2,000需经理预先审批
4. 预订后24小时内取消可全额退款
## 安全指引
- 所有国际行程需向差旅安全团队报备
- 超过5天的行程需购买差旅保险
- 随身携带公司应急联系方式卡
""";
File.WriteAllText(Path.Combine(travelSkillPath, "SKILL.md"), travelSkillMd);
Console.WriteLine("✅ travel-policy Skill 创建完成");
Console.WriteLine();
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 重新创建包含两个 Skills 的 Provider 和 Agent
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
var multiSkillProvider = new FileAgentSkillsProvider(
skillPath: Path.Combine(Directory.GetCurrentDirectory(), "skills")
);
AIAgent multiSkillAgent = chatClient.AsAIAgent(new ChatClientAgentOptions
{
Name = "MultiSkillAgent",
ChatOptions = new()
{
Instructions = "You are a helpful corporate assistant. Answer in the same language as the user.",
},
AIContextProviders = [multiSkillProvider],
});
Console.WriteLine("✅ 多技能 Agent 创建成功");
Console.WriteLine();
new {
Agent名称 = "MultiSkillAgent",
已注册技能 = new[] { "expense-report(费用报销)", "travel-policy(差旅政策)" },
系统提示摘要开销 = "~200 tokens(2 × ~100 tokens/skill)"
}.Display();
测试多技能智能路由
分别提问差旅政策和费用报销问题,观察 Agent 如何智能选择加载正确的 Skill。
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine(" 🔀 测试 3:多技能智能路由");
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
// 测试 A:差旅政策问题 → 应加载 travel-policy
string travelQuestion = "我需要预订一张从纽约到伦敦、为期两周项目的航班。我可以乘坐什么舱位?需要审批吗?";
Console.WriteLine("🔵 测试 A:差旅政策问题");
Console.WriteLine($"👤 用户: {travelQuestion}");
Console.WriteLine();
AgentResponse travelResponse = await multiSkillAgent.RunAsync(travelQuestion);
Console.WriteLine($"🤖 Agent: {travelResponse.Text}");
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
// 测试 B:费用报销问题 → 应加载 expense-report
string expenseQuestion = "我上周买了一个 $45/月的项目管理软件订阅,需要什么审批流程?";
Console.WriteLine("🔵 测试 B:费用报销问题");
Console.WriteLine($"👤 用户: {expenseQuestion}");
Console.WriteLine();
AgentResponse expenseResponse = await multiSkillAgent.RunAsync(expenseQuestion);
Console.WriteLine($"🤖 Agent: {expenseResponse.Text}");
Console.WriteLine();
Console.WriteLine("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
Console.WriteLine();
Console.WriteLine("🔍 观察要点:");
new {
智能路由 = "Agent 根据问题内容自动选择加载正确的 Skill",
测试A = "差旅问题 → load_skill('travel-policy')",
测试B = "报销问题 → load_skill('expense-report')",
语言适配 = "Agent 会根据用户语言(中/英)自适应回复"
}.Display();
📊 总结
通过本节课,我们系统学习了 Agent Skills — 一种基于 AIContextProvider 的模块化领域知识管理机制。
核心要点
-
Agent Skills 规范
-
遵循开放规范,支持跨框架互操作 -
核心思想是渐进式披露:摘要展示 → 加载 → 读取资源 -
SKILL.md 文件结构
-
YAML Frontmatter: name+description(技能元数据) -
Markdown Body:完整的领域指令(规则、流程、资源引用) -
资源文件:通过 Markdown 链接引用,按需读取 -
FileAgentSkillsProvider
-
继承 AIContextProvider,重写ProvideAIContextAsync -
自动注册 load_skill和read_skill_resource两个 AI 工具 -
初始化时扫描目录、解析 SKILL.md、验证资源完整性 -
安全机制
-
路径穿越防护 + 符号链接检查 + XML 转义 + 名称格式验证
实际价值
-
✅ 可维护性:业务专家可以直接编辑 Markdown 文件,无需修改代码 -
✅ Token 效率:渐进式披露将 Token 开销降低 90%+ -
✅ 模块化:每个 Skill 独立维护,可即插即用 -
✅ 安全性:内置路径穿越防护和输入验证
🏋️ 课后练习
-
创建自定义 Skill:为你的业务领域创建一个 Skill(如 IT 支持指南、编码规范等),包含至少一个 references/资源文件 -
中文化提示词:使用 FileAgentSkillsProviderOptions自定义中文系统提示词模板 -
结合 Function Calling:创建一个同时具备 Skills(领域知识)和 Function Calling(操作能力)的 Agent


