How LLMs Actually Work — For Builders
12 min
By the end of this lesson you will be able to reason about a language model the way you reason about a database or a network: as a component with a measurable cost per call, a hard capacity limit, a latency profile you can predict, and failure modes you can architect around — and you will have a working script that measures token throughput and latency against any OpenAI-compatible endpoint.
The model is a token machine, and tokens are your unit of everything
Forget the theory-course framing for a moment. As a builder, here is the only mental model you need to start: an LLM is a function that takes a sequence of tokens in and produces tokens out, one at a time. A token is a chunk of text — roughly 4 characters or about 0.75 words in English. "authentication" might be one token; "auth" + "entication" might be two, depending on the tokenizer.
Why does this matter to you and not just to researchers? Because every dimension of your product's LLM behavior is denominated in tokens:
- Cost is billed per token — input tokens and output tokens, usually at different rates.
- Capacity (the context window) is measured in tokens.
- Latency scales with tokens: a fixed-ish time to process the input, then a per-token generation time for the output.
If you internalize one thing today, make it this: when you design an LLM feature, you are designing a token budget. Everything else follows.
The context window is a budget, not a feature
Every model has a context window — the maximum number of tokens it can consider in a single call, covering your system prompt, conversation history, retrieved documents, and the response it generates. Modern models advertise windows from ~128k tokens to over a million.
New builders read "1M token context" and think "great, I'll just stuff everything in." That is the first expensive mistake, for three reasons:
- You pay for every input token, every call. If you send a 50,000-token document with every request in a chat session, you are paying for those 50,000 tokens again on every turn. A 20-turn conversation just cost you a million input tokens.
- Latency grows with input size. More input tokens means longer time-to-first-token. Your "instant" feature becomes a 6-second spinner.
- Attention quality degrades. Models are demonstrably better at using information near the start and end of the context than material buried in the middle. Relevant-but-buried is functionally invisible more often than you would like.
So treat the context window like RAM on an embedded device. Ask of every feature: what is the smallest set of tokens that lets the model do this job well? That question — not "how do I fit more in" — is the origin of retrieval, summarization pipelines, and conversation truncation strategies, all of which you will build later in this course.
Sampling: why temperature is a product decision
The model doesn't output a token — it outputs a probability distribution over every possible next token, and then a sampler picks one. The main knob is temperature:
- Temperature 0 (or near it): the sampler almost always picks the most likely token. Output is close to deterministic — same input, essentially same output. Use this for extraction, classification, structured JSON output, code transforms — anything where you want repeatability and testability.
- Temperature ~0.7–1.0: the sampler takes chances on lower-probability tokens. Output varies between runs. Use this for brainstorming, copywriting variants, conversational personality.
This is not a tuning nicety; it is a product decision. If your feature is "generate 5 headline options," temperature 0 will give you five nearly identical headlines. If your feature is "extract the invoice total as JSON," temperature 1 will occasionally hand you creative arithmetic. Match the knob to the job, and write it down per-feature in your config — you'll build exactly that config in Lesson 4.
The latency and cost mental model
Here's the back-of-envelope model that will serve you in every design review:
```
total_latency ≈ time_to_first_token + (output_tokens × time_per_token)
total_cost ≈ (input_tokens × input_rate) + (output_tokens × output_rate)
```
Two practical consequences:
- Output tokens dominate latency. Generation happens token-by-token, often at tens of tokens per second. A 100-token answer might stream in 2–3 seconds; a 2,000-token answer might take 40+ seconds. If your UX needs speed, constrain output length ("answer in under 50 words") before you shop for a faster model.
- Output tokens usually cost more than input tokens — often 3–5× more per token. A summarization feature (huge input, small output) has a very different cost shape than a generation feature (small input, huge output). We do the full arithmetic in Lesson 3.
And one UX truth: streaming changes perceived latency more than any model choice. A response that starts appearing in 400ms feels fast even if it takes 8 seconds to finish. Build streaming in from day one.
What the model cannot do — and what that forces architecturally
The model is a frozen snapshot of training data with no clock, no calculator, and no network connection. Three hard limits, each with an architectural implication:
- No fresh facts. It doesn't know today's news, your database contents, or your user's account state. It will often confidently generate plausible-looking answers anyway — this is hallucination, and it is not a bug you can prompt away. Implication: any feature that depends on current or private data needs you to retrieve the data and put it in the context. The model reasons over what you supply; your retrieval layer is the actual source of truth.
- No exact math. It predicts tokens; it does not compute. It will get
23 × 17right most of the time and4,382.19 × 0.0725wrong often enough to lose you a customer. Implication: the model decides what to calculate, your code does the calculating. This is the core idea behind tool calling, which you'll implement in the paid modules. - No memory between calls. Every API call is stateless. The "conversation" your users experience exists because you resend the history each turn. Implication: conversation state is your data model, your storage problem, and — see the budget discussion above — your token bill.
Builders who accept these limits early build tool-calling, retrieval-backed, verified systems. Builders who don't spend three months prompt-tweaking a hallucination problem that was never prompt-shaped.
Measure it yourself
Talk is cheap; measurements are yours. This script calls any OpenAI-compatible chat endpoint (OpenAI, Anthropic's compatibility endpoint, Together, Groq, a local Ollama server — anything speaking the same protocol) and reports token counts, time-to-first-token, and tokens per second.
```typescript
// measure-llm.ts — run with: npx tsx measure-llm.ts
// Env: LLM_BASE_URL (e.g. https://api.openai.com/v1), LLM_API_KEY, LLM_MODEL
const BASE_URL = process.env.LLM_BASE_URL ?? "https://api.openai.com/v1";
const API_KEY = process.env.LLM_API_KEY!;
const MODEL = process.env.LLM_MODEL ?? "gpt-4o-mini";
async function measure(prompt: string) {
const start = performance.now();
let firstTokenAt: number | null = null;
let output = "";
const res = await fetch(${BASE_URL}/chat/completions, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: Bearer ${API_KEY},
},
body: JSON.stringify({
model: MODEL,
messages: [{ role: "user", content: prompt }],
stream: true,
stream_options: { include_usage: true },
}),
});
if (!res.ok || !res.body) throw new Error(HTTP ${res.status}: ${await res.text()});
let usage: { prompt_tokens: number; completion_tokens: number } | undefined;
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data: ") || line.includes("[DONE]")) continue;
const chunk = JSON.parse(line.slice(6));
const delta = chunk.choices?.[0]?.delta?.content;
if (delta) {
if (firstTokenAt === null) firstTokenAt = performance.now();
output += delta;
}
if (chunk.usage) usage = chunk.usage;
}
}
const end = performance.now();
const genMs = end - (firstTokenAt ?? end);
console.log(model: ${MODEL});
console.log(input tokens: ${usage?.prompt_tokens ?? "n/a"});
console.log(output tokens: ${usage?.completion_tokens ?? "n/a"});
console.log(time to 1st tok: ${((firstTokenAt ?? end) - start).toFixed(0)} ms);
console.log(total time: ${(end - start).toFixed(0)} ms);
if (usage?.completion_tokens && genMs > 0)
console.log(tokens/sec: ${(usage.completion_tokens / (genMs / 1000)).toFixed(1)});
console.log(---\n${output.slice(0, 200)}...);
}
measure("Explain what a token is in one short paragraph, then in one long paragraph.");
```
Run it a few times. Notice the variance between runs — that variance is why you'll add timeouts and retries in Lesson 4.
Exercise
Run the measurement script against one endpoint with three prompts of increasing requested output length: "answer in one sentence," "answer in one paragraph," and "answer in 500 words" — same question each time. Record time-to-first-token, total latency, and output token count for each. Success looks like: a small table of your three runs, plus one sentence stating the relationship you observed between output tokens and total latency (it should be roughly linear — and now you've proven the lesson's core claim on your own hardware and wallet).
You now think in tokens, budgets, and failure modes — next, we weaponize that by dissecting exactly what goes into the context: the anatomy of a prompt that behaves like an engineered interface instead of a wish.