.NET 中的人工智能(预览版)

随着越来越多的人工智能 (AI) 服务上市,开发人员需要一种方法在他们的 .NET 应用程序中集成这些服务并与之交互。 Microsoft.Extensions.AI 库提供了一种统一的方法来表示生成式 AI 组件,从而实现与各种 AI 服务的无缝集成和互操作性。 本文介绍了该库,并提供了安装说明和使用示例,以帮助你入门。

安装该软件包

若要安装 📦 Microsoft.Extensions.AI NuGet 包,请使用 .NET CLI 或直接将包引用添加到 C# 项目文件:

dotnet add package Microsoft.Extensions.AI --prelease

有关详细信息,请参阅 dotnet add package管理 .NET 应用程序中的包依赖项

用法示例

IChatClient 接口定义一个客户端抽象,负责与提供聊天功能的 AI 服务交互。 它包括发送和接收具有多模式内容(如文本、图像和音频)的消息的方法,无论是作为一个完整的集合还是增量流式传输。 此外,它还提供有关客户端的元数据信息,并允许检索强类型服务。

重要

有关更多用法示例和实际方案,请参阅面向 .NET 开发人员的 AI

本部分内容

IChatClient 接口

以下示例实现 IChatClient 以显示一般结构。

using System.Runtime.CompilerServices;
using Microsoft.Extensions.AI;

public sealed class SampleChatClient(Uri endpoint, string modelId) : IChatClient
{
    public ChatClientMetadata Metadata { get; } = new(nameof(SampleChatClient), endpoint, modelId);

    public async Task<ChatCompletion> CompleteAsync(
        IList<ChatMessage> chatMessages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        // Simulate some operation.
        await Task.Delay(300, cancellationToken);

        // Return a sample chat completion response randomly.
        string[] responses =
        [
            "This is the first sample response.",
            "Here is another example of a response message.",
            "This is yet another response message."
        ];

        return new([new ChatMessage()
        {
            Role = ChatRole.Assistant,
            Text = responses[Random.Shared.Next(responses.Length)],
        }]);
    }

    public async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
        IList<ChatMessage> chatMessages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // Simulate streaming by yielding messages one by one.
        string[] words = ["This ", "is ", "the ", "response ", "for ", "the ", "request."];
        foreach (string word in words)
        {
            // Simulate some operation.
            await Task.Delay(100, cancellationToken);

            // Yield the next message in the response.
            yield return new StreamingChatCompletionUpdate
            {
                Role = ChatRole.Assistant,
                Text = word,
            };
        }
    }

    public object? GetService(Type serviceType, object? serviceKey) => this;

    public TService? GetService<TService>(object? key = null)
        where TService : class => this as TService;

    void IDisposable.Dispose() { }
}

可以在以下 NuGet 包中找到 IChatClient 的其他具体实现:

请求完成聊天

若要请求完成,请调用 IChatClient.CompleteAsync 方法。 请求由一个或多个消息组成,每个消息由一条或多条内容组成。 加速器方法的存在是为了简化常见情况,例如构造对单个文本内容的请求。

using Microsoft.Extensions.AI;

IChatClient client = new SampleChatClient(
    new Uri("http://coolsite.ai"), "target-ai-model");

var response = await client.CompleteAsync("What is AI?");

Console.WriteLine(response.Message);

核心 IChatClient.CompleteAsync 方法接受消息列表。 此列表表示对话中所有消息的历史记录。

using Microsoft.Extensions.AI;

IChatClient client = new SampleChatClient(
    new Uri("http://coolsite.ai"), "target-ai-model");

Console.WriteLine(await client.CompleteAsync(
[
    new(ChatRole.System, "You are a helpful AI assistant"),
    new(ChatRole.User, "What is AI?"),
]));

历史记录中的每条消息都由一个 ChatMessage 对象表示。 ChatMessage 类提供一个 ChatMessage.Role 属性,用于指示消息的角色。 默认情况下使用 ChatRole.User。 以下角色可用:

实例化每个聊天消息,并为其 Contents 属性分配一个新的 TextContent。 可以表示的各种类型的内容,例如一个简单的字符串或一个更复杂的对象,它表示一个包含文本、图像和音频的多模式消息:

请求通过流式处理完成聊天

IChatClient.CompleteStreamingAsync 的输入与 CompleteAsync 的输入相同。 然而,该方法不是将完整的响应作为 ChatCompletion 对象的一部分返回,而是返回一个 IAsyncEnumerable<T>,其中 TStreamingChatCompletionUpdate,提供了一系列更新,这些更新共同形成了单个响应。

using Microsoft.Extensions.AI;

IChatClient client = new SampleChatClient(
    new Uri("http://coolsite.ai"), "target-ai-model");

await foreach (var update in client.CompleteStreamingAsync("What is AI?"))
{
    Console.Write(update);
}

提示

流媒体接口几乎与 AI 用户体验密不可分。 C# 通过其对 IAsyncEnumerable<T> 的支持实现了引人注目的方案,允许以自然且高效的方式流式传输数据。

工具调用

某些模型和服务支持 工具调用,在请求中可以包含用于模型调用函数以收集其他信息的工具。 模型不发送最终响应,而是请求带有特定参数的函数调用。 然后,客户端调用该函数,并将结果与对话历史记录一起发送回模型。 Microsoft.Extensions.AI 库包括各种消息内容类型的抽象,包括函数调用请求和结果。 虽然消费者可以直接与此内容交互,但 Microsoft.Extensions.AI 自动化这些交互并提供:

考虑以下示例,演示随机函数调用:

using System.ComponentModel;
using Microsoft.Extensions.AI;

[Description("Gets the current weather")]
string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5
    ? "It's sunny"
    : "It's raining";

IChatClient client = new ChatClientBuilder(
        new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1"))
    .UseFunctionInvocation()
    .Build();

var response = client.CompleteStreamingAsync(
    "Should I wear a rain coat?",
    new() { Tools = [AIFunctionFactory.Create(GetCurrentWeather)] });

await foreach (var update in response)
{
    Console.Write(update);
}

前面的示例依赖于 📦 Microsoft.Extensions.AI.Ollama NuGet 包。

前面的代码:

  • 定义一个名为 GetCurrentWeather 的函数,返回随机天气预报。
    • 此函数使用 DescriptionAttribute进行修饰,该修饰用于向 AI 服务提供该函数的说明。
  • 使用 OllamaChatClient 实例化 ChatClientBuilder,并将其配置为使用函数调用。
  • 在客户端调用 CompleteStreamingAsync,传递一个提示和一个工具列表,其中包括用 Create 创建的函数。
  • 遍历响应,将每个更新输出到控制台。

缓存响应

如果你熟悉 .NET 中的缓存,那么应该了解 Microsoft.Extensions.AI 提供了其他这样的委托 IChatClient 实现。 DistributedCachingChatClient 是一个 IChatClient,它围绕另一个任意的 IChatClient 实例分层缓存。 当唯一聊天历史记录被提交给 DistributedCachingChatClient 时,它会将其转发给基础客户端,然后在将其发送回使用者之前缓存响应。 下次提交相同的提示,以便在缓存中找到缓存的响应时,DistributedCachingChatClient 将返回缓存的响应,而不需要沿管道转发请求。

using Microsoft.Extensions.AI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

var sampleChatClient = new SampleChatClient(
    new Uri("http://coolsite.ai"), "target-ai-model");

IChatClient client = new ChatClientBuilder(sampleChatClient)
    .UseDistributedCache(new MemoryDistributedCache(
        Options.Create(new MemoryDistributedCacheOptions())))
    .Build();

string[] prompts = ["What is AI?", "What is .NET?", "What is AI?"];

foreach (var prompt in prompts)
{
    await foreach (var update in client.CompleteStreamingAsync(prompt))
    {
        Console.Write(update);
    }

    Console.WriteLine();
}

前面的示例依赖于 📦 Microsoft.Extensions.Caching.Memory NuGet 包。 有关详细信息,请参阅 .NET 中的缓存

使用遥测

另一个委派聊天客户端的示例是 OpenTelemetryChatClient。 此实现遵循生成式 AI 系统的 OpenTelemetry 语义约定。 与其他 IChatClient 委托者类似,它将指标分层,并跨越任何基础 IChatClient 实现,从而提供增强的可观测性。

using Microsoft.Extensions.AI;
using OpenTelemetry.Trace;

// Configure OpenTelemetry exporter
var sourceName = Guid.NewGuid().ToString();
var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
    .AddSource(sourceName)
    .AddConsoleExporter()
    .Build();

var sampleChatClient = new SampleChatClient(
    new Uri("http://coolsite.ai"), "target-ai-model");

IChatClient client = new ChatClientBuilder(sampleChatClient)
    .UseOpenTelemetry(
        sourceName: sourceName,
        configure: static c => c.EnableSensitiveData = true)
    .Build();

Console.WriteLine((await client.CompleteAsync("What is AI?")).Message);

前面的示例依赖于 📦 OpenTelemetry.Exporter.Console NuGet 包。

提供选项

每次调用 CompleteAsyncCompleteStreamingAsync 时,可以选用一个包含附加操作参数的 ChatOptions 实例。 AI 模型和服务中的最常见参数呈现为类型的强类型化属性,例如 ChatOptions.Temperature。 其他参数可以通过 ChatOptions.AdditionalProperties 字典以弱类型的方式按名称提供。

还可以在使用 fluent IChatClient API 构建 ChatClientBuilder 并将调用链接到 ConfigureOptions 扩展方法时指定选项。 此委托客户端包装另一个客户端,并调用提供的委托为每次调用填充一个 ChatOptions 实例。 例如,为了确保 ChatOptions.ModelId 属性默认为特定的模型名称,可以使用以下代码:

using Microsoft.Extensions.AI;

IChatClient client = new ChatClientBuilder(
        new OllamaChatClient(new Uri("http://localhost:11434")))
    .ConfigureOptions(options => options.ModelId ??= "phi3")
    .Build();

// will request "phi3"
Console.WriteLine(await client.CompleteAsync("What is AI?"));

// will request "llama3.1"
Console.WriteLine(await client.CompleteAsync(
    "What is AI?", new() { ModelId = "llama3.1" }));

前面的示例依赖于 📦 Microsoft.Extensions.AI.Ollama NuGet 包。

功能管道

可以对 IChatClient 实例进行分层,以创建组件管道,每个组件添加特定的功能。 这些组件可以来自 Microsoft.Extensions.AI、其他 NuGet 包或自定义实现。 这种方法允许你以各种方式增强 IChatClient 的行为,以满足特定需求。 请考虑以下示例代码,该代码为示例聊天客户端实施了分布式缓存层、函数调用和 OpenTelemetry 跟踪:

using Microsoft.Extensions.AI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;

// Configure OpenTelemetry exporter
var sourceName = Guid.NewGuid().ToString();
var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
    .AddSource(sourceName)
    .AddConsoleExporter()
    .Build();

// Explore changing the order of the intermediate "Use" calls to see that impact
// that has on what gets cached, traced, etc.
IChatClient client = new ChatClientBuilder(
        new OllamaChatClient(new Uri("http://localhost:11434"), "llama3.1"))
    .UseDistributedCache(new MemoryDistributedCache(
        Options.Create(new MemoryDistributedCacheOptions())))
    .UseFunctionInvocation()
    .UseOpenTelemetry(
        sourceName: sourceName,
        configure: static c => c.EnableSensitiveData = true)
    .Build();

ChatOptions options = new()
{
    Tools =
    [
        AIFunctionFactory.Create(
            () => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining",
            name: "GetCurrentWeather",
            description: "Gets the current weather")
    ]
};

for (int i = 0; i < 3; ++i)
{
    List<ChatMessage> history =
    [
        new ChatMessage(ChatRole.System, "You are a helpful AI assistant"),
        new ChatMessage(ChatRole.User, "Do I need an umbrella?")
    ];

    Console.WriteLine(await client.CompleteAsync(history, options));
}

前面的示例依赖于以下 NuGet 包:

自定义 IChatClient 中间件

若要添加其他功能,可以直接实现 IChatClient 或使用 DelegatingChatClient 类。 此类用作创建专门将操作委托给另一个 IChatClient 实例的聊天客户端的基础。 它简化了链接多个客户端,允许调用传递到基础客户端。

DelegatingChatClient 类为 CompleteAsyncCompleteStreamingAsyncDispose 等方法提供了默认实现,这些方法将调用转发到内部客户端。 可以从该类派生并仅重写增强行为所需的方法,同时将其他调用委托给基础实现。 这种方法有助于创建易于扩展和撰写的灵活的模块化聊天客户端。

以下是一个从 DelegatingChatClient 派生的示例类,利用 RateLimiter 提供速率限制功能:

using Microsoft.Extensions.AI;
using System.Runtime.CompilerServices;
using System.Threading.RateLimiting;

public sealed class RateLimitingChatClient(
    IChatClient innerClient, RateLimiter rateLimiter)
        : DelegatingChatClient(innerClient)
{
    public override async Task<ChatCompletion> CompleteAsync(
        IList<ChatMessage> chatMessages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        using var lease = await rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);

        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("Unable to acquire lease.");
        }

        return await base.CompleteAsync(chatMessages, options, cancellationToken)
            .ConfigureAwait(false);
    }

    public override async IAsyncEnumerable<StreamingChatCompletionUpdate> CompleteStreamingAsync(
        IList<ChatMessage> chatMessages,
        ChatOptions? options = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        using var lease = await rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);

        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("Unable to acquire lease.");
        }

        await foreach (var update in base.CompleteStreamingAsync(chatMessages, options, cancellationToken)
            .ConfigureAwait(false))
        {
            yield return update;
        }
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            rateLimiter.Dispose();
        }

        base.Dispose(disposing);
    }
}

前面的示例依赖于 📦 System.Threading.RateLimiting NuGet 包。 将 RateLimitingChatClient 和另一个客户端组合在一起非常简单:

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;

var client = new RateLimitingChatClient(
    new SampleChatClient(new Uri("http://localhost"), "test"),
    new ConcurrencyLimiter(new()
    {
        PermitLimit = 1,
        QueueLimit = int.MaxValue
    }));

await client.CompleteAsync("What color is the sky?");

为了简化这些组件与其他组件的组合,组件作者应该创建一个 Use* 扩展方法,将组件注册到管道中。 例如,请考虑使用以下扩展方法:

namespace Example.One;

// <one>
using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;

public static class RateLimitingChatClientExtensions
{
    public static ChatClientBuilder UseRateLimiting(
        this ChatClientBuilder builder, RateLimiter rateLimiter) =>
        builder.Use(innerClient => new RateLimitingChatClient(innerClient, rateLimiter));
}
// </one>

这样的扩展还可以从 DI 容器查询相关服务;管道使用的 IServiceProvider 作为可选参数传入:

namespace Example.Two;

// <two>
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using System.Threading.RateLimiting;

public static class RateLimitingChatClientExtensions
{
    public static ChatClientBuilder UseRateLimiting(
        this ChatClientBuilder builder, RateLimiter? rateLimiter = null) =>
        builder.Use((innerClient, services) =>
            new RateLimitingChatClient(
                innerClient,
                rateLimiter ?? services.GetRequiredService<RateLimiter>()));
}
// </two>

然后,使用者可以很容易地在他们的管道中使用它,例如:

using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddChatClient(services =>
    new SampleChatClient(new Uri("http://localhost"), "test")
        .AsBuilder()
        .UseDistributedCache()
        .UseRateLimiting()
        .UseOpenTelemetry()
        .Build(services));

using var app = builder.Build();

// Elsewhere in the app
var chatClient = app.Services.GetRequiredService<IChatClient>();

Console.WriteLine(await chatClient.CompleteAsync("What is AI?"));

app.Run();

此示例演示了托管方案,使用者依赖依赖项注入来提供 RateLimiter 实例。 前面的扩展方法演示了在 ChatClientBuilder 上使用 Use 方法。 ChatClientBuilder 还提供 Use 重载,使编写此类委托处理程序变得更加容易。

例如,在前面的 RateLimitingChatClient 示例中,CompleteAsyncCompleteStreamingAsync 的重写只需要在委托给管道中的下一个客户端之前和之后执行工作。 为了在不编写自定义类的情况下实现同样的目的,你可以使用 Use 的重载来接受一个同时用于 CompleteAsyncCompleteStreamingAsync 的委托,从而减少所需的样板代码:

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;

RateLimiter rateLimiter = new ConcurrencyLimiter(new()
{
    PermitLimit = 1,
    QueueLimit = int.MaxValue
});

var client = new SampleChatClient(new Uri("http://localhost"), "test")
    .AsBuilder()
    .UseDistributedCache()
    .Use(async (chatMessages, options, nextAsync, cancellationToken) =>
    {
        using var lease = await rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);

        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("Unable to acquire lease.");
        }

        await nextAsync(chatMessages, options, cancellationToken);
    })
    .UseOpenTelemetry()
    .Build();

// Use client

前面的重载在内部使用了一个 AnonymousDelegatingChatClient,这使得只需要一点额外的代码就可以实现更复杂的模式。 例如,要获得相同的结果,但要从 DI 中检索 RateLimiter

using System.Threading.RateLimiting;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;

var client = new SampleChatClient(new Uri("http://localhost"), "test")
    .AsBuilder()
    .UseDistributedCache()
    .Use(static (innerClient, services) =>
    {
        var rateLimiter = services.GetRequiredService<RateLimiter>();

        return new AnonymousDelegatingChatClient(
            innerClient, async (chatMessages, options, nextAsync, cancellationToken) =>
            {
                using var lease = await rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
                    .ConfigureAwait(false);

                if (!lease.IsAcquired)
                {
                    throw new InvalidOperationException("Unable to acquire lease.");
                }

                await nextAsync(chatMessages, options, cancellationToken);
            });
    })
    .UseOpenTelemetry()
    .Build();

对于开发人员希望指定 CompleteAsyncCompleteStreamingAsync 内联的委托实现的场景,以及能够为每种实现编写不同的实现以专门处理其唯一返回类型的场景,存在 Use 的另一个重载,为每种类型接受一个委托。

依赖项注入

IChatClient 实现通常通过依赖项注入 (DI) 提供给应用程序。 在此示例中,将 IDistributedCache 添加到 DI 容器中,IChatClient也同样被添加。 IChatClient 的注册使用一个生成器,该生成器创建了一个包含缓存客户端(然后将使用从 DI 检索到的 IDistributedCache)和示例客户端的管道。 注入的 IChatClient 可以在应用程序的其他地方检索和使用。

using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

// App setup
var builder = Host.CreateApplicationBuilder();

builder.Services.AddDistributedMemoryCache();
builder.Services.AddChatClient(new SampleChatClient(
        new Uri("http://coolsite.ai"), "target-ai-model"))
    .UseDistributedCache();

using var app = builder.Build();

// Elsewhere in the app
var chatClient = app.Services.GetRequiredService<IChatClient>();

Console.WriteLine(await chatClient.CompleteAsync("What is AI?"));

app.Run();

前面的示例依赖于以下 NuGet 包:

注入的实例和配置可以根据应用程序的当前需求而有所不同,并且可以用不同的密钥注入多个管道。

IEmbeddingGenerator 接口

IEmbeddingGenerator<TInput,TEmbedding> 接口表示嵌入的泛型生成器。 此处,TInput 是嵌入的输入值的类型,TEmbedding 是生成的嵌入的类型,它继承自 Embedding 类。

Embedding 类充当由 IEmbeddingGenerator 生成的嵌入的基类。 它旨在存储和管理与嵌入相关的元数据和数据。 像 Embedding<T> 这样的派生类型提供了具体的嵌入矢量数据。 例如,嵌入会公开 Embedding<T>.Vector 属性以访问其嵌入数据。

IEmbeddingGenerator 接口定义一种方法,以异步方式为输入值的集合生成嵌入,并提供可选配置和取消支持。 它还提供描述生成器的元数据,并允许检索可由生成器或其基础服务提供的强类型服务。

示例实现

请考虑以下 IEmbeddingGenerator 的示例实现,以显示一般结构,但它只生成随机嵌入向量。

using Microsoft.Extensions.AI;

public sealed class SampleEmbeddingGenerator(
    Uri endpoint, string modelId)
        : IEmbeddingGenerator<string, Embedding<float>>
{
    public EmbeddingGeneratorMetadata Metadata { get; } =
        new(nameof(SampleEmbeddingGenerator), endpoint, modelId);

    public async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
        IEnumerable<string> values,
        EmbeddingGenerationOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        // Simulate some async operation
        await Task.Delay(100, cancellationToken);

        // Create random embeddings
        return
        [
            .. from value in values
            select new Embedding<float>(
                Enumerable.Range(0, 384)
                          .Select(_ => Random.Shared.NextSingle())
                          .ToArray())
        ];
    }

    public object? GetService(Type serviceType, object? serviceKey) => this;

    public TService? GetService<TService>(object? key = null)
        where TService : class => this as TService;

    void IDisposable.Dispose() { }
}

前面的代码:

  • 定义实现 IEmbeddingGenerator<string, Embedding<float>> 接口的名为 SampleEmbeddingGenerator 的类。
  • 有一个接受终结点和模型 ID 的主构造函数,用于标识生成器。
  • 公开 Metadata 属性,该属性提供有关生成器的元数据。
  • 实现 GenerateAsync 方法为输入值集合生成嵌入:
    • 通过延迟 100 毫秒来模拟异步操作。
    • 为每个输入值返回随机嵌入。

可以在以下包中找到实际的具体实现:

创建嵌入

使用 IEmbeddingGenerator<TInput,TEmbedding> 执行的主要操作是嵌入生成,这是通过其 GenerateAsync 方法完成的。

using Microsoft.Extensions.AI;

IEmbeddingGenerator<string, Embedding<float>> generator =
    new SampleEmbeddingGenerator(
        new Uri("http://coolsite.ai"), "target-ai-model");

foreach (var embedding in await generator.GenerateAsync(["What is AI?", "What is .NET?"]))
{
    Console.WriteLine(string.Join(", ", embedding.Vector.ToArray()));
}

自定义 IEmbeddingGenerator 中间件

IChatClient一样,IEmbeddingGenerator 实现可以分层。 正如 Microsoft.Extensions.AI 为缓存和遥测提供 IChatClient 的委托实现,它也为 IEmbeddingGenerator 提供一个实现。

using Microsoft.Extensions.AI;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using OpenTelemetry.Trace;

// Configure OpenTelemetry exporter
var sourceName = Guid.NewGuid().ToString();
var tracerProvider = OpenTelemetry.Sdk.CreateTracerProviderBuilder()
    .AddSource(sourceName)
    .AddConsoleExporter()
    .Build();

// Explore changing the order of the intermediate "Use" calls to see that impact
// that has on what gets cached, traced, etc.
var generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(
        new SampleEmbeddingGenerator(new Uri("http://coolsite.ai"), "target-ai-model"))
    .UseDistributedCache(
        new MemoryDistributedCache(
            Options.Create(new MemoryDistributedCacheOptions())))
    .UseOpenTelemetry(sourceName: sourceName)
    .Build();

var embeddings = await generator.GenerateAsync(
[
    "What is AI?",
    "What is .NET?",
    "What is AI?"
]);

foreach (var embedding in embeddings)
{
    Console.WriteLine(string.Join(", ", embedding.Vector.ToArray()));
}

IEmbeddingGenerator 支持生成扩展 IEmbeddingGenerator 的功能的自定义中间件。 DelegatingEmbeddingGenerator<TInput,TEmbedding> 类是 IEmbeddingGenerator<TInput, TEmbedding> 接口的实现,它作为创建嵌入生成器的基类,将其操作委托给另一个 IEmbeddingGenerator<TInput, TEmbedding> 实例。 它允许以任何顺序链接多个生成器,将调用传递给基础生成器。 该类为方法(如 GenerateAsyncDispose)提供默认实现,这些方法将调用转发到内部生成器实例,从而实现灵活的模块化嵌入生成。

以下是这种委托嵌入生成器的示例实现,该生成器对嵌入生成请求进行速率限制:

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;

public class RateLimitingEmbeddingGenerator(
    IEmbeddingGenerator<string, Embedding<float>> innerGenerator, RateLimiter rateLimiter)
        : DelegatingEmbeddingGenerator<string, Embedding<float>>(innerGenerator)
{
    public override async Task<GeneratedEmbeddings<Embedding<float>>> GenerateAsync(
        IEnumerable<string> values,
        EmbeddingGenerationOptions? options = null,
        CancellationToken cancellationToken = default)
    {
        using var lease = await rateLimiter.AcquireAsync(permitCount: 1, cancellationToken)
            .ConfigureAwait(false);

        if (!lease.IsAcquired)
        {
            throw new InvalidOperationException("Unable to acquire lease.");
        }

        return await base.GenerateAsync(values, options, cancellationToken);
    }

    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            rateLimiter.Dispose();
        }

        base.Dispose(disposing);
    }
}

然后,可以围绕任意的 IEmbeddingGenerator<string, Embedding<float>> 进行分层,以对所有执行的嵌入生成操作进行速率限制。

using Microsoft.Extensions.AI;
using System.Threading.RateLimiting;

IEmbeddingGenerator<string, Embedding<float>> generator =
    new RateLimitingEmbeddingGenerator(
        new SampleEmbeddingGenerator(new Uri("http://coolsite.ai"), "target-ai-model"),
        new ConcurrencyLimiter(new()
        {
            PermitLimit = 1,
            QueueLimit = int.MaxValue
        }));

foreach (var embedding in await generator.GenerateAsync(["What is AI?", "What is .NET?"]))
{
    Console.WriteLine(string.Join(", ", embedding.Vector.ToArray()));
}

这样,RateLimitingEmbeddingGenerator 可以与其他 IEmbeddingGenerator<string, Embedding<float>> 实例组合,以提供速率限制功能。

另请参阅