L’ingénierie des prompts et les sorties structurées en .NET : OpenAI, Azure, Anthropic, Gemini
OpenAI, Azure OpenAI, Anthropic ou Gemini pour .NET : résidence, SDK, tarifs, sortie structurée avec JSON Schema, ingénierie de prompts et un IChatClient léger.
Sources: OpenAI vs Azure OpenAI vs Anthropic vs Gemini for .NET: Provider Comparison Guide
Un bot de support qui appelle directement GPT-4o depuis un contrôleur ASP.NET Core peut permettre d’obtenir une démonstration. Ensuite, le client entreprise demande une résidence des données dans une région spécifique, le service de sécurité préfère Claude pour les documents longs, et le service financier veut savoir pourquoi chaque question coûte dix fois plus que ce qui avait été estimé. Chacune de ces demandes vous pousse vers un fournisseur différent, ou vers une abstraction qui vous permet de les remplacer.
OpenAI, Azure OpenAI, Anthropic et Gemini vendent tous la même chose de base : des services de chat gérés, des capacités d’appel d’outils et des fonctionnalités d’embedding sans avoir besoin d’héberger des GPU. Ils diffèrent par leur emballage, les régions couvertes, leurs exigences en matière de conformité, la qualité de leurs SDK et leur mode de facturation. Pour un service .NET destiné à fonctionner pendant des années, ces différences sont plus importantes que le modèle qui était en tête des classements la semaine dernière.
Quel fournisseur pour quel usage
- Plateforme OpenAI : le chemin le plus rapide pour accéder aux derniers modèles. Idéal pour les produits qui sont déjà à l’aise avec les limites des données d’OpenAI.
- Azure OpenAI : lorsque la charge de travail doit être hébergée au sein d’un compte Azure disposant d’identités d’entreprise, de fonctionnalités de réseau et de contrôles régionaux.
- Anthropic : excellente capacité à gérer de longs contextes et suivi précis des instructions. À évaluer pour les agents traitant de nombreux documents.
- Gemini : solutions intégrées à Google Cloud, forces multimodales, et produits proches de Workspace.
Évitez de recourir à plusieurs fournisseurs par défaut. Avec une seule équipe, une seule région et sans contraintes de conformité, un seul fournisseur signifie moins de composants à gérer. Chaque abstraction entraîne des coûts supplémentaires.
Maturité des SDK et paquets à installer
Ce que vous évaluez ici, ce sont les bibliothèques officielles pour les clients, les patterns asynchrones idiomatiques ainsi que la fréquence des modifications brisantes. Un SDK qui entre en conflit avec la durée de vie de HttpClient et l’injection de dépendances vous coûtera plus cher que la facture des tokens. Vous trouverez des packages développés par les éditeurs eux-mêmes ou bien soutenus par la communauté pour chacun de ces éléments, mais vérifiez les versions actuelles de NuGet ainsi que les flux d’authentification (clé API contre Entra ID) avant de standardiser quoi que ce soit.
// 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 est l’élément qui les relie : chaque SDK expose un IChatClient, permettant ainsi au reste de l’application de rester neutre vis-à-vis des fournisseurs.
Mécanismes de tarification et routage des modèles
Chaque fournisseur facture en fonction des tokens, mais les détails influencent le montant total : les tokens d’entrée et de sortie ont des prix différents, les tokens en mémoire sont moins chers, les outils peuvent entraîner des frais supplémentaires, et certains forfaits imposent des minimums. Un contexte long et des agents utilisant beaucoup d’outils font rapidement augmenter la facture. Azure facture via son système de mesure, tandis que les trois autres fournisseurs facturent depuis un compte de fournisseur.
Le service financier va créer un modèle d’économie par unité, il est donc essentiel que votre architecture génère des chiffres d’utilisation en lesquels ils ont confiance. Placez chaque appel de traitement derrière un compteur qui enregistre le modèle, les tokens et les indicateurs fonctionnels. Les budgets doivent être définis dans le code ainsi que dans les alertes de facturation cloud. Le routage en fonction de la complexité des tâches représente la solution la moins coûteuse :
// 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"
};
}
Adéquation avec les entreprises : identité, réseau et localisation
Les questionnaires de sécurité font échouer les projets qui ne sont optimisés que pour la latence de démonstration. Ces questions portent sur l’identité, l’isolation du réseau, la conservation des données et la disponibilité régionale.
Azure OpenAI reprend le système RBAC d’Azure, ainsi que les fonctionnalités Private Link et le choix de région. OpenAI et Anthropic offrent des contrôles d’organisation et des options de non-conservation des données, avec des conditions que vous devez lire attentivement. Gemini suit les modèles IAM et VPC de Google Cloud.
Pour l’authentification dans ASP.NET Core, privilégiez l’identité gérée pour Azure OpenAI. Pour les fournisseurs de clés API, chargez les secrets depuis Key Vault au démarrage et mettez-les à jour sans avoir à redéployer le code. Ne liez jamais les clés des fournisseurs aux JWT des utilisateurs.
La revue juridique doit couvrir l’option de refus d’utilisation à des fins d’entraînement, les sous-traitants et les termes du DPA pour chaque fournisseur que vous activez, et non seulement le principal. Plusieurs fournisseurs signifient plusieurs contrats.
Sortie structurée avec JSON Schema
Le support du JSON Schema et du décodage contraint varie d’un fournisseur à l’autre. Préférez des prestataires capables d’imposer un schéma pour les arguments des outils et les réponses en lesquels vos désérialiseurs C# ont déjà confiance, et recourez à une stratégie de validation et de tentative répétée lorsque le contrôle est insuffisant. Avec Azure OpenAI, vous pouvez rendre le schéma strict :
// 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}");
Deux détails dans cet exemple sont à l’origine de la plupart des erreurs 400 : les noms de schéma doivent être en snake_case, et des types tels que DateTime, Uri ou TimeSpan ne font pas partie des primitives de JSON Schema. Sans strict: true, le respect des normes n’est qu’une tentative approximative.
Appel d’outils, embeddings et RAG
Tous les quatre prennent en charge l’appel d’outils ou de fonctions, chacun avec une structure de charge utile différente. Normalisez les données en un DTO interne ToolCall au niveau de la frontière afin que le code d’orchestration ne doive pas s’adapter selon le fournisseur.
Choisir un fournisseur de chat ne signifie pas forcément choisir ses embeddings. Définissez clairement le modèle d’embedding et sa dimension dans la configuration, et ne les modifiez pas en silence lorsque vous changez de fournisseur de chat. Cachez activement les embeddings ainsi que les résultats déterministes des outils, mais ne cachez pas les complétions de chat brutes contenant des données personnelles de l’utilisateur à moins qu’une politique de conservation ne s’y applique.
Une abstraction légère en .NET avec IChatClient
Définissez une petite interface de fournisseur avec des méthodes de streaming et non streaming, mappez chaque SDK derrière un adaptateur, et injectez-le via le DI. Conservez les prompts et les outils en dehors des adaptateurs. Évitez les frameworks « tout-en-un » qui cachent les tentatives de réessai et les délais d’attente que vous devrez ajuster. Avec IChatClient, l’enregistrement lit le fournisseur depuis la configuration et enveloppe tout dans 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();
});
Les flags de fonctionnalité doivent sélectionner le fournisseur par locataire ou par compétence. Un client statique unique, codé en dur dans Program.cs, est la cause pour laquelle les exceptions de résidence se transforment en incidents.
Le streaming nécessite également un contrat explicite au niveau de l’abstraction : des tokens de cancellation, une gestion du backpressure, ainsi que des mécanismes permettant aux appels partiels aux outils d’atteindre leurs expéditeurs. Les fournisseurs diffèrent suffisamment à cet égard pour que « simplement attendre la chaîne de caractères » introduise des spécificités du fournisseur dans vos contrôleurs.
Une démonstration de concept fonctionnelle
Toute l’idée tient dans une petite API Web. Lancez le projet et ajoutez les packages nécessaires :
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
Le contrat de demande est partagé par tous les fournisseurs. Le type de sortie reste limité aux primitifs JSON Schema, et les descriptions servent également de guide pour le modèle :
// 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;
}
Le service possède le prompt du système sous forme de contrat stable et versionné, et ne dépend que de IChatClient. Il utilise un mode JSON neutre par rapport aux fournisseurs ; pour une application stricte du schéma sur Azure ou OpenAI, utilisez l’approche SDK directe présentée précédemment.
// 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 enregistre un client par branche de fournisseur et expose une seule interface qui ne sait jamais quel fournisseur a répondu :
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();
Changer de fournisseur consiste à modifier une configuration dans 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"
}
}
}
Exécutez-le et créez un ticket :
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."}'
La réponse revient sous la même forme, quel que soit le fournisseur configuré :
{
"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"
}
Ingénierie des prompts, évaluations et comparaisons équitables
Lorsque vous comparez les qualités, maintenez inchangés les prompts ainsi que la température, et ne modifiez que le fournisseur et le modèle. Sinon, vous comparez plutôt la chance liée à l’ingénierie des prompts qu’aux fournisseurs eux-mêmes. Gardez les modèles de prompts versionnés et testez-les en mode A/B indépendamment de l’adaptateur du fournisseur, car chaque changement de modèle annule vos expériences avec les prompts.
Émettez des enregistrements OpenTelemetry autour des appels au fournisseur, en y incluant comme attributs le fournisseur, le modèle, le nombre de tokens et la raison de fin. Associez ces métriques en ligne à des ensembles d’évaluation hors ligne afin que le passage à un modèle moins cher soit une décision fondée sur des preuves plutôt que sur de l’espoir.
Les budgets de latence diffèrent également. Certains fournisseurs transmettent rapidement le premier token ; d’autres se distinguent pour les réponses longues. Mesurez à la fois le temps nécessaire pour obtenir le premier token et le temps global pour votre charge de travail avant de choisir un modèle par défaut.
Exécution de plusieurs fournisseurs en production
- Canary : conserver un prompt golden par environnement qui garantit un JSON conforme au schéma ainsi qu’un objectif de performance en termes de latence, et empêcher les déploiements qui en violent les conditions pour le fournisseur principal.
- Filtres de contenu : les filtres de contenu d’Azure et d’autres couches de sécurité renvoient des formats d’erreur différents. Associez-les une fois aux exceptions propres à votre domaine.
- Redondance : déterminer quels erreurs peuvent être réessayées sur un fournisseur secondaire et lesquelles doivent entraîner une interruption immédiate, comme les erreurs d’autorisation ou les refus de politique. Une redondance aveugle peut contredire les engagements de résidence.
- Équipe de veille : former les personnes concernées aux tableaux de bord de facturation de chaque fournisseur activé. Il est plus facile d’arrêter une augmentation soudaine des requêtes à 2 heures du matin lorsque le guide opérationnel indique précisément l’interface correspondante.
- Déprovisionnement : désactiver les clés, supprimer les inscriptions d’applications, effacer les ensembles de données d’évaluation contenant du texte des clients, et archiver les rapports de coûts.
Checklist rapide pour la prise de décision
Une résidence requise ? Commencez par Azure OpenAI. Des documents longs ? Testez Anthropic. Vous êtes déjà sur GCP ? Évaluez Gemini. Besoin des derniers modèles d’OpenAI dès leur sortie ? Utilisez la plateforme d’OpenAI. Besoin de deux de ces solutions ? Créez d’abord un adaptateur léger et un système de suivi de l’utilisation.
Le choix du fournisseur relève d’une décision d’architecture d’entreprise avec des conséquences en termes de SDK et de coûts. Comparez les options en fonction de la résidence, de l’identification, de la transparence des prix, de la sortie structurée et de l’adaptation opérationnelle.