Premier appel à un LLM depuis un script .fsx
Durée indicative : environ 25 min ; elle dépend surtout de l'installation et des réponses du modèle sur votre processeur.
Le problème que ce module résout
Au module précédent, vous avez vu qu'un agent commence par une requête HTTP vers un modèle. Encore faut-il un modèle à appeler, et une façon de l'appeler depuis F# qui ne vous enferme pas chez un fournisseur.
Ce module met en place les deux, une fois pour toutes : un modèle qui tourne sur votre machine avec Ollama (gratuit, sans clé d'API, sans carte graphique), et un fichier llm.fsx qui sera le seul endroit du cours à savoir quel modèle vous utilisez. Tous les scripts suivants le réutiliseront.
Avant de commencer : l'installation (une seule fois)
Le téléchargement du modèle n'est pas compté dans la durée du module : si ce n'est pas déjà fait, lancez-le maintenant et lisez la suite pendant qu'il avance.
1. Le SDK .NET 10. Comme au premier palier, vérifiez la liste des SDK installés :
dotnet --list-sdks
Une ligne commençant par 10. doit apparaître. Les scripts de ce cours ont été testés avec le SDK 10.0.401 ; ils n'ont pas été testés avec .NET 8 ou 9, installez donc .NET 10 (la version LTS actuelle) si besoin.
2. Ollama. Téléchargez et installez Ollama depuis le site officiel ollama.com (Windows, macOS ou Linux). Une fois installé, Ollama tourne en arrière-plan et écoute sur http://localhost:11434. Le cours a été testé avec Ollama 0.34.0.
3. Le modèle. Dans un terminal :
ollama pull qwen3.5:4b
ollama list
qwen3.5:4b est un modèle d'environ 4 milliards de paramètres qui sait demander des outils. Le fichier téléchargé fait environ 3,4 Go (jusqu'à 4 Go pour la variante Mac). ollama list doit ensuite le faire apparaître. La mémoire nécessaire sur un ordinateur portable n'a pas été mesurée pour ce cours. Si votre machine peine, sachez que six modules (1, 3, 4, 6, 9, 10) se font sans Ollama ; le module 8 demande seulement qu'Ollama soit démarré pour un de ses scénarios ; les modules 2, 5 et 7 ont besoin du modèle, et montrent une sortie réelle que vous pouvez lire si vous ne pouvez pas l'exécuter.
Anatomie d'un script .fsx
Au premier palier, vous tapiez des expressions dans dotnet fsi, terminées par ;;. Ici, vous écrivez des fichiers .fsx et vous les exécutez d'un bloc :
dotnet fsi bonjour.fsx
Trois différences avec la session interactive :
;;devient inutile : le script est lu en entier, du haut vers le bas.dotnet fsiaccepte encore les;;dans un fichier (du code collé depuis la session interactive fonctionne tel quel), mais les scripts de ce cours n'en utilisent pas ;#r "nuget: Nom, version"télécharge un package NuGet et le référence, comme un<PackageReference>dans un.csproj— mais en une ligne, dans le script lui-même. Le premier lancement prend donc quelques secondes de plus, le temps du téléchargement ;#load "autre.fsx"inclut un autre script. Son contenu devient accessible comme un module dont le nom est celui du fichier avec une majuscule :llm.fsxdevientLlm.
Créez un dossier de travail (par exemple agent-fsharp) : tous les fichiers du cours y vivront côte à côte.
llm.fsx : le seul fichier qui connaît le modèle
Créez llm.fsx avec ce contenu :
// llm.fsx — le SEUL fichier qui sait quel fournisseur et quel modèle vous utilisez.
// Testé le 2026-10-27 avec .NET SDK 10.0.401, Ollama 0.34.0, qwen3.5:4b.
#r "nuget: Microsoft.Extensions.AI, 10.11.0"
#r "nuget: OllamaSharp, 5.5.0"
open System
open System.Net.Http
open Microsoft.Extensions.AI
open OllamaSharp
// Adresse d'Ollama : localhost par défaut, ou la variable d'environnement OLLAMA_URL.
let adresse =
match Environment.GetEnvironmentVariable "OLLAMA_URL" with
| null
| "" -> "http://localhost:11434"
| url -> url
let modele = "qwen3.5:4b"
// Construit un IChatClient. Le délai est explicite : sur CPU, une réponse
// peut prendre des dizaines de secondes.
let creerClient (adresseServeur: string) (delai: TimeSpan) : IChatClient =
let http = new HttpClient(BaseAddress = Uri adresseServeur, Timeout = delai)
new OllamaApiClient(http, modele)
// Le client utilisé par tous les scripts du cours.
let client: IChatClient = creerClient adresse (TimeSpan.FromMinutes 3.0)
// Options de base : pas de « réflexion » (thinking), température 0.
let creerOptions () : ChatOptions =
ChatOptions(
Reasoning = ReasoningOptions(Effort = ReasoningEffort.None),
Temperature = Nullable 0.0f
)
Ligne par ligne, avec l'équivalent C# en tête :
- Les deux
#rfixent des versions exactes.Microsoft.Extensions.AIfournit l'interfaceIChatClient;OllamaSharpen fournit une implémentation pour Ollama. C'est le chemin que recommande le guide de démarrage de Microsoft Learn pour un modèle local. Vous croiserez dans des articles de 2024–2025 un packageMicrosoft.Extensions.AI.Ollama: il est marqué obsolète sur NuGet, au profit d'OllamaSharp. new HttpClient(BaseAddress = ..., Timeout = ...): la syntaxePropriété = valeurentre les parenthèses du constructeur est l'équivalent F# de l'initialiseur d'objet C#new HttpClient { BaseAddress = ..., Timeout = ... }. Le délai de 3 minutes est volontairement large : sur processeur seul, la première réponse inclut le chargement du modèle en mémoire.creerClient ... : IChatClientrenvoie unOllamaApiClient, mais l'annotation de retour le présente comme unIChatClient. Tout le reste du cours ne parlera qu'à cette interface — c'est ce qui vous permettra, au module 10, de changer de modèle en ne modifiant que ce fichier.ReasoningEffort.Nonedésactive le mode « réflexion » du modèle. Ollama documente ce réglage (le champthinkde son API), et OllamaSharp traduit l'option deMicrosoft.Extensions.AIvers ce champ. Sans lui,qwen3.5:4b« réfléchit » avant de répondre : observé le 2026-10-27 sur le processeur de test, la même question a pris 50,9 s avec réflexion contre 5,6 s sans.Temperature = Nullable 0.0f(0.0fest unfloat32, lefloatde C#, emballé dans leNullablequ'attend la propriété) rend les réponses plus stables d'une exécution à l'autre, sans les rendre identiques à coup sûr : vous le constaterez dès le module 5.
Attendre une Task dans un script
Les méthodes de IChatClient sont asynchrones et renvoient une Task, exactement comme en C#. Dans un script, le plus simple est de bloquer jusqu'au résultat, avec la même méthode que vous utiliseriez dans un Main C# non asynchrone :
client.GetResponseAsync(question, creerOptions ()).GetAwaiter().GetResult()
Pour information seulement : l'équivalent F# de async/await s'écrit task { let! reponse = client.GetResponseAsync(...) ... }. Ce cours ne s'en sert pas, pour garder les scripts lisibles de haut en bas.
Exercice — votre premier appel
Créez bonjour.fsx à côté de llm.fsx :
// bonjour.fsx — module 02 : une première question au modèle local.
#load "llm.fsx"
open System.Diagnostics
open Microsoft.Extensions.AI
open Llm
let question = "En une phrase, qu'est-ce qu'un agent IA ?"
let chrono = Stopwatch.StartNew()
let reponse = client.GetResponseAsync(question, creerOptions ()).GetAwaiter().GetResult()
printfn "Modèle : %s" modele
printfn "Réponse : %s" reponse.Text
printfn "Durée : %.1f s" chrono.Elapsed.TotalSeconds
Le open Microsoft.Extensions.AI est nécessaire même si Llm l'ouvre déjà : la surcharge GetResponseAsync(string, options) est une méthode d'extension, et un open dans un fichier chargé ne s'applique pas au fichier qui le charge.
Lancez :
dotnet fsi bonjour.fsx
Sortie attendue — seules les deux lignes marquées comme variables changent d'une machine à l'autre :
Modèle : qwen3.5:4b
Réponse : <texte variable>
Durée : <variable> s
À titre d'exemple, lors d'une exécution réelle observée le 2026-10-27 avec qwen3.5:4b sur processeur seul, la ligne Réponse contenait : « Un agent IA est un système logiciel capable d'observer son environnement, de prendre des décisions autonomes et d'exécuter des actions pour atteindre des objectifs spécifiques sans nécessiter d'intervention humaine constante. » La ligne Durée indiquait 14,4 s au premier lancement, puis 5,1 s au second, une fois le modèle chargé.
Votre phrase sera probablement différente : c'est normal. Ce qui compte : le script va au bout, sans exception, et la ligne Réponse n'est pas vide.
Ensuite, deux vérifications rapides :
- Remplacez la question par une question de votre choix, relancez, et observez la durée du second lancement.
- Sans relire le module, désignez la ligne qui crée l'objet qui parle au modèle, et le fichier où elle se trouve.
Réponse à la vérification 2
C'est new OllamaApiClient(http, modele), dans la fonction creerClient de llm.fsx. bonjour.fsx ne fait qu'utiliser Llm.client, à travers l'interface IChatClient : il ne sait pas qu'il parle à Ollama.
Si votre sortie diffère
- Le premier appel est long (10 à 30 secondes) : Ollama charge le modèle en mémoire au premier appel. Ce n'est pas une erreur ; relancez, le second appel est plus court.
HttpRequestException…Connection refused: Ollama ne tourne pas. Démarrez-le (application Ollama, ouollama servedans un terminal) et relancez.HttpRequestException…404 (Not Found): le modèle n'est pas téléchargé, ou son nom est mal orthographié. Vérifiez avecollama list.TaskCanceledException…HttpClient.Timeout: la réponse a dépassé 3 minutes. Votre machine est probablement trop chargée ; fermez des applications ou augmentez le délai dansllm.fsx.
Ces exceptions brutes sont volontaires : le module 8 les transformera en valeurs que votre code traite. Les scripts complets du cours, publiés dans le dépôt fsharp-ai-agents-samples (dossier fr/), ajoutent en plus une vérification dans llm.fsx : bonjour.fsx et outil.fsx y appellent verifierOllama (), qui remplace la pile d'exception par une ligne indiquant quoi faire.
Si votre machine ne permet pas de faire tourner le modèle, gardez la sortie réelle ci-dessus comme référence et passez au module suivant : il se fait sans Ollama.
Ce que vous venez de faire
Vous avez installé un modèle local, écrit un script .fsx qui référence deux packages NuGet en deux lignes, et interrogé ce modèle à travers IChatClient, une interface qui ne dit rien du fournisseur. Vous avez aussi isolé dans llm.fsx tout ce qui concerne le modèle, et appris à lire une réponse lente comme une caractéristique du processeur, pas comme une panne.
Au module suivant, vous laissez le modèle de côté pour écrire le cœur métier de l'assistant : le statut d'une commande. Une chaîne de caractères ne suffit pas à le décrire — et c'est là qu'apparaît votre première union discriminée.
Vérifiez votre compréhension
Sur un portable sans carte graphique, vous lancez `dotnet fsi resume.fsx` pour la première fois : rien ne s'affiche pendant une vingtaine de secondes, puis une réponse non vide apparaît, sans exception. Que concluez-vous ?
Vous écrivez un nouveau script, `traduire.fsx`, à côté de `llm.fsx`, qui doit interroger le même modèle que `bonjour.fsx`. Quelle approche respecte la structure du cours ?
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.