1. 利用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的注册。最后我们调用AgentRunAsync方法,并传入一首中文古典诗词,来查看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,其资源和脚本类型也是对应的AgentFileSkillResourceAgentFileSkillScript。如下面的代码所示,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);
}

通过AddResourceAddScript方法添加的资源和脚本类型也与之匹配,分别是具有如下定义的AgentInlineSkillResourceAgentInlineSkillScript类型。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,而资源和脚本则是可选的,所以对应的属性ResourcesScripts被定义成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类时,利用具有如下定义的AgentSkillResourceAttributeAgentSkillScriptAttribute将就具有合法签名的方法来表示资源和脚本。资源除了定义成方法之外也可以定义成属性。定义在AgentClassSkill<TSelf>中的ResourcesScripts属性会通过反射扫描这些被标记的方法和属性,并将它们转换成AgentSkillResourceAgentSkillScript对象,并最终返回资源和脚本列表。

[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的脚本。

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

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文件和脚本文件的扩展名。

public sealed class AgentFileSkillsSourceOptions
{
	public IEnumerable<string>? AllowedResourceExtensions { get; set; }
	public IEnumerable<string>? AllowedScriptExtensions { get; set; }
}

3.2 DelegatingAgentSkillsSource

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

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("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对象的构建最终都会落在最后一个构造函数上。

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($"{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,并查看输出的工具列表和系统指令。

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=?");

C# 复制 全屏

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐