Головна / Статті

Інженерія запитів та структурований вихід у .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

Бот підтримки, який викликає GPT-4o безпосередньо з контролера ASP.NET Core, може допомогти отримати демо-версію. Потім корпоративний клієнт просить про розміщення даних у певному регіоні, відділ безпеки вимагає використання 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 слідує моделям Google Cloud IAM та VPC.

Щодо автентифікації в 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 навколо викликів постачальника з атрибутами кількості постачальників, моделей, токенів та причини завершення. Поєднуйте ці онлайн-метрики з наборами даних для офлайн-оцінки, щоб перехід на дешевшу модель був рішенням, підкріпленим доказами, а не надією.

Бюджети на затримку також відрізняються. Деякі постачальники передають перший токен швидше; інші мають перевагу при довгих процесах обробки. Вимірюйте як час отримання першого токена, так і загальний час виконання для вашого завдання, перш ніж обрати стандартний варіант.

Використання кількох постачальників у продакшені

  • Canary: зберігайте по одному «золотому» запиту на кожне середовище, який перевіряє наявність JSON у відповідності до схеми та дотримання показників задовільності щодо затримки, а також блокуйте розгортання, які порушують ці вимоги у основного постачальника.
  • Фільтри контенту: фільтри контенту Azure та інші механізми безпеки повертають різні формати помилок. Один раз прив’яжіть їх до відповідних винятків у вашій системі.
  • Перехід на резервний сервер: вирішіть, які помилки можна спробувати виправити на резервному постачальнику, а які потрібно безумовно проігнорувати, наприклад помилки авторизації чи відмови за правилами. Автоматичний перехід може порушити обіцянки щодо розташування даних.
  • Оперативна підтримка: навчіть співробітників працювати з панелями обліку кожного активованого постачальника. Легше зупинити раптовий сплеск активності о 2 годині ночі, якщо інструкції вказують точну назву порталу.
  • Видалення ресурсів: вимкніть ключі, видаліть реєстрації додатків, очистіть набори даних для тестування, які містять текст клієнтів, та архівуйте звіти про витрати.

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

Потрібна резиденція? Почніть з Azure OpenAI. Довгі документи? Використовуйте Benchmark Anthropic. Вже працюєте з GCP? Оцініть Gemini. Потрібні найновіші моделі OpenAI в день їх випуску? Використовуйте платформу OpenAI. Потрібні два з вищезазначених рішення? Спочатку створіть легкий адаптер та інструмент для вимірювання обсягу використання.

Вибір постачальника — це рішення з точки зору корпоративної архітектури, яке впливає на SDK та витрати. Порівнюйте за критеріями резиденції, ідентифікації, прозорості ціноутворення, структурованого виводу та сумісності з операційними процесами.