Understand what really happens between your prompt and ChatGPT's answer — tokenization, embeddings, positional encoding, self-attention, and transformers — explained with a mobile shop analogy, runnable Python, and diagrams.
Most explanations of Generative AI are either hand-wavy ("it's like a brain!") or drop you straight into matrix multiplication. Neither helps you build anything.
This post takes the middle path. After 4+ years of full-stack development — the last stretch of it shipping LLM features into production — the mental model below is the one I actually use when debugging why a prompt behaves strangely. You'll finish knowing what happens to your text at each stage, and why each stage exists.
No maths degree required. We'll use a mobile shop in Bengaluru the whole way through.
You run a mobile shop. A customer walks in:
"Anna, ₹15,000 budget alli best camera phone idya?" (Bro, do you have a good camera phone in the ₹15,000 range?)
You don't answer instantly. In about a second, you:
Those five steps are, in order, tokenization, embeddings, positional encoding, self-attention, and generation. That's the whole architecture. The rest of this post is just detail on each.
The generative AI pipeline — prompt to tokens to embeddings through attention to output, with generated tokens fed back in
A generative model does not retrieve a stored answer. It predicts the next token, appends it, and repeats — each prediction conditioned on everything before it. That loop is why ChatGPT streams word by word instead of appearing all at once.
Models do not read characters or words. They read tokens — chunks of text somewhere between a character and a word.
You split the customer's sentence into meaningful pieces automatically. The model does the same with a fixed vocabulary learned during training.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
print(tokenizer.tokenize("Best camera phone under 15000"))
# ['best', 'camera', 'phone', 'under', '15', '##00', '##0']
Look at what happened to 15000. It became three tokens, because that exact number wasn't common enough in training to earn its own vocabulary entry. The ## prefix marks a continuation of the previous token.
This isn't trivia — it explains three things you'll hit in practice:
Languages written in non-Latin scripts — Kannada, Hindi, Tamil — tokenize far less efficiently, often 2–3× more tokens for the same content. If you're building for Indian language users, measure real token counts before estimating cost.
A token ID is just an index — camera might be token 4,382. The number carries no meaning; 4,382 isn't "near" 4,383 in any useful sense.
So each token is mapped to an embedding: a long list of numbers positioned so that related meanings sit close together.
In the shop, you know "battery backup", "long-lasting", and "charge holds well" are the same request. Embeddings are how software gets that.
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
vectors = model.encode(["camera quality", "photo clarity", "battery life"])
sim = util.cos_sim(vectors, vectors)
print(f"camera vs photo: {sim[0][1]:.2f}") # ~0.74 — closely related
print(f"camera vs battery: {sim[0][2]:.2f}") # ~0.19 — unrelated
The model was never told "camera relates to photo". It learned that from seeing them used in similar contexts across billions of sentences.
This same mechanism powers semantic search and is the foundation of RAG.
Here's a genuinely surprising fact: the core of a transformer has no built-in sense of order. It sees a set of embeddings, not a sequence.
Without help, these would be identical to the model:
You'd never confuse them. The model would, so position information is added into the embeddings before processing.
| Token | Position |
|---|---|
best | 0 |
camera | 1 |
phone | 2 |
under | 3 |
15000 | 4 |
Positional encoding is the reason a model can't simply accept unlimited input. It's trained over a fixed range of positions, and text beyond that range sits in territory the model never learned to interpret. That's the real constraint behind the context limit.
This is the mechanism that made modern AI work, and it's more intuitive than its reputation suggests.
The customer says: "Phone beku under 15k, good battery, 5G, best camera."
To answer, you weigh the parts. Budget is a hard filter. "Good" attaches to "battery", not to "phone". You do this without thinking.
Self-attention is the model doing exactly that, numerically. Every token looks at every other token and produces a weight for it — how much that other token should influence its own interpretation.
Self-attention — each token weighs every other token, showing why "good" binds to "battery"
When processing battery, the model assigns high weight to good (it's the modifier) and low weight to under (irrelevant here). The weights sum to 1, so attention is a budget being allocated.
This is why LLMs handle long-range dependencies that older models couldn't. In "The phone I bought last year from the shop near the station still has great battery", battery connects to phone across fourteen words — attention makes that link directly, in one step.
Attention is computed over everything in your prompt. Bury an instruction in the middle of a long document and it competes for weight with thousands of other tokens. Putting critical instructions at the start or end of a prompt measurably improves adherence — that's attention, not superstition.
Generation is a loop, not a single answer. Step through it:
A transformer is these pieces stacked in layers — attention, then a feed-forward network, repeated dozens of times.
Early layers capture grammar. Deeper layers capture meaning and intent. It's the difference between a new hire who knows the words and a veteran assistant who knows what the customer actually wants.
| Letter | Meaning | In shop terms |
|---|---|---|
| Generative | Produces new text token by token | Composes an answer, doesn't recite one |
| Pretrained | Learned from a huge general corpus first | Read every catalogue before day one |
| Transformer | The attention-based architecture above | The reasoning engine |
Two completely different activities, constantly confused:
| Training | Inference | |
|---|---|---|
| When | Once, before release | Every time you send a prompt |
| Shop equivalent | Assistant studies the catalogues | Assistant serves a customer |
| Cost | Millions of dollars, months | Fractions of a cent, milliseconds |
| Changes the model? | Yes — weights update | No — weights are frozen |
At inference the weights are read-only. Correcting a model in conversation changes nothing beyond that conversation's context — the next session starts fresh. To give a model lasting access to your information you need RAG or fine-tuning, not repetition.
The model predicts plausible tokens; it does not calculate. It often gets sums right because it saw similar sums during training, which makes the failures unpredictable and easy to miss.
# ❌ Wrong — trusting the model to compute
"What is 18.5% of 47,320?"
# ✅ Correct — let the model write code, then execute it
"Write a Python expression for 18.5% of 47320. Return only the expression."
For anything numeric, use tool calling and run the actual computation.
Filling a 128k window with everything you have usually makes answers worse. Attention is a finite budget spread across all tokens, and relevant instructions get diluted. Send the least context that contains the answer.
Temperature controls randomness in token selection, not accuracy. Low temperature makes output more deterministic — repeatable, not more correct. A confidently wrong answer at temperature 0 is still wrong.
Estimating cost or context usage by word count will underestimate, badly, for code, JSON, and non-English text. Count tokens with the real tokenizer:
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
print(len(tok.tokenize(your_prompt)))
Three short experiments that make the abstract concrete.
pip install transformers sentence-transformers
Tokenize each string and count the tokens.
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("bert-base-uncased")
for text in [
"hello world",
"antidisestablishmentarianism",
"15000",
"ಮೊಬೈಲ್ ಫೋನ್",
'{"price": 15000, "brand": "Samsung"}',
]:
tokens = tok.tokenize(text)
print(f"{len(tokens):3d} tokens | {text[:36]:38s} | {tokens[:6]}")
Which costs more tokens — the long English word, or the two Kannada words? Note the answer; it directly affects what you'd charge users.
Embed six phrases and find which pairs the model considers related.
from sentence_transformers import SentenceTransformer, util
model = SentenceTransformer("all-MiniLM-L6-v2")
phrases = [
"good camera phone", "great photo quality", "long battery life",
"charge lasts all day", "fast processor", "budget under 15000",
]
vectors = model.encode(phrases)
sim = util.cos_sim(vectors, vectors)
for i in range(len(phrases)):
for j in range(i + 1, len(phrases)):
if sim[i][j] > 0.5:
print(f"{sim[i][j]:.2f} {phrases[i]} <-> {phrases[j]}")
The pairs that surface share almost no keywords. That's meaning, not matching.
Send the same instruction to any LLM twice — once at the top of a long block of text, once at the bottom. Compare how faithfully each is followed. This is attention dilution, and you can measure it in five minutes.
Save each task as its own file and run with python task1.py. The first run downloads model weights (~500MB total) — after that everything is local and offline.

Written by Sharanayya R Tenginamath
Software Engineer at McD BERL with 4+ years building scalable full-stack applications with React.js, Next.js, TypeScript, FastAPI and Python. Available to join from Oct 12, 2026.
A practical guide to Retrieval-Augmented Generation — chunking, embeddings, vector search, and prompt assembly — with runnable Python, architecture diagrams, and the mistakes that break RAG in production.
13 min read