Форум программистов, компьютерный форум, киберфорум
stackOverflow
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  

Изучаем новый шаблон ИИ-чата .NET AI Chat Web App

Запись от stackOverflow размещена 10.07.2025 в 21:37
Показов 2570 Комментарии 0

Нажмите на изображение для увеличения
Название: Изучаем новый шаблон ИИ-чата .NET AI Chat Web App.jpg
Просмотров: 279
Размер:	215.5 Кб
ID:	10974
В .NET появилось интересное обновление - новый шаблон ИИ-чата под названием .NET AI Chat Web App. Когда я впервые наткнулся на анонс этого шаблона, то сразу понял, что Microsoft наконец-то среагировала на взрывной рост популярности чат-интерфейсов после успеха ChatGPT.

Суть шаблона проста - он предоставляет готовый каркас для создания веб-приложения с чат-интерфейсом, работающим на основе больших языковых моделей (LLM). Как разработчик, который не раз сталкивался с необходимостью интеграции ИИ в свои проекты, могу сказать, что появление подобного шаблона - отличная новость. Раньше приходилось собирать подобные решения буквально по кирпичикам, а теперь у нас есть структурированный подход.

Почему Microsoft вообще решила создать такой шаблон? Думаю, причин несколько. Во-первых, нельзя отрицать, что ChatGPT и подобные ему системы произвели настоящую революцию в представлении людей о том, каким должно быть взаимодействие с компьютером. Чат-интерфейсы стали не просто модной фишкой, а реальным инструментом решения задач. Во-вторых, Microsoft активно инвестирует в OpenAI и другие ИИ-технологии, так что им выгодно облегчать разработчикам доступ к этим инструментам.

Шаблон позволяет быстро выбрать провайдера языковой модели (GitHub Models, OpenAI, Azure OpenAI или Ollama для локального запуска) и хранилище векторных вложений (локальное на основе JSON, Azure AI Search или Qdrant в Docker-контейнере). Это дает гибкость при выборе инфраструктуры - можно начать с бесплатных или локальных решений, а потом легко перейти на более мощные облачные сервисы. Любопытно, что шаблон использует технику RAG (Retrieval-Augmented Generation) - это когда ИИ не просто генерирует ответы из своей "внутренней памяти", а опирается на конкретные документы, которые мы ему предоставляем. Такой подход позволяет создавать более надежные и контролируемые чат-системы, которые не будут выдумывать факты.

Первое впечатление: устанавливаем и запускаем приложение



Итак, давайте установим этот новый шаблон. Процесс оказался проще, чем я ожидал - достаточно выполнить одну команду в терминале:

Bash
1
dotnet new install Microsoft.Extensions.AI.Templates
После выполнения этой команды шаблон устанавливается, и в консоли появляется сообщение об успешной установке, а также информация о том, что стал доступен шаблон с коротким именем aichatweb. Помимо этого, в выводе указывается, что шаблон поддерживает C# и использует теги Common/AI/Web/Blazor/.NET Aspire. Теперь самое интересное - выбор конфигурации шаблона. Здесь нам нужно определиться с тремя ключевыми аспектами:

1. Использовать ли Aspire? Я выбрал "да", потому что Aspire - это новый подход Microsoft к разработке облачных приложений, и он значительно упрощает конфигурацию и запуск.

2. Какого провайдера LLM использовать? На выбор предлагаются:
- GitHub Models (бесплатный вариант для разработчиков),
- OpenAI (требует платного аккаунта),
- Azure OpenAI (требует подписку Azure),
- Ollama (локальный запуск на вашей машине).
Я остановился на GitHub Models - это самый быстрый способ начать работу без дополнительных затрат.

3. Какое векторное хранилище использовать для данных:
- Local (простой JSON-файл на диске),
- Azure AI Search (автоматизированное управление данными в облаке),
- Qdrant (векторная база данных в Docker-контейнере).
Для начала я выбрал локальное хранилище - оно идеально подходит для прототипирования.

Создаем проект с помощью команды:

Bash
1
dotnet new aichatweb --output ModernDotNetShowChat --provider githubmodels --vector-store local --aspire true
После выполнения команды в указаной директории появляется полноценное решение, состоящее из нескольких проектов:
  • Веб-проект с приложением чата.
  • Проект "Service Defaults" - рекомендуемая практика для приложений Aspire.
  • Проект "App Host" - хост Aspire, который связывает зависимости.

В корне решения также находится файл README.md с инструкциями по дальнейшей настройке. Для нашей конфигурации нужно только настроить GitHub Models. Чтобы получить токен GitHub Models, я перешел на страницу github.com/marketplace/models, выбрал модель из выпадающего списка, нажал на "Get developer key", создал новый токен без дополнительных разрешений. Обратите внимание, что с мая 2025 года для токенов GitHub Models требуется разрешение model:read, так что будьте внимательны при создании токена.

Полученный токен нужно добавить как строку подключения в проект Aspire AppHost. Я использовал команду:

Bash
1
2
cd ModernDotNetShowChat.AppHost
dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
Где YOUR-API-KEY нужно заменить на полученный токен.

После этого запускаем приложение, выполнив запуск проекта Aspire AppHost. Первый запуск может занять некоторое время, так как приложение выполняет "заглатывание" (ingestion) PDF-файлов, которые по умолчанию находятся в каталоге контента.
И вот, перед нами появляется стандартный интерфейс чата, очень похожий на ChatGPT или GitHub Copilot Chat. Можно задавать вопросы о содержимом загруженных PDF-файлов, и ассистент отвечает на них, ссылаясь на конкретные места в документах.

Конвертирование web app(jquery mobile) в mobile app(Android/IOS)
Я сделал проект(Web Service), написанный на языках C#, JavaScript, HTML, SQL . К нему можно...

Ошибка import App from './app.vue' - net::ERR_ABORTED 404
На строке import App from './app.vue' браузер выдаёт ошибку "net::ERR_ABORTED 404" в файле...

Изучаем ASP.NET
Приветик всем ! Значит о чём это я ... Решил вот (по великому знаку свыше ... пожеланию босса)...

VS 2008, C#: 1 Error. Constructor on type 'App.App.Forms.FormBase' not found. в FormDerived [Design]
и это после того, как добавил в котструктор базовой формы параметр. теперь в design только ее и...


Разбираем структуру проекта и базовые компоненты



Теперь, когда у нас есть рабочее приложение, давайте заглянем под капот и разберем его архитектуру. После создания шаблона у нас получилось решение с тремя основными проектами. Открыв его в IDE, я увидел следующую структуру:

C#
1
2
3
4
ModernDotNetShowChat/
├── ModernDotNetShowChat.AppHost/       # Проект хоста Aspire
├── ModernDotNetShowChat.ServiceDefaults/ # Настройки сервисов по умолчанию 
└── ModernDotNetShowChat.Web/           # Основное веб-приложение
Такая структура - классический подход для приложений на базе Aspire. Если вы впервые сталкиваетесь с Aspire, это архитектурный паттерн от Microsoft для создания современных распределенных приложений с минимумом настроек.
В проекте AppHost определяется общая конфигурация приложения. Это центральное место, где регистрируются все сервисы и их зависимости. Заглянув в Program.cs этого проекта, можно увидеть всю архитектуру на ладони:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
var builder = DistributedApplication.CreateBuilder(args);
 
var openai = builder.AddConnectionString("openai");
 
var ingestionCache = builder.AddSqlite("ingestionCache");
 
var webApp = builder.AddProject<Projects.ModernDotNetShowChat_Web>("aichatweb-app");
webApp.WithReference(openai);
webApp
 .WithReference(ingestionCache)
 .WaitFor(ingestionCache);
 
builder.Build().Run();
Обратите внимание на лаконичность кода! Здесь регистрируются три основных компонента:
1. Строка подключения к OpenAI (GitHub Models).
2. База данных SQLite, которая используется как кэш для обработаных документов.
3. Веб-приложение, которое зависит от предыдущих двух компонентов.

Метод WaitFor() гарантирует, что приложение не стартанет, пока база данных не будет инициализирована. Это простое решение распространенной проблемы с порядком запуска компонентов.

ServiceDefaults - это проект, который содержит общие настройки для всех сервисов в приложении. Обычно тут находятся конфигурации логирования, трассировки, метрик и прочих кросс-функциональных аспектов.

Теперь перейдем к самому интересному - проекту Web. Это Blazor Server приложение, в котором реализована вся логика чат-бота. Ключевые компоненты:

1. Components/Pages/Chat.razor - основной интерфейс чата.
2. Components/Pages/Index.razor - стартовая страница.
3. Data/ - папка с моделями данных и сервисами доступа к данным.
4. wwwroot/Data/ - папка с PDF-файлами для обработки.
5. Services/ - сервисы для работы с ИИ и векторными хранилищами.

Изюминка приложения - это способ, которым оно обрабатывает PDF-документы. Для этого используется класс DataIngestor, который извлекает текст из документов, создает эмбеддинги (векторные представления текста) с помощью языковой модели и сохраняет их в векторном хранилище.

C#
1
2
3
await DataIngestor.IngestDataAsync(
 app.Services,
 new PDFDirectorySource(Path.Combine(builder.Environment.WebRootPath, "Data")));
Эта строка запускает процесс обработки документов при старте приложения. PDFDirectorySource - это класс, который умеет читать PDF-файлы из указаной директории и преобразовывать их в текст. Стоит отметить, что приложение не просто хранит текст целиком, а разбивает его на параграфы и создает эмбеддинги для каждого параграфа. Это позволяет потом эффективно искать релевантные фрагменты текста для ответов на вопросы пользователя.

Весь этот механизм основан на концепции RAG (Retrieval-Augmented Generation), которая стала стандартом для систем, работающих с документами. Суть в том, что мы не полагаемся только на "знания", заложеные в модель, а обогащаем их конкретными данными из наших документов.

Конфигурация приложения через appsettings.json и секреты разработчика



Любое серьезное приложение требует гибкой конфигурации, и наш ИИ-чат не исключение. Разбирая файлы проекта, я обнаружил стандартный для .NET Core подход - использование appsettings.json для хранения настроек. В appsettings.json веб-проекта находятся базовые настройки логирования, но самое интересное - отсутствие каких-либо ключей API или конфиденциальных данных. И это правильно! Никогда не стоит хранить секретные ключи в открытом виде в репозитории - это классическая ошибка безопасности, которую я сам не раз наблюдал в проектах начинающих разработчиков.

Вместо этого шаблон активно использует механизм секретов разработчика (User Secrets). Если вы помните, при настройке GitHub Models мы добавляли ключ API через команду:

Bash
1
dotnet user-secrets set ConnectionStrings:openai "Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
Что происходит за кулисами? Секреты разработчика хранятся вне директории проекта, в специальном зашифрованом файле JSON в профиле пользователя. Для Windows это обычно путь %APPDATA%\Microsoft\UserSecrets\<user_secrets_id>\secrets.json, а для Linux и macOS - ~/.microsoft/usersecrets/<user_secrets_id>/secrets.json. Идентификатор секретов (user_secrets_id) указывается в файле проекта .csproj в элементе <UserSecretsId>. В нашем шаблоне уже настроена эта интеграция, что избавляет нас от лишних телодвижений.

Интересно, что в Aspire-приложении секреты, добавленые в проект AppHost, автоматически передаются в зависимые сервисы. Это можно увидеть в коде AppHost, где строка подключения openai добавляется к веб-приложению:

C#
1
webApp.WithReference(openai);
При развертывании на продакшн обычно используются другие механизмы - переменные среды или системы управления секретами вроде Azure Key Vault. В шаблоне это уже предусмотрено благодаря стандартному подходу .NET к конфигурации - секреты загружаются в тот же объект IConfiguration, что и настройки из appsettings.json, и код приложения работает одинаково независимо от источника данных.

Помимо ключа API для ИИ-сервиса, в конфигурации приложения есть несколько интересных параметров, которые можно настроить через appsettings.json или переменные среды:
  • Настройки для векторного хранилища (путь к файлу JSON для локального хранилища)
  • Конфигурация кэша обработанных документов (строка подключения к SQLite)
  • Параметры логирования и телеметрии

Если вы захотите изменить модель, используемую для генерации эмбеддингов или для чата, это можно сделать в файле Program.cs веб-проекта:

C#
1
2
3
4
5
openai.AddChatClient("gpt-4o-mini") // Модель для чата
.UseFunctionInvocation()
.UseOpenTelemetry(configure: c =>
    c.EnableSensitiveData = builder.Environment.IsDevelopment());
openai.AddEmbeddingGenerator("text-embedding-3-small"); // Модель для эмбеддингов
Такой подход дает гибкость при выборе моделей в зависимости от ваших потребностей и бюджета. Например, для продакшна можно использовать более мощные (и дорогие) модели, а для разработки - более экономичные варианты.

Архитектура решения: как устроен чат изнутри



Заглянув глубже в архитектуру нашего чат-приложения, я обнаружил ряд интересных решений, которые стоит разобрать. В основе шаблона лежит несколько ключевых компонентов, образующих гибкую и расширяемую систему. Веб-приложение, созданное по шаблону, представляет собой Blazor Server, но с особым акцентом на интеграцию с искуственным интеллектом. Если разбить его на функциональные части, получится примерно такая картина:

1. Клиент чата для ИИ-моделей - обертка над API провайдера, которая абстрагирует работу с конкретной моделью и предоставляет унифицированный интерфейс для общения с ней. В нашем случае это GitHub Models, но архитектура позволяет легко заменить его на другого провайдера.
2. Генератор эмбеддингов - компонент, преобразующий текст в числовые векторы, которые отражают семантическое значение текста. Это критически важная часть системы, так как именно эмбеддинги позволяют искать релевантные фрагменты текста при ответе на вопросы.
3. Векторное хранилище - отвечает за хранение и поиск по эмбеддингам. В случае локального хранилища это просто JSON-файл, но интерфейс IVectorStore позволяет подключить любую другую реализацию.
4. Кэш обработки данных - реализованный через EF Core `DbContext`, который отслеживает, какие документы были обработаны и превращены в эмбеддинги. Это позволяет избежать повторной обработки и ускоряет перезапуск приложения.
5. Инжестор данных - компонент, который извлекает текст из документов, создает эмбеддинги и сохраняет их в векторном хранилище. Его особенность в том, что он работает с абстракцией IIngestionSource, которая может быть реализована для разных типов документов.
6. Компонент семантического поиска - связующее звено между пользовательским запросом и хранилищем эмбеддингов. Он преобразует вопрос пользователя в эмбеддинг и ищет наиболее релевантные фрагменты в векторном хранилище.
7. Пользовательский интерфейс чата - Blazor-компонент, который отображает сообщения и обрабатывает ввод пользователя.

Ключевая особенность архитектуры - это то, как организовано взаимодействие с языковой моделью. Вместо того чтобы позволить модели самостоятельно генерировать ответы, шаблон реализует подход RAG, где модель получает доступ к локальной базе знаний через специальную функцию поиска.

Анализ основных классов и их взаимодействия



Погружаясь в исходный код шаблона, я выделил несколько ключевых классов, которые формируют его ядро. Понимание их взаимодействия даст нам полную картину того, как работает приложение.

Начнем с класса DataIngestor - он стоит в сердце процесса подготовки данных. Его конструктор принимает четыре зависимости:

C#
1
2
3
4
5
6
7
8
public class DataIngestor(
    ILogger<DataIngestor> logger,
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    IVectorStore vectorStore,
    IngestionCacheDbContext ingestionCacheDb)
{
    // Реализация методов
}
Заметьте, как используется неявная инициализация полей через параметры конструктора - этот синтаксис появился в C# 12 и существенно упрощает код.

Основной метод класса - IngestDataAsync, который принимает источник данных (IIngestionSource) и выполняет всю работу по обработке документов. Здесь прослеживается четкое разделение ответственности: DataIngestor не знает, откуда берутся документы - он просто знает, как их обработать. Сам источник данных представлен интерфейсом IIngestionSource, который определяет методы для получения списка документов и создания записей для них. В шаблоне есть реализация PDFDirectorySource, которая умеет читать PDF-файлы, но вы можете создать свою реализацию для любого другого типа данных.

Особый интерес представляет класс SemanticSearch, который используется для поиска релевантных фрагментов текста:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public class SemanticSearch(
    IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
    IVectorStore vectorStore)
{
    public async Task<IReadOnlyList<SemanticSearchRecord>> SearchAsync(
        string searchPhrase, 
        string? filenameFilter = null, 
        int maxResults = 5)
    {
        // Реализация поиска
    }
}
Этот класс связывает воедино векторное хранилище и генератор эмбеддингов. Он преобразует поисковую фразу в эмбеддинг и ищет похожие эмбеддинги в хранилище, используя косинусное сходство векторов. Интерфейс IVectorStore абстрагирует работу с хранилищем векторов и определяет операции для получения "коллекций" векторов, их создания, удаления и поиска. В шаблоне есть реализация JsonVectorStore, которая хранит векторы в JSON-файле, но в реальных проектах вы, скорее всего, захотите использовать специализированую векторную базу данных.

Отдельно стоит отметить класс IngestionCacheDbContext - это стандартный EF Core `DbContext`, который хранит метаданные об обработаных документах. Его структура проста:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class IngestionCacheDbContext : DbContext
{
    public DbSet<IngestedDocument> Documents { get; set; } = null!;
    
    // Конфигурация контекста
}
 
public class IngestedDocument
{
    public string Id { get; set; } = "";
    public string SourceId { get; set; } = "";
    public List<IngestedRecord> Records { get; set; } = new();
}
 
public class IngestedRecord
{
    public string Id { get; set; } = "";
    public string DocumentId { get; set; } = "";
}
Такая структура позволяет эффективно отслеживать, какие документы были обработаны и какие записи для них созданы в векторном хранилище. Фронтенд-часть представлена компонентом Chat.razor, который взаимодействует с IChatClient для отправки сообщений и получения ответов. Интересно, что этот компонент настраивает языковую модель на использование специальной функции поиска:

C#
1
chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)];

Паттерны проектирования в действии



Изучая исходный код шаблона .NET AI Chat Web App, я обнаружил ряд классических паттернов проектирования, которые делают архитектуру гибкой и расширяемой. Microsoft явно уделила внимание не только функциональности, но и качеству кода, что в долгосрочной перспективе критически важно для любого проекта.

Первый и самый очевидный паттерн - Внедрение зависимостей (Dependency Injection). Он пронизывает весь шаблон и является краеугольным камнем архитектуры. Взгляните на конструктор DataIngestor:

C#
1
2
3
4
5
public class DataIngestor(
   ILogger<DataIngestor> logger,
   IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator,
   IVectorStore vectorStore,
   IngestionCacheDbContext ingestionCacheDb) { /*...*/ }
Все зависимости передаются извне, что делает класс тестируемым и упрощает замену компонентов. Это особенно важно в контексте ИИ-приложений, где вы можете захотеть переключиться между разными провайдерами или моделями.

Следующий паттерн, который сразу бросается в глаза - Стратегия (Strategy). Он реализован через интерфейсы IIngestionSource, IVectorStore и IEmbeddingGenerator. Каждый из этих интерфейсов может иметь несколько реализаций, и клиентский код не зависит от конкретной реализации. Например, для извлечения данных из документов шаблон использует PDFDirectorySource, но вы легко можете создать свою реализацию для работы с веб-страницами, базами данных или любыми другими источниками. Аналогично, JsonVectorStore можно заменить на QdrantVectorStore или AzureAISearchVectorStore без изменения остального кода.

Мне также понравилось использование паттерна Репозиторий (Repository) для работы с векторным хранилищем. Интерфейс IVectorStore абстрагирует операции CRUD и поиска, скрывая детали реализации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IVectorStore
{
    IVectorCollection<TKey, TValue> GetCollection<TKey, TValue>(string name);
}
 
public interface IVectorCollection<TKey, TValue>
{
    Task CreateCollectionIfNotExistsAsync();
    Task DeleteCollectionAsync();
    Task<TKey> UpsertAsync(TValue value);
    IAsyncEnumerable<TKey> UpsertBatchAsync(IEnumerable<KeyValuePair<TKey, TValue>> values);
    Task DeleteAsync(TKey key);
    Task DeleteBatchAsync(IEnumerable<TKey> keys);
    Task<KeyValuePair<TKey, float>[]> SearchAsync(ReadOnlyMemory<float> vectorQuery, int limit = 1);
}
Еще один интересный паттерн - Команда (Command) в сочетании с Фабричным методом (Factory Method). Когда ИИ-модель нуждается в дополнительных функциях, шаблон использует AIFunctionFactory.Create() для создания функций, которые может вызывать ИИ:

C#
1
chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)];
Этот подход позволяет динамически расширять возможности ИИ-модели, добавляя новые функции без изменения основного кода.

В шаблоне также прослеживается паттерн Медиатор (Mediator), где Chat.razor выступает в роли центрального компонента, координирующего взаимодействие между пользовательским интерфейсом и различными сервисами.

С точки зрения архитектурных паттернов, шаблон явно следует принципу Разделения ответственности (Separation of Concerns). Каждый компонент имеет четко определеную роль:
DataIngestor отвечает за обработку документов,
SemanticSearch занимается поиском релевантных фрагментов,
IngestionCacheDbContext отслеживает состояние обработки,
Chat.razor управляет пользовательским интерфейсом,

Такое разделение делает код понятным и упрощает его поддержку.

Интересно, что в шаблоне также прослеживается подход, похожий на CQRS (Command Query Responsibility Segregation) - разделение операций чтения и записи. Запись (обработка документов) происходит асинхронно при старте приложения, а чтение (поиск релевантных фрагментов) - во время обработки запросов пользователя. Отдельно хочу отметить использование Unit of Work в связке с Entity Framework Core для отслеживания обработаных документов. Это позволяет эффективно управлять транзакциями и сохранять согласованность данных.

Все эти паттерны вместе создают гибкую и расширяемую архитектуру, которая может адаптироваться к различным сценариям использования. Радует, что Microsoft не пошла по пути простейшего решения, а создала действительно продуманный шаблон, который может служить основой для серьезных проектов.

Интеграция с AI-сервисами: настройка и конфигурация



Настройка интеграции с AI-сервисами оказалась одним из самых интересных аспектов работы с шаблоном. Микрософт тут действительно постаралась создать унифицированную абстракцию для взаимодействия с различными провайдерами ИИ.

В файле Program.cs веб-проекта можно найти основную конфигурацию для работы с провайдером искуственного интеллекта. Для GitHub Models (который мы выбрали в нашем примере) настройка выглядит следующим образом:

C#
1
2
3
4
5
6
var openai = builder.AddAzureOpenAIClient("openai");
openai.AddChatClient("gpt-4o-mini")
    .UseFunctionInvocation()
    .UseOpenTelemetry(configure: c => 
        c.EnableSensitiveData = builder.Environment.IsDevelopment());
openai.AddEmbeddingGenerator("text-embedding-3-small");
Обратите внимание, что несмотря на использование GitHub Models, метод называется AddAzureOpenAIClient. Это связано с тем, что GitHub Models внутри работает через Azure OpenAI Service, просто предоставляет к нему бесплатный доступ для разработчиков.

Интересная особенность - настройка модели разделена на две части: клиент чата (AddChatClient) и генератор эмбеддингов (AddEmbeddingGenerator). Это дает возможность использовать разные модели для разных задач, что может быть критично с точки зрения оптимизации затрат и производительности. Метод UseFunctionInvocation() особенно заинтересовал меня. Он активирует возможность вызова функций из языковой модели - то есть модель может не просто генерировать текст, а выполнять определенные действия в вашем приложении. В нашем случае это функция поиска по обработаным документам.

Если вы захотите использовать другого провайдера, настройка будет немного отличаться. Например, для "классического" OpenAI:

C#
1
2
3
4
var openai = builder.AddOpenAIClient("openai");
openai.AddChatClient("gpt-4o")
    .UseFunctionInvocation();
openai.AddEmbeddingGenerator("text-embedding-3-large");
А для локального Ollama:

C#
1
2
3
4
var ollama = builder.AddOllamaClient("ollama");
ollama.AddChatClient("llama3.2")
    .UseFunctionInvocation();
ollama.AddEmbeddingGenerator("all-minilm");
Модель взаимодействия с AI-сервисами построена на базе сервисов Microsoft.Extensions.AI, которые предоставляют абстракцию над различными провайдерами. Это позволяет легко переключаться между разными сервисами без существенных изменений в коде.

Важно понимать, что при использовании разных провайдеров меняются не только строки подключения, но и доступные модели. Например, если вы используете Ollama, вам доступны только те модели, которые вы скачали локально. А при использовании Azure OpenAI, вам нужно сначала развернуть модели в вашем ресурсе Azure. Внутри шаблона все эти различия скрыты за единым интерфейсом IChatClient, который обеспечивает унифицированный способ взаимодействия с разными моделями. Это прекрасный пример паттерна Стратегия в действии.

Работа с OpenAI API и альтернативными провайдерами



Глубже погрузившись в механизмы работы с различными провайдерами ИИ, я обнаружил, что шаблон предоставляет действительно гибкую абстракцию. Давайте разберем, как именно происходит взаимодействие с OpenAI API и другими провайдерами на уровне кода.

В основе всего лежит пакет Microsoft.Extensions.AI.OpenAI, который предоставляет обертку над официальным SDK от OpenAI. Если заглянуть в файл .csproj веб-проекта, можно увидеть зависимости от нескольких пакетов:

XML
1
2
3
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="1.0.0-preview.2.25232.2" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" Version="1.0.0-preview.2.25232.2" />
<PackageReference Include="Microsoft.Extensions.AI.Ollama" Version="1.0.0-preview.2.25232.2" />
Интересно, что в зависимости от выбранного провайдера шаблон использует разные классы, но все они реализуют общие интерфейсы из пакета Microsoft.Extensions.AI.Abstractions. Это позволяет менять провайдеров без изменения основного кода. Основной интерфейс для взаимодействия с языковыми моделями - IChatClient. Он определяет метод GetChatCompletionsAsync, который принимает сообщения и возвращает ответ модели. Есть также асинхронная версия с потоковой передачей ответа - GetStreamingChatCompletionsAsync. При работе с OpenAI вызовы API происходят примерно так:

C#
1
2
3
4
5
6
7
8
var messages = new List<ChatMessage>
{
    new(ChatRole.System, "Вы помощник, который отвечает на вопросы о загруженных документах."),
    new(ChatRole.User, "Какие часы доступны в продаже?")
};
 
var response = await chatClient.GetChatCompletionsAsync(messages, chatOptions);
var reply = response.Content;
Для Azure OpenAI все выглядит идентично, просто используется другой класс для создания клиента. Это возможно благодаря тому, что оба сервиса используют одинаковый API-интерфейс.

С Ollama дела обстоят немного иначе. Этот провайдер запускает модели локально, что дает преимущества в приватности и отсутствии интернет-зависимости, но ограничивает выбор доступных моделей. Для его использования требуется запущеный Docker-контейнер:

C#
1
2
var ollama = builder.AddOllamaClient("ollama");
ollama.AddChatClient("llama3.2");
Интересная особенность шаблона - он абстрагирует не только сам вызов API, но и более сложные механизмы, например, вызов функций. При использовании OpenAI это соответствует "Function Calling", в других провайдерах могут быть аналогичные механизмы под другими названиями. Но для разработчика все выглядит единообразно:

C#
1
chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)];
Один из недостатков, который я заметил - не все провайдеры поддерживают одинаковый набор функций. Например, не все модели могут вызывать функции, и не все провайдеры поддерживают одинаковые типы эмбеддингов. Это нужно учитывать при выборе провайдера и проектировании приложения.

Что касается выбора конкретного провайдера, тут стоит руководствоваться несколькими критериями:
  1. Бюджет (GitHub Models бесплатен для разработки, но имеет ограничения).
  2. Требования к приватности (Ollama работает локально, что важно для конфиденциальных данных).
  3. Необходимые возможности модели (некоторые функции доступны только в определенных моделях).
  4. Производительность и SLA (Azure OpenAI обычно предоставляет более стабильную производительность).

В целом, шаблон предоставляет исключительно гибкий механизм для работы с различными провайдерами ИИ, позволяя сосредоточиться на бизнес-логике, а не на деталях интеграции.

Абстракция провайдеров и создание единого интерфейса для разных AI-сервисов



Одна из самых сильных сторон шаблона .NET AI Chat Web App - это продуманная система абстракций для различных AI-провайдеров. В процессе изучения кода я был приятно удивлен, насколько элегантно Microsoft решила проблему переключения между разными сервисами искуственного интеллекта.

Вся эта магия происходит благодаря пакету Microsoft.Extensions.AI.Abstractions, который определяет ключевые интерфейсы для взаимодействия с языковыми моделями. Основные абстракции, которые я обнаружил в коде:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public interface IChatClient
{
    Task<ChatResponse> GetChatCompletionsAsync(
        IEnumerable<ChatMessage> messages, 
        ChatOptions? options = null,
        CancellationToken cancellationToken = default);
        
    IAsyncEnumerable<ChatStreamingResponse> GetStreamingChatCompletionsAsync(
        IEnumerable<ChatMessage> messages,
        ChatOptions? options = null,
        CancellationToken cancellationToken = default);
}
 
public interface IEmbeddingGenerator<TInput, TOutput>
{
    Task<TOutput> GenerateEmbeddingAsync(
        TInput input, 
        CancellationToken cancellationToken = default);
        
    Task<IReadOnlyList<TOutput>> GenerateEmbeddingsAsync(
        IEnumerable<TInput> inputs,
        CancellationToken cancellationToken = default);
}
Эти интерфейсы служат единой точкой входа для всех операций с ИИ, независимо от того, какой провайдер используется под капотом. Особенно мне понравилось, что здесь нет никаких намеков на конкретную реализацию - чистая абстракция!

Для реализации этих интерфейсов в шаблоне используются различные провайдеры. Например, для OpenAI это OpenAIChatClient и OpenAIEmbeddingGenerator, для Ollama - аналогичные классы из пакета Ollama. Но благодаря DI-контейнеру клиентский код никогда не взаимодействует с этими конкретными реализациями напрямую. Регистрация этих сервисов происходит через методы расширения, которые добавляют удобный флюент-интерфейс:

C#
1
2
3
4
5
6
7
8
// Для GitHub Models / Azure OpenAI
var openai = builder.AddAzureOpenAIClient("openai");
 
// Для "чистого" OpenAI
var openai = builder.AddOpenAIClient("openai");
 
// Для Ollama
var ollama = builder.AddOllamaClient("ollama");
Посмотрите, как изящно решена проблема конфигурации - независимо от провайдера, мы используем один и тот же паттерн для настройки клиентов. За этими методами скрывается сложная логика создания и регистрации соответствующих сервисов в DI-контейнере.

Любопытная деталь: реализации интерфейсов для разных провайдеров могут иметь свои особенности и ограничения. Например, не все провайдеры поддерживают вызов функций или работу с определеными типами эмбеддингов. Шаблон решает эту проблему через механизм "возможностей" (capabilities), который позволяет клиентскому коду проверять, доступна ли та или иная функциональность. Если вы хотите добавить поддержку нового провайдера, вам нужно реализовать эти базовые интерфейсы для вашего сервиса и зарегистрировать их в DI-контейнере. Это делает шаблон невероятно расширяемым - вы можете добавить поддержку любого ИИ-сервиса, даже того, который еще не существовал на момент создания шаблона!

Эта система абстракций - пример того, как грамотное применение паттерна "Стратегия" и принципа инверсии зависимостей из SOLID может сделать код не только более гибким, но и готовым к будущим изменениям в ландшафте ИИ-технологий.

Управление контекстом диалога и мониторинг расходов API



Один из ключевых вопросов, который меня всегда беспокоит при работе с ИИ-системами - как эффективно управлять контекстом диалога и не разориться на API-запросах. В шаблоне .NET AI Chat Web App эти аспекты продуманы достаточно хорошо. Начнем с контекста диалога. Если заглянуть в компонент Chat.razor, можно увидеть, что история сообщений хранится в простом списке:

C#
1
2
3
4
5
6
7
private readonly List<ChatMessage> messages = new();
 
protected override void OnInitialized()
{
    messages.Add(new(ChatRole.System, SystemPrompt));
    chatOptions.Tools = [AIFunctionFactory.Create(SearchAsync)];
}
При каждом обмене сообщениями этот список пополняется новыми записями - сначала вопросом пользователя, затем ответом модели. Все последующие запросы к API отправляются с полной историей диалога, что позволяет модели "помнить" предыдущие вопросы и отвечать в контексте беседы.

Однако тут есть подводный камень - чем длиннее становится диалог, тем больше токенов отправляется при каждом запросе. А больше токенов - значит больше денег! В реальных проектах я обычно реализую "скользящее окно" для истории сообщений, оставляя только N последних сообщений или ограничивая общее количество токенов. В текущей версии шаблона такой механизм не реализован явно, но его легко добавить. Примерно так:

C#
1
2
3
4
5
6
7
8
9
10
// Ограничиваем историю 10 последними сообщениями
if (messages.Count > MAX_MESSAGES) 
{
    // Оставляем системный промпт и последние сообщения
    var systemPrompt = messages[0];
    messages.RemoveRange(1, messages.Count - MAX_MESSAGES);
    
    // Добавляем суммаризацию предыдущей беседы
    messages.Insert(1, new(ChatRole.System, "Контекст предыдущей беседы: ..."));
}
Что касается мониторинга расходов API, шаблон предлагает интеграцию с OpenTelemetry:

C#
1
2
3
4
openai.AddChatClient("gpt-4o-mini")
    .UseFunctionInvocation()
    .UseOpenTelemetry(configure: c => 
        c.EnableSensitiveData = builder.Environment.IsDevelopment());
Это позволяет отслеживать все вызовы API, их длительность и даже содержимое запросов (в режиме разработки). Собирая эти метрики, можно построить мониторинг расходов и создать алерты при превышении бюджета.
В рабочих проектах я часто добавляю прокси-слой между приложением и API, который ведет точный учет использованых токенов и стоимости каждого запроса. Это можно реализовать, создав декоратор для IChatClient:

C#
1
2
3
4
5
6
7
public class UsageTrackingChatClient : IChatClient 
{
    private readonly IChatClient _innerClient;
    private readonly IUsageTracker _tracker;
    
    // Реализация методов с подсчетом токенов
}
Также стоит обратить внимание на кэширование результатов для часто задаваемых вопросов - это может существенно сократить расходы на API.

Фронтенд и пользовательский интерфейс



Перейдем к анализу пользовательского интерфейса шаблона. Как я уже упоминал, фронтенд построен на Blazor Server - технологии, которая позволяет создавать интерактивные веб-приложения с использованием C# вместо JavaScript. Такой подход дает несколько интересных преимуществ для чат-приложения.

Центральный компонент интерфейса - Chat.razor, расположенный в директории Components/Pages. Его структура достаточно проста и интуитивно понятна:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@page "/chat"
@inject IChatClient ChatClient
@inject SemanticSearch Search
@implements IDisposable
 
<PageTitle>Chat</PageTitle>
 
<div class="chat-container">
    <div class="message-list" id="messageList">
        @foreach (var msg in displayMessages)
        {
            <div class="@GetMessageClass(msg)">
                <div class="message-content">
                    @((MarkupString)FormatMessage(msg.Content))
                </div>
            </div>
        }
        
        @if (isLoading)
        {
            <div class="message assistant">
                <div class="message-content">
                    <div class="loading-indicator"></div>
                </div>
            </div>
        }
    </div>
    
    <div class="input-area">
        <textarea @bind="userInput" @onkeydown="HandleKeyDown" placeholder="Type your message..." disabled="@isLoading"></textarea>
        <button @onclick="SendMessage" disabled="@isLoading">Send</button>
    </div>
</div>
Что тут интересного? Во-первых, сообщения отображаются с помощью циклического рендеринга списка displayMessages. Каждое сообщение получает свой CSS-класс в зависимости от роли (пользователь или ассистент), что позволяет визуально различать их в интерфейсе.

Во-вторых, обратите внимание на (MarkupString)FormatMessage(msg.Content) - этот код преобразует обычный текст в HTML-разметку. Это позволяет языковой модели возвращать форматированный текст, например, с выделением, списками или даже цитатами из найденых документов. Для обработки потоковых ответов от ИИ используется интересный механизм. Вместо того чтобы ждать полного ответа, шаблон показывает ответ постепенно, по мере его получения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private async Task SendMessage()
{
    // ... обработка ввода ...
    
    isLoading = true;
    StateHasChanged();
    
    try 
    {
        var chatResponse = ChatClient.GetStreamingChatCompletionsAsync(messages, chatOptions);
        
        string assistantResponse = "";
        await foreach (var chunk in chatResponse)
        {
            assistantResponse += chunk.Content;
            displayMessages[^1] = new DisplayMessage 
            { 
                Role = "assistant", 
                Content = assistantResponse 
            };
            StateHasChanged();
        }
        
        messages.Add(new ChatMessage(ChatRole.Assistant, assistantResponse));
    }
    finally
    {
        isLoading = false;
        StateHasChanged();
    }
}
Такой подход создает ощущение "живого" диалога, как в ChatGPT, где ответы появляются постепенно, а не все сразу.
Стоит отметить и индикатор загрузки (isLoading), который блокирует отправку новых сообщений во время ожидания ответа. Это простое, но важное решение для UX, которое предотвращает отправку нескольких сообщений подряд.

В шаблоне также есть адаптивный дизайн, который хорошо работает как на десктопе, так и на мобильных устройствах. Стили определены в файле wwwroot/css/chat.css и используют современные подходы к верстке с применением flexbox.

Blazor-компоненты для чата и обработка потоковых ответов



Углубившись в изучение фронтенд-части шаблона, я обнаружил несколько интересных Blazor-компонентов, которые заслуживают отдельного разбора. Кроме основного компонента Chat.razor, там есть еще несколько вспомогательных, которые вместе создают удобный пользовательский опыт.

Одним из таких компонентов является MessageRenderer.razor, который отвечает за правильное отображение разных типов сообщений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@using Markdig
 
<div class="@GetMessageClass()">
    @if (IsCitation())
    {
        <div class="citation">
            <span class="citation-source">@GetSourceInfo()</span>
            <div class="citation-content">@((MarkupString)FormatCitation())</div>
        </div>
    }
    else
    {
        @((MarkupString)FormatContent())
    }
</div>
 
@code {
    [Parameter] public string Content { get; set; } = "";
    [Parameter] public string Role { get; set; } = "";
    
    private string FormatContent()
    {
        // Преобразование Markdown в HTML
        return Markdown.ToHtml(Content ?? "");
    }
}
Этот компонент использует библиотеку Markdig для преобразования Markdown-разметки в HTML, что позволяет языковой модели возвращать форматированный текст. Причем компонент умеет распознавать цитаты и источники - очень важная функция для RAG-приложений, где модель должна ссылаться на конкретные места в документах.

Обработка потоковых ответов реализована особенно интересно. В шаблоне используется метод GetStreamingChatCompletionsAsync, который возвращает не просто строку, а IAsyncEnumerable<ChatStreamingResponse>. Это позволяет обрабатывать ответ модели по частям, по мере его поступления:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async Task RenderStreamingResponse(IAsyncEnumerable<ChatStreamingResponse> responseStream)
{
    string fullResponse = "";
    await foreach (var chunk in responseStream)
    {
        if (!string.IsNullOrEmpty(chunk.Content))
        {
            fullResponse += chunk.Content;
            currentResponse = fullResponse;
            await InvokeAsync(StateHasChanged);
        }
    }
    
    // Добавляем полный ответ в историю
    messages.Add(new ChatMessage(ChatRole.Assistant, fullResponse));
}
Этот подход дает два важных преимущества. Во-первых, пользователь видит ответ постепенно, что создает ощущение "живого" диалога. Во-вторых, мы можем начать показывать ответ до того, как он будет полностью сгенерирован, что особенно важно для длинных ответов. Заметьте вызов InvokeAsync(StateHasChanged) после обновления ответа - это критически важно в Blazor для перерендеринга компонента. Без этого интерфейс не будет обновляться, пока не завершится весь метод. Интересно, что шаблон также включает механизм отмены запросов через CancellationTokenSource:

C#
1
2
3
4
5
6
7
8
private CancellationTokenSource? _cancellationTokenSource;
 
private async Task CancelResponse()
{
    _cancellationTokenSource?.Cancel();
    isGenerating = false;
    await InvokeAsync(StateHasChanged);
}
Это позволяет пользователю прервать генерацию, если ответ слишком длинный или не соответствует ожиданиям - маленькая, но очень полезная функция, особенно когда работаешь с моделями, которые иногда "увлекаются" и генерируют слишком многословные ответы.

Реактивность интерфейса и индикаторы состояния



Работая с чат-приложениями, я всегда обращаю особое внимание на реактивность интерфейса — насколько быстро и плавно он реагирует на действия пользователя. В шаблоне .NET AI Chat Web App реактивность реализована на удивление хорошо, учитывая, что Blazor Server иногда критикуют за задержки из-за SignalR-соединения.

Ключевым элементом реактивности в Blazor является метод StateHasChanged(), который сигнализирует фреймворку о необходимости перерендерить компонент. В шаблоне этот метод используется в нескольких критических местах:

C#
1
2
3
4
5
6
7
8
9
10
11
private async Task HandleTypingIndicator()
{
    isTyping = true;
    await InvokeAsync(StateHasChanged);
    
    // Задержка для отображения индикатора печати
    await Task.Delay(typingIndicatorDelay);
    
    isTyping = false;
    await InvokeAsync(StateHasChanged);
}
Обратите внимание на InvokeAsync — это важный паттерн для Blazor, который гарантирует, что обновление UI произойдет в правильном контексте синхронизации. Я часто наблюдал, как разработчики забывают об этом, из-за чего возникают странные ошибки "изменение состояния было обнаружено во время рендеринга".

Индикаторы состояния играют огромную роль в UX чат-приложений. В шаблоне реализовано несколько типов таких индикаторов:

1. Индикатор загрузки — появляется, когда система ожидает ответа от LLM:
HTML5
1
2
3
4
5
6
7
8
   @if (isLoading)
   {
       <div class="message assistant">
           <div class="message-content">
               <div class="loading-indicator"></div>
           </div>
       </div>
   }
2. Блокировка ввода — во время генерации ответа текстовое поле и кнопка отправки становятся неактивными:
HTML5
1
2
3
   <textarea @bind="userInput" placeholder="Type your message..." 
             disabled="@isLoading"></textarea>
   <button @onclick="SendMessage" disabled="@isLoading">Send</button>
3. Индикатор печати — небольшая анимация, показывающая, что ассистент "печатает" ответ. Это психологически важный элемент, создающий ощущение диалога с живым собеседником.

Что мне особенно понравилось — использование CSS-анимаций для создания плавных переходов между состояниями. Например, для индикатора загрузки:

CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.loading-indicator {
    display: inline-block;
    position: relative;
    width: 50px;
    height: 10px;
}
 
.loading-indicator::after {
    content: "...";
    animation: dots 1.5s steps(4, end) infinite;
}
 
@keyframes dots {
    0%, 20% { content: "."; }
    40% { content: ".."; }
    60% { content: "..."; }
    80%, 100% { content: ""; }
}
В реальных проектах я обычно добавляю еще один уровень индикаторов — для отображения статуса соединения с сервером. Это особенно важно для Blazor Server, где потеря SignalR-соединения может привести к неработоспособности всего приложения. Также стоит отметить реализацию мгновенной обратной связи при отправке сообщения — сообщение пользователя появляется в чате сразу после нажатия кнопки, не дожидаясь ответа от сервера. Этот простой трюк значительно улучшает ощущение отзывчивости интерфейса:

C#
1
2
3
4
5
6
7
8
private async Task SendMessage()
{
    // Добавляем сообщение пользователя сразу, не дожидаясь ответа
    displayMessages.Add(new DisplayMessage { Role = "user", Content = userInput });
    StateHasChanged();
    
    // Далее идет код отправки запроса к API...
}

Безопасность и производительность



Разрабатывая чат-приложения на основе ИИ, я всегда особое внимание уделяю вопросам безопасности и производительности. Шаблон .NET AI Chat Web App предлагает несколько интересных решений в этих областях, хотя для рабочего окружения их придется еще доработать.

В плане безопасности первое, что бросается в глаза — хорошо организованная защита API-ключей. Шаблон использует механизм секретов разработчика для хранения чувствительных данных, что значительно снижает риск их случайной утечки через репозиторий. Этот подход реализован через стандартные возможности .NET:

C#
1
builder.Configuration.AddUserSecrets<Program>();
Однако для боевых приложений одних секретов недостаточно. В реальных проектах я бы рекомендовал использовать Azure Key Vault или другие специализированные хранилища секретов. К счастью, переход на них не потребует серьезной переработки кода — достаточно лишь настроить соответствующий провайдер конфигурации. Что касается аутентификации и авторизации, в шаблоне они отсутствуют. Для многопользовательских чат-приложений необходимо добавить как минимум базовую аутентификацию, а лучше — полноценную систему ролей. Я обычно использую Identity Server или встроенную в ASP.NET Core систему Identity.

С точки зрения защиты данных стоит отметить, что обмен сообщениями между браузером и сервером в Blazor Server происходит через SignalR, и эти данные по умолчанию не шифруются. В продакшене необходимо обязательно настроить HTTPS.

Переходя к производительности, отмечу несколько важных аспектов. Blazor Server хорошо работает при небольшом количестве одновременных пользователей, но может столкнуться с проблемами масштабирования при высоких нагрузках. Это связано с тем, что для каждого пользователя на сервере хранится состояние сессии. Один из способов оптимизации — кэширование результатов запросов к языковым моделям. В шаблоне эта функциональность не реализована, но ее можно добавить, создав простой сервис-декоратор для IChatClient:

C#
1
2
3
4
5
6
7
public class CachingChatClient : IChatClient
{
   private readonly IChatClient _innerClient;
   private readonly IMemoryCache _cache;
   
   // Реализация методов с кэшированием часто задаваемых вопросов
}
Также важно оптимизировать размер и структуру эмбеддингов, особенно если вы работаете с большими объемами данных. Векторное хранилище на основе JSON-файла подходит только для прототипирования — в реальных сценариях нужно использовать специализированные базы данных типа Qdrant или Pinecone.

Защита API-ключей и данных пользователей



Когда дело касается чат-приложений на базе ИИ, безопасность стоит на первом месте. Особенно критичен вопрос защиты API-ключей - эти цифровые пропуска в мир языковых моделей часто стоят реальных денег, и их утечка может обернуться серьезными финансовыми потерями. В шаблоне .NET AI Chat Web App реализован базовый уровень защиты через механизм секретов разработчика, но для боевого применения этого недостаточно. Я на своём опыте убедился, что простое использование User Secrets - это только начало пути к надежной защите.

Первое, что я обычно делаю в продакшен-приложениях - настраиваю механизм ротации ключей. Даже если ключ утечет, злоумышленники смогут использовать его лишь ограниченное время. Реализовать ротацию можно через Azure Key Vault и настроить автоматическое обновление ключей:

C#
1
2
3
4
5
6
7
8
9
10
11
// Настройка клиента Key Vault
var secretClient = new SecretClient(
    new Uri("https://your-keyvault.vault.azure.net/"), 
    new DefaultAzureCredential());
 
// Получение текущего ключа
KeyVaultSecret secret = await secretClient.GetSecretAsync("OpenAIKey");
string apiKey = secret.Value;
 
// Обновление ключа при необходимости
await secretClient.SetSecretAsync("OpenAIKey", GenerateNewApiKey());
Для защиты данных пользователей в чате важно реализовать шифрование как при передаче, так и при хранении. Я бы добавил к шаблону слой шифрования для хранимых сообщений с использованием DataProtectionProvider:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EncryptedChatStorage : IChatStorage
{
    private readonly IDataProtector _protector;
    
    public EncryptedChatStorage(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("ChatMessages");
    }
    
    public async Task SaveMessageAsync(ChatMessage message)
    {
        var encryptedContent = _protector.Protect(message.Content);
        // Сохранение зашифрованного сообщения
    }
}
Другой важный аспект безопасности - ограничение доступа к API. В шаблоне отсутствует механизм квотирования и троттлинга запросов. Я обычно реализую middleware, который отслежывает количество запросов и блокирует излишне активных пользователей:

C#
1
2
3
4
5
6
app.UseMiddleware<ApiRateLimitingMiddleware>();
 
public class ApiRateLimitingMiddleware
{
    // Реализация ограничения запросов по IP, пользователю или другим параметрам
}
И последний, но не менее важный момент - обработка данных пользователя в соответствии с законодательством. Для соблюдения GDPR и других норм необходимо предусмотреть возможность экспорта и удаления пользовательских данных.

Практические доработки: расширяем функционал



Шаблон .NET AI Chat Web App предоставляет отличную базу, но в реальных проектах его функциональности может быть недостаточно. Имея опыт внедрения подобных решений, я хочу поделиться несколькими практическими доработками, которые значительно расширяют возможности шаблона.

Первое, что напрашивается - добавление поддержки дополнительных типов документов. По умолчанию шаблон работает только с PDF-файлами, но можно легко реализовать поддержку Word, Excel или даже HTML-страниц. Для этого достаточно создать новую реализацию интерфейса IIngestionSource:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class WebpageIngestionSource : IIngestionSource
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    
    public WebpageIngestionSource(string baseUrl)
    {
        _httpClient = new HttpClient();
        _baseUrl = baseUrl;
    }
    
    public async Task<IEnumerable<IngestedDocument>> GetNewOrModifiedDocumentsAsync(
        IEnumerable<IngestedDocument> existingDocuments)
    {
        // Логика получения HTML-страниц и их содержимого
    }
    
    // Реализация остальных методов интерфейса
}
Другое полезное улучшение - мультиязычная поддержка. Добавление механизма перевода или использование многоязычных моделей позволит вашему чат-боту общаться с пользователями на их родном языке. Это можно реализовать с помощью промежуточного слоя перевода:

C#
1
2
3
4
5
6
7
public class MultilingualChatClient : IChatClient
{
    private readonly IChatClient _innerClient;
    private readonly ITranslationService _translator;
    
    // Логика перевода запросов и ответов
}
Третья важная доработка - персонализация ответов. Можно модифицировать системный промпт, чтобы адаптировать стиль и содержание ответов под конкретного пользователя или его роль:

C#
1
2
3
4
5
6
7
8
private string GetSystemPrompt(UserProfile userProfile)
{
    return $@"
    Ты ассистент, который отвечает на вопросы о {userProfile.InterestArea}.
    Используй {userProfile.PreferredStyle} стиль общения.
    Уровень технических деталей: {userProfile.TechnicalLevel}.
    ";
}
Еще одно полезное дополнение - интеграция с внешними источниками данных в реальном времени. Например, можно добавить возможность запрашивать актуальную информацию через API:

C#
1
2
3
4
5
6
7
[Description("Получает актуальные данные из внешнего API")]
private async Task<string> GetLiveDataAsync(
    [Description("Тип запрашиваемых данных")] string dataType)
{
    // Логика запроса к внешнему API
    return await _externalDataService.GetDataAsync(dataType);
}
Не забудьте также добавить механизм обратной связи, позволяющий пользователям оценивать качество ответов - это бесценно для улучшения промптов и настройки моделей.

Контейнеризация приложения с помощью Docker



Разработав чат-приложение на базе .NET AI Chat Web App, логичным следующим шагом становится его контейнеризация. Docker предоставляет идеальную среду для упаковки и развертывания нашего ИИ-чата, обеспечивая стабильность работы независимо от инфраструктуры.

Давайте создадим базовый Dockerfile для нашего приложения. Я предпочитаю использовать многоэтапную сборку, которая помогает существенно уменьшить размер финального образа:

Windows Batch file
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
 
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY ["ModernDotNetShowChat.AppHost/ModernDotNetShowChat.AppHost.csproj", "ModernDotNetShowChat.AppHost/"]
COPY ["ModernDotNetShowChat.ServiceDefaults/ModernDotNetShowChat.ServiceDefaults.csproj", "ModernDotNetShowChat.ServiceDefaults/"]
COPY ["ModernDotNetShowChat.Web/ModernDotNetShowChat.Web.csproj", "ModernDotNetShowChat.Web/"]
RUN dotnet restore "ModernDotNetShowChat.AppHost/ModernDotNetShowChat.AppHost.csproj"
COPY . .
WORKDIR "/src/ModernDotNetShowChat.AppHost"
RUN dotnet build "ModernDotNetShowChat.AppHost.csproj" -c Release -o /app/build
 
FROM build AS publish
RUN dotnet publish "ModernDotNetShowChat.AppHost.csproj" -c Release -o /app/publish /p:UseAppHost=false
 
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "ModernDotNetShowChat.AppHost.dll"]
Однако есть нюанс с секретами. В Docker-контейнере мы не можем использовать User Secrets, поэтому нужно перестроить нашу систему конфигурации. Проще всего использовать переменные среды:

Windows Batch file
1
2
# Добавляем в финальный образ
ENV ConnectionStrings__openai="Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY"
Конечно, в реальном CI/CD пайплайне эти значения должны быть подставлены из защищенного хранилища секретов, а не прописаны в Dockerfile напрямую.

Поскольку наше приложение использует SQLite для кэша обработаных документов, необходимо позаботиться о персистентности данных. Для этого лучше использовать Docker volumes:

Bash
1
2
3
4
5
6
docker run -d \
  --name aichat \
  -p 8080:80 \
  -v aichat-data:/app/data \
  -e ConnectionStrings__openai="Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY" \
  moderndotnetshowchat
Если вы решили использовать Qdrant вместо локального JSON-хранилища, имеет смысл настроить Docker Compose для управления обоими контейнерами:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
version: '3.8'
 
services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:80"
    environment:
      - ConnectionStrings__openai=Endpoint=https://models.inference.ai.azure.com;Key=YOUR-API-KEY
      - VectorStore__Type=Qdrant
      - VectorStore__ConnectionString=http://qdrant:6333
    depends_on:
      - qdrant
    volumes:
      - aichat-data:/app/data
 
  qdrant:
    image: qdrant/qdrant
    ports:
      - "6333:6333"
    volumes:
      - qdrant-data:/qdrant/storage
 
volumes:
  aichat-data:
  qdrant-data:
При контейнеризации приложения с Aspire стоит помнить, что Aspire изначально нацелен на Kubernetes, а не на Docker. Поэтому может потребоваться некоторая адаптация, чтобы все работало корректно в простом Docker-окружении.

Для оптимизации размера образа я обычно использую несколько приемов:
1. Строго контролирую, какие файлы копируются в образ через .dockerignore.
2. Использую alpine-версии базовых образов, где это возможно.
3. Объединяю несколько RUN-команд, чтобы уменьшить количество слоев.

Когда приложение упаковано в контейнер, его гораздо проще масштабировать и развертывать в различных средах — от локальной разработки до полноценных облачных кластеров.

Отличия App и Application. Отличия App.Current.Shutdown() и Application.Current.Shutdown()
Чем отличаются классы App и Application? И чем отличаются методы App.Current.Shutdown() и...

Можно ли как-то переместить файлы app.xmal в папку и app.config?
Перемещаю файл app.xmal, но в итоге элементы перестают видеть свойства прописанные в этом файле....

App.UseRouting() и App.UseEndpoints()
Добрый день всем форумчанам! Ребят, вот читаю и форумы и статьи, нигде не могу дельно найти...

Написать простой анонимный chat с использованием: ASP.NET MVC 2 библиотеки ExtJS
с ASP ни разу не работал. подкинули вот такое задание. самому интересно, но не понимаю КАК...

VS2019, в чем разница, приложение WPF (.Net Framework) и App WPF (.NET Core)
Открыл сейчас VS2019, чтобы &quot;создать новый проект&quot; и не знаю на какую кнопочку нажать (я не...

Разница между ASP.NET Core 2, ASP.NET Core MVC, ASP.NET MVC 5 и ASP.NET WEBAPI 2
Здравствуйте. Я в бекенд разработке полный ноль. В чем разница между вышеперечисленными...

Изучаем C# с пользой
Как я уже писал в своем самом первом посте, мне очень нравится C# как язык программирования. В...

Изучаем C#
Всем привет. Начал изучать C#. Вот какие возникли вопросы. Буду рад получить ответы: 1.Есть ли в...

Обучение программированию по книге "Изучаем С#"
Я ничего не знал о С# и почемуто купил книгу &quot;Изучаем С#&quot; (3-е издание 2014) Эндрю Стиллмен и...

Ошибки в Save the Humans (книга "Изучаем С#")
Как известно у многих возникают проблемы с первой игрой из этой книги - Изучаем С# 3-издание....

Не работают примеры из книги "Эндрю Стиллмен, Грин - Изучаем c#"
Форумчане, уже отчаялся самостоятельно разобраться. Раз уж вы проходили все примеры по книге -...

Книга "Изучаем с#". Проблема с проектом "Guys"
Здравствуйте! Проект компилируется, но при нажатии на кнопки вылезают ошибки одного рода. Текст...

Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
Всего комментариев 0
Комментарии
 
Новые блоги и статьи
Музыка, написанная Искусственным Интеллектом
volvo 04.12.2025
Всем привет. Некоторое время назад меня заинтересовало, что уже умеет ИИ в плане написания музыки для песен, и, собственно, исполнения этих самых песен. Стихов у нас много, уже вышли 4 книги, еще 3. . .
От async/await к виртуальным потокам в Python
IndentationError 23.11.2025
Армин Ронахер поставил под сомнение async/ await. Создатель Flask заявляет: цветные функции - провал, виртуальные потоки - решение. Не threading-динозавры, а новое поколение лёгких потоков. Откат?. . .
Поиск "дружественных имён" СОМ портов
Argus19 22.11.2025
Поиск "дружественных имён" СОМ портов На странице: https:/ / norseev. ru/ 2018/ 01/ 04/ comportlist_windows/ нашёл схожую тему. Там приведён код на С++, который показывает только имена СОМ портов, типа,. . .
Сколько Государство потратило денег на меня, обеспечивая инсулином.
Programma_Boinc 20.11.2025
Сколько Государство потратило денег на меня, обеспечивая инсулином. Вот решила сделать интересный приблизительный подсчет, сколько государство потратило на меня денег на покупку инсулинов. . . .
Ломающие изменения в C#.NStar Alpha
Etyuhibosecyu 20.11.2025
Уже можно не только тестировать, но и пользоваться C#. NStar - писать оконные приложения, содержащие надписи, кнопки, текстовые поля и даже изображения, например, моя игра "Три в ряд" написана на этом. . .
Мысли в слух
kumehtar 18.11.2025
Кстати, совсем недавно имел разговор на тему медитаций с людьми. И обнаружил, что они вообще не понимают что такое медитация и зачем она нужна. Самые базовые вещи. Для них это - когда просто люди. . .
Создание Single Page Application на фреймах
krapotkin 16.11.2025
Статья исключительно для начинающих. Подходы оригинальностью не блещут. В век Веб все очень привыкли к дизайну Single-Page-Application . Быстренько разберем подход "на фреймах". Мы делаем одну. . .
Фото: Daniel Greenwood
kumehtar 13.11.2025
Расскажи мне о Мире, бродяга
kumehtar 12.11.2025
— Расскажи мне о Мире, бродяга, Ты же видел моря и метели. Как сменялись короны и стяги, Как эпохи стрелою летели. - Этот мир — это крылья и горы, Снег и пламя, любовь и тревоги, И бескрайние. . .
PowerShell Snippets
iNNOKENTIY21 11.11.2025
Модуль PowerShell 5. 1+ : Snippets. psm1 У меня модуль расположен в пользовательской папке модулей, по умолчанию: \Documents\WindowsPowerShell\Modules\Snippets\ А в самом низу файла-профиля. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2025, CyberForum.ru