Teilen über


Künstliche Intelligenz in .NET (Vorschau)

Mit einer wachsenden Vielfalt an verfügbaren KI-Diensten (Künstliche Intelligenz) benötigen Entwickler eine Möglichkeit, diese Dienste in ihre .NET-Anwendungen zu integrieren und mit ihnen zu interagieren. Die Microsoft.Extensions.AI-Bibliothek bietet einen einheitlichen Ansatz für die Darstellung von generativen KI-Komponenten, die eine nahtlose Integration und Interoperabilität mit verschiedenen KI-Diensten ermöglichen. Dieser Artikel führt die Bibliothek ein und enthält Installationsanweisungen und Verwendungsbeispiele, die Ihnen bei den ersten Schritten helfen.

Paket installieren

Um das 📦 Microsoft.Extensions.AI NuGet-Paket zu installieren, verwenden Sie die .NET CLI, oder fügen Sie ihrer C#-Projektdatei einen Paketverweis direkt hinzu:

dotnet add package Microsoft.Extensions.AI --prerelease

Weitere Informationen finden Sie unter dotnet add package oder Verwalten von Paketabhängigkeiten in .NET-Anwendungen.

Anwendungsbeispiele

Die IChatClient-Schnittstelle definiert eine Client-Abstraktion, die für die Interaktion mit KI-Diensten verantwortlich ist, die Chatfunktionen bereitstellen. Es enthält Methoden zum Senden und Empfangen von Nachrichten mit multimodalem Inhalt (wie z. B. Text, Bilder und Audio), entweder als kompletter Satz oder inkrementell gestreamt. Darüber hinaus stellt es Metadateninformationen über den Client bereit und bietet die Möglichkeit, stark typisierte Dienste abzurufen.

Wichtig

Weitere Verwendungsbeispiele und reale Szenarien finden Sie unter KI für .NET-Entwickler.

In diesem Abschnitt

Die IChatClient-Schnittstelle

Das folgende Beispiel implementiert IChatClient, um die allgemeine Struktur zu zeigen.

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

Weitere konkrete Implementierungen von IChatClient finden Sie in den folgenden NuGet-Paketen:

Chat-Vervollständigung anfordern

Um eine Vervollständigung anzufordern, rufen Sie die Methode IChatClient.CompleteAsync auf. Die Anforderung besteht aus einer oder mehreren Nachrichten, die jeweils aus einem oder mehreren Inhalten bestehen. Es gibt Accelerator-Methoden, die allgemeine Fälle vereinfachen, wie z. B. das Erstellen einer Anforderung für ein einzelnes Stück Textinhalt.

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

Die Kernmethode IChatClient.CompleteAsync akzeptiert eine Liste von Nachrichten. Diese Liste stellt den Verlauf aller Nachrichten dar, die Teil der Unterhaltung sind.

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

Jede Nachricht im Verlauf wird durch ein ChatMessage-Objekt dargestellt. Die ChatMessage-Klasse bietet eine Eigenschaft ChatMessage.Role, die die Rolle der Nachricht angibt. Standardmäßig wird das ChatRole.User verwendet. Die folgenden Rollen sind verfügbar:

  • ChatRole.Assistant: Legt das Verhalten des Assistenten fest oder weist ihn an.
  • ChatRole.System: Stellt Antworten auf vom System angewiesene, vom Benutzer aufgeforderte Eingaben bereit.
  • ChatRole.Tool: Liefert zusätzliche Informationen und Verweise für Chat-Vervollständigungen.
  • ChatRole.User: Bietet Eingaben für Chat-Vervollständigungen.

Jede Nachricht wird instanziiert, indem ihrer Eigenschaft Contents eine neue TextContent zugewiesen wird. Es gibt verschiedene Inhaltstypen, die dargestellt werden können, z. B. eine einfache Zeichenfolge oder ein komplexeres Objekt, das eine multimodale Nachricht mit Text, Bildern und Audio darstellt.

Chat-Vervollständigung mit Streaming anfordern

Die Eingaben für IChatClient.CompleteStreamingAsync sind mit denen von CompleteAsync identisch. Anstatt jedoch die vollständige Antwort als Element eines ChatCompletion-Objekts zurückzugeben, gibt die Methode ein IAsyncEnumerable<T>-Objekt zurück, wobei TStreamingChatCompletionUpdate ist und einen Strom von Aktualisierungen liefert, die zusammen die einzige Antwort bilden.

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

Tipp

Streaming-APIs sind fast gleichbedeutend mit KI-Benutzererfahrungen. C# ermöglicht überzeugende Szenarien durch seine IAsyncEnumerable<T>-Unterstützung und bietet eine natürliche und effiziente Methode zum Streamen von Daten.

Tool-Aufrufe

Einige Modelle und Dienste unterstützen Tool-Aufrufe, bei denen Anfragen Tools enthalten können, mit denen das Modell Funktionen zum Sammeln zusätzlicher Informationen aufruft. Anstatt eine endgültige Antwort zu senden, fordert das Modell einen Funktionsaufruf mit bestimmten Argumenten an. Der Client ruft dann die Funktion auf und sendet die Ergebnisse zusammen mit dem Unterhaltungsverlauf an das Modell zurück. Die Microsoft.Extensions.AI-Bibliothek enthält Abstraktionen für verschiedene Nachrichteninhaltstypen, einschließlich Funktionsaufrufanforderungen und -ergebnisse. Während Verbraucher direkt mit diesen Inhalten interagieren können, automatisiert Microsoft.Extensions.AI diese Interaktionen und bietet Folgendes:

  • AIFunction: Stellt eine Funktion dar, die einem KI-Dienst beschrieben und aufgerufen werden kann.
  • AIFunctionFactory: Bietet Factory-Methoden zum Erstellen allgemein verwendeter Implementierungen von AIFunction.
  • FunctionInvokingChatClient: Verpackt ein IChatClient, um Funktionalitäten für automatische Funktionsaufrufe hinzuzufügen.

Betrachten Sie das folgende Beispiel, das einen zufälligen Funktionsaufruf veranschaulicht:

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

Das vorangehende Beispiel hängt vom 📦 Microsoft.Extensions.AI.Ollama NuGet-Paket ab.

Der vorangehende Code:

  • Definiert eine Funktion namens GetCurrentWeather, die eine zufällige Wettervorhersage zurückgibt.
    • Diese Funktion ist mit einem DescriptionAttribute ausgezeichnet, das dazu dient, dem KI-Dienst eine Beschreibung der Funktion zur Verfügung zu stellen.
  • Instanziiert eine ChatClientBuilder mit einer OllamaChatClient und konfiguriert sie für die Verwendung von Funktionsaufrufen.
  • Ruft CompleteStreamingAsync auf dem Client auf und übergibt einen Prompt und eine Liste von Tools, die eine mit Create erstellte Funktion enthält.
  • Iteriert über die Antwort und gibt jede Aktualisierung auf der Konsole aus.

Antworten aus dem Cache

Wenn Sie mit Zwischenspeichern in .NET vertraut sind, ist es gut zu wissen, dass Microsoft.Extensions.AI andere solche delegierenden IChatClient-Implementierungen bietet. Das DistributedCachingChatClient ist ein IChatClient, das das Zwischenspeichern über eine andere beliebige IChatClient- Instanz legt. Wenn ein eindeutiger Chatverlauf an die DistributedCachingChatClient gesendet wird, leitet sie ihn an den zugrunde liegenden Client weiter und speichert die Antwort im Zwischenspeicher, bevor sie an den Verbraucher zurückgeschickt wird. Wenn die gleiche Eingabeaufforderung das nächste Mal übermittelt wird, sodass eine zwischengespeicherte Antwort im Cache gefunden werden kann, gibt die DistributedCachingChatClient die zwischengespeicherte Antwort zurück, anstatt die Anforderung entlang der Pipeline weiterzuleiten.

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

Das vorherige Beispiel hängt vom 📦 Microsoft.Extensions.Caching.Memory NuGet-Paket ab. Weitere Informationen finden Sie unter Zwischenspeichern in .NET.

Verwenden der Telemetrie

Ein weiteres Beispiel für einen delegierenden Chat-Client ist die OpenTelemetryChatClient. Diese Implementierung entspricht den OpenTelemetry-Semantikkonventionen für generative KI-Systeme. Ähnlich wie andere IChatClient-Delegatoren legt er eine Schicht von Metriken und Spans um jede zugrundeliegende IChatClient-Implementierung und bietet so eine verbesserte Beobachtbarkeit.

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

Das vorherige Beispiel hängt vom 📦 OpenTelemetry.Exporter.Console NuGet-Paket ab.

Bieten Sie Optionen

Jeder Aufruf von CompleteAsync oder CompleteStreamingAsync kann optional eine ChatOptions-Instanz liefern, die zusätzliche Parameter für den Vorgang enthält. Die gängigsten Parameter bei KI-Modellen und Diensten werden als stark typisierte Eigenschaften des Typs angezeigt, wie z. B. ChatOptions.Temperature. Andere Parameter können über das ChatOptions.AdditionalProperties-Wörterbuch in einer schwach typisierten Weise namentlich angegeben werden.

Sie können auch Optionen angeben, wenn Sie ein IChatClient mit der fluent ChatClientBuilder API erstellen und einen Aufruf der ConfigureOptions-Erweiterungsmethode verketten. Dieser delegierende Client verpackt einen anderen Client und ruft den bereitgestellten Delegaten auf, um bei jedem Aufruf eine ChatOptions-Instanz zu befüllen. Um beispielsweise sicherzustellen, dass die Eigenschaft ChatOptions.ModelId standardmäßig einen bestimmten Modellnamen enthält, können Sie den folgenden Code verwenden:

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

Das vorangehende Beispiel hängt vom 📦 Microsoft.Extensions.AI.Ollama NuGet-Paket ab.

Funktionalitäts-Pipelines

IChatClient Instanzen können überschichtet werden, um eine Pipeline von Komponenten zu erstellen, wobei jeweils bestimmte Funktionen hinzugefügt werden. Diese Komponenten können aus Microsoft.Extensions.AI, anderen NuGet-Paketen oder benutzerdefinierten Implementierungen stammen. Mit diesem Ansatz können Sie das Verhalten der IChatClient auf verschiedene Weise erweitern, um Ihre spezifischen Anforderungen zu erfüllen. Betrachten Sie den folgenden Beispiel-Code, der einen verteilten Cache, Funktionsaufrufe und OpenTelemetry Tracing um einen Beispiel-Chat-Client legt:

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

Das vorangehende Beispiel hängt von den folgenden NuGet-Paketen ab:

Angepasste IChatClient-Middleware

Um zusätzliche Funktionen hinzuzufügen, können Sie IChatClient direkt implementieren oder die DelegatingChatClient Klasse verwenden. Diese Klasse dient als Basis zum Erstellen von Chatclients, die Vorgänge an eine andere IChatClient-Instanz delegieren. Es vereinfacht das Verketten mehrerer Clients, sodass Anrufe an einen zugrunde liegenden Client übergeben werden können.

Die DelegatingChatClient-Klasse stellt Standardimplementierungen für Methoden wie CompleteAsync, CompleteStreamingAsync und Dispose bereit, die Aufrufe an den inneren Client weiterleiten. Sie können von dieser Klasse ableiten und nur die Methoden überschreiben, die Sie zum Verbessern des Verhaltens benötigen, wobei Sie andere Aufrufe an die Basisimplementierung delegieren. Dieser Ansatz hilft beim Erstellen flexibler und modularer Chatclients, die einfach zu erweitern und zu verfassen sind.

Im Folgenden sehen Sie eine Beispielklasse, die von DelegatingChatClient abgeleitet wurde, um eine Funktion zur Bewertungsbegrenzung bereitzustellen, die RateLimiter verwendet:

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

Das vorherige Beispiel hängt vom 📦 System.Threading.RateLimiting NuGet-Paket ab. Die Komposition des RateLimitingChatClient mit einem anderen Client ist ganz einfach:

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

Um die Komposition solcher Komponenten mit anderen zu vereinfachen, sollten die Autoren der Komponente eine Use*-Erweiterungsmethode erstellen, um die Komponente in einer Pipeline zu registrieren. Betrachten Sie beispielsweise die folgende Erweiterungsmethode:

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>

Solche Erweiterungen können auch relevante Dienste aus dem DI-Container abfragen; das von der Pipeline verwendete IServiceProvider wird als optionaler Parameter übergeben:

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>

Der Verbraucher kann dies dann z. B. einfach in seiner Pipeline verwenden:

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

Dieses Beispiel demonstriert ein gehostetes Szenario, bei dem der Verbraucher auf Dependency-Injection angewiesen ist, um die RateLimiter-Instanz bereitzustellen. Die vorangehenden Erweiterungsmethoden demonstrieren die Verwendung einer Use-Methode auf ChatClientBuilder. Die ChatClientBuilder bietet auch Use-Überladungen, die es einfacher machen, solche delegierenden Handler zu schreiben.

Im früheren Beispiel RateLimitingChatClient müssen die Überschreibungen von CompleteAsync und CompleteStreamingAsync nur vor und nach der Delegierung an den nächsten Client in der Pipeline ausgeführt werden. Um dasselbe zu erreichen, ohne eine angepasste Klasse schreiben zu müssen, können Sie eine Überladung von Use verwenden, die einen Delegaten akzeptiert, der sowohl für CompleteAsync als auch für CompleteStreamingAsync verwendet wird, wodurch sich die erforderliche Boilerplate reduziert:

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

Die vorangehende Überladung verwendet intern ein AnonymousDelegatingChatClient, das kompliziertere Strukturierungen mit nur wenig zusätzlichem Code ermöglicht. Zum Beispiel, um das gleiche Ergebnis zu erzielen, aber mit dem RateLimiter, das von DI abgerufen wird:

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

Für Szenarien, in denen der Entwickler delegierende Implementierungen von CompleteAsync und CompleteStreamingAsync inline angeben möchte und in denen es wichtig ist, für jede Implementierung eine andere schreiben zu können, um ihre einzigartigen Rückgabetypen speziell zu behandeln, gibt es eine weitere Überladung von Use, die für jede einen Delegaten akzeptiert.

Dependency Injection

IChatClient-Implementierungen werden einer Anwendung in der Regel über Dependency-Injection (DI) zur Verfügung gestellt. In diesem Beispiel wird ein IDistributedCache in den DI-Container eingefügt, ebenso wie ein IChatClient. Bei der Registrierung für IChatClient wird ein Builder eingesetzt, der eine Pipeline erstellt, die einen Caching-Client (der dann einen IDistributedCache verwendet, der von DI abgerufen wird) und den Beispiel-Client enthält. Das injizierte IChatClient kann an anderer Stelle in der App abgerufen und verwendet werden.

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

Das vorangehende Beispiel hängt von den folgenden NuGet-Paketen ab:

Welche Instanz und Konfiguration eingefügt wird, kann je nach den aktuellen Anforderungen der Anwendung unterschiedlich sein, und mehrere Pipelines können mit unterschiedlichen Schlüsseln eingefügt werden.

Die IEmbeddingGenerator-Schnittstelle

Die Schnittstelle IEmbeddingGenerator<TInput,TEmbedding> stellt einen generischen Generator für Einbettungen dar. Dabei ist TInput der Typ der einzubettenden Eingabewerte und TEmbedding der Typ der generierten Einbettung, die von der Klasse Embedding erbt.

Die Klasse Embedding dient als Basisklasse für Einbettungen, die von einer IEmbeddingGenerator generiert werden. Es wurde entwickelt, um die Metadaten und Daten zu speichern und zu verwalten, die mit Einbettungen verknüpft sind. Abgeleitete Typen wie Embedding<T> stellen die konkreten Einbettungsvektordaten bereit. Eine Einbettung stellt zum Beispiel eine Embedding<T>.Vector-Eigenschaft zur Verfügung, um auf ihre Einbettungsdaten zuzugreifen.

Die Schnittstelle IEmbeddingGenerator definiert eine Methode zur asynchronen Generierung von Einbettungen für eine Sammlung von Eingabewerten, mit optionaler Unterstützung für Konfiguration und Abbruch. Außerdem werden Metadaten bereitgestellt, die den Generator beschreiben, und es ermöglicht den Abruf von stark typisierten Diensten, die entweder vom Generator selbst oder von seinen zugrunde liegenden Diensten bereitgestellt werden können.

Beispielimplementierung

Betrachten Sie die folgende Beispiel-Implementierung einer IEmbeddingGenerator, um die allgemeine Struktur zu zeigen, die aber nur zufällige Einbettungsvektoren generiert.

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

Der vorangehende Code:

  • Definiert eine Klasse namens SampleEmbeddingGenerator, die die IEmbeddingGenerator<string, Embedding<float>>-Schnittstelle implementiert.
  • Verfügt über einen primären Konstruktor, der einen Endpunkt und eine Modell-ID akzeptiert, die zum Identifizieren des Generators verwendet werden.
  • Stellt eine Metadata-Eigenschaft zur Verfügung, die Metadaten über den Generierer liefert.
  • Implementiert die Methode GenerateAsync, um Einbettungen für eine Sammlung von Eingabewerten zu generieren:
    • Simuliert einen asynchronen Vorgang, indem er 100 Millisekunden verzögert.
    • Gibt zufällige Einbettungen für jeden Eingabewert zurück.

Die konkreten Implementierungen finden Sie in den folgenden Paketen:

Erstellen von Einbettungen

Der primäre Vorgang, der mit IEmbeddingGenerator<TInput,TEmbedding> ausgeführt wird, ist die Generierung von Einbettungen, die mit der Methode GenerateAsync durchgeführt wird.

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

Angepasste IEmbeddingGenerator-Middleware

Wie bei IChatClientkönnen IEmbeddingGenerator-Implementierungen gestapelt werden. So wie Microsoft.Extensions.AI delegierende Implementierungen von IChatClient für das Zwischenspeichern und die Telemetrie bereitstellt, so bietet es auch eine Implementierung für 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()));
}

Die IEmbeddingGenerator ermöglicht die Erstellung angepasster Middleware, die die Funktionalität einer IEmbeddingGenerator erweitert. Die DelegatingEmbeddingGenerator<TInput,TEmbedding>-Klasse ist eine Implementierung der IEmbeddingGenerator<TInput, TEmbedding>-Schnittstelle, die als Basisklasse zum Erstellen von Einbettungsgeneratoren dient, die ihre Vorgänge an eine andere IEmbeddingGenerator<TInput, TEmbedding>-Instanz delegieren. Sie ermöglicht das Verketten mehrerer Generatoren in beliebiger Reihenfolge, indem Aufrufe an einen zugrunde liegenden Generator übergeben werden. Die Klasse stellt Standardimplementierungen für Methoden wie GenerateAsync und Disposebereit, die die Aufrufe an die innere Generatorinstanz weiterleiten und dadurch eine flexible und modulare Erstellung von Einbettungen ermöglichen.

Im Folgenden finden Sie eine Beispielimplementierung eines solchen delegierenden Einbettungsgenerierers, der Anfragen zur Generierung von Einbettungen begrenzt:

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

Dieser kann dann um einen beliebigen IEmbeddingGenerator<string, Embedding<float>> herum geschichtet werden, um die Bewertung aller durchgeführten Vorgänge der Einbettungsgenerierung zu begrenzen.

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

Auf diese Weise kann die RateLimitingEmbeddingGenerator-Instanz mit anderen IEmbeddingGenerator<string, Embedding<float>>-Instanzen kombiniert werden, um Bewertungsbegrenzungsfunktionen bereitzustellen.

Weitere Informationen