Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Интерфейс IChatClient определяет абстракцию клиента, отвечающую за взаимодействие со службами ИИ, предоставляющими возможности чата. Он включает методы отправки и получения сообщений с мультимодальным содержимым (например, текстом, изображениями и звуком), либо как полный набор, либо передача потоком. Кроме того, он позволяет получать строго типизированные службы, предоставляемые клиентом или его базовыми службами.
Библиотеки .NET, предоставляющие клиентам языковые модели и службы, могут предоставлять реализацию IChatClient интерфейса. Затем любые потребители интерфейса могут легко взаимодействовать с этими моделями и службами через абстракции. Примеры см. в разделе "Примеры реализации ".
Запросить ответ в чате
С помощью экземпляра IChatClientможно вызвать IChatClient.GetResponseAsync метод для отправки запроса и получения ответа. Запрос состоит из одного или нескольких сообщений, каждый из которых состоит из одного или нескольких частей содержимого. Методы акселератора существуют для упрощения распространенных случаев, таких как создание запроса для одного фрагмента текстового содержимого.
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?"));
Основной метод IChatClient.GetResponseAsync принимает список сообщений. Этот список представляет историю всех сообщений, входящих в беседу.
Console.WriteLine(await client.GetResponseAsync(
[
new(ChatRole.System, "You are a helpful AI assistant"),
new(ChatRole.User, "What is AI?"),
]));
Возвращаемый из ChatResponseGetResponseAsync предоставляет список экземпляров ChatMessage, представляющих одно или несколько сообщений, созданных в рамках операции. В распространенных случаях существует только одно сообщение ответа, но в некоторых ситуациях может быть несколько сообщений. Список сообщений упорядочивается таким образом, чтобы последнее сообщение в списке представляло окончательное сообщение запросу. Чтобы предоставить все эти ответные сообщения обратно в службу в последующем запросе, можно добавить сообщения из ответа обратно в список сообщений.
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);
}
Запросить ответ в потоковом чате.
Входные данные для IChatClient.GetStreamingResponseAsync идентичны GetResponseAsync. Однако вместо возврата полного ответа в рамках объекта ChatResponse метод возвращает IAsyncEnumerable<T>, где TChatResponseUpdate, предоставляя поток обновлений, которые коллективно формируют единый ответ.
await foreach (ChatResponseUpdate update in client.GetStreamingResponseAsync("What is AI?"))
{
Console.Write(update);
}
Подсказка
API потоковой передачи почти синонимы пользовательского интерфейса искусственного интеллекта. C# обеспечивает убедительные сценарии с поддержкой IAsyncEnumerable<T>, что позволяет обеспечить естественный и эффективный способ потоковой передачи данных.
Как и в случае с GetResponseAsync, можно добавить обновления из IChatClient.GetStreamingResponseAsync к списку сообщений. Так как обновления являются отдельными частями ответа, вы можете использовать вспомогательные средства, такие как ToChatResponse(IEnumerable<ChatResponseUpdate>), чтобы составлять одно или несколько обновлений в одном экземпляре ChatResponse.
Вспомогательные функции, такие как AddMessages, компонуют ChatResponse, затем извлекают составленные сообщения из ответа и добавляют их в список.
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(chatHistory))
{
Console.Write(update);
updates.Add(update);
}
Console.WriteLine();
chatHistory.AddMessages(updates);
}
Запуск инструмента
Некоторые модели и службы поддерживают вызов средства. Чтобы собрать дополнительные сведения, можно настроить ChatOptions с информацией о средствах (обычно методах .NET), чтобы модель могла запрашивать их выполнение клиентом. Вместо отправки окончательного ответа модель запрашивает вызов функции с определенными аргументами. Затем клиент вызывает функцию и отправляет результаты обратно в модель с журналом бесед. Библиотека Microsoft.Extensions.AI.Abstractions включает абстракции для различных типов контента сообщений, включая запросы и результаты вызова функции. Хотя пользователи могут напрямую взаимодействовать с этим содержимым, IChatClient предоставляет вспомогательные средства, которые могут автоматически вызывать инструменты в ответ на соответствующие запросы. Библиотеки Microsoft.Extensions.AI.Abstractions и Microsoft.Extensions.AI предоставляют следующие типы:
- AIFunction: представляет функцию, которую можно описать в модели ИИ и вызвать.
-
AIFunctionFactory: предоставляет методы для создания фабричных объектов, представляющих методы .NET
AIFunction. -
FunctionInvokingChatClient: Оборачивает
IChatClientв другойIChatClient, добавляющий возможности автоматического вызова функций.
В следующем примере показано вызов случайной функции (этот пример зависит от 📦 пакета 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);
}
Предыдущий код:
- Определяет функцию с именем
GetCurrentWeather, которая возвращает случайный прогноз погоды. - Создает экземпляр ChatClientBuilder, используя
OllamaSharp.OllamaApiClient, и настраивает его для использования вызова функции. - Вызывает
GetStreamingResponseAsyncна клиенте, передавая подсказку и список инструментов, включающих функцию, созданную с Create. - Выполняет итерацию по ответу, выводя каждое обновление на консоль.
Дополнительные сведения о создании функций ИИ см. в разделе "Доступ к данным" в функциях ИИ.
С помощью средств протокола контекста модели (MCP) можно также использовать IChatClient. Дополнительные сведения см. в разделе "Создание минимального клиента MCP".
Ответы из кэша
Если вы знакомы с кэшированием в .NET, хорошо знать, что Microsoft.Extensions.AI обеспечивает делегирование IChatClient реализаций для кэширования.
DistributedCachingChatClient — это IChatClient, которая накладывает кэширование на другой произвольный экземпляр IChatClient. Когда журнальная запись нового чата передается в DistributedCachingChatClient, она пересылается базовому клиенту, затем ответ кэшируется и отправляется обратно пользователю. При следующей отправке такого же запроса, когда можно будет найти кэшированный ответ в кэше, DistributedCachingChatClient возвращает кэшированный ответ, вместо переадресации запроса по конвейеру.
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();
}
Этот пример зависит от 📦 пакета NuGet Microsoft.Extensions.Caching.Memory . Дополнительные сведения см. в разделе Кэширование в.NET.
Использование телеметрии
Еще одним примером делегированного клиента чата является OpenTelemetryChatClient. Эта реализация соответствует семантическим конвенциям OpenTelemetry для генеративных систем ИИ. Аналогично другим IChatClient делегаторам, он добавляет слои метрик и окружает произвольные 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);
(Предыдущий пример зависит от 📦 пакета NuGet OpenTelemetry.Exporter.Console .)
Кроме того, LoggingChatClient и соответствующий UseLogging(ChatClientBuilder, ILoggerFactory, Action<LoggingChatClient>) метод предоставляют простой способ записи записей журнала для ILogger каждого запроса и ответа.
Предоставление параметров
Каждый вызов GetResponseAsync или GetStreamingResponseAsync может опционально включать экземпляр ChatOptions, содержащий дополнительные параметры для операции. Наиболее распространенные параметры моделей и служб ИИ отображаются как строго типизированные свойства типа, например ChatOptions.Temperature. Другие параметры могут быть предоставлены по имени в слабо типизированном виде через ChatOptions.AdditionalProperties словарь или через экземпляр параметров, который базовый поставщик понимает, используя ChatOptions.RawRepresentationFactory свойство.
Кроме того, можно указать параметры при создании IChatClient при помощи API ChatClientBuilder, использующего fluent-интерфейс, связав вызов метода расширения ConfigureOptions(ChatClientBuilder, Action<ChatOptions>). Этот клиент делегирования оборачивает другого клиента и вызывает предоставленный делегат, чтобы заполнить экземпляр ChatOptions при каждом вызове. Например, чтобы убедиться, что свойство ChatOptions.ModelId по умолчанию имеет определенное имя модели, можно использовать следующий код:
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" }));
Цепочки функциональности
IChatClient экземпляры можно располагать слоями, чтобы создать конвейер из компонентов, каждый из которых добавляет дополнительную функциональность. Эти компоненты могут поступать из Microsoft.Extensions.AI, других пакетов NuGet или пользовательских реализаций. Этот подход позволяет расширить поведение IChatClient различными способами для удовлетворения конкретных потребностей. Рассмотрим следующий фрагмент кода, который накладывает уровни распределенного кэша, вызова функций и трассировки OpenTelemetry на пример клиента чата.
// 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();
Настраиваемое промежуточное программное обеспечение IChatClient
Чтобы добавить дополнительные функциональные возможности, можно реализовать IChatClient напрямую или использовать класс DelegatingChatClient. Этот класс служит основой для создания клиентов чата, которые делегируют операции другому экземпляру класса IChatClient. Это упрощает цепочку нескольких клиентов, что позволяет передавать вызовы базовому клиенту.
Класс DelegatingChatClient предоставляет реализации по умолчанию для таких методов, как GetResponseAsync, GetStreamingResponseAsyncи Dispose, которые перенаправяют вызовы внутреннего клиента. Затем производный класс может переопределить только те методы, поведение которых необходимо расширить, при этом делегируя другие вызовы базовой реализации. Этот подход полезен для создания гибких и модульных клиентов чата, которые легко расширить и создать.
Ниже приведен пример класса, производный от DelegatingChatClient, который использует библиотеку System.Threading.RateLimiting для предоставления функциональности ограничения скорости.
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);
}
}
Как и в других IChatClient реализациях, RateLimitingChatClient можно компоновать.
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?"));
Чтобы упростить интеграцию таких компонентов с другими, разработчики компонентов должны создавать расширяющий метод Use* для регистрации компонента в конвейере. Например, рассмотрим следующий UseRateLimiting метод расширения:
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)
);
}
Такие расширения также могут запрашивать соответствующие службы из контейнера DI; IServiceProvider, используемый конвейером, передается как необязательный параметр:
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>())
);
}
Теперь потребителю легко использовать это в конвейере, например:
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));
Предыдущие методы расширения демонстрируют использование метода Use на ChatClientBuilder.
ChatClientBuilder также предоставляет Use перегрузки, которые упрощают создание таких делегирующих обработчиков. Например, в предыдущем RateLimitingChatClient примере переопределения для GetResponseAsync и GetStreamingResponseAsync необходимо выполнить только те действия, которые выполняются до и после делегирования следующему клиенту в конвейере. Чтобы добиться того же, не создавая пользовательский класс, можно использовать перегрузку Use, которая принимает делегат, используемый как для GetResponseAsync, так и для GetStreamingResponseAsync, уменьшая объём шаблонного кода.
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();
В ситуациях, когда для GetResponseAsync и GetStreamingResponseAsync требуется разная реализация для обработки их уникальных типов возвращаемых значений, можно использовать перегрузку Use(Func<IEnumerable<ChatMessage>,ChatOptions,IChatClient,CancellationToken,
Task<ChatResponse>>, Func<IEnumerable<ChatMessage>,ChatOptions,
IChatClient,CancellationToken,IAsyncEnumerable<ChatResponseUpdate>>), принимающую делегат для каждого из них.
Внедрение зависимостей
IChatClientреализации часто предоставляются приложению с помощью внедрения зависимостей (DI). В следующем примере в контейнер DI добавляется IDistributedCache, как и IChatClient. Регистрация IChatClient использует конструктор, создающий конвейер, содержащий клиент кэширования (который затем использует извлеченный IDistributedCache из DI) и образец клиента. Внедренный IChatClient можно получить и использовать в другом месте в приложении.
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?"));
Какие экземпляры и конфигурации внедряются, могут отличаться в зависимости от текущих потребностей приложения, а несколько конвейеров можно внедрить с разными ключами.
Клиенты без состояния и клиенты с состоянием
Службы без отслеживания состояния требуют отправки всех соответствующих журналов бесед по каждому запросу. Напротив, сервисы с сохранением состояния хранят историю и требуют отправки только дополнительных сообщений с запросом. Интерфейс IChatClient предназначен для обеспечения поддержки как бесстатусных, так и с состоянием служб искусственного интеллекта.
При работе со службой без состояния пользователи поддерживают список всех сообщений. Они включают все полученные сообщения ответа и предоставляют список при последующих взаимодействиях.
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);
}
Для служб с отслеживанием состояния может быть уже известно идентификатор, используемый для соответствующей беседы. Этот идентификатор можно поместить в ChatOptions.ConversationId. Затем использование следует той же схеме, за исключением необходимости вручную вести историю.
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));
}
Некоторые службы могут поддерживать автоматическое создание идентификатора беседы для запроса, который не имеет одного, или создание нового идентификатора беседы, представляющего текущее состояние беседы после включения последнего раунда сообщений. В таких случаях можно передать ChatResponse.ConversationId на ChatOptions.ConversationId для последующих запросов. Рассмотрим пример.
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;
}
Если вы не знаете заранее, является ли служба бессостоянием или с состоянием, можно проверить ответ ConversationId и принять меры на основе его значения. Если установлено, это значение распространяется на опции и история очищается, чтобы не отправить её повторно. Если ответ ConversationId не установлен, то сообщение ответа добавляется в журнал, чтобы оно было отправлено обратно в службу при следующем обращении.
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);
}
}
Примеры реализации
Следующий пример реализует IChatClient для отображения общей структуры.
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<ChatResponse> GetResponseAsync(
IEnumerable<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(
ChatRole.Assistant,
responses[Random.Shared.Next(responses.Length)]
));
}
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
IEnumerable<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 ChatResponseUpdate(ChatRole.Assistant, 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() { }
}
Для более реалистичных и конкретных реализаций IChatClient, см. следующие примеры:
Сокращение чата (экспериментальное)
Это важно
Эта функция является экспериментальной и подвержена изменению.
Сокращение чата помогает управлять журналом бесед, ограничивая количество сообщений или суммируя старые сообщения, когда беседа превышает указанную длину. Библиотека Microsoft.Extensions.AI предоставляет сокращения, такие как MessageCountingChatReducer это ограничивает количество несистемных сообщений, и SummarizingChatReducer автоматически суммирует старые сообщения при сохранении контекста.