[MAF预定义的AIContextProvider-02]AgentSkillsProvider——将Agent Skills引入MAF

Skills针对Agent的重要性是不言而喻的。从本质上讲,Agent Skills就是随着用户与LLM对话的推进,动态加载被称为Skill作为提示词的一种机制。在大部分实现中,Skill的内容会被封装成角色为Tool的消息被添加到对话历史中,因为这样可以借助针对对话历史的压缩实现对老旧Skill的卸载。Agent Skills依然是输入增强的一种形式,所以Agent Skills在MAF中是被AgentSkillsProviderAIContextProvider引入的。

1. 利用AgentSkillsProvider引入Agent Skills

在正式介绍AgentSkillsProvider针对Agent Skills机制的设计和实现原理之前,我们先通过一个简单的实例演示一下Agent Skills在MAF中的编程模式。我们定义了一个名为translator的Skill,它的功能是将中文古典诗词翻译成英文。我们将这个Markdown文件保存在./skills/translator/SKILL.md路径下,内容如下:

markdown 复制代码
---
name: translator
description: 将中文古典诗词精确翻译成地道的英文
---

## 详细指令

你是一位精通汉学与英美文学的翻译大师。请严格按照以下 3 条规则处理输入的古典诗词:

1. **必须提供三种翻译变体**:
   - **变体一:古典韵律版**(必须**押韵**,注重节拍和英诗的传统美感)
   - **变体二:现代诗**(不强求押韵,注重现代诗的流动感)
   - **变体三:孤寂禅意版**(字句极简,精准保留原诗的**禅意**与留白,不做过度解释)

2. **输出格式**:
   请直接以 Markdown 标题列出这三种变体,并在最下方提供必要的背景(典故)补充。

具体的演示程序如下所示:我们根据存储Skill的目录./skills创建了一个AgentSkillsProvider实例。然后我们创建了一个OpenAIClient对象,并将其转换IChatClient对象。在调用AsAIAgent方法将IChatClient转换成IAgent的过程中,我们利用指定的ChatClientAgentOptions完成了针对AgentSkillsProvider的注册。最后我们调用AgentRunAsync方法,并传入一首中文古典诗词,来查看Agent基于这个Skill生成的翻译结果。

csharp 复制代码
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);

输出:

markdown 复制代码
### 变体一:古典韵律版(押韵)

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的四个元素。

csharp 复制代码
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为每个字段提供了详细的规范。

csharp 复制代码
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用于读取这个资源的内容。

csharp 复制代码
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对象。

csharp 复制代码
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文件的路径。

csharp 复制代码
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,其资源和脚本类型也是对应的AgentFileSkillResourceAgentFileSkillScript。如下面的代码所示,AgentFileSkillResource是一个内部类型。不论是构建一个AgentFileSkillResource对象,还是构建一个AgentFileSkillScript对象,我们都需要提供一个文件的完整路径。对于资源来说,ReadAsync方法会读取这个文件的内容并返回;对于脚本来说,RunAsync方法会利用提供的Runner来执行这个脚本文件。这个指定脚本的Runner体现为一个类型为AgentFileSkillScriptRunner的委托对象,该委托的三个输入参数分别代表触发这个脚本的Skill对象、这个脚本对象以及LLM传入的参数,返回值则是脚本执行后的结果。

csharp 复制代码
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直接绑定元数据、提示词指令、静态资源以及可执行的脚本函数。

csharp 复制代码
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);
}

通过AddResourceAddScript方法添加的资源和脚本类型也与之匹配,分别是具有如下定义的AgentInlineSkillResourceAgentInlineSkillScript类型。AgentInlineSkillResource使用该委托对象来读取资源的内容,AgentInlineSkillScript使用该对象表示可执行的脚本 。由于AIFunctionFactory可以将一个委托对象转换成一个AIFunction对象,所以资源读取和脚本都可以通过执行AIFunction对象的方式来完成。

csharp 复制代码
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,而资源和脚本则是可选的,所以对应的属性ResourcesScripts被定义成virtual成员,默认返回null。

csharp 复制代码
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类时,利用具有如下定义的AgentSkillResourceAttributeAgentSkillScriptAttribute将就具有合法签名的方法来表示资源和脚本。资源除了定义成方法之外也可以定义成属性。定义在AgentClassSkill<TSelf>中的ResourcesScripts属性会通过反射扫描这些被标记的方法和属性,并将它们转换成AgentSkillResourceAgentSkillScript对象,并最终返回资源和脚本列表。

csharp 复制代码
[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的方法要求是无参数,或者只包含类型为IServiceProviderCancellationToken的参数。标注在方法或者属性上的DescriptionAttribute特性将用于描述对应的资源和脚本。除此之外,ResourcesScripts属性还会验证资源和脚本名称的唯一性,确保不会有重复的资源和脚本名称。

MAF官方文档提供了如下这个UnitConverterSkill类型,旨在完成英里/公里以及磅/千克之间的转换。我们可以看到它通过标记AgentSkillResourceAttribute特性来定义了一个名为conversion-table的资源,通过标记AgentSkillScriptAttribute特性来定义了一个名为convert的脚本。

csharp 复制代码
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集合。

csharp 复制代码
public abstract class AgentSkillsSource
{
	public abstract Task<IList<AgentSkill>> GetSkillsAsync(
        CancellationToken cancellationToken = default);
}

3.1 AgentInMemorySkillsSource & AgentFileSkillsSource

MAF内置AgentInMemorySkillsSourceAgentFileSkillsSource这两种。前者直接从内存中提供Skill集合,后者则从文件系统中加载Skill集合。它们提供AgentSkill的具体类型就是前面介绍的AgentInlineSkillAgentFileSkill

csharp 复制代码
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提供了AllowedResourceExtensionsAllowedScriptExtensions这两个配置选项,允许我们指定Skill文件和脚本文件的扩展名。

csharp 复制代码
public sealed class AgentFileSkillsSourceOptions
{
	public IEnumerable<string>? AllowedResourceExtensions { get; set; }
	public IEnumerable<string>? AllowedScriptExtensions { get; set; }
}

3.2 DelegatingAgentSkillsSource

DelegatingAgentSkillsSource可视为针对AgentSkillsSource的中间件或者装饰器,我们可用将一组中间件对象装饰到一个AgentSkillsSource对象上实现对提供的Skill集合进行过滤、转换或增强。派生于它的DeduplicatingAgentSkillsSourceFilteringAgentSkillsSource分别提供了去重和过滤的功能。

csharp 复制代码
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对象。

csharp 复制代码
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传入的参数,返回值则是脚本执行后的结果。

csharp 复制代码
public delegate Task<object?> AgentFileSkillScriptRunner(
    AgentFileSkill skill, 
    AgentFileSkillScript script, 
    AIFunctionArguments arguments, 
    CancellationToken cancellationToken);

MAF官方文档中提供了采用子进程 执行Python脚本的AgentFileSkillScriptRunner实现,但是它提供的方法签名与AgentFileSkillScriptRunner委托定义并不兼容,我做了相应修改。

csharp 复制代码
using System.Diagnostics;
using System.Text.Json;

static async Task<object?> RunAsync(
    AgentFileSkill skill,
    AgentFileSkillScript script,
    AIFunctionArguments args,
    CancellationToken cancellationToken)
{
    var psi = new ProcessStartInfo("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($"--{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对象的构建最终都会落在最后一个构造函数上。

csharp 复制代码
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提供一些额外的配置选项。

csharp 复制代码
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相关的工具和系统指令。

csharp 复制代码
class TrackingContextProvider : AIContextProvider
{
    protected override ValueTask<AIContext> InvokingCoreAsync(InvokingContext context, CancellationToken cancellationToken = default)
    {
        var aiContext = context.AIContext!;
        Console.WriteLine($"{new string('-', 50)}Tools{new string('-', 50)}");
        foreach (var tool in aiContext.Tools!)
        {
            if (tool is AIFunction function)
            {
                Console.WriteLine($"""
                    **{function.Name}**
                    Description: {function.Description}
                    JsonSchema: 
                    {JsonSerializer.Serialize(function.JsonSchema, new JsonSerializerOptions { WriteIndented = true })}

                    """);
            }           
        }

        Console.WriteLine($"""
            {new string('-', 50)}Instructions{new string('-', 50)}
                {aiContext.Instructions}
            """);

        return base.InvokingCoreAsync(context, cancellationToken);
    }
}

我们将TrackingContextProvider应用到如下的这段演示程序中:我们创建了一个AgentInlineSkill对象,并为它添加了一个资源和一个脚本。然后我们创建了一个AgentSkillsProvider对象,并将这个Skill注册到其中。接着我们创建了一个OpenAIClient对象,并将其转换IChatClient对象。在调用AsAIAgent方法将IChatClient转换成IAgent的过程中,我们利用指定的ChatClientAgentOptions完成了针对AgentSkillsProviderTrackingContextProvider的注册。最后我们调用Agent的RunAsync方法来触发这个Skill,并查看输出的工具列表和系统指令。

csharp 复制代码
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("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var skill = new AgentInlineSkill(name: "fake-skill", description: "This is a fake skill for testing", instructions: "Instruction from fake-skill")
    .AddResource(name: "fake-resource", value: "Value of a fake resource for testing.")
    .AddScript(name: "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: "1+1=?");
markdown 复制代码
下面两端输出分别对应系统指令和工具列表:

```markdown
--------------------------------------------------Tools--------------------------------------------------
**load_skill**
Description: Loads the full content of a specific skill
JsonSchema:
{
  "type": "object",
  "properties": {
    "skillName": {
      "type": "string"
    }
  },
  "required": [
    "skillName"
  ]
}

**read_skill_resource**
Description: Reads a resource associated with a skill, such as references, assets, or dynamic data.
JsonSchema:
{
  "type": "object",
  "properties": {
    "skillName": {
      "type": "string"
    },
    "resourceName": {
      "type": "string"
    }
  },
  "required": [
    "skillName",
    "resourceName"
  ]
}

**run_skill_script**
Description: Runs a script associated with a skill.
JsonSchema:
{
  "type": "object",
  "properties": {
    "skillName": {
      "type": "string"
    },
    "scriptName": {
      "type": "string"
    },
    "arguments": {
      "default": null
    }
  },
  "required": [
    "skillName",
    "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 `load_skill` to retrieve the skill's instructions.
- Follow the provided guidance.
- Use `read_skill_resource` to read any referenced resources, using the name exactly as listed
   (e.g. `"style-guide"` not `"style-guide.md"`, `"references/FAQ.md"` not `"FAQ.md"`).
- Use `run_skill_script` to run referenced scripts, using the name exactly as listed.
Only load what is needed, when it is needed.

从输出可以看出,AgentSkillsProvider为我们提供了三个工具:load_skillread_skill_resourcerun_skill_script,它们分别用于加载Skill的内容、读取Skill的资源以及执行Skill的脚本。同时系统指令中也包含了关于如何使用这些工具来使用Skill的详细说明。系统指令不仅包含所有Skill的元数据,还提供针对上述三个工具的指示性说明。

6 AgentSkillsProviderBuilder

虽然MAF为AgentSkillsProvider提供了四个重载的构造函数,但是更方便的构建方式还是借助于AgentSkillsProviderBuilder实现的Builder模式。通过Builder模式,我们可用先创建一个AgentSkillsProviderBuilder对象,然后通过链式调用的方式来添加Skill、配置选项,最后调用Build方法来构建出一个AgentSkillsProvider对象。

csharp 复制代码
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();
}

方法说明如下:

  • UseFileSkillUseFileSkills:从指定的文件路径加载Skill文件,支持单个路径和多个路径的重载。可以选择提供文件选项和脚本执行器;
  • UseSkillUseSkills:直接添加一个或多个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.py

SKILL.md文件、conversion-table.md文件和convert.py文件的内容如下:

markdown 复制代码
---
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.

- 1. Review the **references/conversion-table.md** resource to find the correct factor.
- 2. Use `--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
- 3. Present the result clearly with both units.
markdown 复制代码
# 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  |
python 复制代码
import argparse,json

def main():
    parser = argparse.ArgumentParser(description='Unit Converter')
    parser.add_argument('--value', type=float, help='The value to convert')
    parser.add_argument('--factor', type=float, help='The conversion factor')
    args = parser.parse_args()
    result = args.value * args.factor
    print(json.dumps({"result": result,"value": args.value, "factor": args.factor}))

if __name__ == "__main__":
    main()

如下所示的是完整的演示程序。我们利用AgentSkillsProviderBuilder来构建一个AgentSkillsProvider对象,并调用其UseFileSkillsUseFileScriptRunner方法定义Skill的目录和ScriptRunnerScriptRunner指向的RunAsync方法会启动一个子进程来执行Python脚本,并将脚本的输出作为结果返回。最后我们创建了一个Agent对象,并调用RunAsync方法来运行这个Agent,传入一个需要单位转换的消息。

csharp 复制代码
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("MODEL")!;
var apiKey = Environment.GetEnvironmentVariable("API_KEY")!;
var endpoint = Environment.GetEnvironmentVariable("OPENAI_URL")!;

var agentSkillsProvider = new AgentSkillsProviderBuilder()
    .UseFileSkills(["./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: "一公斤有几磅?");
var inex = 1;
foreach(var message in response.Messages)
{
    Console.WriteLine($"\n{new string('-', 40)}Message {inex++}{new string('-', 40)}");
    PrintMessage(message);
}

static async Task<object?> RunAsync(
    AgentFileSkill skill,
    AgentFileSkillScript script,
    AIFunctionArguments args,
    CancellationToken cancellationToken)
{
    var psi = new ProcessStartInfo("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($"--{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($"Role: {message.Role}");
    Console.WriteLine("Contents:");
    foreach(var content in message.Contents ?? [])
    {
        switch (content)
        {
            case FunctionCallContent call:
                Console.WriteLine($"{new string(' ', 4)}FunctionCall");
                Console.WriteLine($"{new string(' ', 8)}Name:  {call.Name}");
                Console.WriteLine($"{new string(' ', 8)}CallId: {call.CallId}");
                if (call.Arguments is not null)
                {
                    Console.WriteLine($"{new string(' ', 8)}Arguments");
                    foreach (var (k, v) in call.Arguments)
                    {
                        Console.WriteLine($"{new string(' ', 12)}{k} = {v}");
                    }
                }
                break;
            case FunctionResultContent result:
                Console.WriteLine($"{new string(' ', 4)}FunctionResult:");
                string resultString;

                if (result.Result is JsonElement jeResult && jeResult.ValueKind == JsonValueKind.String)
                {
                    resultString = jeResult.GetString()!;
                }
                else 
                {
                    resultString = $"\"{result}\"";
                }

                foreach (var line in resultString.Split(Environment.NewLine))
                {
                    Console.WriteLine($"{new string(' ', 8)}{line}");
                }
                break;
            case TextContent text:
                Console.WriteLine($"{new string(' ', 4)}Text: {text.Text}");
                break;
        }
    }
}

由于我们提出的是一个很常规的问题,为了验证Agent是否通过我们提供的Skill对问题作答,我们将整个过程涉及的对话历史输出来啊。程序之后会输出如下所示的七个消息。

markdown 复制代码
----------------------------------------Message 1----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  load_skill
        CallId: call_8gfwzx1ybDJc7i9NYJeDfmhq
        Arguments
            skillName = unit-converter

----------------------------------------Message 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.

        - 1. Review the **references/conversion-table.md** resource to find the correct factor.
        - 2. Use `--value <value> --factor <factor>` options to run the **scripts/convert.py** script.
        - 3. Present the result clearly with both units.

----------------------------------------Message 3----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  read_skill_resource
        CallId: call_sjcJBsl4ihmpY6sEMIjD1dfK
        Arguments
            skillName = unit-converter
            resourceName = references/conversion-table.md

----------------------------------------Message 4----------------------------------------
Role: tool
Contents:
    FunctionResult:
        # 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  |

----------------------------------------Message 5----------------------------------------
Role: assistant
Contents:
    FunctionCall
        Name:  run_skill_script
        CallId: call_VRIBhSYxFmX2OkVKiwA7YOqu
        Arguments
            skillName = unit-converter
            scriptName = scripts/convert.py
            arguments = {"value":1,"factor":2.20462}

----------------------------------------Message 6----------------------------------------
Role: tool
Contents:
    FunctionResult:
        {"result": 2.20462, "value": 1.0, "factor": 2.20462}

----------------------------------------Message 7----------------------------------------
Role: assistant
Contents:
    Text: 1 公斤 ≈ **2.20462 磅**。

通常也可以近似记为:**1 公斤 ≈ 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的脚本,并且需要传入两个参数valuefactor,于是又回复一个针对run_skill_script工具的函数调用,参数为Skill名称、脚本名称和脚本参数;
  • Agent调用工具来执行这个脚本,脚本执行完成后将结果作为工具结果返回给LLM;
  • LLM接收到脚本的执行结果后,结合之前加载的Skill内容和资源内容,生成最终的回答文本;
相关推荐
冬奇Lab1 小时前
Agent 系列(13):Agent 安全与防护——提示词注入、工具滥用、数据泄露怎么防
人工智能·llm·agent
小满Autumn2 小时前
MVVM Light 架构笔记:定位器、命令、消息与 IoC 实践
笔记·学习·架构·c#·上位机·mvvm
实在智能RPA2 小时前
药企GMP合规自动化破局:实在Agent的功能完整度评估与落地实践
运维·人工智能·ai·自动化
布列瑟农的星空2 小时前
低码类平台Agent通用架构设计
agent
小满Autumn3 小时前
CommunityToolkit.Mvvm 架构笔记:现代 MVVM、源生成器与工程化实践
笔记·架构·c#·.net·wpf·mvvm
加号34 小时前
【C#】 JSON 序列化与反序列化:从入门到最佳实践
c#·json
勇往直前plus4 小时前
智能体记忆概述
人工智能·python·ai
装不满的克莱因瓶5 小时前
学习并掌握 LangChain 检索器的作用,实现让 LLM 动态调用知识库功能
人工智能·python·ai·langchain·llm·agent·智能体