利用AgentSkillsProvider引入Agent Skills
在正式介绍AgentSkillsProvider针对Agent Skills机制的设计和实现原理之前,我们先通过一个简单的实例演示一下Agent Skills在MAF中的编程模式。我们定义了一个名为translator的Skill,它的功能是将中文古典诗词翻译成英文。我们将这个Markdown文件保存在./skills/translator/SKILL.md路径下,内容如下:
---
name: translator
description: 将中文古典诗词精确翻译成地道的英文
---
## 详细指令
你是一位精通汉学与英美文学的翻译大师。请严格按照以下 3 条规则处理输入的古典诗词:
1. **必须提供三种翻译变体**:
- **变体一:古典韵律版**(必须**押韵**,注重节拍和英诗的传统美感)
- **变体二:现代诗**(不强求押韵,注重现代诗的流动感)
- **变体三:孤寂禅意版**(字句极简,精准保留原诗的**禅意**与留白,不做过度解释)
2. **输出格式**:
请直接以 Markdown 标题列出这三种变体,并在最下方提供必要的背景(典故)补充。
具体的演示程序如下所示:我们根据存储Skill的目录./skills创建了一个AgentSkillsProvider实例。然后我们创建了一个OpenAIClient对象,并将其转换IChatClient对象。在调用AsAIAgent方法将IChatClient转换成IAgent的过程中,我们利用指定的ChatClientAgentOptions完成了针对AgentSkillsProvider的注册。最后我们调用Agent的RunAsync方法,并传入一首中文古典诗词,来查看Agent基于这个Skill生成的翻译结果。
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
using System.ClientModel;
DotEnv.Load();
var model = Environment.GetEnvironmentVariable("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;
var agentSkillsProvider = new AgentSkillsProvider(["./skills"]);
var agent = new OpenAIClient(
credential: new ApiKeyCredential(key: apiKey),
options: new OpenAIClientOptions { Endpoint = new Uri(endpoint) })
.GetChatClient(model:model)
.AsIChatClient()
.AsAIAgent(options: new ChatClientAgentOptions { AIContextProviders = [agentSkillsProvider] });
var response = await agent.RunAsync(message: "把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。");
Console.WriteLine(response.Text);
输出:
### 变体一:古典韵律版(押韵)
One moon above a thousand streams is cast,
In every home the breath of spring holds fast.
Where waters flow, there shines the selfsame light;
Cloudless for miles on miles—the boundless height.
---
### 变体二:现代诗版
The same moon
rests on a thousand rivers.
Spring enters every house.
Where there is water,
there is moonlight.
Ten thousand miles—
no clouds,
only sky.
---
### 变体三:孤寂禅意版
One moon.
A thousand rivers.
Water—moon.
Sky—endless.
---
### 背景补充
“千江有水千江月”常见于佛家语境,强调“月”象征真理或佛性,“水”象征众生之心——水清则月现,意指同一真理在万象中显现;“万里无云万里天”则寓意心境澄明,无所遮蔽。整组诗句体现出华夏诗学中“万物一体”的宇宙观与禅宗“明心见性”的思想。
2. AgentSkill
Skill在MAF中被定义为一个抽象类AgentSkill,它定义了组成一个完整Skill的四个元素。
public abstract class AgentSkill
{
public abstract AgentSkillFrontmatter Frontmatter { get; }
public abstract string Content { get; }
public virtual IReadOnlyList<AgentSkillResource>? Resources => null;
public virtual IReadOnlyList<AgentSkillScript>? Scripts => null;
}
属性成员说明如下:
- Frontmatter:Skill的元数据,包含Skill的名称、描述等信息。因元数据定义在
SKILL.md文件的YAML Frontmatter中而得名; - Content:Skill的内容,通常是一些指令性的文本,指导Agent如何使用这个Skill来完成特定的任务;
- Resources:向LLM提供执行该Skill所需的外部静态资源。比如提示词模板、固定的知识库片段、提示用的示例等;
- Scripts:向LLM提供可执行的脚本。LLM在触发该Skill后,可以通过执行这些脚本来完成特定的逻辑操作(如数据处理、API请求等);
具有如下定义的AgentSkillFrontmatter提供Skill的名片。包含Skill名称、功能描述、许可证(License)以及兼容性信息。主要用于LLM的Skill发现(Discovery)阶段,让LLM知道何时该调用这个Skill。Frontmatter Spec为每个字段提供了详细的规范。
public sealed class AgentSkillFrontmatter
{
public string Name { get; }
public string Description { get; }
public string? License { get; set; }
public string? Compatibility{ get; set; }
public string? AllowedTools { get; set; }
public AdditionalPropertiesDictionary? Metadata { get; set; }
}
属性成员定义如下:
- Name:名称,必须唯一;
- Description:描述信息,简要说明这个Skill的功能和用途;
- License:许可证信息,说明这个Skill的使用和分发权限;
- Compatibility:兼容性信息,说明这个Skill适用于哪些类型的Agent或环境;
- AllowedTools:允许使用的工具列表;
- Metadata:一个可选的字典,用于存储Skill的其他元数据信息,允许Skill提供更多的自定义属性;
具有如下定义的抽象类AgentSkillResource描述Skill资源,它们为特定Skill提供补充性的静态内容、数据参考或静态资产。AgentSkillResource除了为每个资源提供一个名称和一个可选的描述信息外,还定义了一个抽象方法ReadAsync用于读取这个资源的内容。
public abstract class AgentSkillResource
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> ReadAsync(IServiceProvider? serviceProvider = null, CancellationToken cancellationToken = default);
}
为Skill提供的脚本通过如下的抽象类AgentSkillScript定义,除了提供一个名称和一个可选的描述信息外,还定义了一个抽象方法RunAsync用于执行这个脚本。AgentSkillScript相当于一个AIFunction对象表示的工具函数,而且传入RunAsync方法的脚本参数直接就是一个AIFunctionArguments对象。
public abstract class AgentSkillScript
{
public string Name { get; }
public string? Description { get; }
public abstract Task<object?> RunAsync(
AgentSkill skill,
AIFunctionArguments arguments,
CancellationToken cancellationToken = default);
}
public class AIFunctionArguments : IDictionary<string, object?>
MAF为作为抽象类的AgentSkill提供了三种具体的实现方式,分别是:
- AgentFileSkill:基于文件的Skill定义;
- AgentInlineSkill:将Skill元数据、指令内容、资源和脚本直接封装在一个
AgentInlineSkill对象中; AgentClassSkill<T>:通过继承AgentClassSkill<T>,并利用特性标记成员的方式来定义Skill的资源和脚本;
2.1 AgentFileSkill
基于文件定义的Skill对应如下这个AgentFileSkill类,我们在构建此对象使需要指定Skill文件的路径。
public sealed class AgentFileSkill : AgentSkill
{
public override AgentSkillFrontmatter Frontmatter { get; }
public override string Content { get; }
public string Path { get; }
public override IReadOnlyList<AgentSkillResource> Resources { get; }
public override IReadOnlyList<AgentSkillScript> Scripts { get; }
internal AgentFileSkill(
AgentSkillFrontmatter frontmatter,
string content,
string path,
IReadOnlyList<AgentSkillResource>? resources = null,
IReadOnlyList<AgentSkillScript>? scripts = null);
}
一般来说,通过AgentFileSkill类型表示的Skill,其资源和脚本类型也是对应的AgentFileSkillResource和AgentFileSkillScript。如下面的代码所示,AgentFileSkillResource是一个内部类型。不论是构建一个AgentFileSkillResource对象,还是构建一个AgentFileSkillScript对象,我们都需要提供一个文件的完整路径。对于资源来说,ReadAsync方法会读取这个文件的内容并返回;对于脚本来说,RunAsync方法会利用提供的Runner来执行这个脚本文件。这个指定脚本的Runner体现为一个类型为AgentFileSkillScriptRunner的委托对象,该委托的三个输入参数分别代表触发这个脚本的Skill对象、这个脚本对象以及LLM传入的参数,返回值则是脚本执行后的结果。
internal sealed class AgentFileSkillResource : AgentSkillResource
{
public string FullPath { get; }
public AgentFileSkillResource(string name, string fullPath);
public override async Task<object?> ReadAsync(
IServiceProvider? serviceProvider = null,
CancellationToken cancellationToken = default);
}
public sealed class AgentFileSkillScript : AgentSkillScript
{
public string FullPath { get; }
internal AgentFileSkillScript(
string name,
string fullPath,
AgentFileSkillScriptRunner? runner = null);
public override async Task<object?> RunAsync(
AgentSkill skill,
AIFunctionArguments arguments,
CancellationToken cancellationToken = default);
}
public delegate Task<object?> AgentFileSkillScriptRunner(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments arguments,
CancellationToken cancellationToken);
2.2 AgentInlineSkill
AgentInlineSkill提供了一种纯代码驱动(Code-First)的方式。我们可以创建一个AgentInlineSkill对象,并利用定义的Fluent API直接绑定元数据、提示词指令、静态资源以及可执行的脚本函数。
public sealed class AgentInlineSkill : AgentSkill
{
public override AgentSkillFrontmatter Frontmatter { get; }
public override string Content { get; }
public override IReadOnlyList<AgentSkillResource>? Resources { get; }
public override IReadOnlyList<AgentSkillScript>? Scripts { get; }
public AgentInlineSkill(AgentSkillFrontmatter frontmatter, string instructions);
public AgentInlineSkill(
string name,
string description,
string instructions,
string? license = null,
string? compatibility = null,
string? allowedTools = null,
AdditionalPropertiesDictionary? metadata = null);
public AgentInlineSkill AddResource(string name, object value, string? description = null);
public AgentInlineSkill AddResource(string name, Delegate method, string? description = null);
public AgentInlineSkill AddScript(string name, Delegate method, string? description = null);
}
通过AddResource和AddScript方法添加的资源和脚本类型也与之匹配,分别是具有如下定义的AgentInlineSkillResource和AgentInlineSkillScript类型。AgentInlineSkillResource使用该委托对象来读取资源的内容,AgentInlineSkillScript使用该对象表示可执行的脚本。由于AIFunctionFactory可以将一个委托对象转换成一个AIFunction对象,所以资源读取和脚本都可以通过执行AIFunction对象的方式来完成。
internal sealed class AgentInlineSkillResource : AgentSkillResource
{
public AgentInlineSkillResource(string name, object value, string? description = null);
public AgentInlineSkillResource(string name, Delegate method, string? description = null);
public override async Task<object?> ReadAsync(
IServiceProvider? serviceProvider = null,
CancellationToken cancellationToken = default);
}
internal sealed class AgentInlineSkillScript : AgentSkillScript
{
public JsonElement? ParametersSchema { get; }
public AgentInlineSkillScript(string name, Delegate method, string? description = null);
public override async Task<object?> RunAsync(
AgentSkill skill,
AIFunctionArguments arguments,
CancellationToken cancellationToken = default);
}
2.3 AgentClassSkill
除了上述两种内置的Skill定义方式,我们同样可以通过继承AgentClassSkill<TSelf>来实现自定义的Skill类型,以满足特定的需求。对应组成Skill的三种基本元素,最为核心的指令是必需的,所以抽象类AgentClassSkill<TSelf>将其定义成必需实现的抽象属性Instructions,而资源和脚本则是可选的,所以对应的属性Resources和Scripts被定义成virtual成员,默认返回null。
public abstract class AgentClassSkill<TSelf> : AgentSkill where TSelf : AgentClassSkill<TSelf>
{
protected abstract string Instructions { get; }
protected virtual JsonSerializerOptions? SerializerOptions => null;
public override string Content {get;}
public override IReadOnlyList<AgentSkillResource>? Resources{get;}
public override IReadOnlyList<AgentSkillScript>? Scripts {get;}
}
当我们继承AgentClassSkill<TSelf>来定义一个Skill类时,利用具有如下定义的AgentSkillResourceAttribute和AgentSkillScriptAttribute将就具有合法签名的方法来表示资源和脚本。资源除了定义成方法之外也可以定义成属性。定义在AgentClassSkill<TSelf>中的Resources和Scripts属性会通过反射扫描这些被标记的方法和属性,并将它们转换成AgentSkillResource和AgentSkillScript对象,并最终返回资源和脚本列表。
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class AgentSkillResourceAttribute : Attribute
{
public string? Name { get; }
public AgentSkillResourceAttribute();
public AgentSkillResourceAttribute(string name);
}
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class AgentSkillScriptAttribute : Attribute
{;
public string? Name { get; }
public AgentSkillScriptAttribute();
public AgentSkillScriptAttribute(string name);
}
标注了AgentSkillResourceAttribute的方法要求是无参数,或者只包含类型为IServiceProvider和CancellationToken的参数。标注在方法或者属性上的DescriptionAttribute特性将用于描述对应的资源和脚本。除此之外,Resources和Scripts属性还会验证资源和脚本名称的唯一性,确保不会有重复的资源和脚本名称。
MAF官方文档提供了如下这个UnitConverterSkill类型,旨在完成英里/公里以及磅/千克之间的转换。我们可以看到它通过标记AgentSkillResourceAttribute特性来定义了一个名为conversion-table的资源,通过标记AgentSkillScriptAttribute特性来定义了一个名为convert的脚本。
using System.ComponentModel;
using System.Text.Json;
using Microsoft.Agents.AI;
internal sealed class UnitConverterSkill : AgentClassSkill<UnitConverterSkill>
{
public override AgentSkillFrontmatter Frontmatter { get; } = new(
"unit-converter",
"Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.");
protected override string Instructions => """
Use this skill when the user asks to convert between units.
1. Review the conversion-table resource to find the correct factor.
2. Use the convert script, passing the value and factor from the table.
3. Present the result clearly with both units.
""";
[AgentSkillResource("conversion-table")]
[Description("Lookup table of multiplication factors for common unit conversions.")]
public string ConversionTable => """
# Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|------------|------------|----------|
| miles | kilometers | 1.60934 |
| kilometers | miles | 0.621371 |
| pounds | kilograms | 0.453592 |
| kilograms | pounds | 2.20462 |
""";
[AgentSkillScript("convert")]
[Description("Multiplies a value by a conversion factor and returns the result as JSON.")]
private static string ConvertUnits(double value, double factor)
{
double result = Math.Round(value * factor, 4);
return JsonSerializer.Serialize(new { value, factor, result });
}
}
3. AgentSkillsSource
我们可用为AgentSkillsProvider提供来源于不同渠道的Skill,如下这个AgentSkillsSource抽象了Skill的来源,派生于它的子类都可以利用重写的GetSkillsAsync方法来为AgentSkillsProvider提供Skill集合。
public abstract class AgentSkillsSource
{
public abstract Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}
3.1 AgentInMemorySkillsSource & AgentFileSkillsSource
MAF内置AgentInMemorySkillsSource和AgentFileSkillsSource这两种。前者直接从内存中提供Skill集合,后者则从文件系统中加载Skill集合。它们提供AgentSkill的具体类型就是前面介绍的AgentInlineSkill和AgentFileSkill。
internal sealed class AgentInMemorySkillsSource : AgentSkillsSource
{
public AgentInMemorySkillsSource(IEnumerable<AgentSkill> skills);
public override Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}
internal sealed class AgentFileSkillsSource : AgentSkillsSource
{
public AgentFileSkillsSource(
string skillPath,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? options = null, 更多推荐


所有评论(0)