Skip to content
Software Atlas

How LLMs Work

What actually happens between your prompt and the reply: tokens, the next-token loop, sampling and temperature, the context window, attention, and why models confidently make things up — explained for engineers who build with them.

⏱ ~40 min read14 sections10 interactive widgetsGo · JavaScript
FoundationsPractitionerStaff-level
Foundations§01

The 30-second version

In one sentenceAn LLM predicts the next token (a word or a piece of a word), in a loop. Text goes in, the model gives a probability for every possible next token, one is picked and appended, and the whole thing goes back in. Repeat until done.

That's it. There's no database of facts inside, no plan for the whole answer, no memory between calls. A chat reply that looks like it was written in one go was really built one small piece at a time, and each piece was chosen by asking: given everything so far, what usually comes next?

Those pieces are the tokens: a common word, part of a word, or a punctuation mark (§02). Step through one reply:

The generation loop
Press Next: the model predicts one token, it's appended, and the whole text goes back in.
illustrative
Input to this run5 tokens
The·capital·of·France·is
↓ the model reads all of it ↓
Next-token probabilities (top 5)picked: ·Paris
·Paris86.0%
·a5.0%
·the3.0%
·located2.0%
·known1.0%
The whole prompt goes in. Out comes a probability for every possible next token (only the top 5 are shown). Paris is by far the most likely, so it is picked.
step 1 / 4
How this data was made: The tokens are a real o200k_base split. The probabilities were written by hand to be plausible; no model ran. The end token is shown as <end>; real tokenizers give it a special id.

Why this one idea matters to you as an engineer

Almost every surprising thing about LLMs follows from the loop:

What you noticeWhy (from the loop)Where
Limits and prices are in "tokens", not wordsThe model reads and writes tokens§02–§03
Same prompt, different answersThe next token is picked at random, weighted by probability§04–§05
It "forgets" earlier messagesIt only sees what you send in this one call§06
It confidently invents factsIt picks likely text; nothing checks if it's true§09
Text streams in word by word, and long prompts make the first word slowOne full model run per token, after reading the whole prompt§11

This page is the foundation for the rest of the AI Engineering area: Embeddings, Prompting, RAG and LLMs in Production all build on it. No math beyond a little arithmetic, and no training math at all: this is about using models as a software engineer.

Foundations§02

Tokens: what the model actually reads

In one sentenceA model never sees letters. A Tokenizer first cuts your text into tokens and replaces each one with a number, its token id. The model reads and writes only those numbers.

The tokenizer has a fixed list of every token it knows, called its vocabulary, usually 100,000 to 200,000 entries. Common words like " the" are one token. Rare words are split into pieces that are in the list. Try it on real text:

Tokenizer explorer
Pick a text. Each colored box is one token, exactly as a real tokenizer splits it.
Textreal data
The quick brown fox jumps over the lazy dog.
Tokens (o200k_base)
The·quick·brown·fox·jumps·over·the·lazy·dog.
44
characters
44
UTF-8 bytes
10
tokens
4.4
chars per token
Same text with cl100k_base: 10 tokens (the same). A box marked “2 tokens” holds a character whose UTF-8 bytes were split across tokens. · = space, ↵ = newline, ⇥ = tab.
How this data was made: Real. Precomputed with the gpt-tokenizer npm package (v4.1.0): o200k_base (the encoding OpenAI documents for its GPT-4o-era models, about 200k tokens) and cl100k_base (GPT-4 / GPT-3.5, about 100k). Other model families (Claude, Gemini, Llama…) use their own tokenizers, so their counts differ. Script: data-gen.mjs.txt in this topic's folder.

What to notice

  1. Spaces are part of tokens. "hello" and " hello" (with a leading space) are different tokens with different ids. So are " Hello" and " HEL"+"LO". To the model, these are unrelated numbers that it has learned are related.
  2. English is cheap, other languages often aren't. The English sentence averages over 4 characters per token. Vietnamese and Japanese pack fewer characters into each token, especially with the older cl100k_base, where the Vietnamese sentence takes twice as many tokens.
  3. Code and JSON are full of small tokens. Punctuation like {", ": and ;\n each cost a token. Indentation costs tokens too.
  4. Numbers are chopped into chunks. 1234567 becomes 123 · 456 · 7. The model never sees "one million two hundred…", which is one reason LLMs are unreliable at arithmetic.
  5. One emoji can be two tokens. Most emoji are 4 bytes in UTF-8, and some tokens hold only some of those bytes. Some emoji are several characters glued together: 👩‍💻 is 👩 + an invisible "zero-width joiner" + 💻, and the joiner is a token of its own. (Bytes, code points and UTF-8 are explained in Text & Encodings.)

Why not just use letters, or whole words?

  • Letters would make every text very long: this paragraph would be hundreds of steps instead of dozens. The model runs once per token (§01), so that would be slow and expensive, and it would have to learn spelling before it could learn meaning.
  • Whole words need an impossibly large list (every name, typo, URL, word in every language, every variable name in every codebase), and anything not in the list can't be read at all.

Tokens are the compromise: frequent text gets short, single tokens; anything else can still be spelled out from smaller pieces, down to single bytes. §03 shows how that list is built.

Practitioner§03

Where tokens come from, and why counts matter

In one sentenceMost tokenizers are built with byte-pair encoding (BPE): start with single bytes, then keep gluing together the pair of neighbors that appears most often. Frequent chunks of text end up as single tokens.

Here is the whole algorithm, run for real on a tiny word list. Watch low, new, er and est appear:

Training a tiny BPE tokenizer
Start from single letters. Repeatedly glue together the pair that appears most often.
real (toy)
Training words, split with the current rules(× how often)
low×6
lower×3
lowest×3
slow×3
slower×2
slowest×2
new×4
newer×3
newest×4
old×3
older×2
oldest×2
Merge rules learned0
  1. none yet: every letter is its own token
Vocabulary9 tokens
delnorstw
Every word starts as single letters. The tokenizer counts every pair of neighbors across all words, weighted by how often each word appears.
step 1 / 11
How this data was made: Real, but tiny. The merges were computed by the script data-gen.mjs.txt from these 12 hand-written words and counts (ties broken alphabetically). Production tokenizers start from bytes, not letters, and learn their merges from terabytes of text.

A real tokenizer does exactly this, with three differences: it starts from the 256 possible bytes (so any text, in any language, can always be encoded), it learns from a huge text collection, and it stops after roughly 100,000–200,000 merges. The merge list is then frozen. Encoding new text means applying the same merges in the same order, so the same text always gives the same tokens.

The merge list depends on the text it was learned from. o200k_base has whole-word tokens like " trời" and " đẹp", so it encodes the Vietnamese sentence in §02 with half the tokens of cl100k_base, which has to spell those words from smaller pieces. A larger vocabulary, built from a broader mix of languages, buys shorter token sequences.

Why token counts matter

Limits are in tokens. The Context window (§06) and the maximum reply length are counted in tokens, not characters or words.

Cost is in tokens. Model APIs bill per token, usually at one price for input tokens and a higher one for output tokens. The same document can cost noticeably more in one language than another, or with one model family than another, purely because of tokenization.

A rough rule of thumb, for English prose only: about 4 characters, or ¾ of a word, per token. It's fine for a back-of-the-envelope estimate ("a 3,000-word document is about 4,000 tokens"). It's wrong for code, JSON, numbers and most other languages, as §02 showed. Never use it to decide whether a request will fit; count with the provider's tokenizer.

The strawberry problem

Ask a model how many r's are in "strawberry" and it may well say two. In §02's explorer, " strawberry" is one token: id 101830 in o200k_base. The model doesn't see ten letters; it sees one number. To count the r's, it has to remember how that token is spelled, from patterns in its training text, and that memory can be wrong.

The same blind spot shows up as trouble with reversing strings, counting words exactly, rhyming, and spotting a typo inside a long word. Newer models are trained to work around it, and often get these right, but the fix in your application is simple: if code can compute it, let code compute it, and give the model the result.

Foundations§04

Predicting the next token: scores to probabilities

In one sentenceEach time the model runs, it outputs one score for every token in its vocabulary. A function called softmax turns those scores into probabilities that add up to 100%. Then one token is chosen.

What "the model" is, in one paragraph

An LLM is a neural network: each token id is first swapped for a long list of numbers that stands for that token (an embedding, see Embeddings), and then layer after layer of simple arithmetic (multiply, add) is applied to those numbers. The numbers it multiplies by are its weights, billions of them, set during training (§08). Pushing your text through all the layers once is called a forward pass. You don't need to know what happens in each layer to build with LLMs; you need to know what comes out.

What comes out

What comes out of one forward pass is a list of about 200,000 numbers, one per vocabulary token, called logits. A higher logit means "this token is more likely to come next". Here are 8 of them, for the text The cat sat on the:

From scores to probabilities
Prompt: The cat sat on the → what comes next? (8 of the ~200,000 tokens shown)
Tokenillustrative
·mat5.0
·floor3.6
·couch3.2
·bed2.8
·windowsill2.4
·keyboard2.0
·roof1.2
·moon-1.0
The model's raw output is one score (a logit) per token in its vocabulary. Bigger means “more likely next”. Scores can be negative and don't add up to anything.
How this data was made: The scores were written by hand to look like a plausible model output; no model ran. The arithmetic (ex, dividing by the sum) is computed live and is exactly what real models do.

Walk through the three stages in the widget. The arithmetic is the whole of softmax:

  1. Raise e (about 2.718) to the power of each score. Negative scores become small positive numbers; big scores become much bigger.
  2. Add them all up.
  3. Divide each one by that total. Now every value is between 0 and 1, and they sum to 1.
function softmax(logits) {
  const exps = logits.map((x) => Math.exp(x));    // step 1
  const sum = exps.reduce((a, b) => a + b, 0);    // step 2
  return exps.map((e) => e / sum);                // step 3
}

console.log(softmax([5.0, 3.6, 3.2]).map((p) => p.toFixed(3)));
// [ '0.708', '0.175', '0.117' ]

Then: pick one

The simplest way to choose is to always take the most likely token. That's called greedy decoding, and here it picks " mat". But notice the model doesn't know the answer is "mat": it says "mat" about 60% of the time, "floor" about 15%, and so on. The probabilities describe what text like this usually continues with, not what is true. Keep that in mind for §09.

Most of the time, apps don't pick greedily; they sample, which is the next section.

Practitioner§05

Choosing a token: temperature and top-p

In one sentenceInstead of always taking the top token, models usually sample: pick at random, weighted by probability. Temperature makes the odds sharper or flatter first; top-p cuts off the unlikely tail.

Why not always pick the top token? Because greedy text tends to be bland and can get stuck repeating itself ("the cat sat on the mat. the cat sat on the mat…"). A little randomness gives more natural, varied text. It's also why the same prompt can give different answers.

Temperature: divide the scores, then softmax

Temperature is one number, T. Before softmax, every logit is divided by T:

  • T below 1 makes the gaps between scores bigger, so the favorite gets even more likely (sharper).
  • T = 1 leaves the model's probabilities as they are.
  • T above 1 shrinks the gaps, so unlikely tokens get a real chance (flatter).
  • T = 0 would mean dividing by zero, so it's defined as "always take the top token": greedy.

Top-p (also called nucleus sampling) runs after that: sort tokens by probability, keep the smallest group whose probabilities add up to at least p (say 0.9), drop the rest, and scale the survivors back up to 100%. It stops a rare, silly token from ever being picked, without making the common choices less varied.

Try both, and press Sample 100× at a few temperatures:

Next-token sampler
Prompt: The cat sat on the. Reshape the distribution, then sample from it.
illustrative
Probability after temperature and top-pbars = chance · counts = your samples
·mat59.8%
·floor14.8%
·couch9.9%
·bed6.6%
·windowsill4.4%
·keyboard3.0%
·roof1.3%
·moon0.1%
no samples yet
Temperature 1 keeps the model's own probabilities. Most samples are  mat, but about 4 in 10 aren't.
How this data was made: The 8 scores (logits) are illustrative, written by hand. Temperature (divide every score by T before softmax), top-p and the sampling itself are real math, computed live in your browser.

Here's the same thing as code. It's short because it's just the widget's math:

function sampleNext(logits, temperature) {
  if (temperature === 0) return logits.indexOf(Math.max(...logits)); // greedy
  const scaled = logits.map((x) => x / temperature);  // ← temperature is just this
  const probs = softmax(scaled);                      // from §04
  let r = Math.random();                              // a number in [0, 1)
  for (let i = 0; i < probs.length; i++) {
    r -= probs[i];                                    // walk along the bars…
    if (r < 0) return i;                              // …until r runs out
  }
  return probs.length - 1;                            // guard against rounding
}

A real language model you can run: bigrams

Everything above used hand-written scores. Here's the same loop with a real model: a bigram model, trained by counting which word follows which in 21 short sentences. It looks at only one previous word. Generate a few sentences at temperature 0, 1 and 2:

A real (tiny) language model: bigrams
It predicts the next word from only the previous word, using counts from 21 short sentences.
real model
Generated text0 tokens
press “Generate 10 tokens”
After “start of sentence”count → chance
the1990%
a210%
Same loop as an LLM: look at the context, get a probability for each next word, sample, append, repeat. The big differences are scale and context: this model sees 1 previous word and learned from 136 tokens (words and periods); an LLM sees its whole context window and learned from trillions of tokens. The output is grammatical-ish and says things that never happened (“the dog ate the server”) — a preview of §09.
How this data was made: Real. The script data-gen.mjs.txt counted which word follows which in 21 sentences written for this page (words are the tokens here). Temperature is applied to the counts (chance ∝ count1/T). Sampling runs in your browser. A “.” ends a sentence and the next word is picked as a sentence start.

At temperature 0 it gets stuck: “the cat sat on the cat sat on the…”, because after “the” the top word is “cat” (tied with “dog” at 6 each; ties go alphabetically), then “sat”, then “on”, then “the” again. That is the repetition problem from the top of this section, in miniature. At 2 it wanders into odd but grammatical-ish combinations. An LLM is this same loop, with a context of thousands of tokens instead of one word, and billions of learned weights instead of a table of counts.

Which settings to use

TaskTypical choice
Extraction, classification, code, anything with one right answerLow temperature (0–0.3), or the provider's default
Chat, explanationsThe provider's default (often 1, or close to it)
Brainstorming, varied draftsHigher (around 1–1.2); generate several and pick

Change one of temperature or top-p, not both; they interact. And check what your API allows: as of October 2026, Anthropic's Messages API returns an error if you send temperature, top_p or top_k (top-k: keep only the k most likely tokens, a cruder cousin of top-p) with API version 2026-10-21, so all requests use the default settings. Several other providers' reasoning-focused models accept only the default value too. The mechanism still runs on the server; you just can't tune it there.

Why temperature 0 still isn't perfectly repeatable

Temperature 0 removes randomness from the choice. It doesn't guarantee the scores are identical every time. Servers process many users' requests together in a batch, and the batch size changes with load. Different batch sizes can make the GPU add up the same numbers in a different order, and with floating-point numbers, a different order can give slightly different results (see Numbers in Computers). When two tokens are nearly tied, a tiny difference flips the pick, and every token after that differs.

In a September 2025 write-up, researchers at Thinking Machines Lab reported sending the same prompt 1,000 times at temperature 0 to an open model served with the vLLM inference server, and getting 80 different completions. They traced it to exactly this batch effect and showed that specially written "batch-invariant" GPU code made all 1,000 identical, at a cost in speed. Hosted APIs don't generally promise identical outputs.

Foundations§06

The context window: the model's entire world

In one sentenceThe Context window is everything the model sees in one call: instructions, the conversation so far, any pasted documents, and the reply it's writing. It has a fixed maximum size in tokens. Anything not in it doesn't exist for the model.

The model has no memory

LLM APIs are stateless: each request stands alone, and the server forgets it once the reply is sent. So how does a chat app remember your name? Your app keeps the conversation and sends all of it, every time:

Call 1:  [system] + [user: Hi, I'm Mai]                                     → "Hi Mai!"
Call 2:  [system] + [user: Hi, I'm Mai] + [assistant: Hi Mai!] + [user: …]  → …
Call 3:  [system] + … every earlier message … + [user: What's my name?]     → "Mai"

The System prompt (the app's standing instructions) goes in front of every call too. Three consequences follow:

  1. Every message makes later calls bigger. Message 30 of a chat re-sends messages 1–29. You pay for those input tokens again on every call, and long inputs are slower to start (§11).
  2. The window fills up. Once system prompt + history + room for the reply exceeds the limit, something has to go.
  3. "Memory" features are your code. If a product remembers things across chats, it's storing notes somewhere and pasting them into the prompt.

When the window is full

Play the conversation below. The window sizes are tiny so you can see it happen; the token counts are real.

The context window
Add messages one by one. Everything that fits is re-sent on every call; the rest is gone.
Window size:
What the model receives this call62 + 20 reserved for the reply / 120
systemuserassistantsummaryreply room
system · 13 tok · always keptYou are a helpful assistant for a bike shop. Answer briefly.
user · 18 tokHi! My name is Mai and I ride a 2019 Trek FX 3.
assistant · 17 tokHi Mai! The FX 3 is a great hybrid. How can I help?
user · 14 tokMy rear brake squeaks when it rains. What should I check?
Each call sends the system prompt plus as many recent messages as fit, leaving room for the reply. Total so far: 62 tokens of messages.
How this data was made: Token counts are real (o200k_base, message text only; real APIs add a few formatting tokens per message). The window sizes are tiny on purpose; real ones are thousands of times bigger. The model replies are illustrative, and the summary was written by hand.

The common strategies:

  • Drop the oldest messages (a "sliding window"). Simple, and recent context is usually what matters. But a fact from early on, like the user's name, silently disappears.
  • Summarize the oldest messages into a short note and keep that instead. Keeps the gist, loses detail, and costs an extra model call to write the summary. Some APIs can do this server-side for you.
  • Keep the important things pinned: the system prompt always; key facts in a short "notes" block you maintain in code.
  • Retrieve instead of including everything. Store documents outside the prompt and paste in only the relevant parts per question. That's RAG.

Whichever you choose, if you don't handle it, the API does it for you in the least friendly way: an error saying the request is too long.

How big are windows?

Big, and growing. As of October 2026, Anthropic's current models, for example, accept up to 1 million tokens of input per request, and other providers offer windows from about a hundred thousand to over a million tokens. Check the model's documentation; the limit is per model, not per provider.

A bigger window doesn't mean you should fill it. More tokens cost more and take longer to process (§11). And models don't use every part of a long context equally well: a 2023 study, "Lost in the Middle" (Liu et al., published in TACL 2024), found that the models it tested answered best when the relevant information was at the start or end of a long input, and noticeably worse when it was buried in the middle. Newer models have improved, but the practical advice stands: put the instructions and the most important material where they're easy to find, and don't paste in what isn't needed.

Practitioner§07

Attention: which earlier tokens matter

In one sentenceInside the model, each token looks back at the earlier tokens and decides how much each one matters for it. That step is called attention, and it's how a model connects "it" to "the cat" ten words earlier.

Take The cat sat on the mat because it was tired. To predict what comes after "it", the model has to work out what "it" refers to. The word "it" alone says nothing; the information is in "cat", several tokens back. Attention is the mechanism that pulls it in.

The intuition

At each layer of the network, every token does three things:

  1. Asks a question, like "I'm a pronoun: which noun am I about?"
  2. Scores every earlier token (and itself) on how well it answers that question. The scores go through a softmax (§04), so they add up to 100%.
  3. Mixes in information from those tokens in those proportions: 50% of what "cat" carries, 20% of "mat", a little of the rest.

After that, the "it" token isn't just "it" anymore; it carries "it, probably the cat". The next layer builds on that, and so on through dozens of layers.

Attention: who looks at whom
Click a token (or a row) to see which earlier tokens it pays attention to.
TheThecatcatsatsatononthethematmatbecausebecauseititwaswastiredtired..
Row = the token looking. Column = the token looked at. Brighter = more attention. The empty upper triangle is the rule that a token can't see tokens after it.
“it” attends toillustrative
The3%
·cat52%
·sat6%
·on2%
·the3%
·mat22%
·because8%
·it4%
To guess what follows “it”, the model needs to know what “it” is. In this head, “it” puts about half its attention on “cat” and some on “mat”. That mix of information flows into the next layer. (Cats get tired; mats don't.)
How this data was made: The tokens are a real o200k_base split. The attention weights are illustrative, written by hand to show typical patterns; they did not come from a model. A real model has dozens of layers, each with many heads, and every head produces a grid like this.

What the picture shows

  • The empty triangle. A token can only attend to tokens before it (and itself). When the model is predicting the next token, the future doesn't exist yet, so it's masked out during training too. That's why it's called a "causal" model.
  • Many heads. Each layer runs many attention heads side by side. Each one asks a different question: one tracks what pronouns refer to, one looks at the previous word, one matches an opening bracket to its closing bracket. Nobody programs these. They emerge from training, and researchers study them after the fact.
  • The weights aren't fixed. Unlike the model's weights (§04), attention scores are computed fresh for every input. Change the sentence to "…because it was soft." and a well-trained model shifts attention from "cat" toward "mat".

Why it matters for you

This design is called a transformer (from a 2017 paper, "Attention Is All You Need"), and it's what every major LLM is built on. Two practical facts come straight out of it:

  • Every token can reach every earlier token, however far back. That's what makes long documents, long code files and long conversations workable at all.
  • The work grows with length. Each new token attends to all the tokens before it, so a longer context means more work per token and more memory. That's one reason long prompts cost more and why §11 exists.
Foundations§08

Training vs. inference

In one sentenceA model is trained once, for weeks, at enormous cost. Every API call after that is inference: running the finished model with its weights frozen. Nothing you send changes it.
Where a model comes from, and what your API call touches
Training happens once, for weeks. Your call only runs the finished model.
Huge text collection
web pages, books, code, papers… up to a cutoff date
1 · Pretraining
guess the next token, check, nudge the weights. Billions of times.
→ a base model: continues any text
2 · Tuning
example conversations + human (or AI) feedback on which answers are better
→ an assistant model: follows instructions
3 · Inference (your API call)
weights are frozen. Your prompt in, tokens out. Nothing is learned.
🗓 Knows nothing after its knowledge cutoff, unless you put it in the prompt.
🧊 Doesn't learn from your chats while answering. (A provider may use data to train a future model; that's a policy question, not how a call works.)
📦 Everything it “knows” is squeezed into the weights: billions of numbers, not a database of facts.

Pretraining is where nearly all the knowledge comes from. The model is shown a vast amount of text, and at every position it predicts the next token, compares its guess with the real one, and nudges its weights slightly toward the right answer. Repeated over trillions of tokens, that single task forces it to pick up grammar, facts, coding patterns and a lot of reasoning-shaped behavior, because all of those help predict text. The result of pretraining is a base model: give it "Dear Sir," and it continues the letter. Ask it a question and it might continue with three more questions, because that's what text on the internet often does.

Tuning turns that into an assistant. The base model is trained further on a much smaller set of example conversations (a prompt and a good answer), and then on feedback about which of two answers people, or another model, preferred. This fine-tuning teaches it to answer instead of continue, to follow instructions and formats, and to decline some requests. It adds behavior far more than knowledge.

What this means when you call it

  • It doesn't learn from your conversation. If it gets something wrong and you correct it, it uses the correction for the rest of this conversation, because the correction is in the context (§06). Next conversation, the mistake is back.
  • It has a knowledge cutoff. The training text stops at some date. Ask about anything later (a new library version, last week's outage, today's date) and it either says it doesn't know or, worse, makes up something plausible (§09). If the answer depends on fresh or private information, put that information in the prompt.
  • Its knowledge is fuzzy, not stored. There's no row in a table saying "the capital of France is Paris". There are weights that make "Paris" very likely after that phrase. Popular facts are reliable; rare ones (a small library's API, a specific paper's page numbers) are shaky.
  • Your data and training are a policy question. Whether a provider may use your API traffic to train future models is set by their terms and your account settings, not by how a call works. Check before sending sensitive data.
Foundations§09

Why LLMs hallucinate

In one sentenceA model writes the most plausible next token, not the checked one. Most of the time plausible and true line up. When they don't, you get a Hallucination: fluent, specific, confident, and false.

Nothing in the loop from §01 looks anything up. When the model "knows" a fact well, the true continuation has a high probability and wins. When it doesn't, there's no alarm, no empty result, no null: the loop still has to output some token, and it picks one that fits the pattern. Watch a citation get built that way:

How a fake citation gets written
Each step picks a likely token. None of them checks a fact.
illustrative
Context
User: Cite a peer-reviewed paper on preventing cache stampedes.
Assistant: See Chen▍
Candidates for this steppicked: Chen
·Vattani9.0%
·Chen8.0%
·Smith6.0%
·Zhang6.0%
·I3.0%
No first author is clearly likely, but the loop must output some token. The real author (Vattani) is only slightly ahead of plausible surnames, and “ I” (the start of “I'm not sure…”) is just another low-probability token. Sampling picks Chen.
step 1 / 6
How this data was made: All probabilities are illustrative, written by hand. The real paper is: A. Vattani, F. Chierichetti, K. Lowenstein, “Optimal Probabilistic Cache Stampede Prevention”, PVLDB 8(8), 2015. The point: the true answer and a fake one come out of the same process, and the output looks equally confident either way.

Why it happens

  1. Likely isn't true. The model learned what text looks like. A made-up citation looks exactly like a real one: same format, plausible names, a reasonable year.
  2. "I don't know" isn't the default. Saying "I'm not sure" is just another continuation, and it competes with confident-sounding ones. Tuning makes models admit uncertainty more often, but they still can't reliably tell what they know from what they're guessing.
  3. Each token commits. Once "Chen et al." is written, the rest of the answer is built to be consistent with it. The model doesn't go back and revise; it continues what's there.
  4. Rare facts are fuzzy. Popular facts appear thousands of times in training text; a specific paper's title or a function in a niche library might appear a handful of times or never (§08).

Where it bites hardest

  • Citations, quotes, URLs and references: exact strings the model can't verify. Treat every one as unverified until checked.
  • Numbers: statistics, prices, dates, version numbers, line numbers. Tokenized in chunks (§02), easy to get "almost right".
  • APIs and code: function names and flags that should exist, by analogy with similar libraries. The code looks idiomatic and fails to compile, or worse, compiles and does something else.
  • Anything after the knowledge cutoff or private to your company: the model has never seen it.

What reduces it

You can't switch hallucination off with a setting (a low temperature only makes the model more consistent, including consistently wrong). You design around it:

  • Ground the answer. Put the facts in the prompt (retrieved documents, database rows, tool results) and instruct the model to answer only from them and to say when they don't contain the answer. That's grounding, and RAG is the standard way to do it.
  • Use tools for facts and arithmetic. Let the model call a search, a database query or a calculator and use the result (Tool calling; see Agents & Tool Calling).
  • Verify in code. Check that cited documents exist and contain the quoted text, that generated JSON matches a schema, that code compiles and tests pass.
  • Measure. Run a set of questions with known answers on every prompt or model change, and track how often it's right (Evaluating LLM Apps).
  • Design the UI for it. Show sources, let users check them, and don't present generated text as authoritative where a mistake is costly.
Practitioner§10

One API call, end to end

In one sentenceCalling a model is one HTTPS request: the whole conversation goes in, the next reply comes out, along with how many tokens went each way and why it stopped. Everything from §01–§06 shows up in the request.

Here's a minimal chat call to Anthropic's Messages API, as documented in October 2026 (API version 2026-10-21). Other providers name things differently (messages vs input, max_tokens vs max_output_tokens), but every one has the same parts.

chat.mjs
let history = []; // the whole conversation lives in YOUR program

async function ask(userText) {
  const messages = [...history, { role: "user", content: userText }];
  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "x-api-key": process.env.ANTHROPIC_API_KEY, // never hard-code a key
      "anthropic-version": "2026-10-21",
    },
    body: JSON.stringify({
      model: "claude-opus-5-5",
      max_tokens: 16000, // cap on the reply's length, in tokens
      system: "You are a helpful assistant for a bike shop. Answer briefly.",
      messages, // ← the WHOLE conversation, every call
    }),
    signal: AbortSignal.timeout(120_000), // replies can take a while; never wait forever
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  const msg = await res.json();
  console.log(msg.usage.input_tokens, msg.usage.output_tokens, msg.stop_reason);
  if (msg.stop_reason === "refusal") throw new Error("the model declined this request"); // history unchanged
  if (msg.stop_reason === "max_tokens") console.warn("reply was cut off at max_tokens"); // show it as incomplete
  history = [...messages, { role: "assistant", content: msg.content }]; // success: keep both turns
  return msg.content.filter((b) => b.type === "text").map((b) => b.text).join("");
}

console.log(await ask("My name is Mai. Which brake pads fit a 2019 Trek FX 3?"));

Not run here (it needs an API key, and this site never calls a model). The Go version compiles with go vet; its imports are bytes, encoding/json, errors, fmt, io, log, net/http, os, slices and time.

Three details make it safer than the shortest possible version. It sets a timeout (2 minutes here), because a long reply can take a while and a hung connection shouldn't hang your app. It adds the new turns to history only after a successful reply, so a failed call doesn't leave a dangling user message. And it acts on stop_reason (explained below): a refusal becomes an error, and a cut-off reply is flagged. Keep the key in an environment variable or a secrets manager (Secrets), never in code or in the browser.

Reading the response

The response is JSON shaped like this (the values here are illustrative):

{
  "role": "assistant",
  "content": [{ "type": "text", "text": "Your FX 3 has Shimano hydraulic disc brakes…" }],
  "stop_reason": "end_turn",
  "usage": { "input_tokens": 41, "output_tokens": 63 }
}
  • usage is the token bill for this call. input_tokens grows with every message in history; that's §06's "every message makes later calls bigger", in numbers. Log both counts on every call.
  • stop_reason says why generation ended. end_turn means the model picked its end token (§01). max_tokens means it hit your max_tokens cap and the reply is cut off mid-sentence: show that to users as incomplete, or raise the cap. Other values cover tool calls, stop strings you defined and refusals.
  • No temperature. In this API version you can't send it (§05), so the request has none. On APIs that accept it, it's one more field next to max_tokens.

Trimming history to a budget

The code above grows history forever. Here's the "drop oldest" strategy from §06, runnable without an API key. It works on the same Message shape as ask(): user turns are plain strings, assistant turns are the block arrays the API returned, so it counts only the text inside them. (Skip that step and an array's length gets estimated instead of its text, and nothing is ever trimmed.) It uses a rough estimate (§03's ~4 characters per token) so it can run anywhere; for real numbers, call the provider's token counter (Anthropic's is POST /v1/messages/count_tokens, as of October 2026).

// The text in a message: content is a string (what we send) or an array of
// blocks (what the API returns). Only "text" blocks count here.
const textOf = (content) =>
  typeof content === "string"
    ? content
    : content.filter((b) => b.type === "text").map((b) => b.text).join("");

// Rough estimate: about 4 characters per token for English text.
// For real numbers, use your provider's token-counting endpoint.
const estimateTokens = (content) => Math.ceil(textOf(content).length / 4);

// Keep the newest messages that fit in `budget` tokens; drop the oldest.
function fitHistory(messages, budget) {
  const kept = [];
  let used = 0;
  for (let i = messages.length - 1; i >= 0; i--) { // newest → oldest
    const cost = estimateTokens(messages[i].content);
    if (used + cost > budget) break;               // ← everything older is dropped
    kept.unshift(messages[i]);
    used += cost;
  }
  while (kept.length && kept[0].role !== "user") { // the API wants a user message first
    used -= estimateTokens(kept.shift().content);
  }
  return { kept, used };
}

// A history shaped like ask() builds it: user turns are strings,
// assistant turns are the block arrays the API returned.
const user = (text) => ({ role: "user", content: text });
const assistant = (text) => ({ role: "assistant", content: [{ type: "text", text }] });
const history = [
  user("Hi! My name is Mai and I ride a 2019 Trek FX 3."),
  assistant("Hi Mai! The FX 3 is a great hybrid. How can I help?"),
  user("My rear brake squeaks when it rains. What should I check?"),
  assistant("Wet rim or disc brakes often squeak. Clean the rotor with isopropyl alcohol and check the pads for glazing or wear."),
  user("The pads look shiny. Should I replace them?"),
  assistant("Shiny pads are glazed. You can sand them lightly, but new resin pads are cheap and usually fix it."),
  user("What was my name again?"),
];

const { kept, used } = fitHistory(history, 70);
console.log(`kept ${kept.length} of ${history.length} messages, ~${used} tokens`);
for (const m of kept) console.log(`${m.role.padEnd(9)} ${textOf(m.content).slice(0, 40)}`);

Real output (both versions print the same):

kept 3 of 7 messages, ~42 tokens
user      The pads look shiny. Should I replace th
assistant Shiny pads are glazed. You can sand them
user      What was my name again?

And there's the bug from §06, in real code: the message with Mai's name was dropped, so the model will be asked "What was my name again?" without it.

Staff-level§11

Staff-level: why tokens come out one at a time

In one sentenceGenerating a reply takes one forward pass per output token, and each pass needs the token before it. Reading the prompt, though, happens all at once. Those two facts explain LLM latency and most of its cost.

Two phases: prefill and decode

Prefill handles the whole prompt. Every prompt token is already known, so the model can process all of them in parallel, which a GPU (the kind of chip AI models run on) does very efficiently. Prefill ends by producing the first output token. Its length grows with the prompt, so it dominates time to first token.

Decode produces everything after that, one token per pass. These passes are sequential: token 7 can't be computed before token 6 exists, because token 6 is part of its input. Decode speed (tokens per second) sets how fast text streams after it starts.

Prefill, decode and the KV cache
Each row is one pass through the model. Each pass outputs one token.
Thecatsatonthematbecauseitwastiredcomputed
prefill5
decode 11
decode 21
decode 31
decode 41
computed in this passread from the KV cachetoken produced
5 positions
computed so far for 1 output token (cache on)
10,000-token prompt, 500-token reply:
cache on: 10,499 · off: 5,124,750
Prefill: the whole prompt is processed in one pass, all positions in parallel (GPUs are great at that). It ends by producing the first token. This is most of the wait before the first token appears.
step 1 / 5
How this data was made: The position counts are real arithmetic (with cache: prompt + one per later token; without: every pass redoes the whole prefix). The sentence is illustrative. “Positions computed” is a simplification: with a cache each new token still reads all cached entries, so long contexts make each step somewhat slower and use more GPU memory.

The KV cache

In each pass, every token computes keys and values for attention (§07): what it offers to tokens after it, and what it passes along. Those don't change once computed, because a token never attends to tokens after it. So the server keeps them in GPU memory, in the KV cache, and each decode pass only computes the newest token. That turns the quadratic "recompute everything" work from the widget into roughly one position's worth of computation per new token.

The cache isn't free:

  • It costs memory, proportional to the number of tokens in context, for every request being served at once. Long contexts mean fewer requests fit on one GPU, which is part of why long-context requests can be priced higher.
  • Each decode step still reads the whole cache. Attention looks at every earlier token, so decode slows down a bit as the context grows.

What this means for your app

You observeBecauseWhat to do
Long pause, then fast streamingPrefill of a long prompt, then cheap decode stepsShorten prompts; stream the reply (SSE) so users see progress
Output tokens usually cost more than input tokensPartly because each output token needs its own sequential pass, while input is processed in parallel (providers set prices for their own reasons)Ask for concise output; cap max_tokens
Long reply = long waitTime ≈ prefill + (output tokens × time per token)Generate less, or split the task
Repeated big system prompt or document is slow and costlyPrefill redone on every callUse the provider's prompt caching: it keeps the processed prefix (the KV cache for it) between calls
Throughput drops under loadServers batch many users' decode steps together on one GPUExpect latency to vary with load; set timeouts and retries

Some models also generate hidden "reasoning" tokens before the visible answer. Those are ordinary decode steps too: they add to the wait before the first visible token and are usually billed as output. LLMs in Production covers streaming, caching, retries and cost control in depth.

Practitioner§12

Pitfalls gallery

#1

Assuming the model remembers

A chat feature sends only the newest message, or a background job "continues" a conversation by sending just a follow-up. The model has no idea what came before (§06). Every call must carry everything the model needs: system prompt, relevant history, relevant data.

#2

Counting characters instead of tokens

if (text.length < 100_000) send(text) assumes a fixed ratio of characters to tokens. Code, JSON, numbers and non-English text can need two or three times more tokens than English prose (§02), and the request fails, or costs far more than planned. Count with the provider's tokenizer or token-counting endpoint, and keep a margin.

#3

Expecting byte-identical output at temperature 0

A test asserts reply === "Positive", and it passes 99 times, then fails in CI for a reply of "positive.". Temperature 0 removes sampling randomness but not every source of variation (§05), and some APIs don't let you set it at all. Parse and validate output instead of comparing strings, and test quality with evals over many inputs.

#4

Showing citations, numbers or API names without checking

A support bot quotes "section 4.2 of our refund policy", which doesn't exist; a coding assistant calls fs.readJSON(), which isn't in Node's standard library. Specific-looking output is the most likely to be invented (§09). Ground answers in retrieved text and verify citations, links and code in your own code before users see them.

#5

Ignoring stop_reason

The reply is truncated because it hit max_tokens, and the app saves half a JSON object, or shows a sentence that stops mid-word. Always check why generation stopped (§10). On max_tokens, retry with a higher cap, ask for shorter output, or mark the result incomplete.

#6

Asking about things after the knowledge cutoff

"What changed in the latest version of library X?" The model answers fluently about whatever version it saw in training, or invents a changelog (§08). Fetch the current information (docs, release notes, your database) and put it in the prompt.

#7

Burying the important part in a giant prompt

Thirty pages of pasted documents, with the actual instruction in the middle. Long contexts are processed, but not used equally well everywhere, and every extra token adds cost and time to first token (§06, §11). Include only what's relevant, and put instructions and key facts where they stand out: usually at the start or the end.

#8

Treating the system prompt as a security boundary

"The system prompt says never reveal the discount codes, so it's safe to put them in." Text in the context can be talked out of the model, and user-supplied or retrieved text can carry instructions of its own. Never put secrets in a prompt, and enforce permissions in code. AI Security covers prompt injection in depth.

Practitioner§13

Check yourself

Predict the answer before you click. Q1 is covered in §02–§03, Q2 in §06, Q3 in §05, Q4 in §11 and Q5 in §09.

QUESTION 1 / 5
Counting letters

You ask a model “How many r's are in strawberry?” and it says 2. Using §02–§03, what's the best explanation?

QUESTION 2 / 5
The forgetful chatbot

Your chat app calls the API with only the latest user message each time. A user says “My name is Mai”, then later asks “What's my name?”. What happens?

QUESTION 3 / 5
Temperature 0

You set temperature to 0 (on an API that allows it) and send the same prompt 100 times. What should you expect?

QUESTION 4 / 5
Why is the first token slow?

Your app sends a 50,000-token document and asks a one-line question. The reply starts after a long pause, then streams quickly. Why?

QUESTION 5 / 5
A citation from nowhere

A model answers with a citation: author, year, title and venue. What's the right next step before showing it to users?

Answered 0 / 5
Foundations§14

Cheat sheet

🔁
A next-token loopPrompt in → a probability for every token → pick one → append → repeat, until the end token or max_tokens.
🧩
Tokens, not charactersText is split into tokens from a fixed vocabulary (built with BPE). Limits and prices are in tokens. Each model family has its own tokenizer.
📊
Scores → probabilitiesThe model outputs logits; softmax turns them into probabilities. Greedy takes the top one.
🌡️
Temperature & top-pTemperature divides the scores (low = sharper, high = flatter); top-p cuts the unlikely tail. T = 0 still isn't guaranteed identical.
🪟
The window is the whole worldThe API is stateless: re-send what the model needs every call. Full window → drop, summarize or retrieve.
👀
AttentionEach token looks back at earlier ones and mixes in what matters. Work grows with context length.
🧊
Frozen weightsTrained once; inference doesn't learn. Knowledge stops at the cutoff; put fresh facts in the prompt.
🎭
Plausible ≠ trueHallucinations come from picking likely text. Ground, use tools, verify, and run evals.
⏱️
Prefill, then decodeThe prompt is read in parallel (sets time to first token); output comes one pass per token, sped up by the KV cache.
I want to…Do this
Estimate tokens for English prose~4 characters or ¾ of a word per token; for anything that matters, count with the real tokenizer
Keep a chat coherentSend the system prompt + recent history every call; trim or summarize when near the budget
Get more consistent outputLower temperature where the API allows it; constrain the format; validate the result
Stop invented factsPut source material in the prompt (RAG), use tools, verify citations in code
Make it feel fastStream the reply; keep prompts lean; use prompt caching for big repeated prefixes
Know what a call costLog usage.input_tokens / usage.output_tokens and check stop_reason on every response
Finished reading?Mark it complete to track your progress on a roadmap. Saved in this browser only.
Up next in AI EngineeringEmbeddings
Read →