# .NET и Azure для AI-систем: референсная архитектура

Как собрать production-ready AI-систему на .NET и Azure: async-пайплайн, RAG, Polly, observability и контроль стоимости — с рабочим кодом.

> For the index of this section of the site, see [llms.txt](https://anetlab.com/llms.txt)

13 октября 2026 г.11 мин чтения

# .NET и Azure для AI-систем: референсная архитектура

Как собрать production-ready AI-систему на .NET и Azure: async-пайплайн, RAG, Polly, observability и контроль стоимости — с рабочим кодом.

[Виктор Бузин](https://anetlab.com/about.md) · Founder & Principal Engineer, AnetLab

16+ лет в enterprise-разработке: .NET, AI/LLM-системы, RAG, MCP. Автор материала и практик в студии.

## Зачем нужна эта архитектура

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 архитектуре обычно есть такие логические компоненты:

1.  **Client layer**: web app, mobile app или internal tool.
2.  **API gateway / edge**: Azure API Management или reverse proxy для auth, quotas и validation.
3.  **.NET application layer**: ASP.NET Core API, minimal APIs или worker services.
4.  **Orchestration layer**: prompt builder, policy engine, tool invocation и response shaping.
5.  **Retrieval layer**: Azure AI Search, vector indexes, SQL/NoSQL metadata, document chunk store.
6.  **Model layer**: Azure OpenAI или другой LLM endpoint, embeddings endpoint, moderation endpoint.
7.  **Async processing layer**: Service Bus, queues, Durable Functions или worker services.
8.  **Observability layer**: OpenTelemetry, logs, traces, metrics, dashboards, alerts.
9.  **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:

1.  Ingest documents.
2.  Chunk по смысловым границам.
3.  Generate embeddings.
4.  Сохранить vectors и metadata в retrieval engine.
5.  На запросе нормализовать user query.
6.  Получить top-k кандидатов с фильтрами по tenant, language, freshness и permissions.
7.  Собрать grounded prompt с citations.
8.  Отправить в model.
9.  Вернуть 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: архитектура, которая переживает продакшн](https://anetlab.com/blog/rag-na-dotnet.md)
*   [Модернизация legacy .NET: WCF, AppDomains и AI-мост](https://anetlab.com/blog/legacy-dotnet-modernisation.md)
*   [Production RAG: чек-лист из 50 пунктов для продакшна](https://anetlab.com/blog/production-rag-checklist.md)
*   [Архитектура enterprise AI: четыре решения, которые всё определяют](https://anetlab.com/blog/enterprise-ai-architecture.md)

## CTA

Если вы проектируете AI-enabled platform на .NET и Azure, начните с границ: synchronous vs asynchronous, retrieval vs generation, online vs offline processing. Затем добавьте resilience, logging и cost controls до масштабирования функциональности. Такой порядок снижает риски и упрощает развитие архитектуры.

Если система уже работает и нужно понять, где именно течёт бюджет и где ломается качество, — [напишите нам](https://anetlab.com/contact.md). Разберём архитектуру и отдадим план с приоритетами.

Читайте также

20 октября 2026 г.AI/LLM

### [Оценка качества RAG на .NET: Recall@5, MRR и гейт релиза](https://anetlab.com/blog/rag-evaluation-dotnet.md)

Как измерять качество RAG на .NET: Golden Set из 50–100 вопросов, расчёт Recall@5 и MRR, измерение faithfulness и eval-как-гейт-релиза в CI.

[Читать разбор](https://anetlab.com/blog/rag-evaluation-dotnet.md)

[.NET](https://anetlab.com/blog?tag=.NET)[RAG](https://anetlab.com/blog?tag=RAG.md)[Оценка качества](https://anetlab.com/blog?tag=%D0%9E%D1%86%D0%B5%D0%BD%D0%BA%D0%B0%20%D0%BA%D0%B0%D1%87%D0%B5%D1%81%D1%82%D0%B2%D0%B0.md)

06 октября 2026 г.RAG

### [Собираем RAG-пайплайн на .NET: практический разбор](https://anetlab.com/blog/rag-pipeline-dotnet.md)

Пошаговая сборка production-ready RAG на .NET 10 — от извлечения текста до evaluation-пайплайна. С реальными решениями, а не абстракциями.

[Читать разбор](https://anetlab.com/blog/rag-pipeline-dotnet.md)

[.NET](https://anetlab.com/blog?tag=.NET)[AI/LLM](https://anetlab.com/blog?tag=AI%2FLLM.md)[MCP](https://anetlab.com/blog?tag=MCP.md)