'[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF'
[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF
Skills针对Agent的重要性是不言而喻的。从本质上讲,Agent Skills就是随着用户与LLM对话的推进,动态加载被称为Skill作为提示词的一种机制。在大部分实现中,Skill的内容会被封装成角色为Tool的消息被添加到对话历史中,因为这样可以借助针对对话历史的压缩实现对老旧Skill的卸载。Agent Skills依然是输入增强的一种形式,所以Agent Skills在MAF中是被`AgentSkillsProvider`的`AIContextProvider`引入的。
Skills针对Agent的重要性是不言而喻的。从本质上讲,Agent Skills就是随着用户与LLM对话的推进,动态加载被称为Skill作为提示词的一种机制。在大部分实现中,Skill的内容会被封装成角色为Tool的消息被添加到对话历史中,因为这样可以借助针对对话历史的压缩实现对老旧Skill的卸载。Agent Skills依然是输入增强的一种形式,所以Agent Skills在MAF中是被AgentSkillsProvider的AIContextProvider引入的。
1. 利用AgentSkillsProvider引入Agent Skills#
在正式介绍AgentSkillsProvider针对Agent Skills机制的设计和实现原理之前,我们先通过一个简单的实例演示一下Agent Skills在MAF中的编程模式。我们定义了一个名为translator的Skill,它的功能是将中文古典诗词翻译成英文。我们将这个Markdown文件保存在./skills/translator/SKILL.md路径下,内容如下:
---
name: translator
description: 将中文古典诗词精确翻译成地道的英文
---
class=class="hljs-string">"hljs-comment">## 详细指令
你是一位精通汉学与英美文学的翻译大师。请严格按照以下 class="hljs-number">3 条规则处理输入的古典诗词:
class="hljs-number">1. **必须提供三种翻译变体**:
- **变体一:古典韵律版**(必须**押韵**,注重节拍和英诗的传统美感)
- **变体二:现代诗**(不强求押韵,注重现代诗的流动感)
- **变体三:孤寂禅意版**(字句极简,精准保留原诗的**禅意**与留白,不做过度解释)
class="hljs-number">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(class="hljs-string">"MODEL")!;
var apiKey = Environment.GetEnvironmentVariable(class="hljs-string">"API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable(class="hljs-string">"OPENAI_URL")!;
var agentSkillsProvider = new AgentSkillsProvider([class="hljs-string">"./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: class="hljs-string">"把这句诗翻译成英文:千江同一月,万户尽皆春。千江有水千江月,万里无云万里天。");
Console.WriteLine(response.Text);输出:
class=class="hljs-string">"hljs-comment">### 变体一:古典韵律版(押韵)
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.
---
class=class="hljs-string">"hljs-comment">### 变体二:现代诗版
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.
---
class=class="hljs-string">"hljs-comment">### 变体三:孤寂禅意版
One moon.
A thousand rivers.
Water—moon.
Sky—endless.
---
class=class="hljs-string">"hljs-comment">### 背景补充
“千江有水千江月”常见于佛家语境,强调“月”象征真理或佛性,“水”象征众生之心——水清则月现,意指同一真理在万象中显现;“万里无云万里天”则寓意心境澄明,无所遮蔽。整组诗句体现出华夏诗学中“万物一体”的宇宙观与禅宗“明心见性”的思想。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(
class="hljs-string">"unit-converter",
class="hljs-string">"Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.");
protected override string Instructions => class="hljs-string">"""
Use this skill when the user asks to convert between units.
class="hljs-number">1. Review the conversion-table resource to find the correct factor.
class="hljs-number">2. Use the convert script, passing the value and factor from the table.
class="hljs-number">3. Present the result clearly with both units.
class="hljs-string">""";
[AgentSkillResource(class="hljs-string">"conversion-table")]
[Description(class="hljs-string">"Lookup table of multiplication factors for common unit conversions.")]
public string ConversionTable => class="hljs-string">"""
class=class="hljs-string">"hljs-comment"># Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|------------|------------|----------|
| miles | kilometers | class="hljs-number">1.60934 |
| kilometers | miles | class="hljs-number">0.621371 |
| pounds | kilograms | class="hljs-number">0.453592 |
| kilograms | pounds | class="hljs-number">2.20462 |
class="hljs-string">""";
[AgentSkillScript(class="hljs-string">"convert")]
[Description(class="hljs-string">"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, class="hljs-number">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,
ILoggerFactory? loggerFactory = null);
public AgentFileSkillsSource(
IEnumerable<string> skillPaths,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? options = null,
ILoggerFactory? loggerFactory = null);
public override Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}当我们根据指定的路径列表创建对应的AgentFileSkillsSource对象时,默认情况下后者会采用如下的规则加载对应的文件作为Skill、资源和脚本:
- Skill文件:相对路径为
./{skillName}/SKILL.md的Markdown文件,其中skillName是这个Skill的名称,也是这个Skill的唯一标识符; - 资源文件:位于Skill文件同一目录下具有如下扩展名的文件:".md", ".json", ".yaml", ".yml", ".csv", ".xml", ".txt" ;
- 脚本文件:位于Skill文件同一目录下具有如下扩展名的文件:".py", ".js", ".sh", ".ps1", ".cs", ".csx";
AgentFileSkillsSourceOptions提供了AllowedResourceExtensions和AllowedScriptExtensions这两个配置选项,允许我们指定Skill文件和脚本文件的扩展名。
public sealed class AgentFileSkillsSourceOptions
{
public IEnumerable<string>? AllowedResourceExtensions { get; set; }
public IEnumerable<string>? AllowedScriptExtensions { get; set; }
}3.2 DelegatingAgentSkillsSource#
DelegatingAgentSkillsSource可视为针对AgentSkillsSource的中间件或者装饰器,我们可用将一组中间件对象装饰到一个AgentSkillsSource对象上实现对提供的Skill集合进行过滤、转换或增强。派生于它的DeduplicatingAgentSkillsSource和FilteringAgentSkillsSource分别提供了去重和过滤的功能。
internal abstract class DelegatingAgentSkillsSource : AgentSkillsSource
{
protected AgentSkillsSource InnerSource { get; }
protected DelegatingAgentSkillsSource(AgentSkillsSource innerSource)
=>InnerSource = innerSource;
public override Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default)
=> InnerSource.GetSkillsAsync(cancellationToken);
}
internal sealed partial class DeduplicatingAgentSkillsSource : DelegatingAgentSkillsSource
{
public DeduplicatingAgentSkillsSource(
AgentSkillsSource innerSource,
ILoggerFactory? loggerFactory = null);
public override async Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}
internal sealed class FilteringAgentSkillsSource : DelegatingAgentSkillsSource
{
public FilteringAgentSkillsSource(
AgentSkillsSource innerSource,
Func<AgentSkill, bool> predicate,
ILoggerFactory? loggerFactory = null);
public override async Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}3.3 AggregatingAgentSkillsSource#
AggregatingAgentSkillsSource利用组合模式将多个AgentSkillsSource对象聚合成一个AgentSkillsSource对象。
internal sealed class AggregatingAgentSkillsSource : AgentSkillsSource
{
public AggregatingAgentSkillsSource(
IEnumerable<AgentSkillsSource> sources);
public override async Task<IList<AgentSkill>> GetSkillsAsync(
CancellationToken cancellationToken = default);
}3. ScriptRunner#
AgentInlineSkillScript提供的脚本体现为一个可以直接执行的委托对象,而AgentFileSkillScript提供的脚本则是一个存储在文件系统中的脚本文件。脚本文件的执行需要一个AgentFileSkillScriptRunner对象作为执行器,这是一个委托对象,定义了执行这个脚本文件所需的输入参数和返回值。输入参数包括触发这个脚本的Skill对象、这个脚本对象以及LLM传入的参数,返回值则是脚本执行后的结果。
public delegate Task<object?> AgentFileSkillScriptRunner(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments arguments,
CancellationToken cancellationToken);MAF官方文档中提供了采用子进程执行Python脚本的AgentFileSkillScriptRunner实现,但是它提供的方法签名与AgentFileSkillScriptRunner委托定义并不兼容,我做了相应修改。
using System.Diagnostics;
using System.Text.Json;
static async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments args,
CancellationToken cancellationToken)
{
var psi = new ProcessStartInfo(class="hljs-string">"python3")
{
RedirectStandardOutput = true,
UseShellExecute = false,
};
psi.ArgumentList.Add(Path.Combine(skill.Path, script.FullPath));
foreach (var (k, v) in args)
{
if (v is not null)
{
psi.ArgumentList.Add($class="hljs-string">"--{k}");
psi.ArgumentList.Add(v.ToString()!);
}
}
using var process = Process.Start(psi)!;
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return output.Trim();
}由于脚本执行是一项潜在的安全风险操作,类似于上面这种Runner是能用于开发和调试,但并不适合直接在生产环境中使用,后者需要能通提供安全执行环境的沙箱化的Runner。这样的沙箱(Sandbox)可以采用多种计数来实现,比如可以使用容器、WebAssembly或者云端托管服务等。
4. AgentSkillsProvider#
由于Skill在MAF中具有多种定义方式,并对来源进行了抽象,所以MAF提供一系列不同的构造函数通过提供不同来源不同定义形式的Skill来构建AgentSkillsProvider对象。尽管如此,针对AgentSkillsProvider对象的构建最终都会落在最后一个构造函数上。
public sealed class AgentSkillsProvider : AIContextProvider
{
public AgentSkillsProvider(
string skillPath,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? fileOptions = null,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null);
public AgentSkillsProvider(
IEnumerable<string> skillPaths,
AgentFileSkillScriptRunner? scriptRunner = null,
AgentFileSkillsSourceOptions? fileOptions = null,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null);
public AgentSkillsProvider(params AgentInlineSkill[] skills);
public AgentSkillsProvider(
IEnumerable<AgentInlineSkill> skills,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null);
public AgentSkillsProvider(
AgentSkillsSource source,
AgentSkillsProviderOptions? options = null,
ILoggerFactory? loggerFactory = null);
protected override async ValueTask<AIContext> ProvideAIContextAsync(InvokingContext context, CancellationToken cancellationToken = default);
}具体的构造规则如下所示:
- 如果指定的是一个或者多个路径,这些路径将被用来创建
AgentFileSkillsSource对象,并通过装饰DeduplicatingAgentSkillsSource进行去重。最终得到的AgentSkillsSource对象,结合指定的AgentSkillsProviderOptions对象和ILoggerFactory对象来构建AgentSkillsProvider对象; - 如果直接指定了一个或者多个
AgentInlineSkill对象,这些对象将被用来创建一个AgentInMemorySkillsSource对象,并通过装饰DeduplicatingAgentSkillsSource进行去重。最终得到的AgentSkillsSource对象,并结合指定的AgentSkillsProviderOptions对象和ILoggerFactory对象来构建AgentSkillsProvider对象;
构造函数指定的配置选项类型AgentSkillsProviderOptions定于如下,它允许我们为Agent Skills提供一些额外的配置选项。
public sealed class AgentSkillsProviderOptions
{
public string? SkillsInstructionPrompt { get; set; }
public bool ScriptApproval { get; set; }
public bool DisableCaching { get; set; }
}三个配置选项说明如下:
- SkillsInstructionPrompt:一个可选的字符串,用于指导LLM如何使用提供的Skill。这个提示词会被添加到系统提示词中,帮助LLM理解Skill的用途和使用方法;
- ScriptApproval:一个布尔值,指示是否启用脚本审批机制。如果启用,当LLM触发一个包含脚本的Skill时,系统会暂停执行并等待用户批准脚本的执行。这可以防止潜在的恶意或不安全的脚本被执行;
- DisableCaching:一个布尔值,指示是否禁用Skill的缓存机制。默认情况下,
AgentSkillsProvider会缓存从AgentSkillsSource获取的Skill集合,以提高性能。如果设置为true,每次请求都会重新获取Skill集合,适用于Skill内容频繁变化的场景;
和很多Harness功能一样,AgentSkillsProvider针对Agent Skills的实现也建立在LLM一项重要的能力上,这个能力就是根据上下文中的推理任务和提供的工具集选择适合的工具,并生成工具调用的能力。具体来说,Agent Skills采用如下的方式利用这一个能力来实现的。这一切都实现在重写的ProvideAIContextAsync方法中。
AgentSkillsProvider初始化时会获取所有Skill的元数据,这些元数据经过格式化后将成为系统指令的一部分,所以LLM永远都知道自己拥有怎样的Skill;- 注册一系列的工具供LLM动态加载所需的Skill内容、以及读取资源和执行脚本;如果
ScriptApproval选项被启用,脚本执行工具会被装饰一个ApprovalRequiredAIFunction引入审批流程;
5. 查看Agent Skills相关的工具和系统指令#
Agent Skills实现的核心就体现在AgentSkillsProvider提供的系统指令和工具上。为了查看生成的工具和系统指令,我们定义了如下这个TrackingContextProvider,它继承自AIContextProvider,并重写了InvokingCoreAsync方法,在其中我们可以访问到当前上下文中的AIContext对象,并从中获取到工具列表和系统指令。我们可以将这个TrackingContextProvider注册到Agent中来查看Agent Skills相关的工具和系统指令。
class TrackingContextProvider : AIContextProvider
{
protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
{
var aiContext = context.AIContext!;
Console.WriteLine($class="hljs-string">"{new string('-', class="hljs-number">50)}Tools{new string('-', class="hljs-number">50)}");
foreach (var tool in aiContext.Tools!)
{
if (tool is AIFunction function)
{
Console.WriteLine($class="hljs-string">"""
**{function.Name}**
Description: {function.Description}
JsonSchema:
{JsonSerializer.Serialize(function.JsonSchema, new JsonSerializerOptions { WriteIndented = true })}
class="hljs-string">""");
}
}
Console.WriteLine($class="hljs-string">"""
{new string(class="hljs-string">'-', class="hljs-number">50)}Instructions{new string(class="hljs-string">'-', class="hljs-number">50)}
{aiContext.Instructions}
class="hljs-string">""");
return base.InvokingCoreAsync(context, cancellationToken);
}
}我们将TrackingContextProvider应用到如下的这段演示程序中:我们创建了一个AgentInlineSkill对象,并为它添加了一个资源和一个脚本。然后我们创建了一个AgentSkillsProvider对象,并将这个Skill注册到其中。接着我们创建了一个OpenAIClient对象,并将其转换IChatClient对象。在调用AsAIAgent方法将IChatClient转换成IAgent的过程中,我们利用指定的ChatClientAgentOptions完成了针对AgentSkillsProvider和TrackingContextProvider的注册。最后我们调用Agent的RunAsync方法来触发这个Skill,并查看输出的工具列表和系统指令。
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using OpenAI.Responses;
using System.ClientModel;
using System.Text.Json;
DotEnv.Load();
var model = Environment.GetEnvironmentVariable(class="hljs-string">"MODEL")!;
var apiKey = Environment.GetEnvironmentVariable(class="hljs-string">"API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable(class="hljs-string">"OPENAI_URL")!;
var skill = new AgentInlineSkill(name: class="hljs-string">"fake-skill", description: class="hljs-string">"This is a fake skill for testing", instructions: class="hljs-string">"Instruction from fake-skill")
.AddResource(name: class="hljs-string">"fake-resource", value: class="hljs-string">"Value of a fake resource for testing.")
.AddScript(name: class="hljs-string">"fake-script", method: () => { });
var agentSkillsProvider = new AgentSkillsProvider([skill]);
var trackingContextProvider = new TrackingContextProvider();
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, trackingContextProvider] });
await agent.RunAsync(message: class="hljs-string">"class="hljs-number">1+class="hljs-number">1=?");
下面两端输出分别对应系统指令和工具列表:
class="hljs-string">```markdown
--------------------------------------------------Tools--------------------------------------------------
**load_skill**
Description: Loads the full content of a specific skill
JsonSchema:
{
class="hljs-string">"type": class="hljs-string">"object",
class="hljs-string">"properties": {
class="hljs-string">"skillName": {
class="hljs-string">"type": class="hljs-string">"string"
}
},
class="hljs-string">"required": [
class="hljs-string">"skillName"
]
}
**read_skill_resource**
Description: Reads a resource associated with a skill, such as references, assets, or dynamic data.
JsonSchema:
{
class="hljs-string">"type": class="hljs-string">"object",
class="hljs-string">"properties": {
class="hljs-string">"skillName": {
class="hljs-string">"type": class="hljs-string">"string"
},
class="hljs-string">"resourceName": {
class="hljs-string">"type": class="hljs-string">"string"
}
},
class="hljs-string">"required": [
class="hljs-string">"skillName",
class="hljs-string">"resourceName"
]
}
**run_skill_script**
Description: Runs a script associated with a skill.
JsonSchema:
{
class="hljs-string">"type": class="hljs-string">"object",
class="hljs-string">"properties": {
class="hljs-string">"skillName": {
class="hljs-string">"type": class="hljs-string">"string"
},
class="hljs-string">"scriptName": {
class="hljs-string">"type": class="hljs-string">"string"
},
class="hljs-string">"arguments": {
class="hljs-string">"default": null
}
},
class="hljs-string">"required": [
class="hljs-string">"skillName",
class="hljs-string">"scriptName"
]
}
--------------------------------------------------Instructions--------------------------------------------------
You have access to skills containing domain-specific knowledge and capabilities.
Each skill provides specialized instructions, reference documents, and assets for specific tasks.
<available_skills>
<skill>
<name>fake-skill</name>
<description>This is a fake skill for testing</description>
</skill>
</available_skills>
When a task aligns with a skill's domain, follow these steps in exact order:
- Use class="hljs-string">`load_skill` to retrieve the skill's instructions.
- Follow the provided guidance.
- Use class="hljs-string">`read_skill_resource` to read any referenced resources, using the name exactly as listed
(e.g. class="hljs-string">`"style-guide"` not class="hljs-string">`"style-guide.md"`, class="hljs-string">`"references/FAQ.md"` not class="hljs-string">`"FAQ.md"`).
- Use class="hljs-string">`run_skill_script` to run referenced scripts, using the name exactly as listed.
Only load what is needed, when it is needed.从输出可以看出,AgentSkillsProvider为我们提供了三个工具:load_skill、read_skill_resource和run_skill_script,它们分别用于加载Skill的内容、读取Skill的资源以及执行Skill的脚本。同时系统指令中也包含了关于如何使用这些工具来使用Skill的详细说明。系统指令不仅包含所有Skill的元数据,还提供针对上述三个工具的指示性说明。
6 AgentSkillsProviderBuilder#
虽然MAF为AgentSkillsProvider提供了四个重载的构造函数,但是更方便的构建方式还是借助于AgentSkillsProviderBuilder实现的Builder模式。通过Builder模式,我们可用先创建一个AgentSkillsProviderBuilder对象,然后通过链式调用的方式来添加Skill、配置选项,最后调用Build方法来构建出一个AgentSkillsProvider对象。
public sealed class AgentSkillsProviderBuilder
{
public AgentSkillsProviderBuilder UseFileSkill(
string skillPath,
AgentFileSkillsSourceOptions? options = null,
AgentFileSkillScriptRunner? scriptRunner = null);
public AgentSkillsProviderBuilder UseFileSkills(
IEnumerable<string> skillPaths,
AgentFileSkillsSourceOptions? options = null,
AgentFileSkillScriptRunner? scriptRunner = null);
public AgentSkillsProviderBuilder UseSkill(AgentSkill skill);
public AgentSkillsProviderBuilder UseSkills(params AgentSkill[] skills);
public AgentSkillsProviderBuilder UseSkills(IEnumerable<AgentSkill> skills);
public AgentSkillsProviderBuilder UseSource(AgentSkillsSource source);
public AgentSkillsProviderBuilder UsePromptTemplate(string promptTemplate);
public AgentSkillsProviderBuilder UseScriptApproval(bool enabled = true);
public AgentSkillsProviderBuilder UseFileScriptRunner(AgentFileSkillScriptRunner runner);
public AgentSkillsProviderBuilder UseLoggerFactory(ILoggerFactory loggerFactory);
public AgentSkillsProviderBuilder UseFilter(Func<AgentSkill, bool> predicate);
public AgentSkillsProviderBuilder UseOptions(Action<AgentSkillsProviderOptions> configure);
public AgentSkillsProvider Build();
}方法说明如下:
- UseFileSkill和UseFileSkills:从指定的文件路径加载Skill文件,支持单个路径和多个路径的重载。可以选择提供文件选项和脚本执行器;
- UseSkill和UseSkills:直接添加一个或多个AgentSkill对象到构建器中;
- UseSource:添加一个AgentSkillsSource对象作为Skill的来源;
- UsePromptTemplate:设置AgentSkillsProviderOptions的SkillsInstructionPrompt选项;
- UseScriptApproval:设置AgentSkillsProviderOptions的ScriptApproval选项;
- UseFileScriptRunner:设置一个全局的AgentFileSkillScriptRunner,用于执行所有基于文件的Skill脚本;
- UseLoggerFactory:设置一个ILoggerFactory对象,用于AgentSkillsProvider和相关组件的日志记录;
- UseFilter:对当前AgentSkillsSource(如果有多个,将会被封装成AggregatingAgentSkillsSource)装饰一个FilteringAgentSkillsSource对象对源进行过滤;
- UseOptions:提供一个配置AgentSkillsProviderOptions的委托,允许我们直接配置选项;
- Build:构建并返回一个AgentSkillsProvider对象;
7. 一个更加完整的例子#
接下来我们演示一个同时涉及资源和脚本的Agent Skills的例子,具体的演示场景为前面介绍AgentClassSkill<T>时提到的UnitConverterSkill。不过我们不会使用AgentClassSkill<T>来定义这个Skill,依然使用常规的文件。我们为这个Skill命名为unit-converter,目录结构如下:
skills/
└── unit-converter/
├── SKILL.md
├── references
| └──conversion-table.md
└── scripts
└──convert.pySKILL.md文件、conversion-table.md文件和convert.py文件的内容如下:
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
Use this skill when the user asks to convert between units.
- class="hljs-number">1. Review the **references/conversion-table.md** resource to find the correct factor.
- class="hljs-number">2. Use class="hljs-string">`--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
- class="hljs-number">3. Present the result clearly with both units.
class=class="hljs-string">"hljs-comment"># Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|------------|------------|----------|
| miles | kilometers | class="hljs-number">1.60934 |
| kilometers | miles | class="hljs-number">0.621371 |
| pounds | kilograms | class="hljs-number">0.453592 |
| kilograms | pounds | class="hljs-number">2.20462 |
import argparse,json
def main():
parser = argparse.ArgumentParser(description=class="hljs-string">'Unit Converter')
parser.add_argument(class="hljs-string">'--value', type=float, help=class="hljs-string">'The value to convert')
parser.add_argument(class="hljs-string">'--factor', type=float, help=class="hljs-string">'The conversion factor')
args = parser.parse_args()
result = args.value * args.factor
print(json.dumps({class="hljs-string">"result": result,class="hljs-string">"value": args.value, class="hljs-string">"factor": args.factor}))
if __name__ == class="hljs-string">"__main__":
main()如下所示的是完整的演示程序。我们利用AgentSkillsProviderBuilder来构建一个AgentSkillsProvider对象,并调用其UseFileSkills和UseFileScriptRunner方法定义Skill的目录和ScriptRunner。ScriptRunner指向的RunAsync方法会启动一个子进程来执行Python脚本,并将脚本的输出作为结果返回。最后我们创建了一个Agent对象,并调用RunAsync方法来运行这个Agent,传入一个需要单位转换的消息。
using dotenv.net;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;
using System.Diagnostics;
using System.Text.Json;
DotEnv.Load();
var model = Environment.GetEnvironmentVariable(class="hljs-string">"MODEL")!;
var apiKey = Environment.GetEnvironmentVariable(class="hljs-string">"API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable(class="hljs-string">"OPENAI_URL")!;
var agentSkillsProvider = new AgentSkillsProviderBuilder()
.UseFileSkills([class="hljs-string">"./skills"])
.UseFileScriptRunner(new AgentFileSkillScriptRunner(RunAsync))
.Build();
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: class="hljs-string">"一公斤有几磅?");
var inex = class="hljs-number">1;
foreach(var message in response.Messages)
{
Console.WriteLine($class="hljs-string">"\n{new string('-', class="hljs-number">40)}Message {inex++}{new string('-', class="hljs-number">40)}");
PrintMessage(message);
}
static async Task<object?> RunAsync(
AgentFileSkill skill,
AgentFileSkillScript script,
AIFunctionArguments args,
CancellationToken cancellationToken)
{
var psi = new ProcessStartInfo(class="hljs-string">"python3")
{
RedirectStandardOutput = true,
UseShellExecute = false,
};
psi.ArgumentList.Add(Path.Combine(skill.Path, script.FullPath));
foreach (var (k, v) in args)
{
if (v is not null)
{
psi.ArgumentList.Add($class="hljs-string">"--{k}");
psi.ArgumentList.Add(v.ToString()!);
}
}
using var process = Process.Start(psi)!;
string output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync();
return output.Trim();
}
static void PrintMessage(ChatMessage message)
{
Console.WriteLine($class="hljs-string">"Role: {message.Role}");
Console.WriteLine(class="hljs-string">"Contents:");
foreach(var content in message.Contents ?? [])
{
switch (content)
{
case FunctionCallContent call:
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">4)}FunctionCall");
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">8)}Name: {call.Name}");
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">8)}CallId: {call.CallId}");
if (call.Arguments is not null)
{
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">8)}Arguments");
foreach (var (k, v) in call.Arguments)
{
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">12)}{k} = {v}");
}
}
break;
case FunctionResultContent result:
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">4)}FunctionResult:");
string resultString;
if (result.Result is JsonElement jeResult && jeResult.ValueKind == JsonValueKind.String)
{
resultString = jeResult.GetString()!;
}
else
{
resultString = $class="hljs-string">"\"{result}\"";
}
foreach (var line in resultString.Split(Environment.NewLine))
{
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">8)}{line}");
}
break;
case TextContent text:
Console.WriteLine($class="hljs-string">"{new string(' ', class="hljs-number">4)}Text: {text.Text}");
break;
}
}
}由于我们提出的是一个很常规的问题,为了验证Agent是否通过我们提供的Skill对问题作答,我们将整个过程涉及的对话历史输出来啊。程序之后会输出如下所示的七个消息。
----------------------------------------Message class="hljs-number">1----------------------------------------
Role: assistant
Contents:
FunctionCall
Name: load_skill
CallId: call_8gfwzx1ybDJc7i9NYJeDfmhq
Arguments
skillName = unit-converter
----------------------------------------Message class="hljs-number">2----------------------------------------
Role: tool
Contents:
FunctionResult:
---
name: unit-converter
description: Convert between common units using a multiplication factor. Use when asked to convert miles, kilometers, pounds, or kilograms.
---
Use this skill when the user asks to convert between units.
- class="hljs-number">1. Review the **references/conversion-table.md** resource to find the correct factor.
- class="hljs-number">2. Use class="hljs-string">`--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
- class="hljs-number">3. Present the result clearly with both units.
----------------------------------------Message class="hljs-number">3----------------------------------------
Role: assistant
Contents:
FunctionCall
Name: read_skill_resource
CallId: call_sjcJBsl4ihmpY6sEMIjD1dfK
Arguments
skillName = unit-converter
resourceName = references/conversion-table.md
----------------------------------------Message class="hljs-number">4----------------------------------------
Role: tool
Contents:
FunctionResult:
class=class="hljs-string">"hljs-comment"># Conversion Tables
Formula: **result = value × factor**
| From | To | Factor |
|------------|------------|----------|
| miles | kilometers | class="hljs-number">1.60934 |
| kilometers | miles | class="hljs-number">0.621371 |
| pounds | kilograms | class="hljs-number">0.453592 |
| kilograms | pounds | class="hljs-number">2.20462 |
----------------------------------------Message class="hljs-number">5----------------------------------------
Role: assistant
Contents:
FunctionCall
Name: run_skill_script
CallId: call_VRIBhSYxFmX2OkVKiwA7YOqu
Arguments
skillName = unit-converter
scriptName = scripts/convert.py
arguments = {class="hljs-string">"value":class="hljs-number">1,class="hljs-string">"factor":class="hljs-number">2.20462}
----------------------------------------Message class="hljs-number">6----------------------------------------
Role: tool
Contents:
FunctionResult:
{class="hljs-string">"result": class="hljs-number">2.20462, class="hljs-string">"value": class="hljs-number">1.0, class="hljs-string">"factor": class="hljs-number">2.20462}
----------------------------------------Message class="hljs-number">7----------------------------------------
Role: assistant
Contents:
Text: class="hljs-number">1 公斤 ≈ **class="hljs-number">2.20462 磅**。
通常也可以近似记为:**class="hljs-number">1 公斤 ≈ class="hljs-number">2.2 磅**。从上面的输出可以看出Agent内部的执行流程:
- Agent首先调用LLM,后者回复一个消息,并携带一个针对
load_skill工具的函数调用,参数为我们定义的Skill的名称unit-converter; - Agent接收到这个消息后,识别出这是一个函数调用,于是调用对应的工具来加载这个Skill的内容,并将内容作为工具结果返回给LLM;
- LLM接收到Skill的内容后,发现涉及一个命名为
references/conversion-table.md的资源,于是又回复一个针对read_skill_resource工具的函数调用,参数为Skill名称和资源名称; - Agent调用工具来读取这个资源的内容,并将内容作为工具结果返回给LLM;
- LLM接收到资源的内容后,发现需要运行一个命名为
scripts/convert.py的脚本,并且需要传入两个参数value和factor,于是又回复一个针对run_skill_script工具的函数调用,参数为Skill名称、脚本名称和脚本参数; - Agent调用工具来执行这个脚本,脚本执行完成后将结果作为工具结果返回给LLM;
- LLM接收到脚本的执行结果后,结合之前加载的Skill内容和资源内容,生成最终的回答文本;
相关文章
评论