Aller au contenu principal
Nicolas Cousin Tech SolutionsNicolas Cousin Tech Solutions
Module 10 sur 10

Synthèse : choisir le bon type, changer de modèle

Durée indicative : environ 12 min (hors partie facultative).

Où vous en êtes

Neuf modules plus tôt, vous n'aviez jamais appelé un LLM depuis F#. Votre dossier de travail contient maintenant un agent complet, et chacun des trois types promis au premier palier y joue un rôle précis :

Type Où il apparaît Ce qu'il exprime
Union discriminée Statut (module 3) une commande est dans un état parmi quatre, chacun avec ses données
Option trouverCommande (module 4) la commande peut ne pas exister, et c'est normal
Result + union d'erreurs decoder et ToolError (module 6) la demande du modèle peut être invalide, pour une raison nommée
Union discriminée AgentStep (module 7) à chaque tour, le modèle a produit une forme parmi trois
Result + union d'erreurs executerAgent et AgentError (modules 7 et 8) l'agent peut s'arrêter sans réponse, pour une raison nommée

Et autour de ces types, les morceaux d'agent que vous avez écrits : un appel à un modèle local via IChatClient (module 2), un aller-retour d'outil (module 5), une boucle bornée (module 7), des garde-fous typés et des pannes rejouables (module 8), un serveur MCP (module 9).

La règle de choix

Les trois types répondent à trois questions différentes :

  • La valeur peut-elle être absente, sans que ce soit une erreur, et sans qu'il y ait de raison à expliquer ? → Option. « Pas de commande 9999 » est une réponse normale.
  • L'opération peut-elle échouer, et l'appelant a-t-il besoin de savoir pourquoi ? → Result<'T, 'E>, avec une union pour 'E. « Argument manquant » et « JSON invalide » ne se traitent pas pareil.
  • La valeur a-t-elle plusieurs formes métier, toutes légitimes, avec des données différentes ? → une union discriminée à vous. « Expédiée avec un suivi » n'est ni une absence, ni une erreur.

Le piège le plus courant est de choisir au vocabulaire : le mot « erreur » ou « échec » dans l'énoncé ne suffit pas à justifier Result, et « introuvable » ne suffit pas à justifier Option. Posez-vous la question de la raison : est-ce que quelqu'un, en aval, a besoin de la connaître pour agir différemment ?

Exercice — choisir hors du e-commerce

Pour chaque situation, choisissez Option, Result ou une union discriminée à vous, et écrivez la raison décisive en une phrase. Esquissez le type si vous le souhaitez. Ne lisez pas les réponses avant d'avoir écrit les quatre.

  1. Lire le port d'écoute dans un fichier de configuration. La clé peut manquer, contenir "abc", ou contenir 70000 (hors de la plage 1–65535). L'application doit afficher un message qui dit précisément quoi corriger.
  2. Le statut d'un paiement. Un paiement peut être en attente, autorisé (avec un montant), refusé (avec un motif fourni par la banque) ou remboursé (avec une date).
  3. Un cache en mémoire. On cherche un résultat de calcul par sa clé ; s'il n'y est pas, on le recalcule et on le range.
  4. Chercher un utilisateur par e-mail dans un annuaire distant. L'utilisateur peut ne pas exister ; l'annuaire peut aussi être injoignable, ou refuser la connexion pour des identifiants expirés.

Réponses

  1. Result, avec une union d'erreurs comme PortManquant | PortNonNumerique of string | PortHorsPlage of int. Une Option serait tentante (« pas de port valide »), mais elle perdrait ce que l'énoncé exige : dire quoi corriger. Il y a trois raisons différentes, donc un Error qui les nomme.
  2. Une union discriminée : EnAttente | Autorise of montant: decimal | Refuse of motif: string | Rembourse of date: DateOnly. Le mot « refusé » ressemble à un échec, mais un paiement refusé est un état métier légitime, avec ses données — exactement comme Annulee pour une commande. Ce n'est pas une erreur du programme.
  3. Option : l'absence d'une entrée dans un cache est un cas normal et attendu, sans raison à expliquer — l'appelant recalcule, quoi qu'il arrive. Result avec une erreur « CacheMiss » ajouterait une raison dont personne ne se sert.
  4. Les deux, imbriqués : Result<Utilisateur option, ErreurAnnuaire>. « L'utilisateur n'existe pas » est une réponse normale de l'annuaire (Ok None) ; « annuaire injoignable » et « identifiants expirés » sont des échecs avec une raison, qui appellent des réactions différentes (réessayer plus tard, alerter l'exploitation). C'est la situation volontairement ambiguë : si vous avez répondu Option seule, vous confondez « personne » et « panne » ; si vous avez répondu Result seul avec une erreur UtilisateurInexistant, vous traitez un cas normal comme une erreur. Une réponse plus simple (Option seule, ou Result seul) peut se défendre dans un contexte précis ; ce qui compte est de savoir ce que chacune confond.

Changer de modèle sans toucher à l'agent (facultatif)

Depuis le module 2, une seule ligne du cours sait quel modèle vous utilisez : let modele = "qwen3.5:4b", dans llm.fsx. Tout le reste ne parle qu'à IChatClient. Vérifiez-le si vous avez un autre modèle Ollama capable d'utiliser des outils (ollama list les affiche) : changez uniquement cette ligne, puis relancez dotnet fsi demander.fsx sans modifier agent.fsx.

Si le nouveau modèle demande bien l'outil, la sortie garde la même structure qu'au module 7 — marqueurs de tour, demandes décodées, résultats de getOrder —, seules les réponses finales changent. Lors de la préparation du cours, ce changement d'une ligne a été vérifié avec un autre modèle Ollama compatible outils. Attendez-vous à des durées très différentes : un modèle plus gros est nettement plus lent sur processeur seul. Ce cours ne recommande aucun autre modèle que qwen3.5:4b, le seul dont le comportement a été mesuré.

Vers un fournisseur hébergé. Le même principe devrait s'appliquer à un modèle hébergé : il faudrait une bibliothèque qui fournit une implémentation d'IChatClient pour ce fournisseur, et remplacer le corps de creerClient par sa construction (avec une adresse et une clé d'API) ; agent.fsx, decodage.fsx, commandes.fsx et le serveur MCP n'ont pas de raison de changer. Cette variante n'a pas été testée dans ce cours : elle demande en général une clé d'API, souvent payante, certaines options de creerOptions (comme la désactivation de la réflexion) peuvent être traitées différemment selon le fournisseur, et elle envoie l'historique de conversation hors de votre machine — relisez la ligne « confidentialité » du module 8 avant de l'essayer.

La raison pour laquelle cela fonctionne tient en une phrase : votre code dépend d'une interface (IChatClient) et de vos propres types (AgentStep, ToolCall, AgentError), jamais d'un fournisseur. C'est la même discipline que l'injection de dépendances en C#, appliquée à un modèle.

Ce que vous venez de faire

Vous avez relié chaque type du cours à la question qu'il résout, et vous les avez choisis sur quatre situations sans rapport avec le commerce en ligne, dont une où une réponse plus simple pouvait se défendre. Vous avez aussi vu pourquoi changer de modèle ne devrait toucher que llm.fsx : tout le reste du code ne connaît qu'une interface et vos propres types — à vérifier vous-même si vous faites la partie facultative.

Pour continuer

Ce palier s'arrête volontairement avant plusieurs sujets. Ils sont seulement nommés ici, pour que vous sachiez où chercher :

  • La testabilité et le mocking en F# : comment structurer un agent pour le tester proprement — le faux client du module 8 n'en était qu'un aperçu.
  • Le RAG (retrieval-augmented generation) : donner au modèle des documents pertinents à chaque question, au lieu d'un seul outil.
  • Les frameworks d'agents (Microsoft Agent Framework, Semantic Kernel…) : ils automatisent la boucle que vous avez écrite. Vous saurez désormais ce qu'ils font à votre place.
  • System.Text.Json et les unions discriminées : d'après Microsoft Learn, System.Text.Json sait sérialiser et désérialiser les unions F# à partir de .NET 11 (ce cours cible .NET 10), ce qui changera une partie du module 6 ; la frontière par Result restera utile, car elle nomme chaque défaut au lieu de lever une exception.

Vous avez maintenant tout ce qu'il faut pour construire un agent .NET — en F#, et sans une ligne de Python.

Vérifiez votre compréhension

Une fonction cherche les coordonnées d'une adresse dans une base locale embarquée. Si l'adresse n'y figure pas, l'appelant affiche simplement « adresse non trouvée » — la documentation parle pourtant d'« échec de géocodage ». Quel type de retour choisissez-vous ?

Un document est soit en brouillon, soit en relecture (avec le nom du relecteur), soit publié (avec une URL), soit archivé (avec une date). Un collègue propose `Result<UrlPublication, string>`, l'erreur décrivant « pourquoi il n'est pas publié ». Que choisissez-vous ?

Un collègue veut essayer l'agent avec un autre modèle, local ou hébergé. Pourquoi ce changement ne devrait-il toucher que `llm.fsx`, et que reste-t-il à vérifier ?

Envie d'être prévenu des prochains modules ?

L'Académie reste gratuite et en accès libre, sans inscription. Si vous voulez juste être averti par email à la sortie d'un nouveau module, c'est ici — aucune obligation, désinscription en un clic.