First LLM Call from an .fsx Script
Estimated time: about 25 min; it mostly depends on the installation and on the model's responses on your CPU.
The problem this module solves
In the previous module, you saw that an agent starts with an HTTP request to a model. You still need a model to call, and a way to call it from F# that doesn't lock you into a provider.
This module sets up both, once and for all: a model running on your machine with Ollama (free, no API key, no graphics card), and an llm.fsx file that will be the only place in the course that knows which model you use. Every following script will reuse it.
Before you start: installation (one time only)
The model download isn't counted in the module's duration: if you haven't done it yet, start it now and read on while it progresses.
1. The .NET 10 SDK. As in the first tier, check the list of installed SDKs:
dotnet --list-sdks
A line starting with 10. must appear. The scripts in this course were tested with SDK 10.0.401; they weren't tested with .NET 8 or 9, so install .NET 10 (the current LTS version) if needed.
2. Ollama. Download and install Ollama from the official site, ollama.com (Windows, macOS, or Linux). Once installed, Ollama runs in the background and listens on http://localhost:11434. The course was tested with Ollama 0.34.0.
3. The model. In a terminal:
ollama pull qwen3.5:4b
ollama list
qwen3.5:4b is a model with about 4 billion parameters that knows how to request tools. The downloaded file is about 3.4 GB (up to 4 GB for the Mac variant). ollama list should then show it. The memory needed on a laptop wasn't measured for this course. If your machine struggles, note that six modules (1, 3, 4, 6, 9, 10) work without Ollama; module 8 only needs Ollama running for one of its scenarios; modules 2, 5, and 7 need the model, and show real output you can read if you can't run it.
Anatomy of an .fsx script
In the first tier, you typed expressions into dotnet fsi, ending with ;;. Here, you write .fsx files and run them in one go:
dotnet fsi hello.fsx
Three differences from the interactive session:
;;becomes unnecessary: the script is read in full, from top to bottom.dotnet fsistill accepts;;in a file (code pasted from the interactive session works as is), but the scripts in this course don't use it;#r "nuget: Name, version"downloads a NuGet package and references it, like a<PackageReference>in a.csproj— but in one line, in the script itself. The first run therefore takes a few more seconds, the time it takes to download;#load "other.fsx"includes another script. Its content becomes accessible as a module whose name is the file name with a capital letter:llm.fsxbecomesLlm.
Create a working folder (for example fsharp-agent): all the course's files will live there side by side.
llm.fsx: the only file that knows the model
Create llm.fsx with this content:
// llm.fsx — the ONLY file that knows which provider and which model you use.
// Tested on 2026-10-27 with .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
// Ollama address: localhost by default, or the OLLAMA_URL environment variable.
let address =
match Environment.GetEnvironmentVariable "OLLAMA_URL" with
| null
| "" -> "http://localhost:11434"
| url -> url
let model = "qwen3.5:4b"
// Builds an IChatClient. The timeout is explicit: on CPU, a response
// can take tens of seconds.
let createClient (serverAddress: string) (timeout: TimeSpan) : IChatClient =
let http = new HttpClient(BaseAddress = Uri serverAddress, Timeout = timeout)
new OllamaApiClient(http, model)
// The client used by every script in the course.
let client: IChatClient = createClient address (TimeSpan.FromMinutes 3.0)
// Base options: no "thinking", temperature 0.
let createOptions () : ChatOptions =
ChatOptions(
Reasoning = ReasoningOptions(Effort = ReasoningEffort.None),
Temperature = Nullable 0.0f
)
Line by line, with the C# equivalent up front:
- The two
#rlines pin exact versions.Microsoft.Extensions.AIprovides theIChatClientinterface;OllamaSharpprovides an implementation of it for Ollama. This is the path recommended by Microsoft Learn's getting-started guide for a local model. In articles from 2024–2025, you'll come across aMicrosoft.Extensions.AI.Ollamapackage: it's marked deprecated on NuGet, in favor of OllamaSharp. new HttpClient(BaseAddress = ..., Timeout = ...): theProperty = valuesyntax inside the constructor's parentheses is the F# equivalent of the C# object initializernew HttpClient { BaseAddress = ..., Timeout = ... }. The 3-minute timeout is deliberately generous: on a CPU alone, the first response includes loading the model into memory.createClient ... : IChatClientreturns anOllamaApiClient, but the return annotation presents it as anIChatClient. The rest of the course will only ever talk to this interface — that's what will let you, in module 10, switch models by changing only this file.ReasoningEffort.Noneturns off the model's "thinking" mode. Ollama documents this setting (thethinkfield of its API), and OllamaSharp translates theMicrosoft.Extensions.AIoption into that field. Without it,qwen3.5:4b"thinks" before answering: observed on 2026-10-27 on the test CPU, the same question took 50.9 s with thinking versus 5.6 s without.Temperature = Nullable 0.0f(0.0fis afloat32, C#'sfloat, wrapped in theNullablethe property expects) makes answers more stable from one run to the next, without making them guaranteed identical: you'll see this as early as module 5.
Waiting on a Task in a script
IChatClient's methods are asynchronous and return a Task, exactly as in C#. In a script, the simplest approach is to block until the result arrives, with the same method you'd use in a non-async C# Main:
client.GetResponseAsync(question, createOptions ()).GetAwaiter().GetResult()
For information only: the F# equivalent of async/await is written task { let! response = client.GetResponseAsync(...) ... }. This course doesn't use it, to keep the scripts readable from top to bottom.
Exercise — your first call
Create hello.fsx next to llm.fsx:
// hello.fsx — module 02: a first question to the local model.
#load "llm.fsx"
open System.Diagnostics
open Microsoft.Extensions.AI
open Llm
let question = "In one sentence, what is an AI agent?"
let stopwatch = Stopwatch.StartNew()
let response = client.GetResponseAsync(question, createOptions ()).GetAwaiter().GetResult()
printfn "Model : %s" model
printfn "Answer : %s" response.Text
printfn "Duration: %.1f s" stopwatch.Elapsed.TotalSeconds
The open Microsoft.Extensions.AI is needed even though Llm already opens it: the GetResponseAsync(string, options) overload is an extension method, and an open in a loaded file doesn't apply to the file that loads it.
Run:
dotnet fsi hello.fsx
Expected output — only the two lines marked as variable change from one machine to another:
Model : qwen3.5:4b
Answer : <variable text>
Duration: <variable> s
As an example, during a real run observed on 2026-10-30 with qwen3.5:4b on a CPU alone, the Answer line contained: "An AI agent is a software system that can perceive its environment, make decisions based on data or goals, and take autonomous actions to achieve specific outcomes without requiring continuous human intervention." The Duration line showed 13.0 s on the first run, then 3.9 s on the second, once the model was loaded.
Your sentence will probably be different: that's normal. What matters: the script runs to completion, with no exception, and the Answer line isn't empty.
Next, two quick checks:
- Replace the question with one of your own, run again, and observe the duration of the second run.
- Without rereading the module, point to the line that creates the object that talks to the model, and the file it's in.
Answer to check 2
It's new OllamaApiClient(http, model), in the createClient function of llm.fsx. hello.fsx only uses Llm.client, through the IChatClient interface: it doesn't know it's talking to Ollama.
If your output differs
- The first call is slow (10 to 30 seconds): Ollama loads the model into memory on the first call. It's not an error; run again, the second call is shorter.
HttpRequestException…Connection refused: Ollama isn't running. Start it (Ollama app, orollama servein a terminal) and run again.HttpRequestException…404 (Not Found): the model isn't downloaded, or its name is misspelled. Check withollama list.TaskCanceledException…HttpClient.Timeout: the response took longer than 3 minutes. Your machine is probably too busy; close some applications or increase the timeout inllm.fsx.
These raw exceptions are intentional: module 8 will turn them into values your code handles. The course's complete scripts, published in the fsharp-ai-agents-samples repository (en/ folder), also add a check in llm.fsx: hello.fsx and tool.fsx call checkOllama () there, which replaces the exception stack trace with a single line saying what to do.
If your machine can't run the model, keep the real output above as a reference and move on to the next module: it works without Ollama.
What you just did
You installed a local model, wrote an .fsx script that references two NuGet packages in two lines, and queried that model through IChatClient, an interface that says nothing about the provider. You also isolated everything model-related in llm.fsx, and learned to read a slow response as a characteristic of the CPU, not as a failure.
In the next module, you set the model aside to write the assistant's business core: the status of an order. A string isn't enough to describe it — and that's where your first discriminated union appears.
Check your understanding
On a laptop with no graphics card, you run `dotnet fsi summarize.fsx` for the first time: nothing is displayed for about twenty seconds, then a non-empty answer appears, with no exception. What do you conclude?
You're writing a new script, `translate.fsx`, next to `llm.fsx`, which must query the same model as `hello.fsx`. Which approach respects the course's structure?
Want to hear about the next modules?
The Academy stays free and open-access, no sign-up required. If you'd just like to be notified by email when a new module ships, here you go — no obligation, unsubscribe anytime with one click.