What Is an LLM? Large Language Models from a Developer's Point of View
A large language model is a function that predicts the next token. We build that loop token by token with a local gemma3:4b, look at training versus inference, base versus instruct, open-weight versus closed and local versus cloud, and use real outputs to see where the model goes wrong on dates, arithmetic and order questions.
Version note: The examples in this post were tested with Ollama 0.34 on a local model (gemma3:4b), using Node 22.
Guess First
Imagine asking a large language model (LLM) "What is today's date?" Will it tell you the right date, say it does not know, or confidently make one up? And if you ask the same model to multiply two five-digit numbers, will it get the right result?
We will see the answers with a real model halfway through this post. They are also the shortest path to understanding what an LLM is, because they expose the gap between what a model "knows" and what it "produces".
A Program That Predicts the Next Token
If we describe an LLM by how it works rather than by how it looks, one sentence is enough: it is a function that predicts which token comes next after a given text. A token is the unit the model reads and writes; sometimes a word, sometimes part of a word, sometimes a single digit or punctuation mark. We will measure tokens in detail in the third post.
At every step the model computes a probability for every token in its vocabulary, picks one, appends it to the text and starts over. Think of the word suggestions on your phone's keyboard; an LLM is that idea trained with billions of parameters and able to read a much longer context. We can build this loop ourselves and watch it. The TypeScript code below asks the model for a single token each time, appends it to the text and sends the text again:
import ollama from "ollama";
let text = "The novel Nineteen Eighty-Four was written by";
for (let step = 1; step <= 8; step++) {
// raw: true skips the chat template, num_predict: 1 asks for a single token
const res = await ollama.generate({
model: "gemma3:4b",
prompt: text,
raw: true,
options: { temperature: 0, seed: 42, num_predict: 1 },
});
text += res.response;
console.log(`${step}: ${JSON.stringify(res.response)}`);
}
console.log(text);
I will show you how to set up and run the code step by step in the next post; for now, look at the output:
1: " George"
2: " Orwell"
3: " in"
4: " "
5: "1"
6: "9"
7: "4"
8: "9"
The novel Nineteen Eighty-Four was written by George Orwell in 1949
The model did not know in advance that it would say "George Orwell"; on each turn it only picked the next piece. The year arriving as four separate tokens is no accident either: the Gemma 3 tokenizer splits digits one by one. With temperature: 0 we asked for the most likely token at each step, and with seed we asked for a repeatable result; the details of these settings are the topic of the sixth post.
Here is what to notice: there is no database query and no lookup that finds an "Orwell" record. During training the model saw "Nineteen Eighty-Four" and "George Orwell" next to each other in the texts it read, and that relationship is spread across its weights. Right answers and wrong answers come out of the same mechanism.
Training and Inference
An LLM's life has two phases. Training is the phase where the model reads a huge pile of text and learns to predict the next token; for large models it takes weeks and thousands of GPUs. What remains at the end are the weights: billions of numbers that carry everything the model "knows". Inference is using the trained model. Every request where you send a prompt and get an answer is an inference.
As a developer, you are almost always on the inference side. The weights are trained once and then frozen. The model does not learn while you talk to it, it does not remember today what you told it yesterday (we will see this in the fourth post), and it knows nothing that happened after its training data ends. The Gemma 3 model card puts that date at August 2024.
If the model needs new information, you do not retrain it; you put that information inside the prompt. That is exactly what RAG is, and we will get to it in the fourteenth post. Changing the weights a little with your own data is also possible; that is called fine-tuning. But this series focuses on problems you can solve by writing code.
Base Models and Instruct Models
A model that has only learned to continue text is called a base model. Ask a base model a question and it does not feel obliged to answer; it continues the text in a "plausible" way. It may even treat your question as one item on an exam sheet and add more questions below it. Chat assistants are instruct (or chat) models, trained further on top of that to follow instructions. gemma3:4b in Ollama is the instruct version.
An instruct model expects to see the conversation in a specific shape: with special tokens that mark who is speaking and whose turn it is. The model's chat template defines that shape, and Ollama places your messages into it before sending them to the model. If we skip the template with raw: true and hand the model bare text, even an instruct model falls back to its old habit and continues the text:
$ curl -s http://localhost:11434/api/generate -d '{
"model": "gemma3:4b",
"prompt": "The capital of Turkey is",
"raw": true,
"stream": false,
"options": { "temperature": 0, "seed": 42, "num_predict": 40 }
}' | jq '{ response, eval_count, done_reason }'
{
"response": " Ankara.\n* **Istanbul** is the largest city in Turkey.\n* **Izmir** is the second largest city in Turkey.\n\n**Important Turkish Cities:**\n\n* **",
"eval_count": 40,
"done_reason": "length"
}
After "Ankara." the model did not stop; it turned the text into a list and was cut off when it hit the 40-token limit (num_predict). The length value in done_reason says exactly that. On top of that, the second line of the list is wrong: Izmir is Turkey's third largest city, not the second. The model writes fluent, confident sentences, but fluency is not accuracy. This is called hallucination, and it is the topic of the fifteenth post.
Open-Weight and Closed Models, Local and Cloud
For some models you can download the weights; these are called open-weight models. Gemma, Llama, Qwen and Mistral are in this group, and you can download the weights and run them on your own computer or server. On the other side are closed models such as OpenAI's GPT models, Anthropic's Claude and Google's Gemini; you reach them only through the provider's API.
Open-weight is not the same as open source. Downloadable weights do not mean the training data or training code is open, and the license is not always permissive. You can see this by asking Ollama directly:
$ ollama show gemma3:4b
Model
architecture gemma3
parameters 4.3B
context length 131072
embedding length 2560
quantization Q4_K_M
Capabilities
completion
vision
Parameters
stop "<end_of_turn>"
temperature 1
top_k 64
top_p 0.95
License
Gemma Terms of Use
Last modified: February 21, 2024
...
4.3 billion parameters, a context length of 131072 tokens, Q4_K_M quantization and the "Gemma Terms of Use" license. That license is not an OSI-approved open source license; it is Google's own terms of use. Q4_K_M is a quantization format that stores most of the weights at about 4.5 bits each, which is how the model file shrinks to 3.3 GB. We will come back to context length in the fourth post and to quantization in the sixteenth. Also look at the Capabilities section: completion and vision are there, tools is not. So this model cannot do tool calling; we will use qwen3:4b in those posts.
The choice between local and cloud follows from this. With a local model your data never leaves your computer, you pay nothing per token and it works without internet; in exchange you are limited by your hardware, and small models cannot match the quality of large ones. Cloud models are far more capable and accept much longer contexts, but every token has a price and your data goes to the provider. In this series we run the examples locally, because the concepts are the same and you can try everything on your own machine for free. I will describe the cloud providers based on their documentation.
What an LLM Cannot Do
Back to the opening questions. We ask the same model three things:
import ollama from "ollama";
const questions = [
"What is today's date? Answer in one sentence.",
"What is the status of order #1042 at Bookstore? Answer in one sentence.",
"What is 48271 * 9137? Answer with the number only.",
];
for (const question of questions) {
const res = await ollama.chat({
model: "gemma3:4b",
messages: [{ role: "user", content: question }],
options: { temperature: 0, seed: 42 },
});
console.log(`Q: ${question}`);
console.log(`A: ${res.message.content.trim()}\n`);
}
Q: What is today's date? Answer in one sentence.
A: Today is November 3, 2023.
Q: What is the status of order #1042 at Bookstore? Answer in one sentence.
A: Please provide me with the status of order #1042 at Bookstore – I need more information to answer your question!
Q: What is 48271 * 9137? Answer with the number only.
A: 442927997
Three answers, three lessons. For the date, the model made one up: this run happened on November 6, 2026, and the model said November 3, 2023. The model has no clock; it knows today's date only if you put it in the prompt. For the order, it behaved well and asked for information. That is not a guarantee, though; on another question the same model could just as easily invent a shipping status. To reach real order data it needs a tool, and we will build that in the tool calling posts. For the multiplication it answered 442927997; the right answer is 441052127. The model does not calculate; it produces tokens that look like the result of a calculation.
The rule that follows is simple: an LLM is not a database, a clock or a calculator. For current information, exact calculations and data that belongs to your system, you put ordinary code next to the LLM. Where an LLM shines is language: summarizing, classifying, turning text from one shape into another, extracting structure from free text, understanding what the user wants.
How This Series Is Organized
The first block, posts 1–17, is the backbone. In each post I take one concept and show it with C# and TypeScript examples that really run on a local model: the first request and the fields in the response, tokens, the context window, message roles, sampling, streaming, writing prompts, reasoning, structured output, tool calling, embeddings, RAG, hallucination, cost and model choice, and provider-independent code. The second block, posts 18–20, is about evolution: the road from the Transformer to ChatGPT, the models from 2023 to today and how LLM APIs have changed.
The setup is this: Ollama 0.34, gemma3:4b as the default model, qwen3:4b for tool calling and thinking, nomic-embed-text for embeddings and gemma3:12b for size comparisons. On the C# side we have .NET 10 and Microsoft.Extensions.AI, on the TypeScript side Node 22 and the ollama package. The examples revolve around a small Bookstore assistant: book recommendations, order status lookups and review summaries. Code and data are in English; examples whose topic is Turkish itself, such as the token cost of Turkish, are the exception. Every output in these posts was copied from a real run, but the numbers and timings on your machine may differ a little.
If you have read the .NET Core Web API series, the DI and minimal API side will feel familiar; in the seventeenth post we will wire the model into a Web API. The OWASP posts in the Web Security series will also help us with prompt injection.
Common Pitfalls
The first pitfall is treating an LLM as a source of truth. The model says something not because it knows it, but because that answer looks likely given its training data. A fluent, confident sentence is not a correct one; as the Izmir example showed, wrong information arrives in the same tone.
The second is believing the model learns while you talk to it. The weights do not change during inference. Inside a chat the model seems to remember earlier messages, but that is only because the history is sent again with every request. When your application restarts, the model remembers nothing.
The third is taking open-weight for open source and skipping the license. For a model you will use in a commercial project, always check the license in the ollama show output or on the model card.
The fourth is giving the LLM work that code should do. Do dates, money calculations and stock counts, anything that needs precision, in ordinary code and leave the language work to the LLM. A C# method returns the same result on every call; you cannot expect that from an LLM.
A Small Experiment
★☆☆ Change the sentence in next-token.ts, for example start with "The author of The Lord of the Rings is", and follow where the model goes token by token. Raise the loop from 8 to 20 steps and see when the model drifts off topic.
★★☆ Add your own questions to limits.ts: yesterday's news, the population of a small town, the number of days between two dates. Note where the model makes things up and where it asks for information, and compare the answers with a real source.
★★★ Send the curl request above once with raw: true and once with that field removed, and compare the two answers. Then use ollama show gemma3:4b --template to see which template Ollama places your prompt into.
In the next post we will get to know Ollama better: we will list the models with ollama list, send a request to the /api/chat endpoint with curl, then make the same request in C# with Microsoft.Extensions.AI and in TypeScript with the ollama package, and read the token counts and timings in the response one by one.