Compartilhar via


bibliotecas de Microsoft.Extensions.AI

Os desenvolvedores do .NET precisam integrar e interagir com uma variedade crescente de serviços de IA (inteligência artificial) em seus aplicativos. As Microsoft.Extensions.AI bibliotecas fornecem uma abordagem unificada para representar componentes de IA generativos e habilitam a integração e interoperabilidade perfeitas com vários serviços de IA. Este artigo apresenta as bibliotecas e fornece exemplos de uso detalhados para ajudá-lo a começar.

Os pacotes

O 📦 pacote Microsoft.Extensions.AI.Abstractions fornece os principais tipos de troca, incluindo IChatClient e IEmbeddingGenerator<TInput,TEmbedding>. Qualquer biblioteca .NET que forneça um cliente LLM pode implementar a interface IChatClient para habilitar a integração perfeita com o código de consumo.

O pacote 📦 Microsoft.Extensions.AI tem uma dependência implícita no pacote Microsoft.Extensions.AI.Abstractions. Esse pacote permite que você integre facilmente componentes como invocação automática de ferramenta de função, telemetria e cache em seus aplicativos usando padrões familiares de injeção de dependência e middleware. Por exemplo, ele fornece o método de extensão UseOpenTelemetry(ChatClientBuilder, ILoggerFactory, String, Action<OpenTelemetryChatClient>), que adiciona suporte ao OpenTelemetry ao pipeline do cliente do chat.

Qual pacote referenciar

Bibliotecas que fornecem implementações das abstrações normalmente fazem referência apenas Microsoft.Extensions.AI.Abstractions.

Para também ter acesso a utilitários de nível superior para trabalhar com componentes de IA generativos, faça referência ao pacote Microsoft.Extensions.AI em vez disso (que por sua vez faz referência ao Microsoft.Extensions.AI.Abstractions). A maioria dos aplicativos e serviços consumidores deve fazer referência ao pacote Microsoft.Extensions.AI juntamente com uma ou mais bibliotecas que fornecem implementações concretas das abstracções.

Instalar os pacotes

Para obter informações sobre como instalar pacotes NuGet, consulte adicionar pacote dotnet ou gerenciar dependências de pacote em aplicativos .NET.

Exemplos de uso de API

As seguintes subseções mostram exemplos de uso específicos do IChatClient :

As seções a seguir mostram exemplos de uso específicos do IEmbeddingGenerator :

A interface IChatClient.

A interface do IChatClient define uma abstração do cliente responsável por interagir com serviços de IA que fornecem recursos de chat. Ela inclui métodos para enviar e receber mensagens com conteúdo multimodal (como texto, imagens e áudio), como um conjunto completo ou transmitidas de maneira incremental. Além disso, ele permite recuperar serviços fortemente tipados fornecidos pelo cliente ou por seus serviços subjacentes.

As bibliotecas .NET que fornecem clientes para modelos e serviços de linguagem podem fornecer uma implementação da IChatClient interface. Todos os consumidores da interface são capazes de interoperar perfeitamente com esses modelos e serviços por meio das abstrações. Você pode ver uma implementação simples em implementações de exemplo de IChatClient e IEmbeddingGenerator.

Solicitar uma resposta de chat

Com uma instância de IChatClient, você pode chamar o IChatClient.GetResponseAsync método para enviar uma solicitação e obter uma resposta. A solicitação é composta por uma ou mais mensagens, e cada uma delas é composta por uma ou mais partes de conteúdo. Existem métodos do Acelerador para simplificar casos comuns, como a criação de uma solicitação para um único conteúdo de texto.

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(
    new Uri("http://localhost:11434/"), "phi3:mini");

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

O método de IChatClient.GetResponseAsync principal aceita uma lista de mensagens. Essa lista representa o histórico de todas as mensagens que fazem parte da conversa.

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

O ChatResponse que é retornado de GetResponseAsync expõe uma lista de instâncias ChatMessage que representam uma ou mais mensagens geradas como parte da operação. Em casos comuns, há apenas uma mensagem de resposta, mas em algumas situações, pode haver várias mensagens. A lista de mensagens é ordenada, de modo que a última mensagem na lista represente a mensagem final para a solicitação. Para fornecer todas essas mensagens de resposta de volta ao serviço em uma solicitação subsequente, você pode adicionar as mensagens da resposta de volta à lista de mensagens.

List<ChatMessage> history = [];
while (true)
{
    Console.Write("Q: ");
    history.Add(new(ChatRole.User, Console.ReadLine()));

    ChatResponse response = await client.GetResponseAsync(history);
    Console.WriteLine(response);

    history.AddMessages(response);
}

Solicitar uma resposta de chat por transmissão ao vivo

As entradas de IChatClient.GetStreamingResponseAsync são idênticas às de GetResponseAsync. No entanto, em vez de retornar a resposta completa como parte de um objeto ChatResponse, o método retorna um IAsyncEnumerable<T> em que T é ChatResponseUpdate, fornecendo um fluxo de atualizações que formam coletivamente a resposta única.

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

Dica

As APIs de streaming são quase idênticas às experiências de usuário de IA. O C# proporciona cenários atraentes com seu suporte a IAsyncEnumerable<T>, permitindo a transmissão de dados de maneira natural e eficiente.

Assim como GetResponseAsync, você pode adicionar as atualizações de IChatClient.GetStreamingResponseAsync de volta à lista de mensagens. Como as atualizações são partes individuais de uma resposta, você pode usar auxiliares como ToChatResponse(IEnumerable<ChatResponseUpdate>) para compor uma ou mais atualizações em uma única instância ChatResponse.

Auxiliares como AddMessages compõem um ChatResponse e, em seguida, extraem as mensagens compostas da resposta e as adicionam a uma lista.

List<ChatMessage> chatHistory = [];
while (true)
{
    Console.Write("Q: ");
    chatHistory.Add(new(ChatRole.User, Console.ReadLine()));

    List<ChatResponseUpdate> updates = [];
    await foreach (ChatResponseUpdate update in
        client.GetStreamingResponseAsync(history))
    {
        Console.Write(update);
        updates.Add(update);
    }
    Console.WriteLine();

    chatHistory.AddMessages(updates);
}

Ferramenta de chamado

Alguns modelos e serviços dão suporte à chamada de ferramentas. Para coletar informações adicionais, você pode configurar o ChatOptions com informações sobre ferramentas (geralmente métodos .NET) que o modelo pode solicitar que o cliente invoque. Em vez de enviar uma resposta final, o modelo solicita uma invocação de função com argumentos específicos. Em seguida, o cliente invoca a função e envia os resultados de volta para o modelo com o histórico de conversa. A biblioteca de Microsoft.Extensions.AI.Abstractions inclui abstrações para vários tipos de conteúdo de mensagem, incluindo solicitações de chamada de função e resultados. Embora IChatClient os consumidores possam interagir diretamente com esse conteúdo, Microsoft.Extensions.AI fornece auxiliares que podem habilitar a invocação automática das ferramentas em resposta às solicitações correspondentes. As bibliotecas Microsoft.Extensions.AI.Abstractions e Microsoft.Extensions.AI fornecem os seguintes tipos:

  • AIFunction: representa uma função que pode ser descrita para um modelo de IA e invocada.
  • AIFunctionFactory: fornece métodos de fábrica para criar AIFunction instâncias que representam métodos .NET.
  • FunctionInvokingChatClient: encapsula um IChatClient como outro IChatClient que adiciona recursos automáticos de invocação de função.

O exemplo a seguir demonstra uma invocação de função aleatória (este exemplo depende do pacote NuGet OllamaSharp📦):

using Microsoft.Extensions.AI;
using OllamaSharp;

string GetCurrentWeather() => Random.Shared.NextDouble() > 0.5 ? "It's sunny" : "It's raining";

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1");

client = ChatClientBuilderChatClientExtensions
    .AsBuilder(client)
    .UseFunctionInvocation()
    .Build();

ChatOptions options = new() { Tools = [AIFunctionFactory.Create(GetCurrentWeather)] };

var response = client.GetStreamingResponseAsync("Should I wear a rain coat?", options);
await foreach (var update in response)
{
    Console.Write(update);
}

O código anterior:

  • Define uma função chamada GetCurrentWeather que retorna uma previsão de tempo aleatória.
  • Cria uma instância de um ChatClientBuilder com um OllamaSharp.OllamaApiClient e o configura para usar a invocação de função.
  • Chama GetStreamingResponseAsync no cliente, passando um prompt e uma lista de ferramentas que inclui uma função criada com Create.
  • Percorre a resposta, imprimindo cada atualização no console.

Respostas de cache

Se você estiver familiarizado com Cache no .NET, é bom saber que ele Microsoft.Extensions.AI fornece outras implementações de delegação IChatClient. O DistributedCachingChatClient é um IChatClient que aplica camadas de cache em torno de outra instância arbitrária IChatClient. Quando um novo histórico de chat é enviado para o DistributedCachingChatClient, ele o encaminha para o cliente subjacente e, em seguida, armazena em cache a resposta antes de enviá-la de volta ao consumidor. Na próxima vez que a mesma história for enviada, de modo que uma resposta armazenada em cache possa ser encontrada no cache, o DistributedCachingChatClient retorna a resposta armazenada em cache em vez de encaminhar a solicitação ao longo do pipeline.

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

var sampleChatClient = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1");

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.GetStreamingResponseAsync(prompt))
    {
        Console.Write(update);
    }
    Console.WriteLine();
}

Este exemplo depende do 📦 pacote NuGet Microsoft.Extensions.Caching.Memory . Para mais informações, consulte Cache no .NET.

Usar telemetria

Outro exemplo de um cliente de chat de delegação é o OpenTelemetryChatClient. Essa implementação segue as convenções semânticas OpenTelemetry para sistemas de IA generativa. Semelhante a outros delegadores IChatClient, ele abrange métricas e envolve outras implementações arbitrárias IChatClient.

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

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

IChatClient ollamaClient = new OllamaApiClient(
    new Uri("http://localhost:11434/"), "phi3:mini");

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

Console.WriteLine((await client.GetResponseAsync("What is AI?")).Text);

(O exemplo anterior depende do 📦 pacote NuGet OpenTelemetry.Exporter.Console .)

Como alternativa, o método LoggingChatClient e o método correspondente UseLogging(ChatClientBuilder, ILoggerFactory, Action<LoggingChatClient>) oferecem uma maneira simples de registrar entradas de log em um ILogger para cada solicitação e resposta.

Fornecer opções

Cada chamada para GetResponseAsync ou GetStreamingResponseAsync pode fornecer uma instância de ChatOptions contendo parâmetros adicionais para a operação. Os parâmetros mais comuns entre modelos e serviços de IA aparecem como propriedades fortemente tipadas no tipo, como ChatOptions.Temperature. Outros parâmetros podem ser fornecidos pelo nome de forma flexível, por meio do dicionário ChatOptions.AdditionalProperties ou por meio de uma instância de opções que o provedor subjacente entende, por meio da propriedade ChatOptions.RawRepresentationFactory.

Você também pode especificar opções ao criar um IChatClient com a API fluente ChatClientBuilder encadeando uma chamada ao método de extensão ConfigureOptions(ChatClientBuilder, Action<ChatOptions>). Esse cliente de delegação encapsula outro cliente e invoca o delegado fornecido para preencher uma instância de ChatOptions para cada chamada. Por exemplo, para garantir que a propriedade ChatOptions.ModelId seja padronizada para um nome de modelo específico, você pode usar códigos como o seguinte:

using Microsoft.Extensions.AI;
using OllamaSharp;

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"));

client = ChatClientBuilderChatClientExtensions.AsBuilder(client)
    .ConfigureOptions(options => options.ModelId ??= "phi3")
    .Build();

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

Pipelines de funcionalidade

As instâncias de IChatClient podem ser organizadas em camadas para criar um pipeline de componentes que adicionam mais funcionalidades. Esses componentes podem vir da biblioteca Microsoft.Extensions.AI, outros pacotes NuGet ou implementações personalizadas. Essa abordagem permite que você aumente o comportamento do IChatClient de várias maneiras para atender às suas necessidades específicas. Considere o seguinte trecho de código que cria camadas de um cache distribuído, invocação de função e rastreamento OpenTelemetry sobre um cliente de chat de exemplo:

// Explore changing the order of the intermediate "Use" calls.
IChatClient client = new ChatClientBuilder(new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"))
    .UseDistributedCache(new MemoryDistributedCache(Options.Create(new MemoryDistributedCacheOptions())))
    .UseFunctionInvocation()
    .UseOpenTelemetry(sourceName: sourceName, configure: c => c.EnableSensitiveData = true)
    .Build();

Middleware IChatClient personalizado

Para adicionar outras funcionalidades, você pode implementar o IChatClient diretamente ou usar a classe DelegatingChatClient. Essa classe serve como base para criar clientes de chat que delegam operações a outra instância do IChatClient. Ele simplifica o encadeamento de vários clientes, permitindo que as chamadas passem para um cliente subjacente.

A classe DelegatingChatClient fornece implementações padrão para métodos como GetResponseAsync, GetStreamingResponseAsync e Dispose, que encaminham chamadas para o cliente interno. Uma classe derivada pode substituir apenas os métodos necessários para aumentar o comportamento, delegando outras chamadas à implementação base. Essa abordagem é útil para criar clientes de chat flexíveis e modulares que são fáceis de estender e redigir.

Veja a seguir uma classe de exemplo derivada da DelegatingChatClient qual usa a biblioteca System.Threading.RateLimiting para fornecer funcionalidade de limitação de taxa.

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<ChatResponse> GetResponseAsync(
        IEnumerable<ChatMessage> messages,
        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.GetResponseAsync(messages, options, cancellationToken)
            .ConfigureAwait(false);
    }

    public override async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
        IEnumerable<ChatMessage> messages,
        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.GetStreamingResponseAsync(messages, options, cancellationToken)
            .ConfigureAwait(false))
        {
            yield return update;
        }
    }

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

        base.Dispose(disposing);
    }
}

Assim como acontece com outras IChatClient implementações, ela RateLimitingChatClient pode ser composta:

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

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

Console.WriteLine(await client.GetResponseAsync("What color is the sky?"));

Para simplificar a composição desses componentes com outros, os autores de componentes devem criar um método de extensão Use* para registrar o componente no pipeline. Por exemplo, considere o seguinte UseRatingLimiting método de extensão:

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)
        );
}

Essas extensões também podem consultar serviços relevantes do contêiner de DI; o IServiceProvider usado pelo pipeline é passado como um parâmetro opcional:

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,
                services.GetRequiredService<RateLimiter>())
        );
}

Agora é fácil para o consumidor integrar isso em seu processo, por exemplo:

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

IChatClient client = new OllamaApiClient(
    new Uri("http://localhost:11434/"),
    "phi3:mini");

builder.Services.AddChatClient(services =>
        client
        .AsBuilder()
        .UseDistributedCache()
        .UseRateLimiting()
        .UseOpenTelemetry()
        .Build(services));

Os métodos de extensão anteriores demonstram o uso de um método Use em ChatClientBuilder. O ChatClientBuilder também fornece as sobrecargas Use que facilitam a escrita desses manipuladores de delegação. Por exemplo, no exemplo anterior de RateLimitingChatClient, as sobrescrições de GetResponseAsync e GetStreamingResponseAsync só precisam executar tarefas antes e depois de delegar para o próximo cliente no pipeline. Para obter o mesmo resultado sem escrever uma classe personalizada, você pode usar uma sobrecarga de Use que aceita um delegado usado tanto para GetResponseAsync quanto para GetStreamingResponseAsync, reduzindo o código repetitivo necessário.

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

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

IChatClient client = new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1");

client = ChatClientBuilderChatClientExtensions
    .AsBuilder(client)
    .UseDistributedCache()
    .Use(async (messages, 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(messages, options, cancellationToken);
    })
    .UseOpenTelemetry()
    .Build();

Para cenários em que você precisa de uma implementação diferente para GetResponseAsync e GetStreamingResponseAsync para lidar com seus tipos de retorno exclusivos, você pode usar a sobrecarga Use(Func<IEnumerable<ChatMessage>,ChatOptions,IChatClient,CancellationToken, Task<ChatResponse>>, Func<IEnumerable<ChatMessage>,ChatOptions, IChatClient,CancellationToken,IAsyncEnumerable<ChatResponseUpdate>>) que aceita um delegado para cada um.

Injeção de dependência

IChatClientas implementações geralmente são fornecidas a um aplicativo por meio de DI (injeção de dependência). Neste exemplo, um IDistributedCache é adicionado ao contêiner de DI, assim como um IChatClient. O registro do IChatClient usa um construtor que cria um pipeline que contém um cliente de cache (que usa um IDistributedCache recuperado da DI) e o cliente de exemplo. O IChatClient injetado pode ser recuperado e usado em outro lugar no aplicativo.

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

// App setup.
var builder = Host.CreateApplicationBuilder();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddChatClient(new OllamaApiClient(new Uri("http://localhost:11434"), "llama3.1"))
    .UseDistributedCache();
var host = builder.Build();

// Elsewhere in the app.
var chatClient = host.Services.GetRequiredService<IChatClient>();
Console.WriteLine(await chatClient.GetResponseAsync("What is AI?"));

A instância e a configuração injetadas podem diferir com base nas necessidades atuais do aplicativo, e vários pipelines podem ser injetados com chaves diferentes.

Clientes com estado versus sem estado

Os serviços sem estado exigem que todo o histórico de conversas relevante seja enviado de volta em cada solicitação. Por outro lado, os serviços com estado controlam o histórico e exigem que apenas mensagens adicionais sejam enviadas com uma solicitação. A interface IChatClient foi projetada para lidar com serviços de IA sem estado e com suporte a estado.

Ao trabalhar com um serviço sem estado, os chamadores mantêm uma lista de todas as mensagens. Eles adicionam todas as mensagens de resposta recebidas e fornecem a lista de volta nas interações subsequentes.

List<ChatMessage> history = [];
while (true)
{
    Console.Write("Q: ");
    history.Add(new(ChatRole.User, Console.ReadLine()));

    var response = await client.GetResponseAsync(history);
    Console.WriteLine(response);

    history.AddMessages(response);
}

Para serviços com estado, talvez você já conheça o identificador usado para a conversa relevante. Você pode colocar esse identificador em ChatOptions.ConversationId. Em seguida, o uso segue o mesmo padrão, exceto que não é necessário manter um histórico manualmente.

ChatOptions statefulOptions = new() { ConversationId = "my-conversation-id" };
while (true)
{
    Console.Write("Q: ");
    ChatMessage message = new(ChatRole.User, Console.ReadLine());

    Console.WriteLine(await client.GetResponseAsync(message, statefulOptions));
}

Alguns serviços podem dar suporte à criação automática de uma ID de conversa para uma solicitação que não tenha uma ou à criação de uma nova ID de conversa que represente o estado atual da conversa depois de incorporar a última rodada de mensagens. Nesses casos, você pode transferir o ChatResponse.ConversationId para as ChatOptions.ConversationId para solicitações subsequentes. Por exemplo:

ChatOptions options = new();
while (true)
{
    Console.Write("Q: ");
    ChatMessage message = new(ChatRole.User, Console.ReadLine());

    ChatResponse response = await client.GetResponseAsync(message, options);
    Console.WriteLine(response);

    options.ConversationId = response.ConversationId;
}

Se você não souber antecipadamente se o serviço é sem estado ou com estado, você pode verificar a resposta ConversationId e agir com base em seu valor. Se estiver definido, esse valor será propagado para as opções e o histórico será limpo para não reenviar o mesmo histórico novamente. Se a resposta ConversationId não estiver definida, a mensagem de resposta será adicionada ao histórico para que ela seja enviada de volta ao serviço na próxima curva.

List<ChatMessage> chatHistory = [];
ChatOptions chatOptions = new();
while (true)
{
    Console.Write("Q: ");
    chatHistory.Add(new(ChatRole.User, Console.ReadLine()));

    ChatResponse response = await client.GetResponseAsync(chatHistory);
    Console.WriteLine(response);

    chatOptions.ConversationId = response.ConversationId;
    if (response.ConversationId is not null)
    {
        chatHistory.Clear();
    }
    else
    {
        chatHistory.AddMessages(response);
    }
}

A interface IEmbeddingGenerator.

A interface do IEmbeddingGenerator<TInput,TEmbedding> representa um gerador genérico de inserções. Para os parâmetros de tipo genérico, TInput é o tipo de valores de entrada que estão sendo inseridos e TEmbedding é o tipo de inserção gerada, que herda da Embedding classe.

A classe Embedding serve como uma classe base para inserções geradas por um IEmbeddingGenerator. Ela foi criada para armazenar e gerenciar os metadados e os dados associados a inserções. Tipos derivados, como Embedding<T>, fornecem os dados concretos de vetor de inserção. Por exemplo, uma Embedding<float> expõe uma ReadOnlyMemory<float> Vector { get; } propriedade para acesso aos seus dados de inserção.

A interface do IEmbeddingGenerator define um método para gerar inserções de forma assíncrona para uma coleção de valores de entrada, com suporte opcional de configuração e cancelamento. Ela também fornece metadados que descrevem o gerador e permite a recuperação de serviços com tipo forte que podem ser fornecidos pelo gerador ou seus serviços subjacentes.

A maioria dos usuários não precisa implementar a IEmbeddingGenerator interface. No entanto, se você for um autor de biblioteca, poderá ver uma implementação simples em implementações de exemplo de IChatClient e IEmbeddingGenerator.

Criar inserções

A operação primária executada com um IEmbeddingGenerator<TInput,TEmbedding> é a geração de embeddings, realizada com seu método GenerateAsync.

using Microsoft.Extensions.AI;
using OllamaSharp;

IEmbeddingGenerator<string, Embedding<float>> generator =
    new OllamaApiClient(new Uri("http://localhost:11434/"), "phi3:mini");

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

Também existem métodos de extensão de acelerador para simplificar casos comuns, como a geração de um vetor de incorporação a partir de uma única entrada.

ReadOnlyMemory<float> vector = await generator.GenerateVectorAsync("What is AI?");

Pipelines de funcionalidade

Assim como acontece com IChatClient, as implementações de IEmbeddingGenerator podem ser em camadas. Microsoft.Extensions.AI fornece uma implementação de delegação para IEmbeddingGenerator para cache e telemetria.

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

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

// Explore changing the order of the intermediate "Use" calls to see
// what impact that has on what gets cached and traced.
IEmbeddingGenerator<string, Embedding<float>> generator = new EmbeddingGeneratorBuilder<string, Embedding<float>>(
        new OllamaApiClient(new Uri("http://localhost:11434/"), "phi3:mini"))
    .UseDistributedCache(
        new MemoryDistributedCache(
            Options.Create(new MemoryDistributedCacheOptions())))
    .UseOpenTelemetry(sourceName: sourceName)
    .Build();

GeneratedEmbeddings<Embedding<float>> embeddings = await generator.GenerateAsync(
[
    "What is AI?",
    "What is .NET?",
    "What is AI?"
]);

foreach (Embedding<float> embedding in embeddings)
{
    Console.WriteLine(string.Join(", ", embedding.Vector.ToArray()));
}

O IEmbeddingGenerator permite a criação de middleware personalizado que estende a funcionalidade de um IEmbeddingGenerator. A classe DelegatingEmbeddingGenerator<TInput,TEmbedding> é uma implementação da interface do IEmbeddingGenerator<TInput, TEmbedding> que serve como uma classe base para a criação de geradores de inserção que delegam suas operações a outra instância do IEmbeddingGenerator<TInput, TEmbedding>. Ele permite encadear vários geradores em qualquer ordem, transmitindo chamadas para um gerador subjacente. A classe fornece implementações padrão para métodos como GenerateAsync e Dispose, que encaminham as chamadas para a instância do gerador interno, permitindo a geração de inserções flexíveis e modulares.

Veja a seguir um exemplo de implementação de um gerador de incorporação de delegação que limita a taxa de solicitações de geração de incorporação:

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);
    }
}

Isso pode então ser colocado em camadas em torno de um IEmbeddingGenerator<string, Embedding<float>> arbitrário para limitar a taxa de todas as operações de geração de incorporação.

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

IEmbeddingGenerator<string, Embedding<float>> generator =
    new RateLimitingEmbeddingGenerator(
        new OllamaApiClient(new Uri("http://localhost:11434/"), "phi3:mini"),
        new ConcurrencyLimiter(new()
        {
            PermitLimit = 1,
            QueueLimit = int.MaxValue
        }));

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

Dessa forma, o RateLimitingEmbeddingGenerator pode ser composto com outras instâncias IEmbeddingGenerator<string, Embedding<float>> para fornecer funcionalidade de limitação de taxa.

Crie com a Microsoft.Extensions.AI

Você pode começar a criar com a Microsoft.Extensions.AI das seguintes maneiras:

  • Desenvolvedores de biblioteca: se você possui bibliotecas que fornecem clientes para serviços de IA, considere implementar as interfaces em suas bibliotecas. Isso permite que os usuários integrem facilmente seu pacote NuGet por meio das abstrações. Para obter implementações de exemplo, consulte implementações de exemplo de IChatClient e IEmbeddingGenerator.
  • Consumidores de serviços: Se você estiver desenvolvendo bibliotecas que consomem serviços de IA, use as abstrações em vez de codificar para um serviço de IA específico. Essa abordagem oferece aos consumidores a flexibilidade para escolher seu provedor preferencial.
  • Desenvolvedores de aplicativos: use as abstrações para simplificar a integração em seus aplicativos. Isso permite a portabilidade entre modelos e serviços, facilita o teste e a simulação, aproveita o middleware fornecido pelo ecossistema e mantém uma API consistente em todo o aplicativo, mesmo que você use serviços diferentes em diferentes partes do aplicativo.
  • Colaboradores do ecossistema: se você estiver interessado em contribuir para o ecossistema, considere escrever componentes de middleware personalizados.

Para obter mais exemplos, consulte o repositório GitHub dotnet/ai-samples . Para obter um exemplo de ponta a ponta, consulte eShopSupport.

Consulte também