Инжиниринг промптов и структурированный вывод в .NET: OpenAI, Azure, Anthropic, Gemini
OpenAI, Azure OpenAI, Anthropic или Gemini для .NET: условия использования, SDK, цены, структурированный вывод с JSON Schema, инжиниринг промптов и простой IChatClient.
Источники: 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 и затрат. Сравнивайте критерии резидентства, идентификации, прозрачности ценообразования, структурированного вывода и соответствия операционным требованиям.