Sessions 00 → 05 · Everything in one story
Mohamed Noordeen Alaudeen
"You did not just learn tools. You learned how AI thinks."
One month ago, most of you were asking: "How does Google even work?"
Today, after 8 sessions, you can:
That is not a small thing. Let's make sure it sticks.
| Session | Title | The Central Question |
|---|---|---|
| S00 | How Search Engines Work | How does Google decide which result is most relevant? |
| S01 | Text to Numbers | How does a computer understand the meaning of a word? |
| S02 | Transformer Architecture | How does the same word mean different things in different sentences? |
| S03 | GPT Evolution & Alignment | How did next-word prediction become ChatGPT? |
| S04 | BPE + Sampling Parameters | How does an LLM decide which word to generate next? |
| S05 | Local LLMs & API Providers | How do we actually use a model, and own the infrastructure? |
| S06 | Prompt Engineering + DSPy | How do we optimise prompts automatically, instead of guessing? |
| S07 | LangChain Fundamentals | How do we write code that works with any LLM provider? |
Computers only understand numbers. We spent 70 years inventing better and better ways to turn language into numbers, and somewhere along the way, we accidentally built intelligence.
That is what S00 → S05 is about.
Every session left one gap open. The next session filled it.
S00 Keyword search (TF-IDF)
⚠️ MISSING: "car crash" ≠ "automobile accident", same meaning, different words
S01 Word embeddings (Word2Vec, GloVe, FastText)
⚠️ MISSING: "bank" (river) and "bank" (money) get the SAME vector
S02 Transformers + Self-Attention
⚠️ MISSING: Training from scratch = weeks of GPU time, thousands of dollars
S03 Pre-trained GPT → ChatGPT via RLHF
⚠️ MISSING: What actually happens between "prompt" and "response"?
S04 BPE tokens + Temperature + Top-K + Top-P
⚠️ MISSING: API costs money, needs internet, can go down
S05 Local LLMs (Ollama) + Multi-provider (OpenRouter)
⚠️ MISSING: We're still writing prompts by hand, guessing what works
S06 DSPy + Chain-of-Thought + Prompt Optimisers
⚠️ MISSING: Every provider has a different SDK, auth, and response format
S07 LangChain, one unified interface for every provider
"Before AI could understand you, it first had to find you."
A computer sees this sentence:
"The cat sat on the mat"
As this:
[84, 104, 101, 32, 99, 97, 116, 32, 115, 97, 116, ...]
Just bytes. No meaning. No structure. Just numbers.
First challenge in AI: How do we rank documents by relevance to a query, without any intelligence?
Every word in every document gets catalogued:
"python" → [doc_12, doc_47, doc_203, doc_891]
"snake" → [doc_9, doc_47, doc_891]
"Django" → [doc_47, doc_203]
Search "python Django" → intersection → doc_47, doc_203
Fast. Scalable. But naive, every word counts equally.
"the" and "python" get the same weight. That's a problem.
Fix the naive count with two signals combined:
TF (Term Frequency), how often does this word appear in this document?
IDF (Inverse Document Frequency), how rare is this word across all documents?
TF-IDF = TF × log(Total docs / Docs containing word)
"the" → appears everywhere → IDF ≈ 0 → score ≈ 0
"tensorflow" → rare → IDF is high → score is high
Key insight: A word that appears often in one document but rarely everywhere else is the signal word for that document.
Search query: "automobile accident"
TF-IDF finds: documents containing "automobile" and "accident" ✅
TF-IDF misses: "The car crash happened on Highway 5 this morning" ❌
Same meaning. Completely different words. TF-IDF cannot bridge that gap.
⚠️ This is the gap that Session 01 fills.
Q1: What is an inverted index and why does it make search fast?
A data structure that maps each unique word to the list of documents containing it. Instead of scanning every document per query, you look up the word directly and get the document list in sub-millisecond time. A query like "python training" looks up both words, gets their document lists, then intersects them. This is how Elasticsearch, Lucene, and every major search engine indexes content.
Q2: What does TF-IDF measure and how does it rank better than simple word count?
TF (Term Frequency) measures how often a word appears in one document. IDF = log(total docs / docs containing word). Words in every document like "the" get IDF close to 0 and contribute nothing. Rare words get high IDF scores. TF x IDF is high for relevant, rare words and near zero for common filler words. This gives automatic stopword filtering built into the math, no explicit stopword list needed.
Q3: What is the core limitation of TF-IDF and how was it solved?
TF-IDF is purely lexical: it matches exact character strings. "car" and "automobile" have zero computed relationship. No understanding of synonyms, context, or meaning. This semantic gap was solved by word embeddings (Word2Vec, GloVe), which map words to dense vectors where semantically similar words land geometrically close. Modern systems use hybrid search: BM25 for exact keyword recall plus vector search for semantic understanding.
Q4: What is BM25 and why do most search engines use it over raw TF-IDF?
BM25 extends TF-IDF with two fixes. Term saturation: raw TF-IDF grows linearly with word frequency, so a word appearing 100 times scores 10x higher than appearing 10 times. BM25 adds a saturation parameter k1 (typically 1.5) so additional occurrences have diminishing returns. Document length normalization: parameter b penalizes long documents, because finding a rare word in a short document is more significant than in a 50,000-word one. BM25 is the default in Elasticsearch and a strong baseline even against neural retrievers.
Q5: What is IDF when a word appears in every document? Why does this matter?
IDF = log(N/N) = log(1) = 0. The TF-IDF score becomes 0, so this word contributes nothing to ranking regardless of how often it appears. This is the automatic stopword filtering property: "the", "is", "a" appear in every document and score zero without needing an explicit stopword list. Conversely, a word appearing in 3 of 1000 documents gets IDF = log(333) ≈ 5.8, making it a very strong relevance signal.
Q6: Design a production search architecture. Where does the inverted index fit?
Ingestion pipeline: crawl documents, tokenize text, build the inverted index offline. At query time: tokenize the query, look up each token in the inverted index (O(1)), retrieve candidate lists, intersect them, score with BM25, return top-K results. For semantic search: add a vector index alongside and run approximate nearest-neighbor search. Use a reranker to improve precision on top results. This two-stage retrieve-then-rerank pattern is the same architecture used in production RAG systems.
Q7: What are the main limitations of bag-of-words models?
Bag-of-words completely discards word order, so "dog bites man" and "man bites dog" produce identical vectors. It creates sparse, high-dimensional representations with vocabulary sizes of 100,000 or more. It treats all words as independent: "good" and "excellent" are as unrelated as "good" and "airplane." The word "bank" gets the same representation regardless of context. These limitations led directly to word embeddings (semantic gap) and then to Transformers (context-dependence gap).
"What if we could capture what a word means, not just what it says?"
1954 Bag of Words count words, ignore order
1972 TF-IDF weight rare words higher
2013 Word2Vec meaning from surrounding context
2014 GloVe meaning from global co-occurrence
2016 FastText meaning from character pieces (handles typos)
Each method fixed what the previous one got wrong.
Sentence: "I love Python. Python loves me."
Vocab: {I, love, Python, loves, me}
Vector: [1, 2, 2, 1, 1 ]
✅ Simple, fast, works for basic classification tasks
❌ "Dog bites man" and "Man bites dog" → identical vector Word order is completely destroyed.
❌ "good" and "excellent" → zero relationship in this model
Core insight: You shall know a word by the company it keeps., J.R. Firth, 1957
Training task: Given surrounding words, predict the missing one.
"The _____ ran across the road" → model sees: cat, dog, child all fit
"She deposited money at the ___" → model sees: bank fits, not river
After training on billions of sentences, words with similar contexts end up with similar vectors.
Result: geometry encodes meaning.
king - man + woman ≈ queen ✅
Paris - France + Italy ≈ Rome ✅
doctor - man + woman ≈ nurse ✅ (reflects training data bias)
You can add and subtract concepts.
Cosine similarity score:
("car", "automobile") → ~0.92 (nearly identical)("car", "cloud") → ~0.12 (unrelated)This is why RAG works. This is why semantic search works. Everything from M05 onwards relies on this.
GloVe (2014), instead of a sliding window, use global co-occurrence statistics across the entire corpus. More stable, faster to train.
FastText (2016), instead of whole words, train on character n-grams:
"playing" → ["pla", "lay", "ayi", "yin", "ing", "play", "layi", ...]
Advantage: handles typos and words not seen in training:
"playin" (typo) → still gets a reasonable vector from subword pieces ✅
Short review: "Good film" vs "A remarkably brilliant, absolutely wonderful, outstanding film that I thoroughly enjoyed"
Euclidean distance: large, different vector lengths → far apart ❌
Cosine similarity: high, same direction → same meaning ✅
Cosine measures the ANGLE between vectors, not the distance.
Angle = meaning direction. Length = how many words you used.
Length doesn't determine meaning. Direction does.
Q1: What is a word embedding and how is it different from one-hot encoding?
A word embedding maps each word to a dense vector (typically 100 to 300 dimensions) where semantic relationships are preserved as geometry. One-hot encoding maps each word to a sparse binary vector with a 1 at the vocabulary index and 0 everywhere else. Problems with one-hot: no semantic relationship between words, and a 100k vocabulary needs 100k-dimensional vectors. Word embeddings solve both: "king" and "queen" have high cosine similarity, and 300 dimensions represent the entire vocabulary meaningfully.
Q2: How does Word2Vec learn embeddings without human labels?
Word2Vec uses self-supervised learning on a prediction task derived from text structure. Skip-gram: given a center word, predict surrounding context words. CBOW: given context words, predict the center word. Words that appear in similar contexts end up with similar weight vectors. After training on billions of sentences, the network weights converge to vectors that encode usage patterns, which turn out to encode semantic meaning.
Q3: Why cosine similarity and not Euclidean distance for word vectors?
Word vectors have magnitudes that depend on word frequency during training. Common words end up with larger magnitudes. Euclidean distance is magnitude-sensitive: two vectors pointing in the same direction but with different lengths would score as different. Cosine similarity measures only the angle between vectors, ignoring magnitude. Same semantic meaning equals same direction equals high cosine similarity regardless of frequency. Range is -1 (opposite) to +1 (identical direction).
Q4: What is the difference between Skip-gram and CBOW, and which handles rare words better?
Skip-gram: given center word, predict surrounding context words. Each center word generates multiple training examples, so the model focuses deeply on each individual word. CBOW: given context words, predict the center word. CBOW is 3 to 5 times faster but averages context signals. Skip-gram performs better for rare words because each step focuses on one word's context relationships. CBOW works better for frequent words and when training speed matters. Skip-gram with Negative Sampling (SGNS) is the industry standard for high-quality embeddings.
Q5: What is negative sampling and why was it needed?
The naive Word2Vec update computes a gradient over the entire vocabulary (100k words) for each training step: O(vocabulary size) operations per step. Negative sampling replaces the full softmax with a binary classifier: is this (center word, context word) pair real or noise? For each real pair, sample k noise words (5 to 20), and train to distinguish real from noise. This reduces computation from O(vocabulary) to O(k) per update, a 5000 to 20000x speedup. The model still learns meaningful embeddings because distinguishing real context from random noise forces it to capture genuine co-occurrence patterns.
Q6: What is the critical limitation of Word2Vec and GloVe that Transformers solve?
Both Word2Vec and GloVe produce static embeddings: every word gets one fixed vector regardless of context. "Bank" gets the same vector whether it appears near "money" or "river." This polysemy problem is exactly what Transformers solve. Self-attention builds context-aware representations where the same word gets a different vector depending on its surrounding words. That is why BERT largely replaced Word2Vec for downstream NLP tasks.
Q7: What is FastText and when would you use it over Word2Vec?
FastText trains on character n-grams rather than whole words. "Playing" becomes ["pla", "lay", "ayi", "yin", "ing"]. This lets FastText generate a vector for words it never saw during training by composing character pieces. Use FastText when your data has typos or misspellings, you work with morphologically rich languages (German, Turkish), you have domain-specific technical vocabulary, or you work with code tokens. The cost is larger model size and slower inference. For most production tasks today, sentence-transformers (BERT-based) outperform FastText, but it remains a fast baseline with no GPU required.
"What if every word could look at every other word simultaneously, and decide what matters?"
"I went to the bank to deposit money."
"I sat on the bank of the river."
Word2Vec gives "bank" the exact same vector in both sentences.
It was trained as an average across all contexts. It has no ability to adapt.
We need a model that builds a different representation for "bank" based on whether it's surrounded by "money" or "river".
Before 2017, we used Recurrent Neural Networks:
word₁ → word₂ → word₃ → ··· → word₁₀₀ → output
Problems:
The solution wasn't to make RNNs better. It was to replace them entirely.
For every word, ask: "Which other words in this sentence are most relevant to understanding me?"
"The animal didn't cross the street because it was too tired."
Question: What does "it" refer to?
Self-attention says: "animal" (high weight), "street" (low weight)
This assignment happens automatically, learned from data.
Every word in the sentence votes on how relevant every other word is, simultaneously.
Think of it as running a search engine inside the sentence:
| Role | What It Represents |
|---|---|
| Query (Q) | "What am I looking for?", the current word's search |
| Key (K) | "What do I contain?", every other word announces itself |
| Value (V) | "What information do I carry?", the actual content |
Attention score = dot product of Q with each K (how relevant?)
Output = weighted sum of all V, using attention scores as weights
Run self-attention 8 times in parallel, each with different learned projections:
Head 1 → might learn: who does what (subject → verb)
Head 2 → might learn: what describes what (noun → adjective)
Head 3 → might learn: what refers to what (pronoun → antecedent)
Head 4–8 → other patterns, discovered automatically during training
Concatenate all 8 outputs → project down → richer representation
"One person reading the sentence 8 times with 8 different questions."
Self-attention looks at all words simultaneously, it has no built-in sense of word order.
Without positional encoding:
"Dog bites man" = "Man bites dog" (same words, same attention)
Solution: Add a unique positional signal to each word's embedding:
Position 1 → add [sin(1/10000⁰), cos(1/10000⁰), sin(1/10000²/⁵¹²), ...]
Position 2 → add [sin(2/10000⁰), cos(2/10000⁰), sin(2/10000²/⁵¹²), ...]
Sine and cosine at different frequencies → each position gets a unique "fingerprint".
| Component | What It Does |
|---|---|
InputEmbeddings |
Token → dense vector (d_model = 512) |
PositionalEncoding |
Adds position fingerprint to each embedding |
MultiHeadAttention |
8 heads, Q/K/V projections, scaled dot-product |
FeedForward |
Two-layer MLP applied to each position independently |
LayerNorm |
Stabilises training, prevents exploding/vanishing gradients |
ResidualConnection |
Skip connections, gradient highway through deep networks |
EncoderStack |
6 × (Attention + FFN), builds context-rich representations |
DecoderStack |
6 × (Masked Attention + Cross-Attention + FFN), generates output |
You trained this on English → Italian translation.
Q1: Explain self-attention. Walk through Q, K, V.
Each token generates three learned linear projections: Query (what this token is searching for), Key (what this token offers as a label to others), and Value (the actual content this token contributes). Attention score = dot product of Q with each K, scaled by the square root of d_k to prevent softmax saturation, then softmax to get weights. Output = weighted sum of all V vectors using the attention weights. This runs in parallel for all tokens simultaneously, resolving contextual meaning in one pass.
Q2: What is the difference between BERT and GPT architecturally?
BERT uses encoder-only Transformer layers with bidirectional self-attention: every token can attend to every other token in both directions. Pre-trained with Masked Language Modeling. Strong for classification, extraction, and sentence similarity. GPT uses decoder-only Transformer layers with causal self-attention: each token attends only to previous tokens. Pre-trained on next-token prediction. In the era of conversational AI, generation wins: GPT, Claude, Gemini, LLaMA, and Mistral are all decoder-only.
Q3: Why does multi-head attention work better than single-head?
A single attention head must collapse all relationship types into one pattern. Multiple heads run in parallel, each with different learned weight matrices, so each head can simultaneously specialize. One head might track subject-verb dependencies, another pronoun-antecedent resolution, another semantic similarity. The outputs are concatenated and projected back down. Richer representations at the same computational cost as a single larger head.
Q4: Why divide by the square root of d_k in the attention formula?
As the embedding dimension d_k grows, dot products between Q and K grow in magnitude proportionally. Very large dot products push softmax into regions where one token gets probability close to 1 and all others close to 0. In this saturated regime, gradients drop to near zero and the model gets stuck. Dividing by the square root of d_k normalizes the scale, keeping softmax numerically stable with useful gradients.
Q5: Why does a Transformer need positional encoding?
Self-attention treats the input as a set with no inherent order. "The cat sat" and "sat cat The" would produce identical attention patterns without positional encoding. The original paper adds sinusoidal waves at different frequencies to each position. Modern models like Llama and Mistral use RoPE (Rotary Position Embedding), which rotates the Q and K vectors by position-dependent angles, encoding relative position directly into the attention computation and generalizing better to long sequences.
Q6: What does the Feed-Forward sublayer do in each Transformer block?
Each Transformer block has two sublayers. Multi-Head Self-Attention handles relationships between tokens. The Feed-Forward Network (FFN) is applied independently to each token. FFN(x) = max(0, xW1 + b1) x W2 + b2. The FFN is typically 4x wider than the embedding dimension. Research has traced factual knowledge (like "Paris is the capital of France") to specific FFN layer weights. Attention routes context across the sequence; FFN applies per-token transformations and retrieves memorized knowledge.
"A model trained on one task, predict the next word, accidentally learned how to reason, translate, code, and debate."
2017 Attention Is All You Need → The Transformer (Vaswani et al.)
│
2018 GPT-1 117M params → Pre-train + fine-tune works
BERT-base 110M params → Bidirectional understanding
│
2019 GPT-2 1.5B params → Zero-shot. Too dangerous to release.
│
2020 GPT-3 175B params → Few-shot learning at unprecedented scale
│
2022 InstructGPT → RLHF → ChatGPT (OpenAI)
HH-RLHF → Constitutional AI → Claude (Anthropic)
│
2023 GPT-4 · LLaMA · Mistral · Gemini
│
2024+ What you use every day
The old way: Train a separate model for every task (translation, classification, Q&A) from scratch. Needs thousands of labelled examples for each task.
GPT-1's insight: Pre-train on massive unlabelled text first. Language itself teaches the model grammar, facts, and reasoning. Then fine-tune on a small labelled dataset for your specific task.
Pre-training: predict next word on 7000 books
Fine-tuning: show 500 labelled examples for sentiment analysis
Result: beats task-specific models trained on 50,000 examples
"You don't need labels to learn language. Language is its own teacher."
1.5 Billion parameters. 40GB of internet text.
Something unexpected happened: zero-shot task completion.
No fine-tuning. Just a prompt:
Prompt: "Translate to French: The sky is blue"
GPT-2: "Le ciel est bleu" ✅
Prompt: "Summarise this article: ..."
GPT-2: Reasonable summary ✅
OpenAI held back the full model for 9 months, worried about misuse.
Scale is a feature. Emergent capabilities appear at certain parameter counts, not programmed in, just... there.
175 Billion parameters. 570GB of filtered text data (from 45TB of raw Common Crawl).
Few-shot learning, teach it in the prompt:
Prompt:
"Translate to French:
Hello → Bonjour
Thank you → Merci
Good morning → ???"
GPT-3: "Bonjour" ✅
No gradient updates. No retraining. The model learned from the prompt at inference time.
This is called in-context learning, and it changes everything about how we build AI systems.
GPT-3 was powerful. But it was not helpful.
It would:
The alignment problem: A model trained to predict text is NOT the same as a model trained to be helpful, harmless, and honest.
Training task (next-word prediction) ≠ Deployment goal (helpful assistant)
This gap had to be closed. Enter RLHF.
Reinforcement Learning from Human Feedback:
Step 1 Supervised Fine-Tuning
Collect examples of ideal responses from human writers
Fine-tune GPT-3 on those responses
Step 2 Train a Reward Model
Show human raters pairs of responses → "which is better?"
Train a model to predict human preference scores
Step 3 Reinforcement Learning (PPO)
Update the LLM using the reward model as the "judge"
The LLM learns to produce responses that score high
Step 4 Repeat, the reward model improves, the LLM improves
Result: a model that tries to help instead of just predicting text.
GPT is trained to predict the most probable next token.
When training data contains confidently-written wrong statements, the model learns to sound confident, even when wrong.
It has no internal "I don't know" signal.
Question: "Who invented the telephone?"
GPT (unhelpful): "The telephone was invented by
Elisha Gray in 1876." ← confident, wrong
RLHF reduces hallucination but doesn't eliminate it.
This is why RAG (Retrieval-Augmented Generation) exists, you give the model the right answer in the prompt so it doesn't have to guess.
| BERT (Google, 2018) | GPT (OpenAI, 2018+) | |
|---|---|---|
| Architecture | Encoder only | Decoder only |
| Reads text | Bidirectionally (left + right) | Left to right only |
| Training task | Predict masked words | Predict next word |
| Best at | Understanding (classification, Q&A, NER) | Generation (chat, code, writing) |
| ChatGPT era winner | ❌ | ✅ |
BERT understood text better. GPT generated text better. In the era of conversational AI, generation wins.
Today: GPT-4, Claude, Gemini, LLaMA, Mistral, all decoder-only.
Q1: What is in-context learning and which model demonstrated it at scale?
In-context learning is the ability to perform new tasks from examples given in the prompt, with zero parameter updates. GPT-3 (2020, 175B parameters) demonstrated this at unprecedented scale. You give 2 to 3 input/output examples and the model generalizes to new inputs without any gradient updates. This was genuinely surprising: prior models needed explicit fine-tuning for every new task. In practice, it means you can turn a general-purpose LLM into a specialized classifier, extractor, or formatter purely through prompting, with no labeled training data or GPU training required.
Q2: Walk through RLHF step by step. What is the KL penalty for?
RLHF has three stages. Stage 1: Supervised Fine-Tuning (SFT), start from a base model and fine-tune on human-written demonstrations of good responses. Stage 2: Reward Model Training, generate multiple responses to the same prompt, have humans rank them, train a separate model on these preference comparisons. Stage 3: RL with PPO, use Proximal Policy Optimization to update the SFT model to maximize reward model scores. The KL penalty keeps the model from drifting too far from the SFT checkpoint and exploiting the reward model with degenerate outputs.
Q3: What is hallucination and what are the main mitigations?
Hallucination is when a model confidently states something factually false: a fabricated citation, a wrong date, a person who does not exist. It happens because LLMs are trained to predict plausible next tokens, not to verify factual accuracy. There is no built-in "I don't know" signal. Mitigations: RAG grounds answers in retrieved documents (the model must cite specific text), system prompts instruct the model to say it does not know rather than invent, low temperature reduces creative deviations for factual tasks, and validation LLM calls check if the answer is supported by retrieved context.
Q4: What is DPO and how does it differ from RLHF?
DPO (Direct Preference Optimization) achieves the same alignment goal as RLHF but eliminates both the reward model and the RL training loop. RLHF requires three separate training stages: SFT, reward model, then PPO. This is expensive and unstable. DPO's insight: the mathematically optimal RLHF policy has a closed-form solution. You can convert the RL problem directly into a classification task on preference pairs. Given (prompt, preferred response, rejected response), DPO trains the LM to increase the likelihood ratio of preferred over rejected. Simpler, more stable, computationally cheaper. Most modern open-source aligned models use DPO or its variants rather than RLHF.
Q5: What is Constitutional AI?
Constitutional AI (CAI, Anthropic 2022) addresses the scalability bottleneck of RLHF: human labelers must evaluate every output. CAI replaces human safety evaluators with the model itself, guided by a written "constitution" of principles. The model critiques its own response against these principles, generates a revised response, and these AI-generated preference pairs are used for training. Benefits: scales cheaply (principles applied to millions of examples automatically), and alignment criteria are explicit and auditable rather than implicit in human judgments. Claude's alignment training uses Constitutional AI as a core component.
Q6: What are emergent abilities in LLMs?
Emergent abilities appear suddenly at certain model scales, barely present in smaller models and then fully present in larger ones, without being explicitly trained for. A model at 7B parameters might score 5% on a task, and a 70B model suddenly scores 85%: not a gradual improvement but a phase transition. Examples: chain-of-thought reasoning, multi-step arithmetic, in-context learning. For engineers: you cannot predict capabilities by extrapolating from small models. A task that does not work at 7B may become trivial at 70B.
Q7: What were the key differences between GPT-1, GPT-2, and GPT-3?
GPT-1 (2018): 117M parameters, trained on roughly 1B tokens. Proved that unsupervised pre-training followed by fine-tuning works. Required fine-tuning for each downstream task. GPT-2 (2019): 1.5B parameters, trained on WebText (40GB of web text). Same architecture, dramatically bigger. Demonstrated zero-shot task completion: just a prompt, no fine-tuning. OpenAI initially withheld it over misuse concerns. GPT-3 (2020): 175B parameters, trained on 570GB of filtered internet text. Demonstrated in-context few-shot learning at scale: show 3 examples in the prompt and the model generalizes with zero gradient updates. One model could write code, translate, summarize, and answer questions from examples in the prompt alone. The pattern: scale (parameters plus data) was the primary lever, architecture barely changed.
"You've been calling the API like a black box. Time to open it."
When you call an LLM API, the billing unit is not a word. It is a token.
A token is a subword unit, not a character, not a full word.
"ChatGPT" → ["Chat", "G", "PT"] = 3 tokens
"tokenization" → ["token", "ization"] = 2 tokens
"unbelievable" → ["un", "believ", "able"] = 3 tokens
"Hello!" → ["Hello", "!"] = 2 tokens
"2024" → ["20", "24"] = 2 tokens
Rule of thumb: 1 token ≈ 0.75 English words
A 10,000-word document ≈ 13,000 tokens. Miscalculate and you burn budget.
Byte Pair Encoding, used in GPT-2, GPT-3, GPT-4, LLaMA, Mistral, and nearly every modern LLM.
Algorithm:
Start: vocabulary = all individual characters
[h, e, l, l, o, " ", w, o, r, l, d]
Round 1: Most frequent pair: "l" + "o" → merge into "lo"
Round 2: Most frequent pair: "h" + "e" → merge into "he"
Round 3: Most frequent pair: "w" + "o" → merge into "wo"
...
Repeat until vocabulary reaches target size (e.g. 50,000 tokens)
Common words → single tokens.
Rare words → split into meaningful pieces.
Unknown words → handled gracefully, never <UNK>.
After the Transformer runs, it produces raw scores called logits for every word in the vocabulary.
Temperature T divides all logits before converting to probabilities:
Prompt: "The sky is ___"
T = 0.2 → blue: 97%, grey: 2%, red: 1% (almost always "blue")
T = 0.7 → blue: 65%, grey: 20%, red: 10% (mostly "blue", some variety)
T = 1.5 → blue: 32%, grey: 25%, purple: 20%, ... (creative/random)
| Temperature Range | Use Case |
|---|---|
| 0.1 – 0.3 | Factual answers, structured data, SQL, code |
| 0.4 – 0.7 | Balanced chat, explanations |
| 0.8 – 1.4 | Creative writing, brainstorming, storytelling |
After applying temperature, filter to only the K most likely tokens:
Full vocabulary: 50,000 tokens
Top-K = 5: keep only top 5, zero the rest, renormalise, then sample
"The capital of France is ___"
K=5 keeps: Paris(78%), Lyon(8%), Nice(6%), Marseille(5%), Bordeaux(3%)
Sort tokens by probability (highest first). Keep adding to the "nucleus" until cumulative probability reaches P:
P = 0.9, keep the smallest set of tokens summing to ≥ 90% probability
Confident model (one clear answer):
"Paris": 95% → nucleus = just ["Paris"] (tiny)
Uncertain model (many valid words):
"happy", "sad", "curious", "excited", "nervous", ... → nucleus expands
Why Top-P is better than Top-K: Top-K always keeps exactly K tokens regardless of confidence. Top-P adapts, small when the model is sure, large when uncertain.
Production systems usually use temperature + top-P together.
# The ONLY correct order:
# Step 1: Apply temperature
logits = logits / temperature # rescale raw scores
# Step 2: Apply Top-K filter
# Zero out everything outside the top K
# Step 3: Apply Top-P filter
# Zero out everything outside the nucleus
# Step 4: Sample
next_token = sample_from(remaining_probabilities)
Getting the order wrong is a real bug, and a common interview question.
Temperature must come first because it reshapes the distribution before filtering.
Q1: Walk through the BPE algorithm step by step.
Start with a character-level vocabulary. Count all adjacent character pairs across the training corpus. Merge the most frequent pair into a new token. Repeat until the vocabulary reaches the target size (typically 50,000 to 100,000 tokens). Why not word-level: any word not in training becomes UNK, and every inflected form needs its own entry. Why not character-level: sequences become extremely long and attention computation (O(n squared)) becomes 16 to 25 times more expensive. Byte-level BPE (GPT-2 onwards) operates on raw bytes, guaranteeing no unknown tokens ever.
Q2: What is the correct order: temperature, softmax, Top-K, Top-P, sample?
Temperature always comes first because it reshapes the distribution before filtering. Correct order: (1) run Transformer forward pass to get raw logits, (2) divide logits by temperature, (3) apply softmax to convert to probabilities, (4) apply Top-K filter by zeroing out everything below rank K, (5) apply Top-P filter by keeping only the smallest set reaching cumulative probability P, (6) renormalize and sample. Getting this order wrong is a common bug: applying temperature after softmax changes what it does mathematically.
Q3: GPT-4o charges $5 per 1M input tokens. A RAG system sends 3,000 tokens per query at 10,000 queries per day. What is the monthly input cost?
3,000 tokens x 10,000 queries = 30 million input tokens per day. At $5 per 1M tokens = $150 per day for input alone. Monthly: $150 x 30 = $4,500 in input costs. Add output (500 tokens x 10,000 queries = 5M output tokens per day, at $15 per 1M = $75 per day, $2,250 per month). Total: roughly $6,750 per month. Optimization: route simple queries to GPT-4o-mini (30 to 40 times cheaper), reduce context size by keeping top 3 retrieved chunks instead of 10, use prefix caching for repeated system prompts.
Q4: What is the difference between Top-K and Top-P sampling?
Top-K always keeps exactly K candidates. Problem: if the model is very confident (one token at 95%), K=40 still forces sampling from 40 tokens including near-zero probability noise. If the model is uncertain (20 tokens at 5% each), K=40 might be too restrictive. Top-P keeps the smallest set whose cumulative probability reaches P. When confident, Top-P might keep just 2 to 3 tokens. When uncertain, it adapts to keep more. Best practice: use both together (Top-K=40, Top-P=0.95).
Q5: What is the "lost in the middle" problem in RAG?
Research shows LLMs systematically under-attend to information in the middle of long context windows. They perform best on information at the beginning (primacy) and end (recency) of the context, with accuracy dropping for content in the middle. In RAG: if you retrieve 10 documents and the most relevant one lands in the middle, it may effectively be ignored. Mitigations: put the most important retrieved context first or last, limit to top 3 to 5 chunks rather than 10 to 15, use a reranker to ensure the most relevant chunk gets a priority position.
Q6: A model generates "Eiffel Tower is in Berlin" confidently. Is this hallucination or retrieval failure?
This is a factual hallucination: the model generates false information with confidence. Retrieval failure means the correct document was not found. Hallucination means even with the correct document retrieved, the model generates something not supported by it. How to diagnose: check if the correct fact was in the retrieved context. If yes, it is hallucination (the model ignored the grounding evidence). If no, it is retrieval failure (fix chunking, reranking, or the embedding model). They require different fixes: retrieval failure needs better search; hallucination needs stricter grounding prompts and citation enforcement.
"What if you could run GPT-quality AI on your own laptop, for free, without sending a single byte to any company?"
| Problem | Real Impact |
|---|---|
| Cost | 100,000 tokens/day adds up fast in production |
| Privacy | Your prompts, your users' data → goes to their servers |
| Reliability | API outage = your product is down |
| Vendor lock-in | They change pricing, terms, or models → you're stuck |
| Latency | Network round-trip adds 100–500ms every call |
The answer: Run locally for dev/privacy OR use a multi-provider strategy for production.
# Install (Mac / Linux)
curl -fsSL https://ollama.com/install.sh | sh
# Pull a model
ollama pull phi3 # 2.7B params, runs on 8GB RAM, free
ollama pull llama3.2 # 3B params, runs on 8GB RAM, free
ollama pull mistral # 7B params, needs 16GB RAM
# Ask it a question
ollama run phi3
>>> "Explain transformers like I'm 10 years old"
Ollama exposes an OpenAI-compatible REST API at http://localhost:11434
Your Python code doesn't change. Just swap the base_url.
Every provider, OpenAI, Gemini, Anthropic, Ollama, Databricks, accepts this same structure:
messages = [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Explain the Transformer architecture in one paragraph."
},
{
"role": "assistant",
"content": "The Transformer was introduced in..." # previous turn
},
{
"role": "user",
"content": "Now explain RLHF."
}
]
This is the skeleton of every LLM application you will ever build.
# Identical code. Change one string. Different provider.
model = "openai/gpt-4o"
model = "anthropic/claude-3-5-sonnet"
model = "google/gemini-1.5-pro"
model = "meta-llama/llama-3-70b-instruct"
model = "mistralai/mistral-7b-instruct"
No 5 different SDKs. No 5 API keys. One account, one key, transparent pricing.
When to use OpenRouter: Prototyping, model comparison, avoiding vendor lock-in.
Student tip: Start with Gemini (free tier). Use OpenRouter when you want to compare models.
Every LLM has a context limit (8K, 32K, 128K tokens). What if your document is longer?
200-page PDF (300,000 tokens), won't fit
│
SPLIT into 20 chunks of ~15,000 tokens each
│
MAP: Summarise each chunk independently ← 20 parallel LLM calls
│
REDUCE: Merge all 20 chunk summaries ← 1 final LLM call
│
Final summary of the entire 200-page document
Same pattern used in document intelligence pipelines (M11), research assistants (M14), and any production RAG system handling large documents.
| Factor | Local (Ollama / LM Studio) | Cloud (Gemini / OpenRouter) |
|---|---|---|
| Cost | Free forever | Pay per token |
| Privacy | 100%, nothing leaves machine | Data sent to provider |
| Model capability | Limited by your RAM | Access to GPT-4, Claude 3 |
| Speed | Depends on your GPU/CPU | Fast, consistent |
| Internet | Not required | Required |
| Setup time | 10 minutes | Instant (just API key) |
| Best for | Dev, learning, private data, offline | Production, large models, teams |
Q1: What is GGUF format and what does Q4_K_M mean?
GGUF is the standard format for quantized LLM weights created by the llama.cpp project. All model weights, hyperparameters, tokenizer, and metadata are packed into one portable file with no separate config files needed. Q4_K_M breaks down as: Q4 = 4-bit quantization (each weight stored in 4 bits instead of 32), K = K-quant method (groups of roughly 32 weights share a scale factor to minimize quantization error), M = medium size variant (balances accuracy vs speed). Q4_K_M achieves roughly 3% perplexity increase vs FP16, imperceptible in most applications, while reducing a 7B model from 14GB to about 4GB.
Q2: What are the three message roles in the universal chat format?
The universal format is a list of {role, content} dicts supported by every major provider. System: sets overall behavior, persona, and constraints for the entire conversation. User: represents a human turn. Assistant: represents a model turn. Including prior assistant turns is how you inject conversation history for multi-turn dialogue. Every provider (OpenAI, Anthropic, Google, Ollama) accepts this same format, which is why your conversation history logic works across providers without changes.
Q3: What are the limitations of OpenRouter for production use?
OpenRouter is a proxy, not a first-party provider. Limitations: (1) additional latency from the proxy hop (typically 50 to 200ms), (2) you depend on OpenRouter's uptime on top of the underlying provider's uptime, (3) pricing includes a small markup over direct API pricing, (4) some advanced features like fine-tuned model endpoints may not be available through the proxy, (5) enterprise SLAs come from OpenRouter, not the underlying providers. Use it for prototyping, development, and model comparison. For high-throughput production, connect directly to the provider.
Q4: How does INT4 quantization reduce memory while preserving quality?
FP32 weights use 32 bits per parameter. INT4 uses 4 bits, an 8 times reduction. A 7B model goes from roughly 28GB (FP32) to roughly 3.5GB (INT4). The accuracy tradeoff: quantization rounds each weight to the nearest representable value in the 16-step INT4 range. For most weights, this rounding error is small because weight distributions are bell-shaped and concentrated near zero. K-quant methods use per-group scale factors to minimize this error further. The result is models that fit in consumer RAM while performing close to full-precision.
Q5: How much RAM do you need to run a 7B model locally?
A 7B model at Q4_K_M requires roughly 4GB for weights plus 1 to 2GB for KV cache and overhead. Practical minimum: 6 to 8GB available RAM. Guide: 8GB RAM (MacBook Air M1/M2) runs 7B comfortably at 15 to 25 tokens per second using Apple Metal. 16GB handles 7B well or 13B at Q4_K_M. 32GB allows 13B comfortably or a try at 30B. 64GB handles 70B at Q4_K_M (roughly 40GB). Key rule: the model must fit entirely in RAM or VRAM for interactive speeds. Paging to disk is too slow for real use.
Q6: Compare Ollama, LM Studio, and OpenRouter: when would you choose each?
Ollama: CLI-first tool, runs a REST server at localhost:11434 with an OpenAI-compatible API. Best for developers, scripting, Jupyter notebooks, and production local inference. LM Studio: desktop GUI with drag-and-drop model loading, built-in chat interface, and a local OpenAI-compatible server. Best for non-developers and students exploring models visually. OpenRouter: cloud API gateway, no local install. Best for accessing frontier models through one API key, model comparison studies, or when your hardware cannot run the required model size. Decision: need privacy or offline use → Ollama or LM Studio. Need frontier model quality → OpenRouter or direct provider. Building production code → Ollama (stable REST API, Docker-friendly).
Q7: How does map-reduce work for documents that exceed the context window?
A single context window cannot fit a full book or large codebase. Map-reduce splits the document into chunks (map step), processes each chunk independently with the LLM, then combines results (reduce step). For summarization: map generates a summary of each chunk, reduce merges summaries into a final coherent output. For Q&A: map extracts relevant passages, reduce synthesizes the final answer. An alternative is the refine strategy: process chunk 1, pass its output plus chunk 2 to the LLM for refinement, repeat until done. Refine maintains cross-chunk coherence but is sequential and slower. Map-reduce is faster because chunks are processed in parallel. In LangChain: load_summarize_chain(chain_type="map_reduce").
"What if instead of writing the perfect prompt yourself, you let code find it for you?"
A well-written prompt has five parts. You don't need all five every time, but knowing them helps you debug when the model gives you the wrong answer.
| Part | What It Does | Example |
|---|---|---|
| Role | Who should the AI be? | "You are a senior data analyst" |
| Task | What exactly do you want? | "Summarize this into 3 bullet points" |
| Context | What background does it need? | "The audience is non-technical managers" |
| Examples | What does a good answer look like? | Show 2-3 input/output pairs |
| Output format | How should it structure the response? | "Reply only in JSON. No markdown." |
Bad prompt:
Summarize this.
The model doesn't know: how long? What style? What to focus on?
Good prompt:
You are a business analyst.
Summarize the following customer feedback in exactly 3 bullet points.
Each bullet: main complaint + why it matters to the business.
Feedback: "The checkout took 10 minutes and the payment kept
failing. I had to try 4 times. Really frustrating."
Role + task + output format. The model has no room for misunderstanding.
The same question produces completely different answers depending on the role you set.
Without role:
Prompt: "Explain recursion."
Result: A dry textbook definition.
With role:
Prompt: "You are a patient tutor teaching a 16-year-old
who knows Python basics but has never heard of recursion.
Start with a real-world analogy. Then show the simplest
possible code example. Keep it under 150 words."
Result: An analogy about a camera inside a mirror, then
5 lines of Python, in exactly the right tone and length.
Why does this work? The model has seen millions of examples of teaching-style writing. Saying "patient tutor" activates those patterns.
You give 2-3 examples of input and output before asking your real question. The model learns the exact format and labels you want.
Without examples (zero-shot), the model picks its own format:
Review: "Arrived late but product is great."
→ "Mixed sentiment" or "Neutral" or "3/5 stars" (you get something different every time)
With examples (the model locks into your format):
Review: "Fast delivery, great quality." → Positive
Review: "Product broke after one day." → Negative
Review: "Good price but slow shipping." → Mixed
Review: "Arrived late but product is great." →
The model now knows: exactly three labels, one word, no explanation. You don't have to say any of that, because the examples show it.
Without CoT (the model guesses on reasoning tasks):
Q: Train A leaves at 9am at 80km/h, Train B at 10am at 100km/h,
stations are 210km apart. When do they meet?
A: "10:45am" ← often wrong
With CoT (just add "think step by step"):
Q: Same question. Think step by step.
A: At 10am, Train A has traveled 80km. Distance remaining: 130km.
Combined closing speed = 80 + 100 = 180 km/h.
Time to close 130km = 130/180 = 0.72h = 43 minutes.
They meet at 10:43am. ✓
Generating intermediate steps gives the model more context for each next token. It is especially powerful for math, logic, and classification with subtle distinctions. In DSPy, dspy.ChainOfThought adds this automatically.
If the response needs to go into your code, always lock down the format.
Unstructured (your code cannot reliably parse this):
Analyze this review and tell me the sentiment, the main topics,
and whether the customer would recommend it.
→ "The customer seems fairly happy overall, mentioning
delivery speed and product quality. They would likely..."
Structured (your code does json.loads(response) and it works):
Reply with ONLY this JSON. No markdown. No explanation.
{
"sentiment": "positive" | "negative" | "mixed",
"topics": ["list", "of", "topics"],
"would_recommend": true | false
}
One rule: if the LLM output goes into code downstream, always control the format.
You've been writing prompts like this:
"You are a helpful assistant. Classify the intent of this customer message.
Message: {text}
Intent options: {labels}
Answer:"
Then tweaking it manually. Testing. Tweaking again.
This does not scale. You have no way to know if your prompt is optimal. You're guessing.
DSPy's answer: Define what you want in code. Let optimisers find the best prompt automatically.
DSPy (Stanford NLP) treats prompts as learnable parameters, not hand-written strings.
Key concepts:
| Concept | What It Is |
|---|---|
dspy.Signature |
Defines the input/output schema for an LLM call |
dspy.ChainOfThought |
Predictor that adds step-by-step reasoning before answering |
| Optimizer | Automatically finds the best prompt/examples for your task |
| Metric | How you measure "better", e.g. answer_exact_match |
class Classification(dspy.Signature):
"""Classify the customer message into one of the intent labels."""
customer_message = dspy.InputField(
desc="Customer message during customer service interaction"
)
intent_labels = dspy.InputField(
desc="Labels that represent customer intent"
)
answer = dspy.OutputField(
desc="A label best matching the customer's intent"
)
# Chain-of-Thought: think step by step before answering
cot_predictor = dspy.ChainOfThought(Classification)
The docstring becomes the instruction. The fields become the prompt structure.
You define the task. DSPy writes the prompt.
| Optimiser | What It Does | When to Use |
|---|---|---|
| LabeledFewShot | Injects k random training examples into the prompt | Quickest baseline, no real optimisation |
| BootstrapFewShot | Generates additional examples, tests them, keeps only those that pass your metric | When you have some labelled data and want better examples |
| BootstrapFewShot WithRandomSearch | Runs BootstrapFewShot multiple times with different random seeds, picks best program | When you need maximum accuracy and have compute budget |
| MIPROv2 | Optimises the instruction text itself, not just examples | Most powerful, changes the actual prompt wording |
Your labelled training data
│
Run the LLM on each training example
│
Check: does the LLM's output pass the metric?
│
YES → keep this (input, output) pair as a demo
NO → discard
│
Inject only the PASSING demos into the final prompt
The key insight: Instead of hand-picking examples, DSPy runs your metric against the model's own generations and keeps the good ones automatically.
The most powerful DSPy optimiser: it searches over the instruction wording, not just the examples.
// From MiproV2Prompt.json, the optimised instruction found automatically
{
"predict": {
"signature": {
"instructions": "Classify the customer's airline service query into
the correct intent category. Analyze the message
carefully to identify the primary action or
information need expressed..."
}
}
}
Nobody wrote that instruction. The optimiser found it by testing thousands of phrasings against the validation set.
Standard prompting:
Q: "I need to change my flight booking"
A: "flight_change"
Chain-of-Thought:
Q: "I need to change my flight booking"
Reasoning: "The customer is asking to modify an existing reservation.
The key words are 'change' and 'flight booking'.
This is a modification request, not a cancellation."
A: "flight_change"
CoT forces the model to show its work. Errors in reasoning are visible and correctable. Accuracy improves significantly on complex classification and reasoning tasks.
Q1: What are the five parts of a well-written prompt and why does each matter?
Role (who the AI should be): activates relevant writing patterns from training. "You are a senior data analyst" produces very different output than the same task with no role. Task (what exactly to do): be specific, for example "summarize in 3 bullet points" not just "summarize." Context (background it needs): "the audience has no technical background" changes every word choice. Examples (2 to 3 input/output pairs): the most powerful lever for locking in format and label names, the model copies your exact output structure. Output format: critical when code must parse the output, use "Reply ONLY with valid JSON, no markdown, no explanation." Most bad prompts fail because only the task is included.
Q2: What is Chain-of-Thought prompting and why does forcing reasoning help?
CoT prompting forces the model to generate intermediate reasoning steps before producing the final answer. Adding "think step by step" produces explicit reasoning tokens that give the model more context for each subsequent decision. Classic example: "Is 9.11 > 9.9?" Without CoT: "Yes" (wrong). With CoT: "9.11 means 0.11 after the decimal. 9.9 means 0.90. 0.11 < 0.90, so 9.11 < 9.9. No." (correct). CoT helps on multi-step math, logic puzzles, nuanced classification, and code generation. Does not help on simple factual lookups.
Q3: What is DSPy and how does it differ from writing prompts manually?
DSPy (Stanford NLP) treats prompts as learnable parameters rather than hand-written strings. You define a Signature (input/output schema with field descriptions), a metric (function that scores output quality), and a training dataset. An optimizer automatically searches for the best prompt, instruction wording, and few-shot examples. The model never sees your raw prompt: DSPy assembles the optimal one from your signature and the optimized instructions. Manual prompting requires expert iteration; DSPy automates it and is reproducible across model versions.
Q4: What is the difference between LabeledFewShot and BootstrapFewShot?
LabeledFewShot randomly picks k examples from your training set and injects them as few-shot demonstrations. No actual optimization: just in-context learning with randomly selected examples. Quality depends on which k examples happen to be picked. BootstrapFewShot runs the model on training examples, evaluates each prediction against your metric, and keeps only examples where the model was correct. The injected demos are guaranteed to pass your validation criterion, not random samples. This produces significantly higher quality signal than random selection. BootstrapFewShot requires no human-labeled outputs, only human-labeled inputs with a metric to evaluate outputs.
Q5: What can MIPROv2 optimize that BootstrapFewShot cannot?
BootstrapFewShot selects which demonstrations to include but cannot modify the instruction text itself. MIPROv2 searches over both the instruction wording AND the few-shot examples simultaneously. It uses Bayesian optimization to generate and evaluate candidate instruction variants, focusing the search on promising phrasings. MIPROv2 may find "let's work through this step by step to ensure accuracy" outperforms your hand-written "answer the following question" by several points in F1. Use BootstrapFewShot first; upgrade to MIPROv2 when you need further gains and have labeled validation data.
Q6: Give a concrete example where few-shot is strictly necessary over zero-shot.
Format consistency for downstream code parsing. Zero-shot "Classify sentiment as positive, negative, or mixed" produces "Positive", "positive", "POSITIVE", "The sentiment is positive", "I believe this is positive": all different strings, none reliably parseable by code. Few-shot with 3 examples ("Love it!" → "positive", "Awful!" → "negative") forces the model to lock into exactly one of three strings you control. Your if answer == "positive" check works 100% of the time. Rule: whenever downstream code must parse the output or you need consistent label strings for a classifier, few-shot is not optional. It is the only reliable approach.
"You've called OpenAI. You've called Gemini. You've called Ollama. Why are you writing three different codebases?"
Every provider has different:
# OpenAI
response = client.chat.completions.create(model=..., messages=[...])
answer = response.choices[0].message.content
# Anthropic / Claude
response = client.messages.create(model=..., messages=[...])
answer = response.content[0].text
# Gemini
response = client.models.generate_content(model=..., contents=...)
answer = response.text
Different authentication, different response shapes, different streaming APIs.
Every provider switch = rewrite a significant chunk of code.
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_ollama import ChatOllama
# ALL return an AIMessage with .content: identical interface
models = {
"openai": ChatOpenAI(model="gpt-4o-mini"),
"claude": ChatAnthropic(model="claude-haiku-4-5-20251001"),
"gemini": ChatGoogleGenerativeAI(model="gemini-2.5-flash-lite"),
"llama": ChatOllama(model="llama3.2:3b"),
}
for name, model in models.items():
response = model.invoke(["Explain neural networks simply"])
print(f"{name}: {response.content}")
Change one variable. Same code. Every provider.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant."),
("human", "{question}"),
MessagesPlaceholder(variable_name="history")
])
MessagesPlaceholder, injects the full conversation history at that position automatically.
Build the prompt once. Reuse it across any model, any session.
from langchain_core.chat_history import InMemoryChatMessageHistory
# Each user session gets its own history object
history = InMemoryChatMessageHistory()
# Messages are added automatically as the conversation progresses
history.add_user_message("What is self-attention?")
history.add_ai_message("Self-attention is a mechanism that...")
history.add_user_message("How is it different from RNNs?")
# history.messages → passed into MessagesPlaceholder in the prompt
The model sees the full conversation every turn, this is how chatbots remember.
# Without streaming: user waits for full response
response = model.invoke(["Write me a short essay on AI"])
# With streaming: user sees tokens appear in real time
for chunk in model.stream(["Write me a short essay on AI"]):
print(chunk.content, end="", flush=True)
Why streaming matters in production:
from langchain_core.output_parsers import StrOutputParser
# Build a chain: prompt → model → parse to string
chain = prompt | model | StrOutputParser()
# Invoke it
answer = chain.invoke({
"question": "What is LangChain?",
"history": history.messages
})
| pipes the output of one component into the next.
This is LCEL (LangChain Expression Language), composable, readable, provider-agnostic.
Q1: What is LCEL and what does the pipe operator do technically?
LCEL (LangChain Expression Language) is a declarative way to compose LangChain components into pipelines using the | operator. Every component implements the Runnable interface. The | operator chains Runnables: output of the left component becomes input to the right. A chain like
prompt | llm | StrOutputParser()means: (1) prompt formats the input dict into a messages list, (2) llm sends the messages to the provider and returns an AIMessage, (3) StrOutputParser extracts the .content string. The chain itself is also a Runnable, so you can pipe it into more components. Switching providers means replacing only the llm component without touching prompt or parser.
Q2: What is ChatPromptTemplate and what is MessagesPlaceholder used for?
ChatPromptTemplate defines the message structure sent to the LLM as a template with typed variable slots. It takes a list of message tuples: [("system", "You are a {persona}"), ("human", "{question}")]. At runtime, .format_messages() fills in the variables and produces the final messages list. MessagesPlaceholder is a special slot for injecting a dynamic list of messages, specifically used for conversation history. Instead of manually building message dicts for every prior turn, you define MessagesPlaceholder("history") once in the template and pass the history object at runtime. This cleanly separates template definition from runtime data.
Q3: How does streaming work in LangChain and why is it critical for production?
chain.stream(input)returns a Python generator that yields text chunks as the LLM produces them, typically word by word.chain.invoke(input)waits for the complete response. Why streaming matters: users see the first word in under 1 second instead of waiting 5 to 20 seconds for a blank screen. Perceived speed is 5 to 10 times better even if total generation time is the same. Users can stop generation early if the response is going in the wrong direction. In Streamlit:st.write_stream(chain.stream(...))handles the UI update automatically. The same .stream() call works across all providers: LangChain normalizes provider-specific streaming APIs.
Q4: What is the Runnable interface and what are its 5 core methods?
The Runnable interface is the standard protocol every LangChain component implements, enabling composition with |. Five core methods: (1) .invoke(input): synchronous single call, use for simple scripts. (2) .stream(input): synchronous streaming, yields output chunks, use for real-time UIs. (3) .batch(inputs): processes a list of inputs efficiently, often in parallel, use for bulk processing. (4) .ainvoke(input): async version of invoke, use in FastAPI. (5) .astream(input): async streaming generator, use in async web servers. Every component that implements these methods works with |: prompts, LLMs, parsers, and custom RunnableLambda wrappers.
Q5: How would you switch a production chain from GPT-4 to Claude with minimal changes?
Because all LLM wrappers implement the same Runnable interface, you change exactly one line: replace ChatOpenAI with ChatAnthropic and update the model name. The chain
prompt | llm | parseris unchanged. For production-grade switchability, define a factory function that reads from an environment variable:def get_llm(provider=None). Then switching in production means changing one environment variable, no code deployment needed. Caveat: models differ in context length, function calling format, and system message handling. Always test your specific use case after switching.
Q6: What is RunnableWithMessageHistory and how does it simplify multi-user chat?
Manual approach: maintain a message list, append each user and AI message, pass the full list to the chain on every turn. Works but requires boilerplate for every app, and multi-user isolation needs manual session keying. RunnableWithMessageHistory wraps any LCEL chain to automate all of this. You provide a
get_session_history_fnthat maps session_id to a history store. On each call, it retrieves history for the session, injects it into the MessagesPlaceholder, runs the chain, and appends the new turn automatically. Multi-user isolation: passconfig={"configurable": {"session_id": user_id}}. Each user ID gets completely isolated history. Session persistence across restarts requires a Redis or database backend.
Raw Text
│ "How does Google find a result?"
▼
S00 Inverted Index + TF-IDF Keyword matching and ranking
│ ⚠️ "car" ≠ "automobile"
▼
S01 Word Embeddings Semantic meaning in vector space
│ ⚠️ "bank" one vector, context ignored
▼
S02 Transformers + Self-Attention Context-aware representations
│ ⚠️ Training from scratch = weeks + $$$$
▼
S03 Pre-trained GPT → RLHF Aligned, helpful intelligence
│ ⚠️ How does token-by-token generation actually work?
▼
S04 BPE + Temperature + Top-K/P Inside the generation pipeline
│ ⚠️ Cloud APIs cost money and require internet
▼
S05 Ollama + OpenRouter You own the infrastructure
│ ⚠️ We're still writing prompts by hand, guessing what works
▼
S06 DSPy + CoT + BootstrapFewShot Prompts become learnable parameters
│ ⚠️ Every provider needs different code to call
▼
S07 LangChain One interface. Every provider.
Every line is a problem. Every arrow is a solution.
| Concept | 10-Second Answer |
|---|---|
| TF-IDF | Rare words that appear often in one doc = important |
| Word2Vec | Predict context words → similar words get similar vectors |
| Cosine similarity | Angle between vectors, direction = meaning, not length |
| Self-attention | Every word votes on how relevant every other word is |
| Positional encoding | Sine/cosine signals so Transformer knows word order |
| Multi-head attention | 8 attention runs in parallel, each captures different relationships |
| GPT vs BERT | GPT generates (decoder), BERT understands (encoder) |
| RLHF | Humans rank responses → reward model → RL to align the LLM |
| Hallucination | No "I don't know" signal, model predicts probable text, not truth |
| BPE | Iteratively merge frequent character pairs into subword tokens |
| Temperature | Low = deterministic/factual. High = creative/random |
| Top-P | Keep smallest token set summing to P, adapts to model confidence |
| Ollama | Run open-source LLMs locally, OpenAI-compatible API |
| Map-Reduce | Split → summarise each chunk → merge summaries |
| DSPy Signature | Define input/output schema, DSPy writes the prompt |
| ChainOfThought | Step-by-step reasoning before answering |
| BootstrapFewShot | Auto-generate demos, keep only those that pass your metric |
| MIPROv2 | Optimise the instruction text itself automatically |
| LangChain | One .invoke() for every LLM provider |
| ChatPromptTemplate | Typed prompt with variable + history placeholders |
| LCEL chain | Compose prompt → model → parser with the | pipe operator |
| Streaming | .stream() yields tokens as generated, instant perceived response |
| Session | Topic | Key Skill |
|---|---|---|
| S06 | Prompt Engineering + DSPy + CoT | Optimise prompts automatically with code |
| S07 | LangChain | One interface for every LLM provider |
| Topic | What You'll Build |
|---|---|
| Memory & Chatbots | LLMs that remember across conversations |
| RAG Basics | LLMs that answer from your data |
| Production RAG | Hybrid search, reranking, evaluation |
| LangChain Agents | LLMs that take actions in the world |
| LangGraph | Stateful, controlled multi-step agent workflows |
| CrewAI | Teams of agents working together |
| MCP | Connecting agents to real tools and systems |
Eight sessions ago you asked: "How does Google find a result?"
Now you can explain:
This foundation is permanent. Every LLM system in the world, no matter how advanced, sits on exactly these concepts. You understand the ground floor.
Next session, we build higher.