Главная / Статьи

Инжиниринг промптов и структурированный вывод в .NET: OpenAI, Azure, Anthropic, Gemini

OpenAI, Azure OpenAI, Anthropic или Gemini для .NET: условия использования, SDK, цены, структурированный вывод с JSON Schema, инжиниринг промптов и простой IChatClient.

Издатель: REACTAPP.TOP11 мин чтения2544 слов

Источники: OpenAI vs Azure OpenAI vs Anthropic vs Gemini for .NET: Provider Comparison Guide

Бот поддержки, который напрямую из контроллера ASP.NET Core вызывает GPT-4o, может стать основой для демо-версии. Затем корпоративный клиент требует хранения данных в определённом регионе, отдел безопасности предпочитает Claude для обработки длинных документов, а финансовый отдел хочет понять, почему каждый вопрос стоит в десять раз дороже, чем предполагалось. Каждая из этих просьб толкает вас к использованию другого поставщика или к инструменту, позволяющему их заменять.

OpenAI, Azure OpenAI, Anthropic и Gemini продают один и тот же базовый продукт: управляемый чат, возможность вызова инструментов и использование эмбеддингов без необходимости хостинга GPU. Они отличаются упаковкой продукта, регионами обслуживания, требованиями к соответствию нормам, качеством SDK и способом выставления счётов. Для .NET-сервиса, который будет работать годами, именно эти различия имеют большее значение, чем модель, занявшая первое место в рейтинге на прошлой неделе.

Какой поставщик подходит для какой задачи

  • Платформа OpenAI: самый быстрый способ получения новейших моделей. Подходит для продуктов, которые уже работают с ограничениями по данным от OpenAI.
  • Azure OpenAI: когда нагрузка должна находиться внутри тенанта Azure с корпоративной идентификацией, сетевыми настройками и региональным контролем.
  • Anthropic: высокая эффективность при обработке длинных контекстов и точное выполнение инструкций. Стоит рассмотреть его для агентов, работающих с большим количеством документов.
  • Gemini: решения, совместимые с Google Cloud, мультимодальные возможности, а также продукты, близкие к Workspace.

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

Уровень зрелости SDK и пакеты для установки

Здесь рассматриваются официальные клиентские библиотеки, стандартные асинхронные паттерны и частота появления кардинальных изменений. SDK, противоречащее принципам управления жизненным циклом объектов HttpClient и использованию внедрения зависимостей, обойдется вам дороже, чем счет за токены. Для всех четырех категорий можно ожидать пакетов от производителя или хорошо поддерживаемых сообществом решений, но перед тем, как что-либо стандартизировать, обязательно проверьте текущие версии NuGet и способы аутентификации (API-ключи против Entra ID).

// Official OpenAI SDK
dotnet add package OpenAI

// Azure AI Foundry (Azure OpenAI)
dotnet add package Azure.AI.OpenAI

// Anthropic Claude (official, beta as of 2026)
dotnet add package Anthropic

// Gemini via Google Cloud Vertex AI (IChatClient-compatible, January 2026)
dotnet add package Google.Cloud.VertexAI.Extensions --prerelease

// Abstraction layer (ships with all provider adapters)
dotnet add package Microsoft.Extensions.AI

Microsoft.Extensions.AI служит связующим звеном между ними: каждый SDK предоставляет интерфейс IChatClient, что позволяет остальной части приложения оставаться нейтральной по отношению к поставщикам.

Механизмы ценообразования и маршрутизация моделей

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

Отдел финансов будет создавать модель экономики единицы, поэтому ваша архитектура должна генерировать данные о использовании, которым они могут доверять. Каждый вызов для завершения работы должен проходить через счетчик, который фиксирует информацию о модели, токенах и флагах функций. Бюджеты должны быть заданы в коде и в уведомлениях об оплате в облаке. Маршрутизация по сложности задач — самый экономичный способ решения проблемы:

// Partial example — model routing based on task complexity
public class ModelRouter
{
    // Route cheap, simple tasks to the nano model
    // Route complex multi-step reasoning to the full model
    public string SelectModel(TaskComplexity complexity) => complexity switch
    {
        TaskComplexity.Classification  => "gpt-5.4-nano",   // ~20x cheaper than full
        TaskComplexity.Conversational  => "gpt-5.4-mini",   // ~5x cheaper
        TaskComplexity.Reasoning       => "gpt-5.4",         // full capability
        _                              => "gpt-5.4-mini"
    };
}

Соответствие требованиям предприятия: идентификация, сеть и местоположение

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

Azure OpenAI использует систему управления доступом Azure RBAC, технологию Private Link и возможность выбора региона. OpenAI и Anthropic предлагают инструменты контроля за организацией и опции нулевого хранения данных, при этом существуют ограничения, которые необходимо прочитать. Gemini следует моделям IAM и VPC от Google Cloud.

При аутентификации в ASP.NET Core для Azure OpenAI рекомендуется использовать управляемую идентификацию. Для поставщиков API-ключей загружайте секреты из Key Vault при запуске и обновляйте их без необходимости перезапуска приложения. Никогда не связывайте ключи поставщиков с JWT-токенами пользователей.

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

Структурированный вывод с JSON Schema

Поддержка JSON Schema и ограниченной декодировки различается у разных поставщиков. Рекомендуется выбирать тех, кто может обеспечить соблюдение схемы для аргументов инструментов и ответов, которым уже доверяют ваши десериализаторы на C#, а в случаях слабой проверки использовать метод повторной попытки после валидации. С Azure OpenAI вы можете сделать схему строгой:

// Full example — Azure OpenAI structured output with strict schema enforcement
using Azure.AI.OpenAI;
using OpenAI.Chat;
using System.ComponentModel;
using System.Text.Json;

// Define the schema type — only JSON Schema primitives are supported
public class SupportTicketAnalysis
{
    public string Category { get; set; } = string.Empty;        // e.g. "billing", "technical"
    public string Priority { get; set; } = string.Empty;        // "low", "medium", "high", "critical"

    [Description("One to two sentence summary of the issue")]
    public string Summary { get; set; } = string.Empty;

    public string[] RequiredTeams { get; set; } = [];           // which teams must be looped in

    [Description("Estimated resolution time as a human-readable string, e.g. '2 business days'")]
    public string EstimatedResolution { get; set; } = string.Empty;
}

// Build the client — structured outputs require API version 2024-08-01-preview or later
var azureClient = new AzureOpenAIClient(
    new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
    new AzureKeyCredential(Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!),
    new AzureOpenAIClientOptions(AzureOpenAIClientOptions.ServiceVersion.V2024_08_01_Preview));

ChatClient chatClient = azureClient.GetChatClient("gpt-5.4-mini-deployment");

// Configure the request to enforce the schema strictly
var options = new ChatCompletionOptions
{
    ResponseFormat = ChatResponseFormat.CreateJsonSchemaFormat<SupportTicketAnalysis>(
        "support_ticket_analysis",  // snake_case — spaces and hyphens cause 400 errors
        strict: true)               // without this, schema compliance is best-effort only
};

List<ChatMessage> messages =
[
    new SystemChatMessage("You are a support ticket classifier. Analyze the ticket and return a structured assessment."),
    new UserChatMessage("User reports: 'My invoice shows $999 but the contract says $299. Need this fixed immediately.'")
];

// If structured outputs fail with a 400, the schema has unsupported types — check for DateTime, Uri, TimeSpan
ClientResult<ChatCompletion> result = await chatClient.CompleteChatAsync(messages, options);

// JsonSerializer.Deserialize will always succeed if the API call succeeded
SupportTicketAnalysis analysis = JsonSerializer.Deserialize<SupportTicketAnalysis>(
    result.Value.Content.Text)!;

Console.WriteLine(quot;Category: {analysis.Category}");
Console.WriteLine(quot;Priority: {analysis.Priority}");
Console.WriteLine(quot;Summary: {analysis.Summary}");

Два аспекта этого примера вызывают большинство ошибок 400: имена схем должны иметь формат snake_case, а такие типы, как DateTime, Uri или TimeSpan, не являются примитивами JSON Schema. Без параметра strict: true соблюдение стандартов осуществляется лишь при наилучших усилиях.

Вызов инструментов, эмбеддинги и RAG

Все четыре подхода поддерживают вызов инструментов или функций, причем каждый из них использует свою структуру данных. На границе обработки необходимо привести их к внутреннему формату DTO ToolCall, чтобы код оркестрации не различался в зависимости от поставщика.

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

Простая абстракция .NET с IChatClient

Определите небольшой интерфейс поставщика с методами потоковой и непотоковой обработки, привяжите каждый SDK к адаптеру и внедрите его с помощью DI. Разместите подсказки и инструменты вне адаптеров. Избегайте «всемогущих» фреймворков, которые скрывают механизмы повторных попыток и таймаутов, требующие настройки. С помощью IChatClient регистрация загружает информацию о поставщике из конфигурации и оборачивает всё в структуру OpenTelemetry:

// Partial example — Program.cs provider registration with IChatClient
using Microsoft.Extensions.AI;
using Azure.AI.OpenAI;
using OpenAI;
using Anthropic;
using Google.Cloud.VertexAI.Extensions;

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// Read which provider to use from configuration — no code change to switch
string provider = config["AI:Provider"]!; // "azure", "openai", "anthropic", "gemini"

builder.Services.AddSingleton<IChatClient>(sp =>
{
    // Return the appropriate IChatClient implementation based on config
    return provider switch
    {
        "azure" => new AzureOpenAIClient(
                        new Uri(config["AI:Azure:Endpoint"]!),
                        new Azure.AzureKeyCredential(config["AI:Azure:Key"]!))
                    .GetChatClient(config["AI:Azure:Deployment"]!)
                    .AsIChatClient(), // extension method from Azure.AI.OpenAI

        "openai" => new OpenAIClient(config["AI:OpenAI:Key"]!)
                    .GetChatClient(config["AI:OpenAI:Model"]!)
                    .AsIChatClient(),

        "anthropic" => new AnthropicClient(config["AI:Anthropic:Key"]!)
                    .AsIChatClient(), // Anthropic SDK v10+ implements IChatClient

        "gemini" => new VertexAIChatClient(
                        projectId: config["AI:Gemini:ProjectId"]!,
                        location: config["AI:Gemini:Location"]!,
                        modelId: config["AI:Gemini:Model"]!),

        _ => throw new InvalidOperationException(quot;Unknown AI provider: {provider}")
    };
});

// Add OpenTelemetry instrumentation via pipeline decorator — applies to all providers
builder.Services.AddSingleton(sp =>
{
    var inner = sp.GetRequiredService<IChatClient>();
    return new ChatClientBuilder(inner)
        .UseOpenTelemetry(loggerFactory: sp.GetRequiredService<ILoggerFactory>(),
                          sourceName: "YourApp.AI")
        .Build();
});

Флаги функций должны определять поставщика для каждого арендатора или для каждой специальности. Одиночный статический клиент, закодированный вручную в Program.cs, приводит к тому, что исключения, связанные с местоположением, превращаются в инциденты.

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

Рабочий пример концепции

Вся идея помещается в небольшой Web API. Запустите проект и добавьте необходимые пакеты:

dotnet new webapi -n AiProviderPoc --no-openapi
cd AiProviderPoc

dotnet add package Microsoft.Extensions.AI
dotnet add package OpenAI
dotnet add package Azure.AI.OpenAI
dotnet add package Anthropic
dotnet add package Google.Cloud.VertexAI.Extensions --prerelease

Контракт запроса доступен всем поставщикам. Тип вывода ограничивается примитивами JSON Schema, а описания служат руководством для модели:

// PromptContracts.cs — shared across all providers
using System.ComponentModel;

namespace AiProviderPoc;

// The input to every analysis request
public record AnalysisRequest(string TicketText, string? Context = null);

// The structured output — must use only JSON Schema primitives (string, int, bool, arrays)
public class TicketAnalysis
{
    [Description("Category of issue: 'billing', 'technical', 'account', 'feature-request'")]
    public string Category { get; set; } = string.Empty;

    [Description("Severity: 'low', 'medium', 'high', 'critical'")]
    public string Severity { get; set; } = string.Empty;

    [Description("One to two sentence summary of the customer's issue")]
    public string Summary { get; set; } = string.Empty;

    [Description("Suggested action for the support agent, one sentence")]
    public string SuggestedAction { get; set; } = string.Empty;

    public bool RequiresEscalation { get; set; }

    [Description("Estimated time to resolve as human-readable string, e.g. '30 minutes' or '2 business days'")]
    public string EstimatedResolution { get; set; } = string.Empty;
}

Сервис рассматривает системный промпт как стабильный документ с версионированием и зависит только от IChatClient. Он использует нейтральный по поставщику JSON-режим; для строгого соблюдения схемы в Azure или OpenAI следует использовать метод прямого SDK, описанный ранее.

// TicketAnalysisService.cs
using Microsoft.Extensions.AI;
using System.Text.Json;

namespace AiProviderPoc;

public class TicketAnalysisService(IChatClient chatClient)
{
    // The system prompt is the stable contract — versioned and centralized here
    private const string SystemPrompt =
        """
        You are a support ticket classifier for an enterprise software product.
        Analyze the provided support ticket and return a structured assessment.
        Be concise and accurate. Do not invent information not present in the ticket.
        """;

    public async Task<TicketAnalysis> AnalyzeAsync(AnalysisRequest request, CancellationToken ct = default)
    {
        // Build the message list — system prompt is fixed, user content is the ticket
        var messages = new List<ChatMessage>
        {
            new(ChatRole.System, SystemPrompt),
            new(ChatRole.User, request.TicketText)
        };

        // Use JSON mode via ChatOptions — provider-agnostic structured output
        // For strict schema enforcement with Azure/OpenAI, use the SDK-direct approach instead
        var options = new ChatOptions
        {
            ResponseFormat = ChatResponseFormat.Json
        };

        ChatResponse response = await chatClient.GetResponseAsync(messages, options, ct);

        // Deserialize — IChatClient normalizes the response shape across all providers
        return JsonSerializer.Deserialize<TicketAnalysis>(response.Text,
            new JsonSerializerOptions { PropertyNameCaseInsensitive = true })
            ?? throw new InvalidOperationException("Model returned unparseable JSON");
    }
}

Файл Program.cs регистрирует по одному клиенту на каждую ветку провайдера и предоставляет единственный конечный пункт, который никогда не узнаёт, какой поставщик ответил:

using AiProviderPoc.Api;
using Anthropic;
using Azure.AI.OpenAI;
using Google.GenAI;
using Microsoft.Extensions.AI;
using OpenAI;
using System.ClientModel;          // ApiKeyCredential

var builder = WebApplication.CreateBuilder(args);
var config = builder.Configuration;

// Read provider selection from config — change "AI:Provider" to switch providers
string provider = config["AI:Provider"]
    ?? throw new InvalidOperationException("AI:Provider must be set in appsettings");

// Register IChatClient — each branch returns a different SDK's implementation
builder.Services.AddSingleton<IChatClient>(_ =>
{
    return provider.ToLowerInvariant() switch
    {
        "azure" =>
            // Azure AI Foundry: needs endpoint + key + deployment name
            new AzureOpenAIClient(
                new Uri(config["AI:Azure:Endpoint"]!),
                new ApiKeyCredential(config["AI:Azure:Key"]!))
            .GetChatClient(config["AI:Azure:Deployment"]!)
            .AsIChatClient(),

        "openai" =>
            // OpenAI direct: needs API key + model name
            new OpenAIClient(config["AI:OpenAI:Key"]!)
            .GetChatClient(config["AI:OpenAI:Model"]!)
            .AsIChatClient(),

        "anthropic" =>
            // AnthropicClient expects a ClientOptions instance (not a string API key).
            // Create a ClientOptions and pass it to the constructor.
            new AnthropicClient(
                new Anthropic.Core.ClientOptions
                {
                    ApiKey = config["AI:Anthropic:Key"]!
                })
            .AsIChatClient(),

        "gemini" =>
            // Google.GenAI: named params on Client constructor, no ClientConfig class needed.
            // Uses GOOGLE_APPLICATION_CREDENTIALS / ADC for auth; no API key required for Vertex AI.
            new Client(
                project: config["AI:Gemini:ProjectId"]!,
                location: config["AI:Gemini:Location"]!,
                vertexAI: true)
            .AsIChatClient(config["AI:Gemini:Model"]!),

        _ => throw new InvalidOperationException(quot;Unknown provider: '{provider}'. Valid: azure, openai, anthropic, gemini")
    };
});

// Register the service — depends only on IChatClient
builder.Services.AddScoped<TicketAnalysisService>();

var app = builder.Build();

// Single endpoint — provider is invisible here
app.MapPost("/analyze", async (AnalysisRequest request, TicketAnalysisService service, CancellationToken ct) =>
{
    var analysis = await service.AnalyzeAsync(request, ct);
    return Results.Ok(analysis);
});

app.Run();

Чтобы сменить поставщика, достаточно внести изменения в файл appsettings.json:

{
  "AI": {
    "Provider": "azure",
    "Azure": {
      "Endpoint": "https://your-resource.openai.azure.com/",
      "Key": "your-azure-key",
      "Deployment": "gpt-5.4-mini-deployment"
    },
    "OpenAI": {
      "Key": "sk-...",
      "Model": "gpt-4o-mini"
    },
    "Anthropic": {
      "Key": "sk-ant-..."
    },
    "Gemini": {
      "ProjectId": "your-gcp-project",
      "Location": "us-central1",
      "Model": "gemini-2.0-flash"
    }
  }
}

Запустите его и оставьте заявку:

dotnet run

curl -X POST http://localhost:5000/analyze \
  -H "Content-Type: application/json" \
  -d '{"ticketText": "My subscription renews tomorrow but I was charged twice this month. Account #A-1234."}'

Ответ возвращается в том же формате, независимо от того, какой поставщик настроен:

{
  "category": "billing",
  "severity": "high",
  "summary": "Customer was charged twice for their subscription and is seeking a refund before their next renewal date.",
  "suggestedAction": "Review billing history for account A-1234 and initiate a refund for the duplicate charge.",
  "requiresEscalation": false,
  "estimatedResolution": "1 business day"
}

Инжиниринг промптов, оценка и справедливое сравнение

При сравнении качества необходимо фиксировать параметры промптов и температуры, изменяя только поставщика и модель. В противном случае вы сравниваете удачу в работе с промптами, а не самих поставщиков. Сохраняйте версии шаблонов промптов и проводите тесты типа A/B независимо от адаптеров поставщика, иначе каждая замена модели сделает бесполезными ваши эксперименты с промптами.

Генерируйте данные OpenTelemetry для отслеживания вызовов поставщиков, указывая в качестве атрибутов информацию о поставщике, модели, количестве токенов и причине завершения. Сопоставляйте эти онлайн-метрики с наборами данных для офлайн-оценки, чтобы выбор более дешевой модели основывался на фактах, а не на надеждах.

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

Работа с несколькими поставщиками в производстве

  • Канарейка: для каждой среды храните по одному «золотому запросу», утверждающему, что данные представляют собой валидный JSON в соответствии со схемой и имеют заданный показатель задержки, а также блокируйте развертывания, нарушающие эти требования у основного поставщика.
  • Фильтры контента: фильтры контента Azure и другие механизмы безопасности возвращают разные форматы ошибок. Однократно сопоставьте их с исключениями вашей системы.
  • Переключение на резервный источник: определите, какие ошибки можно попытаться исправить на резервном поставщике, а какие требуют немедленного отклонения (например, ошибки авторизации и отказы по правилам). Автоматическое переключение может нарушить обещания относительно местоположения хранения данных.
  • Человек, ответственный в часы кризиса: обучите сотрудников работе с панелями управления оплатой у каждого активного поставщика. Более легко остановить внезапный скачок нагрузки в 2 часа ночи, если в руководстве указан точный адрес портала.
  • Деактивация ресурсов: отключите ключи, удалите регистрации приложений, удалите наборы данных для тестирования, содержащие текст клиентов, и архивируйте отчеты о затратах.

Краткий чек-лист для принятия решения

Требуется резидентство? Начните с Azure OpenAI. Длинные документы? Используйте Benchmark Anthropic. Уже работаете с GCP? Оцените Gemini. Нужны самые новые модели OpenAI сразу после их выпуска? Используйте платформу OpenAI. Нужны два из вышеуказанных решения? Сначала создайте простой адаптер и механизм учета использования.

Выбор поставщика — это решение в рамках корпоративной архитектуры, имеющее последствия с точки зрения SDK и затрат. Сравнивайте критерии резидентства, идентификации, прозрачности ценообразования, структурированного вывода и соответствия операционным требованиям.