.NET и Azure для AI-систем: референсная архитектура
Как собрать production-ready AI-систему на .NET и Azure: async-пайплайн, RAG, Polly, observability и контроль стоимости — с рабочим кодом.
Зачем нужна эта архитектура
AI-enabled системы в production — это не просто вызов LLM из контроллера. В реальной эксплуатации вам нужны identity, авторизация, асинхронное выполнение, retrieval, retry budgets, observability, управление стоимостью и защита данных. .NET и Azure хорошо подходят для такого сценария, потому что дают зрелую платформу, понятную облачную интеграцию и удобный путь к операционной готовности.
Эта reference architecture описывает практичный enterprise-подход: приложение на .NET принимает запрос, проверяет identity и policy, отправляет работу в асинхронный поток там, где это нужно, подмешивает контекст через retrieval, вызывает model endpoint, пишет telemetry и возвращает результат через наблюдаемый и защищённый pipeline.
Ключевой выбор в этой архитектуре — не модель, а граница между синхронным и асинхронным. Всё, что укладывается в 800–1500 мс, должно жить в HTTP-запросе; всё, что дольше, — в фоновой очереди с явным статусом и возможностью отмены. Ошибка в этом выборе стоит дороже всего остального: она либо убивает SLA, либо прячет в шуме HTTP-ошибок все реальные сбои.
Один из наших контуров — ассистент поверх корпоративной документации заказчика: документы разнородные, права различаются на уровне файла, а повторяющиеся вопросы одни и те же. Этот контур показал две вещи: где именно в .NET-стеке выгодно резать стоимость запроса и что нельзя класть в кэш без оглядки на права доступа. Ниже разбираем его на уровне кода.
Базовый .NET AI stack
Практический .NET AI stack обычно включает такие слои:
- ASP.NET Core для API, auth, streaming и orchestration.
- Microsoft.Extensions.AI и связанные абстракции, если нужен model-agnostic подход.
- Semantic Kernel для planners, tool calling, prompt composition и более высокого уровня orchestration.
- Azure OpenAI для chat, embeddings и function calling.
- Azure AI Search или другой vector-capable retrieval layer для RAG.
- Entity Framework Core или другие data access библиотеки для transactional state.
- Azure Service Bus / Storage Queues для async workflows.
- Azure Cache for Redis для горячего контекста и short-lived state.
- OpenTelemetry с Application Insights / Azure Monitor для traces, metrics и logs.
- Polly для retries, circuit breakers и timeout policies.
- IHostedService / BackgroundService для фоновой обработки, indexing и long-running tasks.
Главная идея — не в количестве пакетов, а в разделении ответственности: request handling, orchestration, retrieval, generation и persistence должны быть разнесены по понятным границам.
Общий вид reference architecture
В production-ready .NET + Azure AI архитектуре обычно есть такие логические компоненты:
- Client layer: web app, mobile app или internal tool.
- API gateway / edge: Azure API Management или reverse proxy для auth, quotas и validation.
- .NET application layer: ASP.NET Core API, minimal APIs или worker services.
- Orchestration layer: prompt builder, policy engine, tool invocation и response shaping.
- Retrieval layer: Azure AI Search, vector indexes, SQL/NoSQL metadata, document chunk store.
- Model layer: Azure OpenAI или другой LLM endpoint, embeddings endpoint, moderation endpoint.
- Async processing layer: Service Bus, queues, Durable Functions или worker services.
- Observability layer: OpenTelemetry, logs, traces, metrics, dashboards, alerts.
- Governance and security layer: Managed Identity, Key Vault, content filters, DLP, RBAC, network isolation.
Ключевое правило: синхронные запросы должны быть короткими и предсказуемыми, а всё дорогое, долгое или нестабильное нужно выносить в asynchronous workflows.
Краткое описание диаграммы
Brief для reference architecture diagram:
- Слева: пользователи и enterprise apps, которые обращаются к ASP.NET Core API через API Management.
- В центре сверху: .NET orchestration service с prompt builder, policy checks, tool invocation и response formatter.
- В центре: retrieval services, подключённые к Azure AI Search, document store и embeddings index.
- В центре снизу: async worker services, которые читают сообщения из Service Bus для indexing, summarization и long-running tasks.
- Справа: Azure OpenAI и вспомогательные model endpoints.
- Нижний слой: telemetry pipeline в OpenTelemetry, Application Insights, Log Analytics, dashboards и alerts.
- Боковые рельсы: Managed Identity, Key Vault, private endpoints, rate limits и cost budgets.
Диаграмма должна показывать сплошные стрелки для synchronous request/response и пунктирные стрелки для queue-based async processing.
Async architecture и поток выполнения
Mature AI system не должен блокировать HTTP request во время chunking, embedding, indexing или summarization больших документов. Вместо этого:
- API принимает пользовательский запрос.
- Выполняет identity, authorization, request size и tenant policy checks.
- Пишет job record или command message.
- Worker service обрабатывает retrieval и model interaction, если задача долгая.
- API возвращает correlation ID или lightweight initial response.
- Клиент polling, webhook или websocket получает финальный результат.
Такой подход снижает latency, делает поведение предсказуемым и ограничивает радиус отказа при проблемах модели.
Типовые сценарии для async:
- document ingestion и chunking
- generation of embeddings и reindexing
- summarizing long conversations
- batch classification и extraction
- tool-call fan-out
- human-in-the-loop review queues
Для production важно добавить idempotency keys и deduplication, иначе retries могут создавать дубликаты.
Resilience patterns
Поведение AI-сервисов отличается от обычных REST-вызовов: бывают timeouts, throttling, moderation denials, transient network errors и service overload. Поэтому resilience должна быть явной.
Рекомендуемые подходы:
- Timeouts для каждого outbound model и retrieval call.
- Retries with jitter только для transient failures.
- Circuit breakers для деградирующих зависимостей.
- Bulkheads для изоляции prompt generation, embeddings и search traffic.
- Fallbacks к cache, smaller models или retrieval-only режиму.
- Queue-based buffering для пиков.
- Idempotency для защиты от повторных side effects.
Polly удобно использовать вместе с HttpClient pipeline, чтобы policies были единообразными.
Cost controls
Контроль стоимости — это архитектурная задача, а не постфактум-оптимизация. Расходы на AI растут из-за token volume, длинного context window, повторных retrieval и бесконечных loops.
Практические меры:
- Token budgets per request.
- Prompt trimming и сокращение истории.
- Retrieval top-k limits.
- Model tiering для простых задач.
- Caching embeddings, retrieval results и deterministic outputs.
- Batching offline summarization и embedding jobs.
- Rate limiting по пользователю, tenant или feature.
- Budget alerts на аномальный spend.
Сначала улучшайте качество context, а уже потом увеличивайте model size.
Кейс: семантический кэш по вектору запроса
В корпоративном контуре заказчика пользователи задают одни и те же вопросы десятки раз в день: по регламентам, по составу команды, по срокам. Кэш по точному совпадению строки на них не срабатывает — вопрос отличается формулировкой. Кэш по вектору запроса срабатывает: в контуре, где мы это внедряли, в кэш попадала 30–50% запросов — треть–половина обращений не доходила до генерации.
public sealed class SemanticCache(
IEmbeddingService embedder,
ISemanticCacheStore store,
ILogger<SemanticCache> logger)
{
// Порог подбирается на golden set, а не «на глаз»: при слишком низком
// кэш начинает отдавать ответ на другой вопрос.
private const double MatchThreshold = 0.94;
private const int ProbeLimit = 5;
public async Task<CachedAnswer?> TryGetAsync(
string query, ClaimsPrincipal user, CancellationToken ct)
{
var scope = AccessScope.For(user);
var embedding = await embedder.EmbedAsync(query, ct);
var candidates = await store.SearchAsync(
embedding,
scope, // область доступа — обязательный фильтр
limit: ProbeLimit,
ct);
var best = candidates.FirstOrDefault();
if (best is null || best.Similarity < MatchThreshold) return null;
logger.LogInformation(
"semantic cache hit {Similarity:F3} for {User}",
best.Similarity, user.Identity?.Name);
return best.Answer;
}
}
Три вещи, которые в такой реализации решают всё:
- Область доступа входит в поиск, а не в проверку после. Ответ, собранный для другого набора прав, не должен находиться кандидатом даже при высокой схожести вопроса.
- Порог подбирается на golden set. При слишком низком пороге кэш отдаёт ответ на похожий, но другой вопрос — и это выглядит как «модель галлюцинирует», хотя виноват кэш.
- Совпадение фиксируется в логах. Без
Similarityв телеметрии вы не увидите, какой реальной долей запросов кэш закрывает, и не заметите, что он перестал работать.
Экономия складывается не из одной настройки, а из того, что кэш снимает самый дорогой участок — генерацию с длинным контекстом. При этом embeddings кэшируются отдельно и всегда: они зависят только от текста запроса, а не от прав доступа.
Observability и token-aware logging
В AI-системах observability — это не только HTTP status. Нужно понимать, какая prompt version использовалась, какие retrieval sources попали в ответ, сколько token было потрачено, какие tools были вызваны и где возникла задержка.
Отслеживайте:
- request ID / correlation ID
- tenant и user context с учётом privacy
- prompt template version
- retrieval query и source IDs
- model name и deployment
- input/output token estimates
- latency по этапам: auth, retrieval, generation, post-processing
- retries, fallback mode, moderation outcome
Не логируйте secrets, полный prompt content и документы без необходимости. Используйте structured logging с redaction и sampling.
Кодовый пример: ILogger token logging
using Microsoft.Extensions.Logging;
public sealed class AiRequestLogger
{
private readonly ILogger<AiRequestLogger> _logger;
public AiRequestLogger(ILogger<AiRequestLogger> logger)
{
_logger = logger;
}
public void LogPromptSummary(string correlationId, string model, int inputTokens, int outputTokens, TimeSpan latency)
{
_logger.LogInformation(
"AI request completed. CorrelationId={CorrelationId} Model={Model} InputTokens={InputTokens} OutputTokens={OutputTokens} LatencyMs={LatencyMs}",
correlationId,
model,
inputTokens,
outputTokens,
latency.TotalMilliseconds);
}
}
Такой logging помогает анализировать cost и performance без раскрытия prompt bodies.
RAG integration
Retrieval-Augmented Generation часто отделяет demo от реально полезной системы. RAG повышает factuality, потому что модель получает grounding на внутренних источниках: policies, manuals, tickets, knowledge base articles.
Типовой RAG flow в .NET:
- Ingest documents.
- Chunk по смысловым границам.
- Generate embeddings.
- Сохранить vectors и metadata в retrieval engine.
- На запросе нормализовать user query.
- Получить top-k кандидатов с фильтрами по tenant, language, freshness и permissions.
- Собрать grounded prompt с citations.
- Отправить в model.
- Вернуть answer со source references.
Важно:
- хранить document-level и chunk-level metadata
- использовать стабильный citation format
- фильтровать по permissions до retrieval или сразу после него
- при необходимости re-rank chunks
- отделять offline indexing от online query handling
В RAG лучше работает хорошо отобранный context, чем длинный, но шумный prompt.
Decision matrix
| Scenario | Best pattern | Why |
|---|---|---|
| Нужен быстрый ответ из одной базы знаний | Synchronous RAG в ASP.NET Core | Низкая latency и простой UX |
| Document ingestion или reindexing | Async worker через queue | Нет timeout в request path |
| Multi-step tool use между системами | Orchestrated workflow | Чёткие границы и observability |
| Частые дорогие prompts | Cache + prompt versioning | Снижает token spend |
| Tenant-specific policies и quotas | API gateway + app policy checks | Defense in depth |
| Массовая background summarization | IHostedService или worker pool | Эффективно и масштабируемо |
| Высокие требования к доступности | Fallbacks + circuit breakers + async buffering | Снижает blast radius |
Кодовые примеры
1) Polly + HttpClient pipeline
using System.Net.Http.Headers;
using Microsoft.Extensions.DependencyInjection;
using Polly;
using Polly.Extensions.Http;
var services = new ServiceCollection();
services.AddHttpClient("AzureOpenAI", client =>
{
client.BaseAddress = new Uri("https://your-resource.openai.azure.com/");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.Timeout = TimeSpan.FromSeconds(30);
})
.AddPolicyHandler(GetRetryPolicy())
.AddPolicyHandler(GetCircuitBreakerPolicy());
static IAsyncPolicy<HttpResponseMessage> GetRetryPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => (int)msg.StatusCode == 429)
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromMilliseconds(200 * Math.Pow(2, attempt))
+ TimeSpan.FromMilliseconds(Random.Shared.Next(0, 100)));
}
static IAsyncPolicy<HttpResponseMessage> GetCircuitBreakerPolicy()
{
return HttpPolicyExtensions
.HandleTransientHttpError()
.OrResult(msg => (int)msg.StatusCode == 429)
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
}
2) ILogger token logging
using Microsoft.Extensions.Logging;
public sealed class GenerationTelemetry
{
private readonly ILogger<GenerationTelemetry> _logger;
public GenerationTelemetry(ILogger<GenerationTelemetry> logger)
{
_logger = logger;
}
public void LogGeneration(string requestId, string deployment, int promptTokens, int completionTokens, bool usedRag)
{
_logger.LogInformation(
"Generation telemetry RequestId={RequestId} Deployment={Deployment} PromptTokens={PromptTokens} CompletionTokens={CompletionTokens} UsedRag={UsedRag}",
requestId,
deployment,
promptTokens,
completionTokens,
usedRag);
}
}
3) IHostedService background processing
using System.Threading.Channels;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
public record AiJob(string JobId, string Payload);
public sealed class AiJobQueue
{
private readonly Channel<AiJob> _channel = Channel.CreateUnbounded<AiJob>();
public ValueTask EnqueueAsync(AiJob job) => _channel.Writer.WriteAsync(job);
public IAsyncEnumerable<AiJob> DequeueAllAsync(CancellationToken cancellationToken) => _channel.Reader.ReadAllAsync(cancellationToken);
}
public sealed class AiBackgroundWorker : BackgroundService
{
private readonly AiJobQueue _queue;
private readonly ILogger<AiBackgroundWorker> _logger;
public AiBackgroundWorker(AiJobQueue queue, ILogger<AiBackgroundWorker> logger)
{
_queue = queue;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var job in _queue.DequeueAllAsync(stoppingToken))
{
try
{
_logger.LogInformation("Processing AI job {JobId}", job.JobId);
await ProcessJobAsync(job, stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "AI job failed {JobId}", job.JobId);
}
}
}
private static Task ProcessJobAsync(AiJob job, CancellationToken cancellationToken)
{
// Chunk, retrieve, call model, persist results.
return Task.CompletedTask;
}
}
FAQ
.NET подходит для AI-enabled systems?
Да. .NET особенно силён там, где важны enterprise integration, security, observability и maintainable service architecture вокруг model calls.
Все ли LLM calls должны быть synchronous?
Нет. Быстрые user-facing calls можно оставить synchronous. Всё долгое, batch-oriented или failure-prone лучше перенести в async processing.
Нужен ли Semantic Kernel в каждом проекте?
Нет. Используйте его там, где его abstractions реально помогают orchestration, а не по умолчанию.
Как избежать роста стоимости prompts?
Ограничивайте token budgets, сокращайте историю, кэшируйте deterministic outputs и используйте cheaper models для простых задач.
Какая самая частая ошибка в RAG?
Индексировать слишком много low-quality content и возвращать слишком много chunks. RAG требует relevance и governance.
Как безопасно логировать AI requests?
Логируйте structured metadata, token counts, latency, model version и correlation IDs. Prompt bodies и secrets редактируйте.
Related Articles
- RAG на .NET: архитектура, которая переживает продакшн
- Модернизация legacy .NET: WCF, AppDomains и AI-мост
- Production RAG: чек-лист из 50 пунктов для продакшна
- Архитектура enterprise AI: четыре решения, которые всё определяют
CTA
Если вы проектируете AI-enabled platform на .NET и Azure, начните с границ: synchronous vs asynchronous, retrieval vs generation, online vs offline processing. Затем добавьте resilience, logging и cost controls до масштабирования функциональности. Такой порядок снижает риски и упрощает развитие архитектуры.
Если система уже работает и нужно понять, где именно течёт бюджет и где ломается качество, — напишите нам. Разберём архитектуру и отдадим план с приоритетами.
Оценка качества RAG на .NET: Recall@5, MRR и гейт релиза
Как измерять качество RAG на .NET: Golden Set из 50–100 вопросов, расчёт Recall@5 и MRR, измерение faithfulness и eval-как-гейт-релиза в CI.
Собираем RAG-пайплайн на .NET: практический разбор
Пошаговая сборка production-ready RAG на .NET 10 — от извлечения текста до evaluation-пайплайна. С реальными решениями, а не абстракциями.