Page chargée : Article

Blog

Des agents IA utiles dans un logiciel métier .NET : mon retour de terrain

Comment j’intègre des agents IA avec Semantic Kernel dans des logiciels métier .NET sans faire de gadget : use cases, architecture, guardrails et tests.

On me pose souvent la même question, en réunion comme autour d’une bière : « Et l’IA, tu la mets où dans ton appli ? » Sous-entendu, tout le monde en met, alors il faudrait en mettre aussi.

En ce moment, je travaille sur deux produits qui ont de l’IA en prod. Cobria, un ERP pour le bâtiment (CRM, devis, facturation, planning), et My Impact Tool, une plateforme qui aide les organisateurs d’événements durables à mesurer leur impact. Dans les deux cas, la stack est du .NET assez classique, et l’IA passe par Semantic Kernel.

Je ne vais pas te vendre une révolution. Je vais plutôt te raconter comment je choisis où mettre un LLM, comment je le branche sans qu’il mette le bazar dans le métier, et comment je le teste. Si tu développes un logiciel métier en .NET, ça devrait t’éviter quelques semaines de tâtonnements.

Le test du gadget

Avant d’écrire la moindre ligne de code, je fais passer chaque idée de use case par quatre questions.

  1. Est-ce que la tâche revient souvent ? Si un utilisateur la fait deux fois par an, l’IA ne vaut pas la complexité qu’elle ajoute.
  2. Est-ce que l’utilisateur peut vérifier le résultat d’un coup d’œil ? C’est la question la plus importante. Un champ prérempli se relit en deux secondes. Un calcul que personne ne revérifie, c’est une bombe à retardement.
  3. Est-ce qu’une erreur se rattrape ? Un brouillon faux, on le corrige. Une facture déjà envoyée au client, beaucoup moins.
  4. Est-ce que ça évite un vrai effort ? Recopier des dizaines de valeurs depuis un PDF, oui. Reformuler un texte que l’utilisateur vient d’écrire, rarement.

Sur My Impact Tool, le use case qui coche toutes les cases, c’est l’extraction de documents. Les utilisateurs déposent leurs documents, le LLM en extrait les données utiles et préremplit les formulaires. La tâche est fastidieuse, elle revient à chaque événement, et l’utilisateur relit les champs avant de valider. Même logique pour l’assistant qui l’aide à répondre aux questions du formulaire : il répond à une question précise, au bon endroit, et l’utilisateur reste maître de sa réponse.

À l’inverse, voici ce qui ne passe presque jamais le test :

  • le chatbot généraliste collé dans un coin de l’écran, qui sait tout faire en théorie et rien de précis en pratique ;
  • le résumé de ce que l’utilisateur a déjà sous les yeux ;
  • le texte généré que personne ne relit avant de l’envoyer.

Si ton idée ressemble à l’un de ces trois-là, garde-la pour une démo, pas pour la prod.

L’agent passe par le métier, jamais par la base

C’est la règle sur laquelle je ne transige pas.

Un LLM, traite-le comme un utilisateur que tu ne connais pas. Il est plein de bonne volonté, il hallucine avec beaucoup d’aplomb, et il n’a aucune idée de tes règles métier. Tu ne donnerais pas un accès SQL à un stagiaire le jour de son arrivée. Ne le donne pas non plus à un LLM.

Sur Cobria, le backend est un modular monolith en CQRS, avec Wolverine pour le messaging. Chaque action métier passe par une command, qui porte la validation, les droits, les règles et les domain events. L’agent n’a donc besoin d’aucun accès spécial : il envoie les mêmes commands que l’interface, à travers un plugin Semantic Kernel.

Imaginons un plugin qui prépare un brouillon de devis :

public sealed class QuotePlugin(IMessageBus bus)
{
    [KernelFunction("create_quote_draft")]
    [Description("Crée un brouillon de devis pour un client existant. Le devis reste à valider par l'utilisateur.")]
    public async Task<string> CreateQuoteDraftAsync(
        [Description("Identifiant du client")] Guid customerId,
        [Description("Lignes du devis : désignation, quantité et prix unitaire HT")] QuoteLineInput[] lines,
        CancellationToken cancellationToken)
    {
        // Exactement la même command que le bouton « Nouveau devis » de l'interface
        var quoteId = await bus.InvokeAsync<Guid>(new CreateQuoteDraft(customerId, lines), cancellationToken);
        return $"Brouillon {quoteId} créé, en attente de validation.";
    }
}

Ça paraît banal, mais trois détails font toute la différence.

  • Les descriptions. Le LLM choisit quel tool appeler en lisant ces descriptions. Une description floue, et il appelle le mauvais tool au mauvais moment. Je les écris comme de la doc pour un collègue pressé.
  • Le statut. La fonction crée un brouillon, pas un devis. Le LLM propose, l’utilisateur décide.
  • Le retour. Une phrase simple que le LLM peut reformuler à l’utilisateur. Inutile de lui renvoyer tout l’objet.

Côté branchement, rien de sorcier. On enregistre le plugin et on active le function calling en mode auto, pour que le LLM décide lui-même s’il a besoin d’un tool :

// Le plugin reçoit ses dépendances (IMessageBus...) depuis le conteneur DI de l'application
kernel.Plugins.AddFromType<QuotePlugin>(serviceProvider: services);

var chat = kernel.GetRequiredService<IChatCompletionService>();
var settings = new OpenAIPromptExecutionSettings
{
    // Le LLM choisit seul s'il fait un tool call, et lequel
    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
};

var reply = await chat.GetChatMessageContentAsync(history, settings, kernel, cancellationToken);

Le gros avantage de cette approche, c’est que toute ta sécurité existante s’applique d’office. Tu n’as pas de second chemin à protéger.

Les guardrails qui me laissent dormir

Le tenant ne passe jamais par le LLM. Dans un SaaS multi-tenant, c’est le point qui me fait le plus peur. Si le tenant id était un paramètre de tool, un LLM un peu trop créatif pourrait en halluciner un autre. Chez moi, l’isolation se fait plus bas, avec des global query filters EF Core basés sur l’utilisateur connecté :

modelBuilder.Entity<Quote>().HasQueryFilter(quote => quote.TenantId == currentTenant.Id);

L’agent s’exécute dans le contexte de la requête : il ne peut voir et modifier que ce que l’utilisateur voit déjà.

Les droits sont ceux de l’utilisateur. La command vérifie les permissions comme pour n’importe quel appel. Si l’utilisateur n’a pas le droit de créer un devis, l’agent non plus.

Peu de tools par agent. Je ne donne pas tous les tools à chaque agent. Un agent qui sert à préremplir un formulaire n’a aucune raison de pouvoir envoyer une facture. Moins il a de tools, moins il se trompe de tool.

Human in the loop sur tout ce qui engage. Un envoi, un paiement, une signature passent par une confirmation explicite. L’agent prépare, il ne déclenche pas.

Un retry ne crée pas de doublon. Un tool call peut être rejoué après un timeout ou un retry du LLM. Je rends donc ces commands idempotentes : la même demande traitée deux fois ne crée pas deux devis.

Extraire des données de documents sans croiser les doigts

L’extraction de documents, c’est souvent le use case le plus rentable, et aussi celui où un LLM hallucine le plus volontiers. Quelques principes rendent l’exercice fiable.

Exige du structured output. Pas de texte libre à parser à coups de regex : tu décris le format attendu avec un type C#, et le LLM doit répondre avec un JSON conforme.

public sealed record ExtractedDocument(string? Supplier, string? IssueDate, decimal? AmountExcludingTax);

var settings = new OpenAIPromptExecutionSettings
{
    // Output obligatoirement conforme au type, pas de texte libre
    ResponseFormat = typeof(ExtractedDocument),
};

Préfère un champ vide à un champ halluciné. Les propriétés nullables et une consigne claire dans le prompt (« laisse vide si l’info n’est pas dans le document ») changent tout. Un champ vide se remplit en deux secondes. Une valeur plausible mais fausse passe inaperçue.

Revalide derrière. L’output du LLM passe dans tes validators habituels, exactement comme une saisie utilisateur. Un montant négatif ou une date en 2099 n’a rien à faire dans ton formulaire, même si le LLM en est très fier.

Ne fais pas attendre l’utilisateur. Une extraction peut prendre plusieurs secondes. Mieux vaut la lancer en background (un message Wolverine fait très bien l’affaire) et notifier l’utilisateur quand c’est prêt, plutôt que de le laisser devant un spinner.

Tester un truc qui ne répond jamais deux fois pareil

Bonne nouvelle : la plus grande partie du code autour de l’IA se teste comme d’habitude.

  • Les plugins sont du code normal. Un plugin, c’est une classe qui envoie une command. Tests unitaires et tests d’intégration classiques avec xUnit, rien de spécial.
  • Les tests E2E se font sans le vrai LLM. Pour les tests E2E (j’utilise Playwright), remplace le LLM par un fake qui renvoie des réponses écrites à l’avance. Tes tests restent rapides, gratuits et déterministes.
  • Le comportement du LLM se mesure avec des evals. Garde un petit dataset de cas réels anonymisés, et relance tes evals à chaque changement de prompt ou de modèle. Les assertions portent sur des propriétés, pas sur le texte exact : le bon tool a-t-il été appelé, le JSON est-il valide, les champs attendus sont-ils remplis ?

Et une fois en prod, logge tout. Chaque tool call, sa durée et sa conso de tokens partent dans les logs (Serilog et Loki chez moi). Le jour où un utilisateur te dit que « l’IA a fait n’importe quoi », tu es content de pouvoir retrouver exactement ce qu’elle a fait, et pourquoi. Bonus : en suivant la conso par feature, tu évites la mauvaise surprise sur la facture OpenAI en fin de mois.

Mes conseils si tu te lances

  1. Commence par le use case le plus ennuyeux. C’est souvent le plus rentable, et le moins risqué.
  2. Dessine l’écran de vérification avant d’écrire le prompt. Si l’utilisateur ne peut pas contrôler le résultat facilement, le projet n’est pas prêt.
  3. Monte tes evals avant de peaufiner le prompt. Sinon, tu ajustes à l’aveugle.
  4. Mets une alerte sur ta conso de tokens dès le premier jour. Tu me remercieras.

Le mot de la fin

Un LLM dans un logiciel métier, ce n’est pas magique, et ce n’est pas dangereux par nature non plus. C’est une dépendance comme une autre, juste un peu plus imprévisible que les autres. Traite-la comme telle, derrière ton architecture, avec tes règles, tes droits et tes tests, et elle te rendra de vrais services.

Si tu bosses sur un sujet proche, ou si tu veux un regard extérieur sur ton intégration, écris-moi. J’adore en discuter.