随着 AI Agent 技术的快速发展,如何让 Agent 具备可复用、可扩展的专业能力成为一个重要课题。Agent Skills 规范提供了一种标准化的方式来定义和分发 Agent 技能,而 Microsoft Agent Framework (MAF) 则提供了构建 AI Agent 的强大基础设施。
本文将深入介绍如何基于 MAF 的上下文扩展(AIContextProvider)实现 Agent Skills 的集成,包括核心架构设计、关键组件实现以及实际应用示例。
源码已上传至GitHub,文末扫码,加入「.NET+AI 社区群」,即可获取「.NET+AI 公开资料包」。
Maf.AgentSkills 项目采用了 MAF 官方推荐的 AIContextProviderFactory 模式,实现了与 MAF 的无缝集成。整体架构如下:

Microsoft.Agents.AI - MAF 核心框架Microsoft.Extensions.AI - AI 抽象层YamlDotNet - YAML Frontmatter 解析Microsoft.Extensions.DependencyInjection - 依赖注入支持Agent Skills 的核心理念之一是渐进式披露:Agent 首先只获取技能的元数据(名称和描述),只有在真正需要使用某个技能时,才加载完整的指令内容。
这种设计有几个重要优势:

信息获取流程:

项目严格遵循 MAF 的 AIContextProviderFactory 模式,这是 MAF 推荐的上下文注入方式:
// MAF 标准模式
AIAgent agent = chatClient.CreateAIAgent(new ChatClientAgentOptions
{
AIContextProviderFactory = ctx => new MyContextProvider(
chatClient,
ctx.SerializedState,
ctx.JsonSerializerOptions)
});通过实现 AIContextProvider 抽象类,我们可以:
技能系统涉及文件读取和可能的脚本执行,因此安全性是首要考虑:

SkillsContextProvider 是整个系统的核心,它继承自 MAF 的 AIContextProvider 抽象类:
public sealed class SkillsContextProvider : AIContextProvider
{
private readonly IChatClient _chatClient;
private readonly SkillLoader _skillLoader;
private readonly SkillsOptions _options;
private SkillsState _state;
// 构造函数1:创建新实例
public SkillsContextProvider(IChatClient chatClient, SkillsOptions? options = null)
{
_chatClient = chatClient;
_options = options ?? new SkillsOptions();
var settings = new SkillsSettings(_options.AgentName, _options.ProjectRoot);
_skillLoader = new SkillLoader();
_state = new SkillsState();
// 自动加载技能
LoadSkills(settings);
}
// 构造函数2:从序列化状态恢复(支持线程持久化)
public SkillsContextProvider(
IChatClient chatClient,
JsonElement serializedState,
JsonSerializerOptions? jsonSerializerOptions = null)
{
// 反序列化恢复状态...
}
// 在 Agent 调用前注入技能上下文
public override ValueTask<AIContext> InvokingAsync(
InvokingContext context,
CancellationToken cancellationToken = default)
{
// 生成技能系统提示
var instructions = GenerateSkillsPrompt(_state.AllSkills);
// 创建技能工具
var tools = CreateSkillsTools(_state);
return ValueTask.FromResult(new AIContext
{
Instructions = instructions,
Tools = tools
});
}
// 序列化状态以支持线程持久化
public override JsonElement Serialize(JsonSerializerOptions? jsonSerializerOptions = null)
{
var state = new { Options = _options, State = _state };
return JsonSerializer.SerializeToElement(state, jsonSerializerOptions);
}
}关键设计点:
AIContext 注入技能信息
SkillLoader 负责从文件系统发现和加载技能:
public sealed class SkillLoader
{
private readonly SkillParser _parser;
/// <summary>
/// 从指定目录加载所有技能
/// </summary>
public IEnumerable<SkillMetadata> LoadSkillsFromDirectory(
string skillsDirectory,
SkillSource source)
{
if (!Directory.Exists(skillsDirectory))
yield break;
foreach (var skillDir in Directory.GetDirectories(skillsDirectory))
{
var skill = TryLoadSkill(skillDir, source);
if (skill is not null)
yield return skill;
}
}
private SkillMetadata? TryLoadSkill(string skillDirectory, SkillSource source)
{
var skillFilePath = Path.Combine(skillDirectory, "SKILL.md");
if (!File.Exists(skillFilePath))
return null;
// 安全检查:验证符号链接
if (PathSecurity.IsSymbolicLink(skillFilePath))
{
var realPath = PathSecurity.GetRealPath(skillFilePath);
if (!PathSecurity.IsPathSafe(realPath, skillDirectory))
return null;
}
return _parser.Parse(skillFilePath, source);
}
}技能目录结构:
~/.maf/{agent-name}/skills/ # 用户级技能
{project-root}/.maf/skills/ # 项目级技能(优先级更高)每个技能是一个独立的目录,包含 SKILL.md 文件:
skills/
├── web-research/
│ ├── SKILL.md
│ ├── search.py
│ └── templates/
│ └── report.md
├── code-review/
│ ├── SKILL.md
│ └── checklist.md
└── pdf-tools/
├── SKILL.md
├── split_pdf.py
└── merge_pdf.py技能加载流程:

SkillParser 负责解析 SKILL.md 文件的 YAML Frontmatter:
public sealed class SkillParser
{
private const string FrontmatterDelimiter = "---";
public SkillMetadata Parse(string skillFilePath, SkillSource source)
{
var content = File.ReadAllText(skillFilePath);
var skillDirectory = Path.GetDirectoryName(skillFilePath)!;
var directoryName = Path.GetFileName(skillDirectory);
// 提取 YAML Frontmatter
var frontmatter = ExtractFrontmatter(content);
if (frontmatter is null)
throw new SkillParseException(skillFilePath,
"SKILL.md must have YAML frontmatter delimited by '---'.");
// 解析 YAML
var yamlData = _yamlDeserializer.Deserialize<SkillFrontmatter>(frontmatter);
// 验证必需字段
if (string.IsNullOrWhiteSpace(yamlData.Name))
throw new SkillParseException(skillFilePath, "Skill 'name' is required.");
if (string.IsNullOrWhiteSpace(yamlData.Description))
throw new SkillParseException(skillFilePath, "Skill 'description' is required.");
// 验证名称格式和目录匹配
SkillValidator.ValidateName(yamlData.Name);
SkillValidator.ValidateNameMatchesDirectory(yamlData.Name, directoryName);
return new SkillMetadata(
Name: yamlData.Name,
Description: yamlData.Description,
Path: skillDirectory,
Source: source,
License: yamlData.License,
AllowedTools: AllowedTool.Parse(yamlData.AllowedTools)
);
}
}SKILL.md 格式示例:
---
name: web-research
description: A skill for conducting comprehensive web research
license: MIT
allowed-tools: web_search fetch_url
---
# Web Research Skill
## When to Use
Use this skill when researching topics online...
## Instructions
1. Clarify the research scope
2. Search strategically
3. Synthesize information
...SkillsToolFactory 根据配置创建技能相关的工具:
public sealed class SkillsToolFactory
{
public IReadOnlyList<AITool> CreateTools()
{
var tools = new List<AITool>();
// 默认启用的安全工具
if (_options.EnableReadSkillTool)
tools.Add(new ReadSkillTool(_loader, _stateProvider).ToAIFunction());
if (_options.EnableReadFileTool)
tools.Add(new ReadFileTool(_stateProvider).ToAIFunction());
if (_options.EnableListDirectoryTool)
tools.Add(new ListDirectoryTool(_loader, _stateProvider).ToAIFunction());
// 需要显式启用的高危工具
if (_options.EnableExecuteScriptTool)
tools.Add(new ExecuteScriptTool(_stateProvider, _options).ToAIFunction());
if (_options.EnableRunCommandTool && _options.AllowedCommands.Count > 0)
tools.Add(new RunCommandTool(_stateProvider, _options).ToAIFunction());
return tools;
}
}内置工具:
工具名 | 功能 | 默认状态 |
|---|---|---|
read_skill | 读取 SKILL.md 完整内容 | ✅ 启用 |
read_skill_file | 读取技能目录中的文件 | ✅ 启用 |
list_skill_directory | 列出技能目录内容 | ✅ 启用 |
execute_skill_script | 执行技能中的脚本 | ❌ 禁用 |
run_skill_command | 运行白名单命令 | ❌ 禁用 |
工具创建决策流程:

为了简化使用,项目提供了 ChatClient 的扩展方法:
public static class ChatClientExtensions
{
public static AIAgent CreateSkillsAgent(
this IChatClient chatClient,
Action<SkillsOptions>? configureSkills = null,
Action<ChatClientAgentOptions>? configureAgent = null)
{
var skillsOptions = new SkillsOptions();
configureSkills?.Invoke(skillsOptions);
var agentOptions = new ChatClientAgentOptions
{
AIContextProviderFactory = ctx =>
{
// 检查是否从序列化状态恢复
if (ctx.SerializedState.ValueKind != JsonValueKind.Undefined)
{
return new SkillsContextProvider(
chatClient,
ctx.SerializedState,
ctx.JsonSerializerOptions);
}
// 创建新实例
return new SkillsContextProvider(chatClient, skillsOptions);
}
};
configureAgent?.Invoke(agentOptions);
return chatClient.CreateAIAgent(agentOptions);
}
}以下是 Agent 执行任务时的完整调用流程:

技能信息通过系统提示注入到 Agent 中。系统提示采用渐进式披露的设计:
public static class SkillsPromptTemplates
{
public const string SystemPromptTemplate = """
## Skills System
You have access to a skills library that provides specialized capabilities.
{skills_locations}
**Available Skills:**
{skills_list}
---
### How to Use Skills (Progressive Disclosure) - CRITICAL
Skills follow a **progressive disclosure** pattern - you know they exist
(name + description above), but you **MUST read the full instructions
before using them**.
**MANDATORY Workflow:**
1. **Recognize when a skill applies**: Check if the user's task matches
any skill's description above
2. **Read the skill's full instructions FIRST**: Use `read_skill` tool
to get the complete SKILL.md content
3. **Follow the skill's instructions precisely**: SKILL.md contains
step-by-step workflows and examples
4. **Execute scripts only after reading**: Use the exact script paths
and argument formats from SKILL.md
**IMPORTANT RULES:**
⚠️ **NEVER call `execute_skill_script` without first reading the skill
with `read_skill`**
✅ **Correct Workflow Example:**
```
User: "Split this PDF into pages"
1. Recognize: "split-pdf" skill matches this task
2. Call: read_skill("split-pdf") → Get full instructions
3. Learn: SKILL.md shows the actual script path and argument format
4. Execute: Use the exact command format from SKILL.md
```
Remember: **Read first, then execute.** This ensures you use skills correctly!
""";
}技能状态通过 SkillsState 类管理,支持序列化:
public sealed class SkillsState
{
public IReadOnlyList<SkillMetadata> UserSkills { get; init; } = [];
public IReadOnlyList<SkillMetadata> ProjectSkills { get; init; } = [];
public DateTimeOffset LastRefreshed { get; init; }
/// <summary>
/// 获取所有技能,项目级技能优先级更高
/// </summary>
public IReadOnlyList<SkillMetadata> AllSkills
{
get
{
var projectSkillNames = ProjectSkills
.Select(s => s.Name)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var userSkillsWithoutOverrides = UserSkills
.Where(s => !projectSkillNames.Contains(s.Name));
return [.. ProjectSkills, .. userSkillsWithoutOverrides];
}
}
public SkillMetadata? GetSkill(string name)
{
return ProjectSkills.FirstOrDefault(s =>
s.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
?? UserSkills.FirstOrDefault(s =>
s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
}所有文件操作都经过严格的路径安全验证:
public static class PathSecurity
{
/// <summary>
/// 解析安全路径,防止路径遍历攻击
/// </summary>
public static string? ResolveSafePath(string basePath, string relativePath)
{
var fullPath = Path.GetFullPath(Path.Combine(basePath, relativePath));
var normalizedBase = Path.GetFullPath(basePath);
// 确保解析后的路径仍在基础路径内
if (!fullPath.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase))
return null;
return fullPath;
}
/// <summary>
/// 检查是否是符号链接
/// </summary>
public static bool IsSymbolicLink(string path)
{
var fileInfo = new FileInfo(path);
return fileInfo.Attributes.HasFlag(FileAttributes.ReparsePoint);
}
/// <summary>
/// 验证路径是否安全
/// </summary>
public static bool IsPathSafe(string targetPath, string allowedBasePath)
{
var normalizedTarget = Path.GetFullPath(targetPath);
var normalizedBase = Path.GetFullPath(allowedBasePath);
return normalizedTarget.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase);
}
}using Maf.AgentSkills.Agent;
using OpenAI;
// 创建 ChatClient
var chatClient = new OpenAIClient(apiKey)
.GetChatClient("gpt-4")
.AsIChatClient();
// 创建支持技能的 Agent
var agent = chatClient.CreateSkillsAgent(
configureSkills: options =>
{
options.AgentName = "my-assistant";
options.ProjectRoot = Directory.GetCurrentDirectory();
},
configureAgent: options =>
{
options.ChatOptions = new()
{
Instructions = "You are a helpful assistant."
};
});
// 使用 Agent
var thread = agent.GetNewThread();
var response = await agent.RunAsync("What skills do you have?", thread);
Console.WriteLine(response.Text);技能状态可以随线程一起序列化,支持持久化会话:
// 序列化线程
var serializedThread = thread.Serialize();
// 保存到数据库或文件
await SaveThreadAsync(userId, serializedThread);
// 稍后恢复并继续对话
var restoredThread = agent.DeserializeThread(serializedThread);
var response = await agent.RunAsync("Continue our chat", restoredThread);序列化/反序列化流程:

var builder = Host.CreateApplicationBuilder(args);
// 注册 ChatClient
builder.Services.AddChatClient(sp =>
{
return new OpenAIClient(apiKey)
.GetChatClient("gpt-4")
.AsIChatClient();
});
// 注册技能 Agent
builder.Services.AddSingleton<AIAgent>(sp =>
{
var chatClient = sp.GetRequiredService<IChatClient>();
return chatClient.CreateSkillsAgent(
configureSkills: options =>
{
options.AgentName = "di-agent";
options.ProjectRoot = Directory.GetCurrentDirectory();
options.ToolsOptions.EnableReadSkillTool = true;
options.ToolsOptions.EnableReadFileTool = true;
});
});
var host = builder.Build();
var agent = host.Services.GetRequiredService<AIAgent>();
var thread = agent.GetNewThread();
var path = "E:\\GitHub\\My\\dotnet-agent-skills\\NET+AI:技术栈全景解密.pdf";
var response = await agent.RunAsync($"请将指定目录:{path}的文件拆分前3页", thread);var agent = chatClient.CreateSkillsAgent(
configureSkills: options =>
{
options.AgentName = "power-assistant";
options.ProjectRoot = Directory.GetCurrentDirectory();
// 启用脚本执行(需要显式开启)
options.ToolsOptions.EnableExecuteScriptTool = true;
options.ToolsOptions.AllowedScriptExtensions = [".py", ".ps1", ".cs"];
options.ToolsOptions.ScriptTimeoutSeconds = 60;
// 启用命令执行(白名单模式)
options.ToolsOptions.EnableRunCommandTool = true;
options.ToolsOptions.AllowedCommands = ["git", "npm", "dotnet"];
});项目遵循"默认安全"原则:
EnableExecuteScriptTool = falseEnableRunCommandTool = falseReadSkill, ReadFile, ListDirectory所有文件操作都限制在技能目录内:
// 读取文件时验证路径
var safePath = PathSecurity.ResolveSafePath(skill.Path, relativePath);
if (safePath is null)
{
return JsonSerializer.Serialize(new
{
success = false,
error = "Path traversal attempt detected"
});
}即使启用了脚本执行,也只允许特定扩展名:
public class SkillsToolsOptions
{
public List<string> AllowedScriptExtensions { get; set; } = [".py", ".ps1", ".sh", ".cs"];
public int ScriptTimeoutSeconds { get; set; } = 30;
public int MaxOutputSizeBytes { get; set; } = 50 * 1024; // 50KB
}命令执行采用严格的白名单机制:
options.AllowedCommands = ["git", "npm", "dotnet"]; // 只允许这些命令# 推荐的技能目录结构
my-skill/
├── SKILL.md # 必需:技能定义文件
├── README.md # 可选:详细文档
├── scripts/ # 脚本文件
│ ├── main.py
│ └── utils.py
├── templates/ # 模板文件
│ └── output.md
└── config/ # 配置文件
└── settings.json---
name: my-skill
description: Brief description under 1024 characters
license: MIT
allowed-tools: web_search file_write
---
# Skill Name
## Overview
Clear explanation of what this skill does.
## When to Use
- Situation 1
- Situation 2
## Prerequisites
- Required tools or dependencies
## Instructions
Step-by-step workflow:
1. First step
2. Second step
3. Third step
## Available Scripts
### script.py
- **Purpose**: What it does
- **Arguments**: `--input <file> --output <file>`
- **Example**: `python script.py --input data.csv --output result.json`
## Examples
### Example 1: Basic Usage
...~/.maf/{agent}/skills/):通用技能,适用于多个项目{project}/.maf/skills/):项目特定技能,可覆盖同名用户级技能Maf.AgentSkills 项目展示了如何基于 Microsoft Agent Framework 实现 Agent Skills 集成。
核心设计要点:
AIContextProviderFactory 实现无侵入式集成通过这套实现,开发者可以轻松为 AI Agent 添加可复用的专业技能,使 Agent 能够完成更复杂的任务。