Skip to main content

How language models work

Next-token prediction, what training does and does not give a model, and why that shapes everything you build on top of it.

beginner12 min.NET 10.0

A language model is an unusual dependency to take. It has no schema, its error behaviour is to answer confidently rather than to throw, and it will give you two different strings for the same input. None of that is a defect to be patched around. All of it falls out of one mechanism, and once you have the mechanism the rest of this course is ordinary engineering.

Prediction, not retrieval

Give a model a sequence of and it returns one thing: a probability distribution over every token in its vocabulary for the position that comes next. That is the whole of it. For a ~200,000-token vocabulary, one forward pass produces ~200,000 numbers summing to 1.

Generating a sentence is that step run in a loop. Pick a token from the distribution, append it to the sequence, run the model again on the longer sequence, pick again. This is what autoregressive means, and it has three consequences you will feel immediately:

  • Output is serial. The tenth token cannot be computed before the ninth, which is why streaming exists and why output tokens dominate latency.
  • The model re-reads everything, every token. Cost scales with the length of the conversation, not just with the length of your question.
  • Nothing is looked up. There is no index, no row, no document. There is a function from a token sequence to a distribution.

The picking step is not part of the model. It is sampling code that sits after it, and it is where Temperature, TopP and Seed act. At 0 you take the most probable token every time; raise it and you flatten the distribution and let less likely tokens through. This matters more than it sounds: the model is deterministic, the sampler is not. When you ask for reproducibility later, it is the sampler you are negotiating with, not the weights.

What training produces

Training produces weights — some tens of billions of floating-point numbers — and nothing else. It does not produce a copy of the corpus, an index into it, or any record of what was read.

Two stages get you there:

  1. Pretraining. Predict the next token across a very large body of text. This is where the model acquires syntax, idiom, code structure and a lossy, smeared-out impression of a great many facts.
  2. Post-training. Instruction tuning and preference optimisation, which teach the model to treat a prompt as a request to be satisfied rather than a passage to be continued. A pretrained-only model asked "How do I add a hosted service?" is quite likely to reply with three more questions, because that is what such text usually looks like.

Facts survive this process the way a compressed image survives compression: recognisably, unevenly, and with no way to tell reconstruction from recall. That gives you four properties to design around.

PropertyWhat it means for you
Frozen weightsThe model does not learn from your prompts. Inference changes nothing.
Knowledge cutoffAnything after training is unknown, including your codebase and last week's release.
No provenance"Where did you read that?" is unanswerable unless you supplied the text yourself.
Lossy recallA plausible-looking API signature may simply not exist.

The fix for all four is the same and it is not a better model: put the text in the prompt. That is the entire premise of retrieval-augmented generation, which is a later course in this track. Here it is enough to know why it has to exist.

Consequences for your code

The API shape follows directly from the mechanism. You send a list of messages, you get tokens back, and everything the model knows about your problem is in that list. There is no session, no server-side memory, no handle to a previous call. A "conversation" is your application resending the history each turn.

In .NET the abstraction over that is IChatClient, from Microsoft.Extensions.AI. It is the HttpClient of this space: one interface, many providers, and middleware in between.

SummariseCommand.cs
using Microsoft.Extensions.AI;
 
public sealed class SummariseCommand(IChatClient client)
{
    public async Task<string> RunAsync(string changelog, CancellationToken ct)
    {
        ChatResponse response = await client.GetResponseAsync(
            [
                new ChatMessage(ChatRole.System, "You are a release-notes editor. Be terse."),
                new ChatMessage(ChatRole.User, changelog),
            ],
            cancellationToken: ct);
 
        // Usage is nullable: not every provider reports it, and cached or
        // filtered responses may report it partially.
        Console.WriteLine($"in={response.Usage?.InputTokenCount} out={response.Usage?.OutputTokenCount}");
 
        return response.Text;
    }
}

Note what the class depends on. Not a provider SDK, not an HTTP client, not an API key — just IChatClient. That is deliberate, and it is what makes the next lesson's token accounting and the last lesson's failure handling something you can add without touching this file.

Registering a client

Provider choice is one line, at the composition root, and everything after it is middleware.

Program.cs
using System.ClientModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using OpenAI;
 
var builder = Host.CreateApplicationBuilder(args);
 
// The only provider-specific line in the application.
IChatClient provider = new OpenAIClient(new ApiKeyCredential(builder.Configuration["Ai:Key"]!))
    .GetChatClient("gpt-4o-mini")
    .AsIChatClient();
 
builder.Services
    .AddChatClient(provider)
    .UseLogging()          // prompts and responses at Trace — never at Information
    .UseOpenTelemetry();   // token counts and durations as real metrics
 
var host = builder.Build();

AddChatClient returns a builder, and each Use… call wraps the client in a decorator. Caching, function invocation and telemetry all arrive the same way. It is the same pipeline idea as ASP.NET Core middleware, applied to a model call — which means the interesting production concerns are composable rather than scattered.

Finally, the property that surprises people. Run the same request twice:

Nondeterminism.cs
var options = new ChatOptions { Temperature = 0f, MaxOutputTokens = 200 };
 
string first = (await client.GetResponseAsync("Summarise this changelog.", options)).Text;
string second = (await client.GetResponseAsync("Summarise this changelog.", options)).Text;
 
// Very likely similar. Not guaranteed equal — not even at temperature 0.
Console.WriteLine(first == second);

Temperature 0 removes the sampler's randomness but not the rest: floating-point reduction order varies with server-side batching, and providers reroute traffic between hardware and model revisions without telling you. Treat exact-match assertions on model output as tests that will fail on a Tuesday for no reason. The last lesson in this course is about what to assert instead.

Next: what a token actually is, and why the is a budget you have to manage rather than a limit you occasionally hit.

Check yourself

4 questions about judgement calls from this lesson, 3 of them to pass. Answers are kept in this browser only, and passing does not mark the lesson complete — that stays the button below.

  1. Question 1 of 4A model hands you a `Microsoft.Extensions.AI` method signature that does not exist, and a colleague asks whether it "read an old version of the docs". What actually happened?Choose one answer.
  2. Question 2 of 4You are costing a chat feature that keeps a long conversation. Which of these follow from the model being autoregressive? Select all that apply.Choose every answer that applies.
  3. Question 3 of 4True or false: pinning `Temperature = 0f` makes two identical requests return identical text, so an exact-match assertion on the output is a safe test.Choose one answer.
  4. Question 4 of 4Put the steps of generating one more token in the order they actually happen.Use the arrow buttons to put these in order.
    1. The chosen token is appended and the model runs again on the longer sequence.
    2. One forward pass returns a probability distribution over the whole vocabulary.
    3. Sampling code outside the model picks one token from that distribution.
    4. Your application sends the entire message list, because nothing from the last call was kept.

Your score appears here. Answers are stored in this browser, so they follow the browser and not you.

Practise it

You write this one locally, in your own editor. Nothing here runs or reads your code.

One chat call, and nothing that knows the provider

Build a console app with a single class that answers a question through a model. The class may depend on IChatClient and nothing else — no provider SDK, no HttpClient, no key. Register a real provider at the composition root, add UseLogging() and UseOpenTelemetry(), print the reply and the token usage from response.Usage, then prove the seam by re-running the whole program against a stub IChatClient that returns a canned string, changing only the registration. A real call costs a fraction of a penny against your own key; the stub run costs nothing.

In the repository
exercises/ai-fundamentals/01-first-chat-client
Verify with
dotnet test exercises/ai-fundamentals/01-first-chat-client

Self-reported and not verified. Nothing runs your code or reads your repository — this records what you say you did, in this browser.

Your notes

Plain text, saved in this browser as you type. Nothing is uploaded, and there is no account to sync it to — export from your profile page to carry it elsewhere.