Udostępnij za pośrednictwem


biblioteki Microsoft.Extensions.AI

Deweloperzy platformy .NET muszą integrować i korzystać z rosnących różnych usług sztucznej inteligencji (AI) w swoich aplikacjach. Biblioteki Microsoft.Extensions.AI zapewniają ujednolicone podejście do reprezentowania składników generacyjnych sztucznej inteligencji oraz umożliwiają bezproblemową integrację i współdziałanie z różnymi usługami sztucznej inteligencji. W tym artykule przedstawiono biblioteki i przedstawiono szczegółowe przykłady użycia, które ułatwiają rozpoczęcie pracy.

Pakiety

📦 Pakiet Microsoft.Extensions.AI.Abstractions udostępnia podstawowe typy wymiany, w tym IChatClient i IEmbeddingGenerator<TInput,TEmbedding>. Dowolna biblioteka platformy .NET, która udostępnia klienta LLM, może zaimplementować IChatClient interfejs, aby umożliwić bezproblemową integrację z kodem korzystającym z biblioteki.

Pakiet 📦 Microsoft.Extensions.AI ma niejawną zależność od pakietu Microsoft.Extensions.AI.Abstractions. Ten pakiet umożliwia łatwe integrowanie składników, takich jak wywoływanie narzędzi funkcji automatycznych, telemetria i buforowanie w aplikacjach przy użyciu znanych wzorców wstrzykiwania zależności i oprogramowania pośredniczącego. Na przykład udostępnia metodę rozszerzenia UseOpenTelemetry(ChatClientBuilder, ILoggerFactory, String, Action<OpenTelemetryChatClient>), która dodaje wsparcie OpenTelemetry do kanału klienta czatu.

Do którego pakietu należy się odwołać

Biblioteki, które udostępniają implementacje abstrakcji, zwykle odwołują się tylko do Microsoft.Extensions.AI.Abstractions.

Aby mieć również dostęp do narzędzi na wyższym poziomie do pracy ze składnikami generatywnej sztucznej inteligencji, zamiast tego należy odwołać się do pakietu Microsoft.Extensions.AI, który sam odnosi się do Microsoft.Extensions.AI.Abstractions. Większość korzystających aplikacji i usług powinna odwoływać się do Microsoft.Extensions.AI pakietu wraz z co najmniej jedną biblioteką, która zapewnia konkretne implementacje abstrakcji.

Instalowanie pakietów

Aby uzyskać informacje o sposobie instalowania pakietów NuGet, zobacz dotnet package add or Manage package dependencies in .NET applications (Dodawanie pakietów dotnet lub zarządzanie zależnościami pakietów w aplikacjach platformy .NET).

Przykłady użycia interfejsu API

W poniższych podsekcjach przedstawiono konkretne przykłady użycia elementu IChatClient :

W poniższych sekcjach przedstawiono konkretne przykłady użycia elementu IEmbeddingGenerator :

Interfejs IChatClient

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 umożliwia pobieranie silnie typowanych usług udostępnianych przez klienta lub jego bazowe usługi.

Biblioteki platformy .NET, które udostępniają klientom modele językowe i usługi, mogą zapewnić implementację interfejsu IChatClient . Wszyscy użytkownicy interfejsu mogą bezproblemowo współpracować z tymi modelami i usługami za pośrednictwem abstrakcji. Możesz zobaczyć prostą implementację w przykładowych implementacjach elementów IChatClient i IEmbeddingGenerator.

Żądanie odpowiedzi na czat

Za pomocą wystąpienia IChatClient można wywołać metodę IChatClient.GetResponseAsync, aby wysłać żądanie i uzyskać odpowiedź. Żą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;
using OllamaSharp;

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

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

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

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

Element ChatResponse zwracany z GetResponseAsync ujawnia listę wystąpień ChatMessage, które reprezentują jeden lub więcej komunikatów wygenerowanych w ramach operacji. W typowych przypadkach istnieje tylko jeden komunikat odpowiedzi, ale w niektórych sytuacjach może istnieć wiele komunikatów. Lista komunikatów jest uporządkowana, tak że ostatni komunikat na liście reprezentuje finalną odpowiedź na żądanie. Aby przekazać wszystkie te komunikaty odpowiedzi z powrotem do usługi w kolejnym żądaniu, możesz dodać komunikaty z odpowiedzi z powrotem do listy komunikatów.

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

Zamów odpowiedź na czat na żywo

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

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

Wskazówka

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.

Podobnie jak w przypadku GetResponseAsync programu, możesz dodać aktualizacje z IChatClient.GetStreamingResponseAsync z powrotem do listy komunikatów. Ponieważ aktualizacje są poszczególnymi elementami odpowiedzi, możesz użyć takich pomocników jak ToChatResponse(IEnumerable<ChatResponseUpdate>) do tworzenia jednej lub więcej aktualizacji w jednym wystąpieniu ChatResponse.

Pomocnicy, tacy jak AddMessages, tworzą ChatResponse, a następnie wyodrębniają skomponowane komunikaty z odpowiedzi i dodają je do listy.

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

Uruchamianie narzędzi

Niektóre modele i usługi obsługują wywoływanie narzędzi. Aby zebrać dodatkowe informacje, można skonfigurować ChatOptions informacjami o narzędziach (zwykle metodach .NET), które model może poprosić klienta o wywołanie. 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 z historią konwersacji. Biblioteka Microsoft.Extensions.AI.Abstractions zawiera abstrakcje dla różnych typów zawartości komunikatów, w tym żądania wywołań funkcji i wyniki. Podczas gdy IChatClient konsumenci mogą bezpośrednio korzystać z tej zawartości, Microsoft.Extensions.AI udostępnia pomocników, którzy mogą automatycznie uruchamiać narzędzia w odpowiedzi na odpowiednie żądania. Biblioteki Microsoft.Extensions.AI.Abstractions i Microsoft.Extensions.AI udostępniają następujące typy:

  • AIFunction: reprezentuje funkcję, którą można opisać w modelu sztucznej inteligencji i wywołać.
  • AIFunctionFactory: Udostępnia metody fabryczne do tworzenia wystąpień AIFunction reprezentujących metody .NET.
  • FunctionInvokingChatClient: opakowuje IChatClient element jako inny IChatClient , który dodaje funkcje automatycznego wywołania funkcji.

W poniższym przykładzie pokazano wywołanie funkcji losowej (ten przykład zależy od 📦 pakietu 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);
}

Poprzedni kod:

  • Definiuje funkcję o nazwie GetCurrentWeather, która zwraca losową prognozę pogody.
  • Tworzy wystąpienie ChatClientBuilder za pomocą OllamaSharp.OllamaApiClient i konfiguruje je do wywoływania funkcji.
  • Wywołuje GetStreamingResponseAsync 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 z 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óry nakłada warstwę buforowania na inne dowolne wystąpienie IChatClient. Po przesłaniu nowej historii czatu do DistributedCachingChatClient, przekazuje ją do bazowego klienta, a następnie buforuje odpowiedź przed wysłaniem jej z powrotem do użytkownika. Przy następnym nadesłaniu tego samego zapytania, jeśli w pamięci podręcznej znajduje się buforowana odpowiedź, DistributedCachingChatClient zwraca buforowaną odpowiedź zamiast przesyłać dalej żądanie wzdłuż potoku.

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

Ten przykład zależy 📦 od pakietu NuGet Microsoft.Extensions.Caching.Memory . Aby uzyskać więcej informacji, zobacz pamięć podręczną w .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 w przypadku innych delegatów IChatClient, nakłada metryki i rozciąga się wokół innych dowolnych implementacji 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);

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

Alternatywnie, metoda LoggingChatClient i odpowiadająca jej metoda UseLogging(ChatClientBuilder, ILoggerFactory, Action<LoggingChatClient>) umożliwiają łatwe zapisywanie logów ILogger dla każdego żądania i odpowiedzi.

Podaj opcje

Każde wywołanie GetResponseAsync lub GetStreamingResponseAsync 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ć dostarczane przez nazwę w sposób słabo typizowany, za pośrednictwem ChatOptions.AdditionalProperties słownika lub za pośrednictwem wystąpienia opcji, które rozumie dostawca bazowy, za pośrednictwem ChatOptions.RawRepresentationFactory właściwości.

Opcje można również określić podczas budowania IChatClient przy użyciu płynnego interfejsu API ChatClientBuilder, łącząc wywołanie metody rozszerzeń ConfigureOptions(ChatClientBuilder, Action<ChatOptions>). 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;
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" }));

Linie funkcjonalności

IChatClient instancje mogą być układane warstwowo, aby stworzyć ciąg składników, z których każdy dodaje dodatkowe funkcjonalności. 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 fragment kodu, który organizuje rozproszoną pamięć podręczną, wywołanie funkcji i śledzenie narzędzia OpenTelemetry w ramach przykładowego klienta czatu.

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

Niestandardowe middleware 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 GetResponseAsync, GetStreamingResponseAsynci Dispose, które przekazują wywołania do klienta wewnętrznego. Klasa pochodna może następnie zastąpić tylko metody, których potrzebuje, aby rozszerzyć zachowanie, jednocześnie delegując inne wywołania do implementacji podstawowej. Takie podejście jest przydatne w przypadku tworzenia elastycznych i modułowych klientów czatów, które można łatwo rozszerzać i tworzyć.

Poniżej przedstawiono przykładową klasę pochodną DelegatingChatClient , która używa biblioteki System.Threading.RateLimiting w celu zapewnienia funkcji ograniczania szybkości.

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

Podobnie jak w przypadku innych implementacji IChatClient, RateLimitingChatClient może być złożony.

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

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ą UseRatingLimiting metodę rozszerzenia:

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

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

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

Teraz konsumentowi łatwo jest użyć tego w swojej linii przetwarzania, na przykład:

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

Poprzednie metody rozszerzenia pokazują używanie metody Use na ChatClientBuilder. ChatClientBuilder Udostępnia Use również przeciążenia, które ułatwiają pisanie takich procedur obsługi delegowania. Na przykład we wcześniejszym przykładzie RateLimitingChatClient, przesłonięcia GetResponseAsync i GetStreamingResponseAsync 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 GetResponseAsync, jak i GetStreamingResponseAsync, zmniejszając wymaganą ilość kodu powtarzalnego.

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

W przypadku scenariuszy, w których potrzebujesz innej implementacji dla GetResponseAsync i GetStreamingResponseAsync w celu obsługi ich unikalnych typów zwrotnych, można użyć przeciążenia Use(Func<IEnumerable<ChatMessage>,ChatOptions,IChatClient,CancellationToken, Task<ChatResponse>>, Func<IEnumerable<ChatMessage>,ChatOptions, IChatClient,CancellationToken,IAsyncEnumerable<ChatResponseUpdate>>), które akceptuje delegata dla każdego z nich.

Wstrzykiwanie zależności

IChatClient implementacje są często dostarczane do 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 tworzy potok zawierający klienta buforowania (następnie używa IDistributedCache pobranego z DI) oraz klienta przykładowego. Wstrzyknięty IChatClient można pobrać i wykorzystać w innych miejscach aplikacji.

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

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.

Klienci bezstanowi a stanowe

Usługi bezstanowe, wymagają przesłania całej historii konwersacji za każdym razem, gdy wysyłane jest żądanie. Z kolei usługi stanowe śledzą historię i wymagają wysyłania tylko dodatkowych komunikatów z żądaniem. Interfejs IChatClient jest przeznaczony do obsługi bezstanowych i stanowych usług sztucznej inteligencji.

Podczas pracy z usługą bezstanową wywołujący przechowują listę wszystkich komunikatów. Dodają wszystkie odebrane wiadomości odpowiedzi i przekazują listę przy kolejnych interakcjach.

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

W przypadku usług stanowych możesz już znać identyfikator używany do odpowiedniej konwersacji. Możesz umieścić ten identyfikator w pliku ChatOptions.ConversationId. Następnie użycie jest zgodne z tym samym wzorcem, z tym że nie ma potrzeby ręcznego prowadzenia historii.

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

Niektóre usługi mogą obsługiwać automatyczne tworzenie identyfikatora konwersacji dla żądania, które go nie ma, lub utworzenie nowego identyfikatora konwersacji reprezentującego bieżący stan konwersacji po włączeniu ostatniej rundy wiadomości. W takich przypadkach można przenieść ChatResponse.ConversationId do ChatOptions.ConversationId dla kolejnych żądań. Przykład:

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

Jeśli nie wiesz z góry, czy usługa jest bezstanowa czy stanowa, możesz sprawdzić odpowiedź ConversationId i reagować na podstawie jej wartości. Jeśli jest ustawiony, ta wartość jest propagowana do opcji, a historia zostanie wyczyszczona, aby nie wysyłać ponownie tej samej historii. Jeśli odpowiedź ConversationId nie jest ustawiona, komunikat odpowiedzi zostanie dodany do historii, aby był wysyłany z powrotem do usługi w następnym kroku.

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

Interfejs IEmbeddingGenerator

Interfejs IEmbeddingGenerator<TInput,TEmbedding> reprezentuje ogólny generator osadzonych elementów. W przypadku parametrów typu ogólnego, TInput jest typem wartości wejściowych, które są osadzane, a TEmbedding jest typem wygenerowanego osadzenia, 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 Embedding<float> udostępnia właściwość ReadOnlyMemory<float> Vector { get; }, która umożliwia dostęp 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 mocno typowanych usług, które mogą być udostępniane przez generator lub podstawowe jego usługi.

Większość użytkowników nie musi implementować interfejsu IEmbeddingGenerator . Jeśli jednak jesteś autorem biblioteki, możesz zobaczyć prostą implementację w sekcji Przykładowe implementacje elementów IChatClient i IEmbeddingGenerator.

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

Istnieją również metody rozszerzenia akceleratora, aby uprościć typowe przypadki, takie jak generowanie wektora osadzania z jednego wejścia.

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

Potoki funkcji

Podobnie jak w przypadku IChatClientimplementacje IEmbeddingGenerator mogą być warstwowe. Microsoft.Extensions.AI dostarcza delegującą implementację dla IEmbeddingGenerator, która obsługuje buforowanie i telemetrię.

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

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 przedstawiono przykładową implementację takiego delegującego generatora osadzania, który ogranicza liczbę żądań generowania osadzania.

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ć szybkość wszystkich operacji generowania osadzania.

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

W ten sposób RateLimitingEmbeddingGenerator może być komponowany z innymi instancjami IEmbeddingGenerator<string, Embedding<float>> w celu zapewnienia funkcjonalności ograniczania szybkości.

Tworzenie z użyciem Microsoft.Extensions.AI

Możesz zacząć budować z użyciem Microsoft.Extensions.AI w następujący sposób:

  • Deweloperzy bibliotek: jeśli posiadasz biblioteki, które oferują klientom usługi AI, rozważ zaimplementowanie interfejsów w swoich bibliotekach. Dzięki temu użytkownicy mogą łatwo zintegrować pakiet NuGet za pośrednictwem abstrakcji. Przykłady implementacji można znaleźć w sekcji Przykładowe implementacje IChatClient i IEmbeddingGenerator.
  • konsumenci usług: Jeżeli tworzysz biblioteki, które korzystają z usług AI, używaj abstrakcji zamiast kodować je na stałe do konkretnej usługi AI. Takie podejście zapewnia konsumentom elastyczność wyboru preferowanego dostawcy.
  • deweloperzy aplikacji: użyj abstrakcji, aby uprościć integrację z aplikacjami. Umożliwia to przenośność między modelami i usługami, ułatwia testowanie i pozorowanie, wykorzystuje oprogramowanie pośredniczące dostarczane przez ekosystem i utrzymuje spójny interfejs API w całej aplikacji, nawet jeśli używasz różnych usług w różnych częściach aplikacji.
  • Współautorzy ekosystemu: Jeśli interesuje Cię współtworzenie ekosystemu, rozważ napisanie niestandardowych komponentów oprogramowania pośredniczącego.

Aby uzyskać więcej przykładów, zobacz repozytorium GitHub dotnet/ai-samples . Aby zapoznać się z kompleksowym przykładem, zobacz eShopSupport.

Zobacz także