Partage via


Intelligence artificielle dans .NET (préversion)

En raison du nombre grandissant de services d’intelligence artificielle (IA), les développeurs ont besoin d’un moyen leur permettant d’intégrer ces services dans leurs applications .NET et d’interagir avec eux. La bibliothèque Microsoft.Extensions.AI fournit une approche unifiée pour représenter les composants d’IA générative, ce qui permet une intégration et une interopérabilité transparentes avec différents services IA. Cet article présente la bibliothèque et fournit des instructions d’installation ainsi que des exemples d’utilisation pour vous aider à commencer.

Installer le package

Pour installer le package NuGet 📦 Microsoft.Extensions.AI, utilisez l’interface CLI .NET ou ajoutez une référence de package directement à votre fichier projet C# :

dotnet add package Microsoft.Extensions.AI --prelease

Pour plus d’informations, consultez dotnet add package ou Gérer les dépendances de packages dans les applications .NET.

Exemples d’utilisation

L’interface IChatClient définit une abstraction cliente chargée d’interagir avec les services IA qui fournissent des fonctionnalités de conversation. Elle inclut des méthodes d’envoi et de réception de messages avec du contenu multimodal (par exemple, du texte, des images et de l’audio), en tant qu’ensemble complet ou diffusé de manière incrémentielle. En outre, il fournit des informations sur les métadonnées du client et permet de récupérer des services fortement typés.

Important

Pour plus d’exemples d’utilisation et de scénarios réels, consultez IA pour les développeurs .NET.

Dans cette section

L’interface IChatClient

L’exemple suivant implémente le IChatClient pour afficher la structure générale.

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

Vous trouverez d’autres implémentations concrètes du IChatClient dans les packages NuGet suivants :

Demander la complétion de la conversation

Pour demander une complétion, appeler la méthode IChatClient.CompleteAsync. La requête est composée d’un ou plusieurs messages, chacun composé d’une ou plusieurs parties de contenu. Des méthodes d’accélérateur existent pour simplifier les cas courants, tels que la construction d’une requête pour un seul élément de contenu texte.

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

La méthode principale IChatClient.CompleteAsync accepte une liste de messages. Cette liste représente l’historique de tous les messages qui font partie de la conversation.

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

Chaque message de l’historique est représenté par un objet ChatMessage. La classe ChatMessage fournit une propriété ChatMessage.Role qui indique le rôle du message. ChatRole.User est utilisé par défaut. Les rôles suivants sont disponibles :

  • ChatRole.Assistant: Indique ou définit le comportement de l’assistant.
  • ChatRole.System: fournit des réponses aux entrées sollicitées par le système et déclenchées par l’utilisateur.
  • ChatRole.Tool : fournit des informations et des références supplémentaires pour les achèvements de conversation.
  • ChatRole.User : fournit une entrée pour les achèvements de conversations.

Chaque message de conversation est instancié, assignant à sa propriété Contents une nouvelle TextContent. Il existe différents types de contenu qui peuvent être représentés, tels qu’une chaîne simple ou un objet plus complexe qui représente un message multimodal avec du texte, des images et de l’audio :

Demande de complétion de chat avec streaming

Les entrées dans IChatClient.CompleteStreamingAsync sont identiques à celles de CompleteAsync. Toutefois, au lieu de renvoyer la réponse complète dans le cadre d’un objet ChatCompletion, la méthode retourne un IAsyncEnumerable<T>T est StreamingChatCompletionUpdate, fournissant ainsi un flux de mises à jour qui forment collectivement la réponse unique.

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

Conseil

Les API de streaming sont presque synonymes d’expériences utilisateur IA. C# permet des scénarios convaincants avec sa prise en charge de IAsyncEnumerable<T>, offrant un moyen naturel et efficace de diffuser des données de flux.

Appel de l’outil

Certains modèles et services prennent en charge l’appel d’outils, où les requêtes peuvent inclure des outils permettant au modèle d’invoquer des fonctions pour collecter des informations supplémentaires. Au lieu d’envoyer une réponse finale, le modèle demande un appel de fonction avec des arguments spécifiques. Le client appelle ensuite la fonction et renvoie les résultats au modèle, ainsi que l’historique des conversations. La bibliothèque Microsoft.Extensions.AI inclut des abstractions pour différents types de contenu de message, notamment les demandes d’appel de fonction et les résultats. Bien que les consommateurs puissent interagir directement avec ce contenu, Microsoft.Extensions.AI automatise ces interactions et fournit :

  • AIFunction: Représente une fonction qui peut être décrite dans un service IA et appelée.
  • AIFunctionFactory: Fournit des méthodes de fabrique pour créer des implémentations couramment utilisées de AIFunction.
  • FunctionInvokingChatClient : encapsule un IChatClient pour ajouter des capacités d’invocation automatique de fonctions.

Prenons l’exemple suivant qui illustre un appel de fonction aléatoire :

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

L’exemple précédent dépend du package NuGet 📦 Microsoft.Extensions.AI.Ollama.

Le code précédent :

  • Définit une fonction nommée GetCurrentWeather qui retourne une prévision météorologique aléatoire.
    • Cette fonction est décorée d’un DescriptionAttribute, qui est utilisé pour fournir une description de la fonction au service IA.
  • Instancie un ChatClientBuilder avec un OllamaChatClient et le configure pour utiliser l'invocation de fonction.
  • Appelle CompleteStreamingAsync sur le client, en transmettant une invite et une liste d’outils qui incluent une fonction créée avec Create.
  • Effectue une itération sur la réponse, en imprimant chaque mise à jour sur la console.

Mettre en cache des réponses

Si vous connaissez Mise en cache dans .NET, il est judicieux de savoir que Microsoft.Extensions.AI fournit d’autres implémentations IChatClient de délégation. Le DistributedCachingChatClient est un IChatClient qui applique une mise en cache par couches autour d'une autre instance IChatClient arbitraire. Lorsqu’un historique de conversation unique est envoyé au DistributedCachingChatClient, il le transfère au client sous-jacent, puis met en cache la réponse avant de la renvoyer au consommateur. La prochaine fois que la même invite est envoyée, de sorte qu’une réponse mise en cache se trouve dans le cache, le DistributedCachingChatClient retournera la réponse mise en cache plutôt que de devoir transférer la requête le long du pipeline.

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

L’exemple précédent dépend du package NuGet 📦 Microsoft.Extensions.Caching.Memory. Pour plus d’informations, consultez Mise en cache dans .NET.

Utiliser la télémétrie

Un autre exemple d’un client de conversation délégué est le OpenTelemetryChatClient. Cette implémentation respecte les conventions sémantiques openTelemetry pour les systèmes d’IA générative. Comme pour d’autres délégateurs IChatClient, il superpose les métriques et couvre toute implémentation IChatClient sous-jacente, offrant ainsi une meilleure observabilité.

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

L’exemple précédent dépend du package NuGet 📦 OpenTelemetry.Exporter.Console.

Proposer des options

Chaque appel à CompleteAsync ou CompleteStreamingAsync peut éventuellement fournir une instance de ChatOptions contenant des paramètres supplémentaires pour l’opération. Les paramètres les plus courants parmi les modèles et services d’IA s’affichent comme des propriétés fortement typées sur le type, telles que ChatOptions.Temperature. D’autres paramètres peuvent être fournis au moyen d’un nom de manière faiblement typée via le dictionnaire ChatOptions.AdditionalProperties.

Vous pouvez également spécifier des options lors de la création d’un IChatClient avec l’API fluide ChatClientBuilder et du chaînage d’un appel à la méthode d’extension ConfigureOptions. Ce client délégateur enveloppe un autre client et appelle le délégué fourni pour remplir une instance ChatOptions pour chaque appel. Par exemple, pour vous assurer que la propriété ChatOptions.ModelId est par défaut un nom de modèle particulier, vous pouvez utiliser du code comme suit :

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

L’exemple précédent dépend du package NuGet 📦 Microsoft.Extensions.AI.Ollama.

Pipelines de fonctionnalités

Des instances IChatClient peuvent être superposées pour créer un pipeline de composants, chacune ajoutant ainsi des fonctionnalités spécifiques. Ces composants peuvent provenir de Microsoft.Extensions.AI, d’autres packages NuGet ou d’implémentations personnalisées. Cette approche vous permet d’augmenter le comportement du IChatClient de différentes façons pour répondre à vos besoins spécifiques. Considérez l’exemple de code suivant qui superpose un cache distribué, un appel de fonction et un suivi OpenTelemetry autour d’un exemple de client de conversation :

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

L’exemple précédent dépend des packages NuGet suivants :

Middleware IChatClient personnalisé

Pour ajouter des fonctionnalités supplémentaires, vous pouvez implémenter IChatClient directement ou utiliser la classe DelegatingChatClient. Cette classe sert de base pour créer des clients de conversation qui délèguent des opérations à une autre instance IChatClient. Il simplifie le chaînage de plusieurs clients, ce qui permet aux appels de passer par un client sous-jacent.

La classe DelegatingChatClient fournit des implémentations par défaut pour les méthodes telles que CompleteAsync, CompleteStreamingAsync et Dispose, qui transfèrent les appels au client interne. Vous pouvez dériver de cette classe et remplacer uniquement les méthodes dont vous avez besoin pour améliorer le comportement, tout en déléguant d’autres appels à l’implémentation de base. Cette approche permet de créer des clients de conversation flexibles et modulaires faciles à étendre et à composer.

Voici un exemple de classe dérivée de DelegatingChatClient pour fournir des fonctionnalités de limitation de débit qui utilise le 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);
    }
}

L’exemple précédent dépend du package NuGet 📦 System.Threading.RateLimiting. La composition du RateLimitingChatClient avec un autre client est simple :

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

Pour simplifier l’association de ces composants avec d’autres, les auteurs de composants doivent créer une méthode d’extension Use* pour inscrire le composant dans un pipeline. Par exemple, envisagez la méthode d’extension suivante :

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>

Ces extensions peuvent également demander les services pertinents au conteneur DI ; le IServiceProvider utilisé par le pipeline est transmis en tant que paramètre facultatif :

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>

Le consommateur peut alors facilement l’utiliser dans son pipeline, par exemple :

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

Cet exemple illustre un scénario hébergé, dans lequel le consommateur s’appuie sur l’injection de dépendances pour fournir l’instance RateLimiter. Les méthodes d’extension précédentes illustrent l’utilisation d’une méthode Use sur ChatClientBuilder. Le ChatClientBuilder fournit également des surcharges Use qui facilitent l’écriture de tels gestionnaires de délégation.

Par exemple, dans l’exemple précédent de RateLimitingChatClient, les remplacements de CompleteAsync et CompleteStreamingAsync doivent uniquement effectuer des tâches avant et après la délégation au client suivant dans le pipeline. Pour obtenir le même résultat sans écrire une classe personnalisée, vous pouvez utiliser une surcharge de Use qui accepte un délégué utilisé à la fois pour CompleteAsync et CompleteStreamingAsync, ce qui réduit les lignes de code nécessaires :

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

La surcharge précédente utilise en interne un AnonymousDelegatingChatClient, ce qui permet des modèles plus compliqués avec seulement un peu de code supplémentaire. Par exemple, pour obtenir le même résultat, mais avec le RateLimiter récupéré à partir de 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();

Pour les scénarios où le développeur souhaite spécifier des implémentations déléguées de CompleteAsync et CompleteStreamingAsync en ligne, et où il est important de pouvoir écrire une implémentation différente pour chacune afin de gérer spécialement leurs types de retour uniques, une autre surcharge de Use existe qui accepte un délégué pour chacune.

Injection de dépendances

Les implémentation IChatClient sont généralement fournies à une application via une injection de dépendances (DI). Dans cet exemple, un IDistributedCache est ajouté au conteneur DI, ainsi qu'un IChatClient. L’inscription du IChatClient utilise un générateur qui crée un pipeline contenant un client de mise en cache (qui utilisera ensuite un IDistributedCache récupéré à partir de la DI) et le client d’exemple. Le IChatClient injecté peut être récupéré et utilisé ailleurs dans l’application.

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

L’exemple précédent dépend des packages NuGet suivants :

L'instance et la configuration injectées peuvent différer en fonction des besoins actuels de l'application, et plusieurs pipelines peuvent être injectés avec des clés différentes.

L’interface IEmbeddingGenerator

L’interface IEmbeddingGenerator<TInput,TEmbedding> représente un générateur générique d’incorporations. Ici, TInput est le type de valeurs d’entrée incorporées, et TEmbedding est le type d’incorporation généré, qui hérite de la classe Embedding.

La classe Embedding sert de classe de base pour les incorporations générées par un IEmbeddingGenerator. Il est conçu pour stocker et gérer les métadonnées et les données associées aux incorporations. Les types dérivés comme Embedding<T> fournissent les données vectorielles incorporées concrètes. Par exemple, une incorporation expose une propriété Embedding<T>.Vector pour accéder à ses données d’incorporation.

L’interface IEmbeddingGenerator définit une méthode pour générer de manière asynchrone des incorporations pour une collection de valeurs d’entrée, avec prise en charge facultative de la configuration et de l’annulation. Il fournit également des métadonnées décrivant le générateur et permet la récupération de services fortement typés qui peuvent être fournis par le générateur ou ses services sous-jacents.

Exemple d’implémentation

Considérez l’exemple d’implémentation suivant d’un IEmbeddingGenerator pour afficher la structure générale, mais qui génère simplement des vecteurs d’incorporation aléatoires.

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

Le code précédent :

  • Définit une classe nommée SampleEmbeddingGenerator qui implémente l’interface IEmbeddingGenerator<string, Embedding<float>>.
  • Possède un constructeur principal qui accepte un point de terminaison et un ID de modèle, qui sont utilisés pour identifier le générateur.
  • Expose une propriété Metadata qui fournit des métadonnées sur le générateur.
  • Implémente la méthode GenerateAsync pour générer des incorporations pour une collection de valeurs d’entrée :
    • Simule une opération asynchrone en la retardant de 100 millisecondes.
    • Retourne des incorporations aléatoires pour chaque valeur d’entrée.

Vous trouverez des implémentations concrètes réelles dans les packages suivants :

Créer des embeddings

L’opération principale effectuée avec une IEmbeddingGenerator<TInput,TEmbedding> est la génération d'embeddings, qui est réalisée grâce à sa méthode 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()));
}

Middleware IEmbeddingGenerator personnalisé

Comme avec le IChatClient, les implémentations du IEmbeddingGenerator peuvent être superposées. Tout comme Microsoft.Extensions.AI fournit des implémentations de délégation de IChatClient pour la mise en cache et la télémétrie, il fournit également une implémentation pour 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()));
}

Le IEmbeddingGenerator permet de créer un middleware personnalisé qui étend les fonctionnalités d’un IEmbeddingGenerator. La classe DelegatingEmbeddingGenerator<TInput,TEmbedding> est une implémentation de l’interface IEmbeddingGenerator<TInput, TEmbedding> qui sert de classe de base pour créer des générateurs d’incorporation qui délèguent leurs opérations à une autre instance IEmbeddingGenerator<TInput, TEmbedding>. Elle permet de chaîner plusieurs générateurs dans n’importe quel ordre, en passant des appels à un générateur sous-jacent. La classe fournit des implémentations par défaut pour les méthodes telles que GenerateAsync et Dispose, qui transfèrent les appels à l’instance de générateur interne, ce qui permet une génération d’incorporation flexible et modulaire.

Voici un exemple d’implémentation d’un tel générateur d’incorporation par délégation qui limite les demandes de génération d’incorporation :

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

On peut ensuite le superposer à un IEmbeddingGenerator<string, Embedding<float>> arbitraire pour limiter le débit de toutes les opérations de génération d’incorporations effectuées.

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

De cette façon, le RateLimitingEmbeddingGenerator peut être composé d’autres instances IEmbeddingGenerator<string, Embedding<float>> pour fournir des fonctionnalités de limitation de débit.

Voir aussi