Aller au contenu
Retour aux projets
Étude de casMVP personnelLivré en 202612 min de lecture13 sections

DeskPilot

Triage IA des demandes de support et rédaction de réponses

Les demandes de support entrantes sont classées, leurs entités extraites, et une réponse est rédigée en une seule passe. Claude s'exécute derrière une API ASP.NET Core, avec des sorties d'outils contraintes par schéma, un budget de tokens par locataire et une étape de validation humaine : rien de ce qu'un modèle a écrit n'atteint un client sans avoir été relu.

Orchestration de LLM sous de vraies contraintes de coût, de latence et de justesse.

  • .NET 10
  • ASP.NET Core
  • Claude API
  • Microsoft.Extensions.AI
  • EF Core 10
  • Redis
  • Wolverine
  • SignalR

MVP livré — sources pas encore publiques

01

Le problème qui vaut la peine d'être résolu

Une file de support, c'est surtout de la répétition. Les mêmes vingt questions arrivent sous mille formulations ; chacune doit être lue, catégorisée, rattachée à un compte et à une commande, puis traitée à peu près dans les mêmes termes que la fois précédente. C'est un travail qu'un modèle fait vraiment bien et pour lequel un humain est vraiment gaspillé.

Le piège, c'est qu'appeler un modèle représente les 10 % faciles. Un LLM est une dépendance lente, facturée au token, parfois soumise à des limites de débit, et libre de renvoyer quelque chose qui n'a rien à voir avec la forme demandée. Traitez-le comme un appel de service ordinaire et la première mauvaise journée fait tomber toute la file.

La question d'un MVP n'est pas « le modèle sait-il faire ça ». C'est « que se passe-t-il sur la requête où il ne sait pas ».

02

Où se place le modèle

APIsurface HTTP et canal de streaming
  • Minimal APIs
  • SignalR hub
  • Scalar
  • OpenAPI 3.1
Infrastructurele modèle est une dépendance, pas une couche
  • IChatClient
  • Claude API
  • EF Core 10
  • Redis
  • Wolverine
  • Resilience
Applicationpipeline de triage, budgets, circuit de validation
  • Commands
  • Queries
  • Validators
  • Prompt registry
  • Ports
Domaineun ticket, une classification, un brouillon
  • Ticket
  • Triage
  • Draft
  • TokenBudget
  • Invariants

Un test d'architecture vérifie que les assemblies Domaine et Application ne référencent aucun type Anthropic ni HttpClient. La couche application sait qu'elle peut demander un triage ; elle ne sait pas que c'est un modèle de langage qui répond.

Le modèle vit dans l'infrastructure, derrière un port, exactement comme une base de données.

C'est la décision sur laquelle tout le reste repose. Si le modèle est une couche, chaque évolution le concernant devient une modification de la logique métier. S'il est une dépendance derrière une interface, remplacer Sonnet par Haiku sur un chemin sensible au coût est une modification de configuration, et tester le pipeline ne demande aucun réseau.

Le port est l'IChatClient de Microsoft.Extensions.AI plutôt qu'une interface maison. Il est first-party, il modélise déjà le chat, l'appel d'outils et le streaming, et son pipeline de middlewares est l'endroit où le cache et la télémétrie doivent vivre — ces deux préoccupations ne fuient donc jamais dans les handlers.

03

Le pipeline de triage

Ticket reçuPOST /tickets
valider
FluentValidationforme et taille
avant le modèle
Masquage des données personnellesdéterministe
Contrôle du budgetpar locataire
Recherche en cacheredis
une passe
Claudecontraint par outil
valider la sortie
Contrôle de schémarejet et reprise
Seuil de confiancerouter ou escalader
persister et publier
SQL ServerEF Core 10
Wolverineoutbox
notifier
File des agentsSignalR
Routagerègles
Métriquesotel
Un appel de modèle par ticket. Tout ce qui l'entoure est déterministe.

Observez ce qui entoure l'appel du modèle : trois étapes déterministes avant, deux après. Ce rapport résume toute la conception. Le modèle est une étape du pipeline, pas le pipeline.

04

Récupérer une structure, pas de la prose

Un résultat de triage doit être un enregistrement : catégorie, priorité, entités extraites, corps du brouillon, indice de confiance. Demander du JSON dans un prompt puis parser la réponse fonctionne — jusqu'au moment où ça ne fonctionne plus, et cela échoue à 3 h du matin sur le ticket qui comptait.

  1. 1Déclarer un outil, pas un formatLe schéma de réponse est une définition d'outil que le modèle est invité à appeler. Contraindre la génération à un schéma est un mécanisme, là où « réponds en JSON s'il te plaît » n'est qu'une demande.
  2. 2Valider le résultat quand mêmeLes arguments de l'outil sont désérialisés dans un record C# et validés comme n'importe quelle charge entrante. Une réponse conforme au schéma n'est pas automatiquement une réponse sensée.
  3. 3Une seule reprise, puis escaladeUn résultat mal formé ou invalide est retenté une seule fois, avec l'erreur de validation renvoyée au modèle. Un second échec route le ticket vers un humain, sans triage — ce qui est le bon résultat, pas un mode dégradé.
05

L'humain reste dans la boucle

Chaque brouillon arrive dans la file d'un agent comme une suggestion. L'agent l'accepte, la modifie ou l'écarte, et cette décision est enregistrée sur le ticket. Rien de ce qu'un modèle a écrit n'atteint un client sans avoir été lu.

  • Les taux d'acceptation, de modification et de rejet sont la métrique produit — bien plus utile que n'importe quel benchmark hors ligne, parce qu'ils sont mesurés sur de vrais tickets par les personnes responsables du résultat.
  • Les modifications sont stockées comme des diffs par rapport au brouillon, ce qui en fait un jeu d'évaluation gratuit et qui s'enrichit en continu.
  • Une confiance inférieure au seuil supprime complètement la rédaction et ne fait que router. Une mauvaise suggestion coûte plus de temps à un agent que pas de suggestion du tout.

Le taux de modification est la métrique honnête de justesse. Tout le reste n'en est qu'un indicateur indirect.

06

Le coût est une contrainte de premier ordre

La facturation au token fait du débit une question de budget, ce qui est inhabituel pour un service backend et facile à découvrir trop tard. Les tokens sont comptés à l'entrée, pas rapprochés d'une facture à la fin du mois.

ContrôleMécanismeCe que cela évite
Hiérarchie de modèlesHaiku 4.5 classe ; Sonnet 5 ne rédige que lorsque la confiance le justifiePayer le prix d'un modèle de pointe pour classer un doublon
Cache de réponsesEmpreinte du corps normalisé dans Redis, TTL courtRépondre à la même question deux fois, au plein tarif
Budget par locataireCompteur de tokens vérifié avant l'envoi, appliqué dans le domaineQu'un locataire consomme le quota de tous les autres
Plafond dur en entréeTroncature signalée explicitement avant l'appelQu'un fichier de logs collé coûte plus cher que le ticket
Cache de promptPrompt système et schéma d'outil stables, placés en premierRéexpédier des instructions inchangées à chaque appel

C'est la hiérarchie de modèles qui a le plus compté. La classification est un jugement court et bien borné qu'un modèle rapide et peu coûteux rend presque aussi bien ; la rédaction, elle, est visible par le client. Les séparer a réduit le coût par ticket d'environ deux tiers par rapport à une conception à modèle unique, sans variation mesurable du taux de modification.

07

De la résilience, sans la coder à la main

Les API de modèles imposent des limites de débit et renvoient parfois des erreurs de surcharge ; une tempête de reprises contre une limite de débit aggrave la situation au lieu de l'améliorer. Le problème est résolu, et en 2026 il est résolu par un paquet first-party.

  • Les reprises respectent l'en-tête Retry-After plutôt que de temporiser au hasard.
  • Les 429 et 529 sont retentés ; un 400 dû à un schéma d'outil erroné ne l'est pas — retenter un rejet déterministe est du pur gaspillage.
  • Un disjoncteur ouvert dégrade la fonctionnalité, pas le service : les tickets continuent d'être acceptés et sont mis en file sans triage.
  • Le timeout par tentative est distinct du timeout global : une génération lente ne peut donc pas consommer tout le budget de la requête.
08

Les prompts sont des artefacts versionnés

Un prompt, c'est du comportement. Le modifier sur place, sans version ni évaluation, revient à déployer un changement non testé sur la partie la plus visible du système.

  • Les prompts vivent dans le code source, pas dans une base de données : ils passent donc par la revue et la release avec le code qui en dépend.
  • Chacun porte une version estampillée sur tous les résultats qu'il a produits, ce qui rend « quand la qualité a-t-elle baissé » répondable à partir des données.
  • Une modification doit passer la suite d'évaluation avant d'être fusionnée. Les intuitions et les raisonnements ne suffisent pas.
  • L'identifiant du modèle est une configuration, enregistrée pour chaque résultat à côté de la version du prompt — sans quoi une mise à jour de modèle côté fournisseur est indiscernable d'une régression que vous auriez causée.
09

L'évaluation comme suite de tests

Des tests unitaires ne peuvent pas se prononcer sur du texte généré ; la tentation est donc de ne rien tester et de livrer à l'impression. Il y a à la place deux suites distinctes, dont une seule parle à un modèle.

SuiteExécutionCe qu'elle vérifie
Tests du pipelineÀ chaque commit, sans réseauMasquage, budgets, rejet de schéma, reprise, escalade — contre un IChatClient factice
Suite d'évaluationChaque nuit et avant chaque release, sur l'API réelleJustesse de classification et F1 d'extraction sur un jeu annoté, avec des seuils qui font échouer l'exécution

C'est cette séparation qui garde la CI honnête. La suite déterministe est rapide, gratuite et bloque les merges ; la suite modèle est lente, coûte de l'argent et conditionne les releases. Fusionner les deux donne une exécution à la fois coûteuse et instable — c'est ainsi que des équipes finissent par supprimer leur suite d'évaluation.

10

Le streaming, et pourquoi la file est vivante

La génération prend plusieurs secondes. Un spinner pendant quatre secondes se lit comme une panne ; les mêmes quatre secondes avec des tokens qui arrivent se lisent comme un travail en cours — les brouillons sont donc diffusés à l'agent au fil de leur production.

  • Les tokens du brouillon sont diffusés via SignalR à l'agent qui consulte le ticket.
  • La file partagée se met à jour en direct à mesure que les tickets sont triés : deux agents ne prennent donc pas le même.
  • Un client qui se reconnecte se resynchronise depuis le modèle de lecture, plutôt que de rejouer un flux qu'il a en partie manqué.
  • Uniquement côté client. Le travail interne circule via Wolverine, où l'on dispose de durabilité et de reprise.
11

La stack, et la raison de chaque brique

Chaque dépendance ici mérite sa place, et deux des choix sont spécifiquement des choix de 2026, pas ce que la même liste aurait dit un an plus tôt.

BesoinChoixPourquoi celui-ci
Accès au modèleMicrosoft.Extensions.AIIChatClient first-party avec appel d'outils, streaming et pipeline de middlewares ; aucun SDK .NET officiel d'Anthropic sur lequel s'appuyer
RésilienceMicrosoft.Extensions.Http.ResiliencePipeline standard au-dessus de Polly, branché sur HttpClientFactory — et non assemblé à la main
MessagerieWolverineMassTransit v9 est passé sous licence commerciale ; Wolverine est une alternative open source réellement capable, avec un outbox intégré
Documentation d'APIScalar + OpenAPI intégréSwashbuckle n'est plus le choix par défaut, pour de bonnes raisons ; ASP.NET Core génère lui-même le document désormais
MappingMapperlyGénéré à la compilation : les erreurs de mapping sont des erreurs de compilation, pas des surprises à l'exécution
ValidationFluentValidationGarde les règles de forme des requêtes hors des handlers comme du domaine
Écritures / lecturesEF Core 10 / DapperÉcritures transactionnelles suivies ; SQL explicite pour le tableau de bord des agents
CacheRedisCache de réponses et compteurs de tokens — le seul endroit où un cache se rentabilise de façon mesurable
Tâches de fondHangfireRéinitialisation des budgets, préchauffage du cache, exécutions nocturnes d'évaluation
ObservabilitéSerilog + OpenTelemetryLogs structurés, plus spans et métriques sur les tokens, le coût et le comportement du cache
TestsxUnit v3 + TestcontainersDe vrais SQL Server et Redis dans les tests d'intégration, plutôt que des mocks qui vous donnent raison
12

Observer une dépendance non déterministe

La télémétrie standard répond à la question de savoir si le service est debout. Avec un modèle sur le chemin de la requête, trois autres questions restent sans réponse : combien cela coûte-t-il, quelle est sa qualité en ce moment, et le fournisseur passe-t-il une mauvaise journée.

  • Chaque appel de modèle est un span portant l'identifiant du modèle, la version du prompt, les tokens d'entrée et de sortie, le coût, le succès du cache et la confiance.
  • Le coût par ticket et le taux de modification sont des tableaux de bord, pas des requêtes que quelqu'un pense à lancer.
  • La latence est suivie en p50 et p99, séparément pour la classification et la rédaction — l'une est rapide et prévisible, l'autre n'est ni l'une ni l'autre.
  • Le corps des tickets n'est jamais journalisé. Le span porte une empreinte et un nombre de tokens, ce qui suffit pour déboguer.
13

Les compromis que j'ai acceptés

DécisionCoût
Une seule passe de modèle par ticketPas de raisonnement en plusieurs étapes ; les tickets réellement complexes sont routés, pas traités
Validation humaine obligatoireAucune résolution entièrement automatisée : le temps gagné est donc borné par le débit des agents
Hiérarchie de modèlesDeux prompts et deux références d'évaluation à maintenir au lieu d'un
Cache de réponsesUne réponse périmée est possible après un changement de politique ; le TTL est court précisément pour cela
Wolverine plutôt que MassTransitÉcosystème plus réduit et moins de réponses disponibles quand quelque chose casse à 2 h du matin

Des questions sur son fonctionnement ?

Je peux détailler n'importe laquelle des décisions ci-dessus — le modèle de coûts et le dispositif d'évaluation sont les points les plus intéressants à débattre.