Udostępnij za pośrednictwem


Sztuczna inteligencja na platformie .NET (wersja zapoznawcza)

W przypadku coraz większej gamy dostępnych usług sztucznej inteligencji deweloperzy potrzebują sposobu integrowania tych usług i interakcji z nimi w aplikacjach platformy .NET. Biblioteka Microsoft.Extensions.AI zapewnia ujednolicone podejście do reprezentowania składników generacyjnych sztucznej inteligencji, co umożliwia bezproblemową integrację i współdziałanie z różnymi usługami sztucznej inteligencji. Ten artykuł zawiera wprowadzenie do biblioteki oraz zawiera instrukcje instalacji i przykłady użycia ułatwiające rozpoczęcie pracy.

Instalowanie pakietu

Aby zainstalować pakiet 📦 Microsoft.Extensions.AI NuGet, użyj interfejsu wiersza polecenia platformy .NET lub dodaj odwołanie do pakietu bezpośrednio do pliku projektu C#:

dotnet add package Microsoft.Extensions.AI --prelease

Aby uzyskać więcej informacji, zobacz dotnet add package lub Zarządzanie zależnościami pakietów w aplikacjach .NET.

Przykłady użycia

Interfejs IChatClient definiuje abstrakcję klienta odpowiedzialną za interakcję z usługami sztucznej inteligencji, które zapewniają możliwości czatu. Zawiera metody wysyłania i odbierania wiadomości z treściami multimodalnymi (takimi jak tekst, obrazy i dźwięk) jako kompletne zestawy lub strumieniowo w sposób przyrostowy. Ponadto udostępnia on metadane o kliencie i umożliwia pobieranie silnie typiowanych usług.

Ważny

Aby uzyskać więcej przykładów użycia i rzeczywistych scenariuszy, zobacz AI dla deweloperów platformy .NET.

W tej sekcji

Interfejs IChatClient

Poniższy przykład implementuje IChatClient, aby pokazać ogólną strukturę.

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() { }
}

Inne konkretne implementacje IChatClient można znaleźć w następujących pakietach NuGet:

Uzupełnienie czatu na żądanie

Aby zażądać ukończenia, wywołaj metodę IChatClient.CompleteAsync. Żądanie składa się z co najmniej jednego komunikatu, z których każda składa się z co najmniej jednego fragmentu zawartości. Istnieją metody akceleratora, aby uprościć typowe przypadki, takie jak konstruowanie żądania dla pojedynczego fragmentu zawartości tekstowej.

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

Podstawowa metoda IChatClient.CompleteAsync akceptuje listę komunikatów. Ta lista reprezentuje historię wszystkich wiadomości, które są częścią konwersacji.

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

Każdy komunikat w historii jest reprezentowany przez obiekt ChatMessage. Klasa ChatMessage udostępnia właściwość ChatMessage.Role wskazującą rolę komunikatu. Domyślnie jest używana ChatRole.User. Dostępne są następujące role:

  • ChatRole.Assistant: instruuje lub ustawia zachowanie asystenta.
  • ChatRole.System: zapewnia odpowiedzi na dane wejściowe inicjowane przez użytkownika i poinstruowane przez system.
  • ChatRole.Tool: zawiera dodatkowe informacje i odwołania do ukończenia czatu.
  • ChatRole.User: udostępnia dane wejściowe na potrzeby uzupełniania czatu.

Każda wiadomość czatu jest tworzona, przypisując nową TextContentdo jej właściwości Contents. Istnieją różne typy zawartości, które mogą być reprezentowane, takie jak prosty ciąg lub bardziej złożony obiekt reprezentujący wielomodalny komunikat z tekstem, obrazami i dźwiękiem:

Żądanie ukończenia czatu za pomocą przesyłania strumieniowego

Dane wejściowe IChatClient.CompleteStreamingAsync są identyczne z danymi CompleteAsync. Jednak zamiast zwracać pełną odpowiedź w ramach obiektu ChatCompletion metoda zwraca IAsyncEnumerable<T>, w której T jest StreamingChatCompletionUpdate, zapewniając strumień aktualizacji, które zbiorczo tworzą pojedynczą odpowiedź.

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

Napiwek

Interfejsy API przesyłania strumieniowego są niemal synonimem doświadczeń użytkownika związanych ze sztuczną inteligencją. Język C# umożliwia interesujące scenariusze dzięki obsłudze IAsyncEnumerable<T>, co pozwala na naturalne i wydajne strumieniowanie danych.

Wywoływanie narzędzi

Niektóre modele i usługi obsługują wywoływanie narzędzi , gdzie żądania mogą zawierać narzędzia umożliwiające modelowi wywoływanie funkcji w celu zebrania dodatkowych informacji. Zamiast wysyłać ostateczną odpowiedź, model żąda wywołania funkcji z określonymi argumentami. Następnie klient wywołuje funkcję i wysyła wyniki z powrotem do modelu wraz z historią konwersacji. Biblioteka Microsoft.Extensions.AI zawiera abstrakcje dla różnych typów zawartości komunikatów, w tym żądania wywołań funkcji i wyniki. Użytkownicy mogą bezpośrednio korzystać z tej zawartości, Microsoft.Extensions.AI automatyzuje te interakcje i zapewnia następujące możliwości:

  • AIFunction: reprezentuje funkcję, którą można opisać w usłudze sztucznej inteligencji i wywołać.
  • AIFunctionFactory: udostępnia metody fabryczne do tworzenia powszechnie używanych implementacji AIFunction.
  • FunctionInvokingChatClient: opakowuje IChatClient, aby dodać możliwość automatycznego wywołania funkcji.

Rozważmy następujący przykład, który demonstruje wywołanie funkcji losowej:

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

Powyższy przykład zależy od pakietu 📦 Microsoft.Extensions.AI.Ollama NuGet.

Powyższy kod:

  • Definiuje funkcję o nazwie GetCurrentWeather, która zwraca losową prognozę pogody.
    • Ta funkcja jest ozdobiona DescriptionAttribute, która służy do podawania opisu funkcji w usłudze sztucznej inteligencji.
  • Tworzy wystąpienie ChatClientBuilder za pomocą OllamaChatClient i konfiguruje je do wywoływania funkcji.
  • Wywołuje CompleteStreamingAsync na kliencie, przekazując monit oraz listę narzędzi, w której znajduje się funkcja utworzona za pomocą Create.
  • Iteruje przez odpowiedź, wyświetlając każdą aktualizację na konsoli.

Odpowiedzi pamięci podręcznej

Jeśli znasz buforowanie na platformie .NET, warto wiedzieć, że Microsoft.Extensions.AI udostępnia inne takie delegujące implementacje IChatClient. DistributedCachingChatClient to IChatClient, która warstwuje buforowanie wokół innego dowolnego wystąpienia IChatClient. Gdy do DistributedCachingChatClientzostanie przesłana unikatowa historia czatu, przekazuje ją do klienta bazowego, a następnie buforuje odpowiedź przed wysłaniem jej z powrotem do odbiorcy. Przy następnym przesłaniu tej samej komendy w taki sposób, że w pamięci podręcznej można znaleźć odpowiedź, DistributedCachingChatClient zwraca odpowiedź z pamięci podręcznej zamiast przekazywać żądanie dalej wzdłuż potoku.

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

Powyższy przykład zależy od pakietu NuGet 📦 Microsoft.Extensions.Caching.Memory. Aby uzyskać więcej informacji, zobacz buforowanie na platformie .NET.

Korzystanie z telemetrii

Innym przykładem delegowania klienta czatu jest OpenTelemetryChatClient. Ta implementacja jest zgodna z konwencjami semantycznymi OpenTelemetry dla systemów generowania sztucznej inteligencji. Podobnie jak inne IChatClient delegatory, warstwuje metryki i obejmuje wszystkie podstawowe IChatClient implementacji, zapewniając lepszą obserwację.

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

Powyższy przykład zależy od pakietu NuGet 📦 OpenTelemetry.Exporter.Console.

Podaj opcje

Każde wywołanie CompleteAsync lub CompleteStreamingAsync może opcjonalnie dostarczyć wystąpienie ChatOptions zawierające dodatkowe parametry dla operacji. Najbardziej powszechne parametry modeli i usług sztucznej inteligencji są wyświetlane jako silnie typizowane właściwości typu, takie jak ChatOptions.Temperature. Inne parametry mogą być podawane z użyciem ich nazw w sposób słabo typowany za pośrednictwem słownika ChatOptions.AdditionalProperties.

Opcje można również określić podczas tworzenia IChatClient za pomocą płynnego interfejsu API ChatClientBuilder i łączenia wywołania metody rozszerzenia ConfigureOptions. Ten delegujący klient opakowuje innego klienta i wywołuje dostarczonego delegata, aby wypełniał wystąpienie ChatOptions przy każdym wywołaniu. Aby na przykład upewnić się, że właściwość ChatOptions.ModelId jest domyślnie ustawiona na określoną nazwę modelu, możesz użyć kodu podobnego do następującego:

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

Powyższy przykład zależy od pakietu 📦 Microsoft.Extensions.AI.Ollama NuGet.

Linie funkcjonalności

IChatClient wystąpienia można warstwować w celu utworzenia potoku składników, z których każdy dodaje określoną funkcjonalność. Te składniki mogą pochodzić z Microsoft.Extensions.AI, innych pakietów NuGet lub niestandardowych implementacji. Takie podejście pozwala rozszerzyć zachowanie IChatClient na różne sposoby, aby spełnić określone potrzeby. Rozważmy następujący przykładowy kod, który warstwuje rozproszoną pamięć podręczną, wywołanie funkcji i śledzenie OpenTelemetry wokół przykładowego klienta czatu:

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

Powyższy przykład zależy od następujących pakietów NuGet:

Niestandardowe oprogramowanie pośredniczące IChatClient

Aby dodać dodatkowe funkcje, możesz zaimplementować IChatClient bezpośrednio lub użyć klasy DelegatingChatClient. Ta klasa służy jako podstawa do tworzenia klientów czatu, którzy delegują operacje do innego wystąpienia IChatClient. Upraszcza tworzenie łańcuchów wielu klientów, umożliwiając przekazywanie wywołań do bazowego klienta.

Klasa DelegatingChatClient udostępnia domyślne implementacje metod, takich jak CompleteAsync, CompleteStreamingAsynci Dispose, które przekazują wywołania do klienta wewnętrznego. Możesz dziedziczyć z tej klasy i nadpisać tylko te metody, które są potrzebne do rozszerzenia działania, jednocześnie delegując inne wywołania do implementacji bazowej. Takie podejście ułatwia tworzenie elastycznych i modułowych klientów czatów, które można łatwo rozszerzać i tworzyć.

Poniżej przedstawiono przykładową klasę pochodzącą z DelegatingChatClient w celu zapewnienia funkcji ograniczania szybkości przy użyciu 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);
    }
}

Powyższy przykład zależy od pakietu 📦 System.Threading.RateLimiting NuGet. Kompozycja RateLimitingChatClient z innym klientem jest prosta:

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

Aby uprościć kompozycję takich składników z innymi, autorzy składników powinni utworzyć metodę rozszerzającą Use* w celu zarejestrowania składnika w potoku. Rozważmy na przykład następującą metodę rozszerzenia:

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>

Takie rozszerzenia mogą również wysyłać zapytania o odpowiednie usługi z kontenera DI; IServiceProvider używany przez potok jest przekazywany jako opcjonalny parametr:

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>

Konsument może następnie łatwo użyć tego w swoim procesie przetwarzania danych, na przykład:

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

W tym przykładzie pokazano hostowany scenariusz, w którym użytkownik korzysta z wstrzykiwania zależności w celu udostępnienia wystąpienia RateLimiter. Powyższe metody rozszerzenia pokazują użycie metody Use w ChatClientBuilder. ChatClientBuilder zapewnia również przeciążenia Use, które ułatwiają pisanie takich procedur obsługi delegowania.

Na przykład we wcześniejszym przykładzie RateLimitingChatClient, przesłonięcia CompleteAsync i CompleteStreamingAsync muszą wykonać swoją pracę tylko przed i po delegowaniu do następnego klienta w linii przetwarzania. Aby osiągnąć to samo bez konieczności pisania klasy niestandardowej, można użyć przeciążenia Use, które akceptuje delegat funkcji używany zarówno do CompleteAsync, jak i CompleteStreamingAsync, zmniejszając wymaganą ilość kodu powtarzalnego.

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

Powyższe przeciążenie wewnętrznie wykorzystuje AnonymousDelegatingChatClient, co umożliwia bardziej skomplikowane wzorce przy użyciu tylko niewielkiego dodatkowego kodu. Aby na przykład osiągnąć ten sam wynik, ale z RateLimiter pobranym z DI:

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

W scenariuszach, w których deweloper chce określić delegowanie implementacji CompleteAsync i CompleteStreamingAsync wbudowanych oraz gdzie ważne jest, aby móc napisać inną implementację dla każdego z nich w celu obsługi ich unikatowych typów zwracanych specjalnie, istnieje kolejne przeciążenie Use, które akceptuje delegata dla każdego z nich.

Wstrzykiwanie zależności

IChatClient implementacje są zwykle udostępniane aplikacji za pośrednictwem wstrzykiwania zależności (DI). W tym przykładzie IDistributedCache jest dodawany do kontenera DI, podobnie jak IChatClient. Rejestracja dla IChatClient używa konstruktora, który buduje potok zawierający klienta buforującego (który następnie użyje IDistributedCache pobranego z DI) oraz klienta przykładowego. Wstrzyknięte IChatClient można pobrać i używać w innym miejscu w aplikacji.

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

Powyższy przykład zależy od następujących pakietów NuGet:

Wprowadzone wystąpienie i konfiguracja mogą się różnić w zależności od bieżących potrzeb aplikacji, a wiele potoków można wstrzykiwać za pomocą różnych kluczy.

Interfejs IEmbeddingGenerator

Interfejs IEmbeddingGenerator<TInput,TEmbedding> reprezentuje ogólny generator osadzonych elementów. W tym miejscu TInput jest typem osadzonych wartości wejściowych, a TEmbedding jest typem wygenerowanego osadzania, który dziedziczy z klasy Embedding.

Klasa Embedding służy jako klasa bazowa dla osadzeń generowanych przez IEmbeddingGenerator. Jest ona przeznaczona do przechowywania metadanych i danych skojarzonych z osadzaniem i zarządzania nimi. Typy pochodne, takie jak Embedding<T> zapewniają konkretne dane wektorów osadzania. Na przykład osadzanie uwidacznia właściwość Embedding<T>.Vector w celu uzyskania dostępu do danych osadzania.

Interfejs IEmbeddingGenerator definiuje metodę do asynchronicznego generowania osadzeń dla kolekcji wartości wejściowych, z opcjonalną konfiguracją i obsługą anulowania. Udostępnia również metadane opisujące generator i umożliwia pobieranie silnie typiowanych usług, które mogą być udostępniane przez generator lub jego podstawowe usługi.

Przykładowa implementacja

Rozważmy następującą przykładową implementację IEmbeddingGenerator, aby pokazać ogólną strukturę, ale generuje tylko losowe wektory osadzania.

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() { }
}

Powyższy kod:

  • Definiuje klasę o nazwie SampleEmbeddingGenerator, która implementuje interfejs IEmbeddingGenerator<string, Embedding<float>>.
  • Ma podstawowy konstruktor, który akceptuje punkt końcowy oraz identyfikator modelu, wykorzystywane do identyfikacji generatora.
  • Uwidacznia właściwość Metadata, która udostępnia metadane generatora.
  • Implementuje metodę GenerateAsync do generowania osadzeń dla kolekcji wartości wejściowych:
    • Symuluje operację asynchroniczną przez opóźnienie dla 100 milisekund.
    • Zwraca losowe osadzanie dla każdej wartości wejściowej.

Rzeczywiste konkretne implementacje można znaleźć w następujących pakietach:

Tworzenie osadzeń

Podstawowa operacja wykonywana przy użyciu IEmbeddingGenerator<TInput,TEmbedding> to generowanie osadzeń, które jest realizowane za pomocą metody 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()));
}

Niestandardowe oprogramowanie pośredniczące IEmbeddingGenerator

Podobnie jak w przypadku IChatClientimplementacje IEmbeddingGenerator mogą być warstwowe. Podobnie jak Microsoft.Extensions.AI zapewnia delegowanie implementacji IChatClient na potrzeby buforowania i telemetrii, zapewnia również implementację 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 umożliwia tworzenie niestandardowego oprogramowania pośredniczącego, które rozszerza funkcjonalność IEmbeddingGenerator. Klasa DelegatingEmbeddingGenerator<TInput,TEmbedding> to implementacja interfejsu IEmbeddingGenerator<TInput, TEmbedding>, który służy jako klasa bazowa do tworzenia generatorów osadzania, które delegują swoje operacje do innego wystąpienia IEmbeddingGenerator<TInput, TEmbedding>. Umożliwia łączenie wielu generatorów w dowolnej kolejności z przekazywaniem wywołań do bazowego generatora. Klasa udostępnia domyślne implementacje metod, takich jak GenerateAsync i Dispose, które przekazują wywołania do wewnętrznego wystąpienia generatora, umożliwiając elastyczne i modułowe generowanie osadzonych elementów.

Poniżej znajduje się przykładowa implementacja generatora osadzeń delegującego, który ogranicza tempo żądań generowania osadzeń.

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

Można to następnie warstwować wokół dowolnego IEmbeddingGenerator<string, Embedding<float>>, aby ograniczyć liczbę wykonanych operacji generowania osadzania.

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

W ten sposób RateLimitingEmbeddingGenerator mogą składać się z innych wystąpień IEmbeddingGenerator<string, Embedding<float>> w celu zapewnienia funkcji ograniczania szybkości.

Zobacz też