Intégrer un LLM dans une application .NET avec Microsoft.Extensions.AI : IChatClient, prompts versionnés, sortie structurée, outils, coûts et observabilité.

Intégrer un LLM dans une application .NET avec IChatClient
L'intégration d'un LLM dans une application .NET ne consiste pas à appeler une API et afficher du texte. En production, un modèle de langage devient une capacité supplémentaire du système. Il faut donc le traiter comme un composant applicatif avec ses limites, ses contrats et ses risques.
La bonne approche est de partir du cas d'usage, pas du modèle. Ce guide montre ensuite comment l'intégrer proprement avec Microsoft.Extensions.AI, l'abstraction officielle de .NET pour les modèles de langage.
Commencer par le besoin produit
Un LLM peut servir à beaucoup de choses :
- assister la recherche documentaire ;
- résumer des tickets ou des e-mails ;
- classer du contenu ;
- extraire des données structurées ;
- orchestrer des actions via l'appel d'outils.
Mais tous les cas d'usage ne justifient pas un LLM. Avant d'intégrer quoi que ce soit, définissez :
- la valeur métier attendue ;
- le niveau de précision acceptable ;
- ce qui se passe si la sortie est mauvaise ;
- les données qui peuvent être envoyées au modèle, et à quel fournisseur.
IChatClient : une abstraction plutôt qu'un SDK fournisseur
Microsoft.Extensions.AI définit IChatClient, une interface implémentée par les clients OpenAI, Azure OpenAI, Ollama et d'autres fournisseurs. L'application dépend de cette interface ; le choix du fournisseur se fait une seule fois, à la composition.
dotnet add package Microsoft.Extensions.AI
dotnet add package Microsoft.Extensions.AI.OpenAI
dotnet add package Azure.AI.OpenAI
dotnet add package Azure.Identity
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Extensions.AI;
var builder = WebApplication.CreateBuilder(args);
string endpoint = builder.Configuration["AI:Endpoint"]
?? throw new InvalidOperationException("AI:Endpoint est requis.");
string deployment = builder.Configuration["AI:Deployment"]
?? throw new InvalidOperationException("AI:Deployment est requis.");
builder.Services
.AddChatClient(new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
.GetChatClient(deployment)
.AsIChatClient())
.UseLogging()
.UseOpenTelemetry();
builder.Services.AddScoped<ICustomerSupportAssistant, CustomerSupportAssistant>();
AddChatClient enregistre le client et permet d'empiler des comportements transverses : journalisation, télémétrie, cache distribué ou appel automatique de fonctions. Passer à un autre fournisseur ne change que ces lignes.
Une frontière métier explicite
Le modèle ne doit pas être appelé directement depuis un contrôleur. L'intégration passe par une abstraction métier, nommée d'après le besoin et non d'après la technologie :
public interface ICustomerSupportAssistant
{
Task<SupportReply> DraftReplyAsync(SupportRequest request, CancellationToken ct);
}
public sealed record SupportRequest(string Message, string ProductArea);
public sealed record SupportReply(string Text);
Le reste de l'application ignore qu'un LLM est utilisé. Un test peut fournir une implémentation déterministe, et une panne du fournisseur se traite à un seul endroit.
Garder le prompt hors des endpoints
Le prompt fait partie du comportement applicatif. Il doit donc être structuré, versionné et relu comme du code :
public static class DraftSupportReplyPrompt
{
public const string Version = "support-reply/2026-10";
public const string System = """
You are assisting a support engineer.
Produce a concise answer in French.
Only use the product area and message provided.
Never invent unsupported actions or refunds.
""";
public static string User(SupportRequest request) => $"""
<product_area>{request.ProductArea}</product_area>
<customer_message>
{request.Message}
</customer_message>
""";
}
Les instructions stables sont dans le message système ; les données du client sont délimitées dans le message utilisateur. Le message d'un client reste une donnée non fiable : il ne doit pas pouvoir redéfinir la tâche. La méthode générale est détaillée dans écrire des prompts ChatGPT fiables.
Un pipeline simple et robuste
Une intégration saine suit souvent ce schéma :
- préparer le contexte utile ;
- filtrer les données sensibles ;
- construire le prompt ;
- appeler le modèle avec un délai maximal ;
- valider ou transformer la sortie ;
- journaliser les métadonnées utiles, pas le contenu brut.
public sealed class CustomerSupportAssistant(
IChatClient chatClient,
ILogger<CustomerSupportAssistant> logger) : ICustomerSupportAssistant
{
public async Task<SupportReply> DraftReplyAsync(SupportRequest request, CancellationToken ct)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(ct);
timeout.CancelAfter(TimeSpan.FromSeconds(20));
ChatResponse response = await chatClient.GetResponseAsync(
[
new ChatMessage(ChatRole.System, DraftSupportReplyPrompt.System),
new ChatMessage(ChatRole.User, DraftSupportReplyPrompt.User(request)),
],
new ChatOptions { Temperature = 0.2f, MaxOutputTokens = 400 },
timeout.Token);
logger.LogInformation(
"Support draft generated with prompt {PromptVersion}, {InputTokens} input tokens",
DraftSupportReplyPrompt.Version,
response.Usage?.InputTokenCount);
return new SupportReply(response.Text.Trim());
}
}
Le code reste compréhensible : le modèle est encapsulé, le coût est borné par MaxOutputTokens, le temps par le jeton d'annulation, et la trace relie chaque réponse à une version de prompt.
Structurer la sortie du modèle
Dès qu'un LLM doit alimenter une logique applicative, privilégiez une sortie structurée. GetResponseAsync<T> demande au modèle un JSON conforme au schéma du type, puis le désérialise :
public sealed record TicketClassification(
string Intent,
TicketPriority Priority,
bool RequiresHumanReview);
public enum TicketPriority { Low, Normal, High }
ChatResponse<TicketClassification> classification =
await chatClient.GetResponseAsync<TicketClassification>(
$"Classe ce ticket de support : {ticketText}",
cancellationToken: ct);
if (!classification.TryGetResult(out TicketClassification? result))
{
// The model refused or produced invalid JSON: route to a human
return TicketRouting.ManualReview;
}
Une sortie conforme au schéma n'est pas pour autant correcte : validez ensuite les valeurs métier (une priorité High sur un ticket sans client identifié, par exemple). La sortie libre peut être utile pour l'interface ; la sortie structurée est meilleure pour les flux applicatifs.
Ne pas tout déléguer au LLM
Le LLM est utile pour générer ou reformuler du texte, extraire des informations semi-structurées, classer selon le contexte et raisonner souplement sur du langage naturel. Il ne doit pas décider seul de ce qui est déterministe :
- les droits d'accès restent dans le code ;
- les calculs de prix restent dans le code ;
- la validation stricte reste dans le code ;
- les opérations irréversibles restent sous contrôle explicite.
L'appel d'outils en .NET
Quand le modèle doit invoquer des actions, gardez un cadre strict : le LLM propose, l'application vérifie et exécute. Avec Microsoft.Extensions.AI, une méthode C# devient un outil grâce à AIFunctionFactory.Create, et UseFunctionInvocation() dans la configuration du client exécute automatiquement les appels demandés.
Chaque outil doit avoir un schéma d'entrée clair, des gardes métier, une journalisation et une limite de nombre d'appels. Les outils à effet de bord doivent en plus être soumis à une validation humaine : l'implémentation avec ApprovalRequiredAIFunction est détaillée dans l'approbation humaine des outils d'un agent .NET, et l'architecture d'ensemble dans agent IA en production : architecture et garde-fous.
Gérer les risques réels
Intégrer un LLM en production pose toujours les mêmes sujets : hallucinations, fuite d'informations sensibles, latence, coût, dérive du prompt dans le temps et manque de reproductibilité.
Pour limiter ces risques :
- réduisez le contexte au strict nécessaire ;
- imposez un format de réponse et validez-le ;
- utilisez une température basse pour les workflows critiques ;
- enregistrez la version du prompt et du modèle avec chaque résultat, lorsque c'est autorisé ;
- définissez des chemins de repli : autre modèle, réponse dégradée ou traitement humain. Le routage entre fournisseurs est détaillé dans résilience IA .NET : routage et failover testés.
Observabilité et mesure
Si vous ne mesurez pas le comportement du LLM, vous ne pouvez pas l'améliorer. Surveillez au minimum le temps de réponse, le volume de tokens, le taux d'erreur, le taux de repli, le retour des utilisateurs et le taux de validation humaine lorsqu'il existe.
UseOpenTelemetry() produit déjà des traces et métriques conformes aux conventions sémantiques GenAI ; leur exploitation est détaillée dans observer les LLM .NET avec OpenTelemetry et Aspire. Avant chaque changement de prompt ou de modèle, rejouez un jeu de scénarios : voir les tests d'évaluation IA .NET.
Quand utiliser RAG
Le RAG n'est pas un prérequis universel. Il est utile quand le modèle doit répondre à partir d'une base documentaire actualisée ou spécialisée. Il est moins utile quand le cas d'usage est purement génératif, quand le contexte tient en peu de données stables, ou quand le coût de la recherche dépasse la valeur du cas d'usage.
Introduisez-le pour une raison concrète. S'il se justifie, son ingestion mérite la même rigueur que le reste du code : voir industrialiser l'ingestion RAG .NET avec la CI.
Conclusion
Intégrer un LLM dans une application .NET est un sujet d'architecture applicative, de sécurité et d'exploitation. Le modèle doit rester encapsulé derrière IChatClient et une interface métier, alimenté par un contexte utile et contrôlé par des règles déterministes.
La question n'est pas « comment appeler un modèle ? ». La vraie question est : « comment faire de l'IA une capacité fiable du système ? »

