Condividi tramite


librerie Microsoft.Extensions.AI

Gli sviluppatori .NET devono integrare e interagire con un'ampia gamma di servizi di intelligenza artificiale nelle app. Le Microsoft.Extensions.AI librerie offrono un approccio unificato per rappresentare i componenti generativi di intelligenza artificiale e abilitare l'integrazione e l'interoperabilità senza problemi con vari servizi di intelligenza artificiale. Questo articolo presenta le librerie e fornisce esempi di utilizzo approfonditi per iniziare.

Pacchetti

Il 📦 pacchetto Microsoft.Extensions.AI.Abstractions fornisce i tipi di scambio principali, inclusi IChatClient e IEmbeddingGenerator<TInput,TEmbedding>. Qualsiasi libreria .NET che fornisce un client LLM può implementare l'interfaccia IChatClient per abilitare un'integrazione senza problemi con il codice che lo utilizza.

Il 📦 pacchetto Microsoft.Extensions.AI ha una dipendenza implicita dal Microsoft.Extensions.AI.Abstractions pacchetto. Questo pacchetto consente di integrare facilmente componenti come la chiamata automatica degli strumenti di funzione, la telemetria e la memorizzazione nella cache nelle applicazioni usando modelli familiari di inserimento delle dipendenze e middleware. Ad esempio, fornisce il UseOpenTelemetry(ChatClientBuilder, ILoggerFactory, String, Action<OpenTelemetryChatClient>) metodo di estensione, che aggiunge il supporto OpenTelemetry alla pipeline del client di chat.

Quale pacchetto fare riferimento

Le librerie che forniscono implementazioni delle astrazioni fanno in genere riferimento solo a Microsoft.Extensions.AI.Abstractions.

Per avere accesso anche a utilità avanzate per l'uso dei componenti generativi di intelligenza artificiale, fare invece riferimento al pacchetto Microsoft.Extensions.AI (che a sua volta fa riferimento a Microsoft.Extensions.AI.Abstractions). La maggior parte delle applicazioni e dei servizi che consumano dovrebbe fare riferimento al pacchetto Microsoft.Extensions.AI insieme a una o più librerie che forniscono implementazioni concrete delle astrazioni.

Installare i pacchetti

Per informazioni su come installare pacchetti NuGet, vedere dotnet package add or Manage package dependencies in .NET applications (Aggiungere pacchetti dotnet o Gestire le dipendenze dei pacchetti nelle applicazioni .NET).

Esempi di utilizzo delle API

Le sottosezioni seguenti mostrano esempi specifici di utilizzo di IChatClient :

Le sezioni seguenti illustrano esempi specifici di utilizzo di IEmbeddingGenerator :

Interfaccia IChatClient

L'interfaccia IChatClient definisce un'astrazione client responsabile dell'interazione con i servizi di intelligenza artificiale che forniscono funzionalità di chat. Include metodi per l'invio e la ricezione di messaggi con contenuto multi modale (ad esempio testo, immagini e audio), come set completo o trasmesso in modo incrementale. Consente inoltre di recuperare servizi fortemente tipizzato forniti dal client o dai relativi servizi sottostanti.

Le librerie .NET che forniscono client per modelli di linguaggio e servizi possono fornire un'implementazione dell'interfaccia IChatClient . Tutti i consumer dell'interfaccia sono quindi in grado di interagire perfettamente con questi modelli e servizi tramite le astrazioni. È possibile visualizzare una semplice implementazione in Implementazioni di esempio di IChatClient e IEmbeddingGenerator.

Richiedere una risposta di chat

Con un'istanza di IChatClientè possibile chiamare il IChatClient.GetResponseAsync metodo per inviare una richiesta e ottenere una risposta. La richiesta è costituita da uno o più messaggi, ognuno dei quali è composto da una o più parti di contenuto. Esistono metodi acceleratori per semplificare i casi comuni, ad esempio la creazione di una richiesta per una singola parte di contenuto di testo.

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

Il metodo principale IChatClient.GetResponseAsync accetta un elenco di messaggi. Questo elenco rappresenta la cronologia di tutti i messaggi che fanno parte della conversazione.

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

L'oggetto ChatResponse restituito da GetResponseAsync espone un elenco di ChatMessage istanze che rappresentano uno o più messaggi generati come parte dell'operazione. In casi comuni è presente un solo messaggio di risposta, ma in alcune situazioni possono essere presenti più messaggi. L'elenco dei messaggi viene ordinato, in modo che l'ultimo messaggio nell'elenco rappresenti il messaggio finale della richiesta. Per fornire tutti questi messaggi di risposta al servizio in una richiesta successiva, è possibile aggiungere nuovamente i messaggi dalla risposta all'elenco dei messaggi.

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

Richiedi una risposta in chat in streaming

Gli input per IChatClient.GetStreamingResponseAsync sono identici a quelli di GetResponseAsync. Tuttavia, anziché restituire la risposta completa come parte di un oggetto ChatResponse, il metodo restituisce un IAsyncEnumerable<T> in cui T è ChatResponseUpdate, fornendo un flusso di aggiornamenti che formano collettivamente la singola risposta.

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

Suggerimento

Le API di streaming sono quasi sinonimi di esperienze utente di intelligenza artificiale. C# consente scenari accattivanti con il supporto IAsyncEnumerable<T>, consentendo un modo naturale ed efficiente per trasmettere i dati.

Come con GetResponseAsync, puoi aggiungere nuovamente gli aggiornamenti di IChatClient.GetStreamingResponseAsync nell'elenco dei messaggi. Poiché gli aggiornamenti sono singoli componenti di una risposta, è possibile usare helper come ToChatResponse(IEnumerable<ChatResponseUpdate>) per comporre uno o più aggiornamenti in un'istanza singola ChatResponse.

Aiutanti simili a AddMessages aiutano a comporre un ChatResponse e quindi estrarre i messaggi composti dalla risposta e aggiungerli a un elenco.

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

Richiamo dello strumento

Alcuni modelli e servizi supportano la chiamata di strumenti. Per raccogliere informazioni aggiuntive, è possibile configurare ChatOptions con informazioni sugli strumenti (in genere metodi .NET) che il modello può richiedere al client di richiamare. Anziché inviare una risposta finale, il modello richiede una chiamata di funzione con argomenti specifici. Il client richiama quindi la funzione e invia i risultati al modello con la cronologia delle conversazioni. La libreria Microsoft.Extensions.AI.Abstractions include astrazioni per vari tipi di contenuto del messaggio, incluse le richieste di chiamata di funzione e i risultati. Anche se IChatClient i consumatori possono interagire direttamente con questo contenuto, Microsoft.Extensions.AI fornisce strumenti ausiliari che permettono l'attivazione automatica degli strumenti in risposta alle richieste corrispondenti. Le Microsoft.Extensions.AI.Abstractions librerie e Microsoft.Extensions.AI forniscono i tipi seguenti:

  • AIFunction: rappresenta una funzione che può essere descritta in un modello di intelligenza artificiale e richiamata.
  • AIFunctionFactory: fornisce metodi factory per la creazione di AIFunction istanze che rappresentano metodi .NET.
  • FunctionInvokingChatClient: esegue il wrapping di un oggetto IChatClient come un altro IChatClient oggetto che aggiunge funzionalità automatiche di chiamata alla funzione.

L'esempio seguente illustra una chiamata a una funzione casuale (questo esempio dipende dal 📦 pacchetto 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);
}

Il codice precedente:

  • Definisce una funzione denominata GetCurrentWeather che restituisce una previsione meteo casuale.
  • Crea un'istanza di un ChatClientBuilder con un OllamaSharp.OllamaApiClient e la configura per invocare una funzione.
  • Chiama GetStreamingResponseAsync sul client, passando un prompt e un elenco di strumenti che includono una funzione creata con Create.
  • Itera sulla risposta, stampando ogni aggiornamento sul terminale.

Memorizzare nella cache le risposte

Se si ha familiarità con memorizzazione nella cache in .NET, è utile sapere che Microsoft.Extensions.AI fornisce altre simili implementazioni di delega IChatClient. Il DistributedCachingChatClient è un IChatClient che applica la memorizzazione nella cache a un'altra istanza arbitraria di IChatClient. Quando una nuova cronologia chat viene inviata a DistributedCachingChatClient, la inoltra al client sottostante e quindi memorizza nella cache la risposta prima di inviarla al consumer. Alla successiva invio della stessa cronologia, in modo che sia possibile trovare una risposta memorizzata nella cache, restituisce DistributedCachingChatClient la risposta memorizzata nella cache anziché inoltrare la richiesta lungo la pipeline.

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

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

IChatClient client = new ChatClientBuilder(sampleChatClient)
    .UseDistributedCache(new MemoryDistributedCache(
        Options.Create(new MemoryDistributedCacheOptions())))
    .Build();

string[] prompts = ["What is AI?", "What is .NET?", "What is AI?"];

foreach (var prompt in prompts)
{
    await foreach (var update in client.GetStreamingResponseAsync(prompt))
    {
        Console.Write(update);
    }
    Console.WriteLine();
}

Questo esempio dipende dal 📦 pacchetto NuGet Microsoft.Extensions.Caching.Memory . Per ulteriori informazioni, vedere cache in .NET.

Usare i dati di telemetria

Un altro esempio di client di chat delegante è il OpenTelemetryChatClient. Questa implementazione è conforme alle OpenTelemetry Semantic Conventions for Generative AI systems. Analogamente ad altri delegati IChatClient, posiziona metriche e intervalli attorno ad altre implementazioni arbitrarie 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);

L'esempio precedente dipende 📦 dal pacchetto NuGet OpenTelemetry.Exporter.Console .

In alternativa, il LoggingChatClient e il corrispondente UseLogging(ChatClientBuilder, ILoggerFactory, Action<LoggingChatClient>) forniscono un modo semplice per scrivere voci di log nell'oggetto ILogger per ogni richiesta e risposta.

Specificare le opzioni

Ogni chiamata a GetResponseAsync o GetStreamingResponseAsync può facoltativamente fornire un'istanza di ChatOptions contenente parametri aggiuntivi per l'operazione. I parametri più comuni tra i modelli e i servizi di intelligenza artificiale vengono visualizzati come proprietà fortemente tipizzate nel tipo, ad esempio ChatOptions.Temperature. Altri parametri possono essere forniti per nome in modo debolmente tipizzato, tramite il dizionario, o tramite un'istanza di opzioni che il provider sottostante riconosce, tramite la proprietà ChatOptions.AdditionalProperties.

È anche possibile specificare le opzioni quando si compila un oggetto IChatClient con l'API Fluent ChatClientBuilder concatenando una chiamata al ConfigureOptions(ChatClientBuilder, Action<ChatOptions>) metodo di estensione. Questo client delegante avvolge un altro client e invoca il delegato fornito per popolare un'istanza di ChatOptions per ogni chiamata. Ad esempio, per assicurarsi che per impostazione predefinita la proprietà ChatOptions.ModelId sia un nome di modello specifico, è possibile usare codice simile al seguente:

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

Pipeline delle funzionalità

IChatClient Le istanze possono essere sovrapposte per creare una pipeline di componenti che aggiungono funzionalità aggiuntive. Questi componenti possono provenire da Microsoft.Extensions.AI, altri pacchetti NuGet o implementazioni personalizzate. Questo approccio consente di aumentare il comportamento dei IChatClient in vari modi per soddisfare le esigenze specifiche. Si consideri il frammento di codice seguente che stratifica una cache distribuita, un'invocazione di funzione e un tracciamento OpenTelemetry intorno a un client di chat di esempio.

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

Middleware IChatClient personalizzato

Per aggiungere altre funzionalità, è possibile implementare IChatClient direttamente o usare la classe DelegatingChatClient. Questa classe funge da base per la creazione di client di chat che delegano le operazioni a un'altra istanza di IChatClient. Semplifica il concatenamento di più client, consentendo alle chiamate di passare attraverso un client sottostante.

La classe DelegatingChatClient fornisce implementazioni predefinite per metodi come GetResponseAsync, GetStreamingResponseAsynce Dispose, che inoltrano le chiamate al client interno. Una classe derivata può quindi eseguire l'override solo dei metodi necessari per aumentare il comportamento, delegando altre chiamate all'implementazione di base. Questo approccio è utile per creare client di chat flessibili e modulari facili da estendere e comporre.

Di seguito è riportata una classe di esempio derivata da DelegatingChatClient che usa la libreria System.Threading.RateLimiting per fornire funzionalità di limitazione della frequenza.

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

Come per altre IChatClient implementazioni, RateLimitingChatClient può essere composto:

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

Per semplificare la composizione di tali componenti con altri componenti, gli autori di componenti devono creare un metodo di estensione Use* per registrare il componente in una pipeline. Si consideri ad esempio il metodo di estensione seguente UseRatingLimiting :

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

Tali estensioni possono anche richiedere i servizi pertinenti dal contenitore DI (Dependency Injection); il IServiceProvider usato dalla pipeline viene passato come parametro facoltativo.

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

Ora è facile per il consumatore utilizzarlo nella loro pipeline, ad esempio:

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

I metodi di estensione precedenti illustrano l'uso di un metodo Use su ChatClientBuilder. ChatClientBuilder fornisce anche Use overload che facilitano la scrittura di gestori delegati. Nell'esempio precedente RateLimitingChatClient, gli override di GetResponseAsync e GetStreamingResponseAsync devono solo eseguire operazioni prima e dopo la delega al client successivo nella pipeline. Per ottenere la stessa operazione senza scrivere una classe personalizzata, è possibile usare un overload di Use che accetta un delegato usato sia per GetResponseAsync che per GetStreamingResponseAsync, riducendo il boilerplate necessario:

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

Per gli scenari in cui è necessaria un'implementazione diversa per GetResponseAsync e GetStreamingResponseAsync per gestire i relativi tipi restituiti univoci, è possibile usare l'overload Use(Func<IEnumerable<ChatMessage>,ChatOptions,IChatClient,CancellationToken, Task<ChatResponse>>, Func<IEnumerable<ChatMessage>,ChatOptions, IChatClient,CancellationToken,IAsyncEnumerable<ChatResponseUpdate>>) che accetta un delegato per ognuno di essi.

Iniezione delle dipendenze

IChatClientle implementazioni vengono spesso fornite a un'applicazione tramite iniezione delle dipendenze (DI). In questo esempio, un IDistributedCache viene aggiunto al contenitore DI, così come un IChatClient. La registrazione per IChatClient usa un generatore che crea una pipeline contenente un client di memorizzazione nella cache (che usa quindi un IDistributedCache oggetto recuperato dall'inserimento di dipendenze) e il client di esempio. Il IChatClient inserito può essere recuperato e usato altrove nell'app.

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

L'istanza e la configurazione inserita possono variare in base alle esigenze correnti dell'applicazione e possono essere inserite più pipeline con chiavi diverse.

Client senza stato e con stato

I servizi senza stato richiedono l'invio di tutta la cronologia di conversazione pertinente a ogni richiesta. Al contrario, i servizi con stato tengono traccia della cronologia e richiedono solo messaggi aggiuntivi da inviare con una richiesta. L'interfaccia IChatClient è progettata per gestire i servizi di intelligenza artificiale senza stato e con stato.

Quando si lavora con un servizio senza stato, i chiamanti mantengono un elenco di tutti i messaggi. Aggiungono tutti i messaggi di risposta ricevuti e forniscono nuovamente l'elenco alle interazioni successive.

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

Per i servizi con stato, è possibile conoscere già l'identificatore usato per la conversazione pertinente. È possibile inserire tale identificatore in ChatOptions.ConversationId. L'utilizzo segue quindi lo stesso modello, ad eccezione del fatto che non è necessario gestire manualmente una cronologia.

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

Alcuni servizi possono supportare la creazione automatica di un ID conversazione per una richiesta che non ha uno o la creazione di un nuovo ID conversazione che rappresenta lo stato corrente della conversazione dopo aver incorporato l'ultimo round di messaggi. In questi casi, è possibile trasferire ChatResponse.ConversationId su ChatOptions.ConversationId per le richieste successive. Per esempio:

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

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

    options.ConversationId = response.ConversationId;
}

Se non si sa in anticipo se il servizio è senza stato o con stato, è possibile controllare la risposta ConversationId e agire in base al relativo valore. Se è impostato, tale valore viene propagato alle opzioni e la cronologia viene cancellata in modo da non inviare nuovamente la stessa cronologia. Se la risposta ConversationId non è impostata, il messaggio di risposta viene aggiunto alla cronologia in modo che venga inviato al servizio al turno successivo.

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

Interfaccia IEmbeddingGenerator

L'interfaccia IEmbeddingGenerator<TInput,TEmbedding> rappresenta un generatore generico di incorporamenti. Per i parametri di tipo generico, TInput è il tipo di valori di input incorporati ed TEmbedding è il tipo di incorporamento generato, che eredita dalla Embedding classe .

La classe Embedding funge da classe base per gli incorporamenti generati da un IEmbeddingGenerator. È progettato per archiviare e gestire i metadati e i dati associati agli incorporamenti. I tipi derivati, ad esempio Embedding<T>, forniscono i dati vettoriali di incorporamento concreti. Ad esempio, un oggetto Embedding<float> espone una ReadOnlyMemory<float> Vector { get; } proprietà per l'accesso ai dati di incorporamento.

L'interfaccia IEmbeddingGenerator definisce un metodo per generare in modo asincrono incorporamenti per una raccolta di valori di input, con supporto facoltativo per la configurazione e l'annullamento. Fornisce inoltre metadati che descrivono il generatore e consente il recupero di servizi fortemente tipizzati che possono essere forniti dal generatore o dai suoi servizi di base.

La maggior parte degli utenti non deve implementare l'interfaccia IEmbeddingGenerator . Tuttavia, se si è un autore della libreria, è possibile visualizzare una semplice implementazione in Implementazioni di esempio di IChatClient e IEmbeddingGenerator.

Creare incorporamenti

L'operazione primaria eseguita con un IEmbeddingGenerator<TInput,TEmbedding> è la generazione di incorporamento, che viene eseguita con il relativo metodo 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()));
}

Esistono anche metodi di estensione dell'acceleratore per semplificare i casi comuni, ad esempio la generazione di un vettore di incorporamento da un singolo input.

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

Flussi operativi

Come nel caso di IChatClient, le implementazioni di IEmbeddingGenerator possono essere stratificate. Microsoft.Extensions.AI fornisce un'implementazione di delega per IEmbeddingGenerator la memorizzazione nella cache e la telemetria.

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

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

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

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

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

Il IEmbeddingGenerator consente di creare middleware personalizzato che estende la funzionalità di un IEmbeddingGenerator. La classe DelegatingEmbeddingGenerator<TInput,TEmbedding> è un'implementazione dell'interfaccia IEmbeddingGenerator<TInput, TEmbedding> che funge da classe di base per la creazione di generatori di incorporamento che delegano le operazioni a un'altra istanza di IEmbeddingGenerator<TInput, TEmbedding>. Consente di concatenare più generatori in qualsiasi ordine, passando chiamate a un generatore sottostante. La classe fornisce implementazioni predefinite per metodi quali GenerateAsync e Dispose, che inoltrano le chiamate all'istanza del generatore interno, abilitando la generazione di incorporamenti flessibili e modulari.

Ecco un esempio di implementazione di un generatore delegante di embedding che limita la frequenza delle richieste di generazione di embedding.

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

Questo può quindi essere stratificato intorno a un arbitrario IEmbeddingGenerator<string, Embedding<float>> per limitare il tasso di tutte le operazioni di generazione di incorporazione.

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

In questo modo, RateLimitingEmbeddingGenerator può essere composto con altre IEmbeddingGenerator<string, Embedding<float>> istanze per fornire funzionalità di limitazione della velocità.

Compilazione con Microsoft.Extensions.AI

È possibile iniziare a creare con Microsoft.Extensions.AI nei modi seguenti:

  • Gli sviluppatori di librerie: se si possiedono librerie che forniscono client per i servizi di intelligenza artificiale, è consigliabile implementare le interfacce nelle librerie. In questo modo gli utenti possono integrare facilmente il pacchetto NuGet tramite le astrazioni. Per le implementazioni di esempio, vedere Implementazioni di esempio di IChatClient e IEmbeddingGenerator.
  • Consumatori di servizi: se stai sviluppando librerie che utilizzano servizi di intelligenza artificiale, utilizzare le astrazioni invece di eseguire l'hardcoding per un servizio di intelligenza artificiale specifico. Questo approccio offre ai consumatori la flessibilità necessaria per scegliere il proprio provider preferito.
  • gli sviluppatori di applicazioni: usare le astrazioni per semplificare l'integrazione nelle app. Ciò consente la portabilità tra modelli e servizi, facilita il test e la simulazione, sfrutta il middleware fornito dall'ecosistema e mantiene un'API coerente in tutta l'app, anche se si usano servizi diversi in parti diverse dell'applicazione.
  • collaboratori dell'ecosistema: se siete interessati a contribuire all'ecosistema, potreste scrivere componenti middleware personalizzati.

Per altri esempi, vedere il repository GitHub dotnet/ai-samples . Per un esempio end-to-end, vedere eShopSupport.

Vedere anche