AI EngineeringZero to ProductionHome·About·What’s new·Contact
Appendix · Advanced AI Engineering · Part 9

Big Tech AI-Engineering Patterns

The other interview loop — for AI/ML engineer, applied scientist, and LLM/GenAI engineer roles at Big Tech. Beyond rote pattern-drilling (that's D7), these rounds test whether you can implement the ML primitives from scratch, reason about production LLM systems, and design RAG/agent architectures. This part maps each A1–A8 topic to the questions actually asked, with runnable code and diagrams.

⏱️ ~3 hours🎯 AI/ML interview loop🧠 implement + designrunnable

Learning objectives

  • Implement the ML-from-scratch primitives interviewers ask for (softmax, attention, k-means, cosine kNN).
  • Answer the LLM-systems questions: tokenization, sampling, context management, caching, rate limits.
  • Design a RAG pipeline and an agent loop on a whiteboard, with the right trade-offs.
  • Talk fluently about the "productionizing an LLM feature" system-design round.
How this maps to A1–A8Every section below is a Big-Tech-style AI question anchored to the track: A4 → implement softmax/attention/k-means; A5 → tokenization & sampling; A3 → concurrency & streaming; A6 → structured output & resilience; A7 → RAG & vector search design; A8 → productionization & evals. It's the "can you actually build it?" companion to the DSA round in D7.

1 · Implement softmax & cross-entropy (A4) very common

Here's the moment this question decides your loop. You write softmax in four confident lines, it passes the tiny example, and the interviewer quietly types one big number into the input — say a logit of 1000. Your code returns nan. That single nan is the whole point of the round: it separates people who memorized the softmax formula from people who understand that a formula on paper and a formula on a real machine are different animals. The maths is exact; floating-point arithmetic is not.

So what is softmax, in plain terms? Think of it as a vote-counter that has to turn raw enthusiasm into percentages. Each candidate (each class, each next-token) shouts a number — the logit — and those numbers can be anything: 2.0, -17, 1000. Softmax's job is to convert that noisy shouting into a clean probability distribution: everything positive, everything summing to exactly 1, and the loudest shout getting the biggest slice. It does this by exponentiating (which makes big numbers enormously bigger, exaggerating the winner) and then dividing by the total (which rescales the whole thing back to a tidy 1.0).

The trap — and the entire reason this is an interview question rather than a one-liner — is that exp grows terrifyingly fast. exp(1000) is a number with over 400 digits; your computer can't hold it, so it becomes inf, and inf/inf is nan. The fix is a small piece of algebra that feels like cheating but isn't: subtract the largest logit from every logit before exponentiating. Because that same constant appears in every term of the numerator and the denominator, it cancels perfectly in the ratio — the answer is mathematically identical — but now the biggest exponent is exp(0) = 1 and nothing overflows. The common mistake is to skip this line because "it works on my test." It does work, right up until a real model hands you a confident logit and the whole thing silently returns garbage. Its partner, cross-entropy, is just "how surprised were we by the right answer" — the negative log of the probability softmax assigned to the true class — which is why the two always travel together.

Asked as: "code softmax — now make it numerically stable." The stability trick (subtract the max) is the whole point; forgetting it overflows on large logits.

logits → subtract max (stability) → exp → normalize [2.0, 1.0, 0.1] e^(x−max) [.66,.24,.10] sums to 1 · largest logit → largest probability Softmax = exponentiate then normalize. Subtracting the max before exp shifts values into a safe range without changing the result (it cancels in the ratio) — the detail interviewers look for.
🗺️ How to read this diagram

This picture shows what softmax does: it turns a list of raw scores (called logits) into a list of probabilities that add up to 1. Read it left to right — each arrow is one step of the calculation.

  • Left box [2.0, 1.0, 0.1] — the raw scores the model produced. They can be any size, positive or negative; they are not probabilities yet.
  • First arrow (subtract max, then e^(x−max)) — we shift every number down by the largest one, then raise e (≈2.718) to each. Subtracting the max is the stability trick: it keeps the numbers small enough that the computer won't overflow, and it does not change the final answer.
  • Second arrow (normalize) — divide each result by the total so the whole list sums to exactly 1.
  • Right box [.66, .24, .10] — the probabilities. Notice the biggest input (2.0) became the biggest probability (.66): softmax keeps the ranking, it just rescales into a 0–1 range.

In short: softmax = exponentiate, then divide by the total. The order of the numbers never changes — the largest logit always gets the largest probability.

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
Numerically stable softmax + cross-entropy loss
pythonimport numpy as np

def softmax(logits):
    z = logits - np.max(logits)          # STABILITY: avoid exp overflow
    e = np.exp(z)
    return e / e.sum()

def cross_entropy(logits, label):
    p = softmax(logits)
    return -np.log(p[label] + 1e-12)     # negative log-prob of the true class

print(softmax(np.array([2.0, 1.0, 0.1])).round(2))   # [0.66 0.24 0.1 ]
▶ How this works

This is the diagram above written as real code, plus its partner cross-entropy — the number that measures how wrong a prediction was. Interviewers ask for softmax and expect the stability trick; forgetting it is the classic mistake.

  1. z = logits - np.max(logits) subtracts the largest value from every logit. This is the stability step — it prevents np.exp from blowing up to infinity on big numbers. Because it cancels out in the division, the final probabilities are identical.
  2. e = np.exp(z) raises e to each shifted value, then e / e.sum() divides by the total so the results add to 1 — the two "exponentiate then normalize" steps from the picture.
  3. cross_entropy calls softmax, then returns the negative log of the probability the model gave to the correct answer (p[label]). A confident-and-right model gets a probability near 1, whose log is near 0 → tiny loss. A confident-and-wrong model gets a tiny probability → huge loss.
  4. The tiny 1e-12 added inside the log is a safety guard: log(0) is undefined, so this keeps it from crashing if a probability rounds to exactly zero.

What the output means: The last line prints [0.66 0.24 0.1 ] — the three probabilities, which sum to 1. That matches the green box in the diagram.

Try this: Change the input to [100.0, 1.0, 0.1] and run it — thanks to the - np.max line it still works. Delete that line and the same input overflows to nan (not-a-number). That one line is the whole point of the question.

🔗 Builds onA4 §8 (sampling) & the autograd engine in A4 §6. Follow-ups: temperature scaling, top-k/top-p sampling, log-softmax, why cross-entropy pairs with softmax.

2 · Implement scaled dot-product attention (A4) the signature question advanced

If you learn to write exactly one thing from scratch for a GenAI interview, make it this. "Implement attention" is to LLM roles what "reverse a linked list" is to general software roles — the question that comes up so reliably that not being able to write it is a red flag on its own. The good news: the mechanism that powers every transformer, the thing behind ChatGPT and Claude and every model you've used, fits in about six lines. The intimidating name hides an idea you already understand.

Here's the plain-language version. Imagine every word in a sentence is a person at a networking event, and each one is holding two cards: a question card (the query: "I'm looking for someone who knows about X") and a badge (the key: "here's what I'm about"). To figure out who a given person should listen to, you compare their question card against everyone else's badge and score the matches — a strong match means "pay a lot of attention to that person." Those scores get turned into percentages (that's softmax again, from §1), and finally each person walks away with a blend of what the people they cared about were actually saying (the values). That blend is the output. Attention is, at its core, nothing more exotic than a weighted average — the cleverness is entirely in where the weights come from.

Two details earn the check-mark, and both have intuitive reasons. First, the ÷√d scaling: when your vectors are long (high dimension d), the dot-product scores naturally get bigger just because you're summing more terms — and huge scores push softmax into a corner where it hands almost all the weight to one word and the gradients flatten to nothing during training. Dividing by √d is the volume knob that keeps scores in a sane range no matter the dimension. The common mistake here is dropping that scale entirely because the code "runs fine" on a toy example — it does, until you train at real dimension and the model won't learn. Second, the causal mask: in a decoder, a word must not peek at words that come after it (they haven't been generated yet), so you set those future positions to a huge negative number before softmax, which drives their weight to essentially zero. Get the weighted-average intuition, the √d reason, and the mask, and you've answered the signature question completely.

Asked as: "implement attention" — the single most common GenAI-role question. It's three matmuls, a scale, a softmax, and a weighted sum.

Attention(Q,K,V) = softmax(QKᵀ / √d) · V Q Kᵀ scores /√d softmax · V weights say "how much each token attends to every other" Attention is a weighted average of values. QKᵀ scores every query against every key; dividing by √d keeps gradients stable; softmax turns scores into weights; multiplying by V mixes the values. This is the transformer's core.
🗺️ How to read this diagram

This is the heart of every transformer. Attention lets each word look at every other word and decide how much to pay attention to it. The formula on top (softmax(QKᵀ/√d)·V) is exactly what the boxes below spell out, step by step.

  • Q and Kᵀ boxes — Q (queries) is "what each word is looking for"; K (keys) is "what each word offers." The little ᵀ just means K is flipped so the shapes line up for multiplication.
  • scores /√d box — multiplying Q by Kᵀ gives a grid of scores: how well each word matches each other word. Dividing by √d (the square root of the vector size) keeps those scores from getting so large that the next step breaks.
  • softmax box — turns each row of scores into weights that add up to 1 (same softmax as section 1). Now every word has a set of "how much I care about each other word" percentages.
  • · V box — V (values) is the actual content of each word. Multiplying the weights by V produces a weighted average: each word's output is a blend of the words it cared about most.

In short: Attention is just a weighted average of values, where the weights come from how well queries match keys. Q·Kᵀ scores the matches, √d keeps them stable, softmax turns them into percentages, and ·V mixes the content.

Scaled dot-product attention (self-attention), from scratch
pythonimport numpy as np

def softmax_rows(x):
    x = x - x.max(axis=-1, keepdims=True)
    e = np.exp(x)
    return e / e.sum(axis=-1, keepdims=True)

def attention(Q, K, V, mask=None):
    d = Q.shape[-1]
    scores = Q @ K.T / np.sqrt(d)         # (seq, seq) affinities
    if mask is not None:
        scores = np.where(mask, scores, -1e9)   # causal mask → future = -inf
    weights = softmax_rows(scores)        # each row sums to 1
    return weights @ V                    # weighted sum of value vectors

seq, d = 3, 4
Q = K = V = np.random.rand(seq, d)
print(attention(Q, K, V).shape)           # (3, 4)
▶ How this works

Here is the attention diagram as runnable code — "the signature question" for GenAI roles. It is only a handful of lines: a matrix multiply, a scale, a softmax, and another matrix multiply.

  1. softmax_rows is the section-1 softmax applied to each row separately (axis=-1 means "along the last dimension"). Every row of scores becomes its own set of weights that sum to 1.
  2. Inside attention, Q @ K.T / np.sqrt(d) is the picture's first two boxes at once: @ is matrix multiply (Q times K-flipped), and dividing by np.sqrt(d) is the √d scaling that keeps scores stable.
  3. The optional mask line replaces "not allowed" positions with a huge negative number so softmax drives their weight to ~0. This is how a decoder stops a word from peeking at future words it hasn't generated yet.
  4. weights = softmax_rows(scores) turns scores into percentages, and weights @ V is the final · V box — the weighted average of the value vectors.

What the output means: It prints (3, 4) — the output has the same shape as the input (3 words, each a 4-number vector). Attention transforms the vectors; it doesn't change how many there are.

Try this: The Q, K, V here are random, so the numbers are meaningless — the point is the shape. Change seq, d = 3, 4 to 5, 8 and the printed shape becomes (5, 8). In a real transformer, Q/K/V come from the words, not np.random.rand.

Common follow-upsAdd a causal mask (shown above) for decoder attention; extend to multi-head (split d into h heads, attend in parallel, concat); explain why √d scaling matters (keeps softmax out of the saturated region). All build on vectorization from A4.

3 · k-means & cosine kNN (A4, A7) common

This is the round where "I use a vector database" isn't an answer. Anyone can call a library that finds similar embeddings or clusters them; the interviewer is checking whether you know what that library does when you strip the magic away. Both of these — finding the nearest vectors, and grouping vectors into clusters — turn out to be short loops over arithmetic you already know. If you can write them by hand, you understand your vector DB; if you can't, you're trusting a black box.

Start with cosine kNN, because it's the beating heart of every RAG retrieval you'll ever build. The question "which stored documents are most like this query?" becomes "which stored vectors point in most nearly the same direction as the query vector?" The word direction is the key: two embeddings are similar when they aim the same way, regardless of how long they are. Cosine similarity measures exactly that angle — the analogy is two arrows drawn from the same origin; you care whether they're roughly parallel, not whether one is drawn longer. The tidy trick is that once you rescale every vector to length 1 ("normalize"), the messy cosine formula collapses into a plain dot product, and finding the top-k is just "score everything, sort, take the best few."

k-means answers a different question — "what natural groups exist in this pile of vectors?" — and it's one of the most satisfying algorithms to hold in your head because it's just two moves repeated until they settle. Picture scattering a handful of pins on a map (those are your centroids, the guessed centres of clusters). Move one: assign every town to its nearest pin. Move two: slide each pin to the exact middle of the towns that chose it. Repeat, and the pins drift until they stop moving — that's convergence, and now each pin marks a real cluster. The classic mistake is forgetting that k-means is not deterministic: where you drop the pins initially changes where they end up, which is why smarter initialization (k-means++) and running it several times exist. Interviewers love this one precisely because a correct answer is short but reveals whether you actually understand assignment-then-update, or just memorized a name.

Asked as: "cluster these embeddings" / "find the k nearest vectors." Tests whether you can turn the vector math from A4/A7 into a loop.

cosine kNN: nearest by direction query near far small angle = high cosine = top-k k-means: assign ⇄ update cluster A cluster B centroid slides to the mean, repeat Two vector jobs, both loops over arithmetic. kNN ranks stored vectors by the angle to the query (small angle → high cosine → top-k); k-means repeats assign-to-nearest-centroid then slide-each-centroid-to-its-mean until the centroids stop moving.
🗺️ How to read this diagram

Two separate algorithms sit side by side, split by the dashed line: cosine kNN on the left, one k-means loop on the right.

  • Left panel — arrows are vectors drawn from the same origin. Cosine similarity measures the angle, not the length: the green "near" arrow points almost the same way as the query (small angle → high score), while the dashed "far" arrow points off to the side (big angle → low score). kNN sorts by that angle and keeps the top-k.
  • Right panel — dots are embedding points; the square markers are the centroids (cluster centres). Each point is assigned to its nearest centroid (that colours it into cluster A or B).
  • The payoff — the bottom arrow shows the update step: each centroid slides to the mean of the points that chose it, then you reassign and repeat. When the centroids stop moving, k-means has converged.

In short: kNN = rank by angle, take the best few; k-means = assign to nearest centre, move each centre to its mean, repeat until it settles.

Cosine kNN + one k-means iteration
pythonimport numpy as np

def knn(corpus, query, k):
    corpus = corpus / np.linalg.norm(corpus, axis=1, keepdims=True)
    query = query / np.linalg.norm(query)
    sims = corpus @ query                 # cosine = dot on normalized vectors
    return np.argsort(-sims)[:k]          # top-k indices

def kmeans_step(X, centroids):
    # assign each point to nearest centroid, then recompute centroids
    d = ((X[:, None, :] - centroids[None, :, :]) ** 2).sum(-1)
    labels = d.argmin(axis=1)
    new = np.array([X[labels == c].mean(axis=0) for c in range(len(centroids))])
    return labels, new                   # repeat until centroids stop moving
▶ How this works

Two classic "do something with these embeddings" questions in one block. kNN finds the vectors most similar to a query; k-means groups vectors into clusters. Both are just loops over vector math.

  1. In knn, dividing each vector by np.linalg.norm(...) rescales it to length 1 ("normalizing"). Once vectors are length 1, a plain dot product is the cosine similarity — a score from -1 (opposite) to 1 (identical).
  2. corpus @ query computes that similarity for every stored vector at once. np.argsort(-sims)[:k] sorts from highest to lowest (the minus flips the order) and keeps the top k — the k nearest neighbours.
  3. In kmeans_step, the big bracketed expression measures the distance from every point to every centroid (cluster centre). d.argmin(axis=1) then labels each point with its closest centroid.
  4. The last line recomputes each centroid as the average of the points assigned to it. Repeating this assign-then-recompute loop is the whole k-means algorithm — the centres shuffle around until they stop moving.

Try this: k-means is two steps on repeat: (1) assign each point to the nearest centre, (2) move each centre to the middle of its points. Picture dropping 3 pins on a map, then sliding each pin to the centre of its nearest towns, over and over.

🔗 Builds onCosine similarity + top-k from A4 §4; k-means is the intuition behind the IVF vector index in A7 §3. Follow-ups: k-means++ init, elbow method, why cosine vs Euclidean for embeddings.

4 · Tokenization & sampling questions (A5) common

Every bill you've ever paid to an LLM was measured in tokens, not words — and this round checks whether you know the difference. It's easy to sail through months of prompting without ever confronting what a "token" actually is, right up until you're asked to explain why your 500-word prompt cost you 700 tokens, or why a model that "supports 8,000 tokens" choked on your document. Tokenization and sampling are the two ends of the pipe — how text goes into a model and how words come out — and both hide behaviour that surprises people who only ever worked at the text level.

On the input side: a token is not a word, it's a chunk the model learned to treat as one unit — often a whole common word, but sometimes a fragment, a space-plus-word, or a single punctuation mark. The analogy that sticks is Lego: "unbelievable" might be three studs (un, believ, able) while "cat" is one, and a rare name or a URL can shatter into a surprising number of pieces. This is why token count drifts away from word count, why code and non-English text cost more, and why "fit the prompt in the window" means counting tokens, not characters — the common mistake is estimating budget by word count and blowing the limit on the input the model rejects.

On the output side is sampling, the answer to a question beginners rarely think to ask: once the model has produced probabilities for the next token, how does it actually choose? Always taking the single most likely token (greedy) sounds safe but produces flat, repetitive text that loops on itself. So we introduce controlled randomness. Temperature is the creativity dial — it reshapes the probabilities before choosing: low temperature sharpens toward the favourite (safe, deterministic), high temperature flattens them out (surprising, sometimes unhinged). Top-p (nucleus) sampling is the safety rail on that randomness: instead of ever considering a truly terrible word, it keeps only the smallest shortlist of top candidates whose probabilities add up to p (say 90%), throws away the unreliable long tail, and samples from what's left. Together they're the two knobs behind every "make it more creative" or "make it more focused" setting you've touched — and being able to implement top-p, not just name it, is what earns the check-mark.

Asked as: "why is token count ≠ word count?", "implement top-p (nucleus) sampling", "how would you truncate a prompt to fit the context window?"

input: text → tokens (not words) "unbelievable cat" un believ able cat 2 words → 4 tokens · why count ≠ words output: top-p keeps the nucleus .5 .25 .15 .07 .03 kept: mass ≥ 0.9 tail dropped then renormalize the kept set to sum to 1 Two ends of the pipe. On input, a tokenizer splits text into sub-word chunks, so token count drifts from word count. On output, top-p keeps the smallest set of highest-probability tokens whose mass reaches p and drops the unreliable tail.
🗺️ How to read this diagram

The dashed line splits the two halves of the round: how text goes in (left) and how a token comes out (right).

  • Left, top box — the raw text "unbelievable cat": two words. The arrow down leads to what the model actually sees.
  • Left, amber boxes — the tokens. "unbelievable" shatters into three sub-word pieces (un, believ, able) while "cat" stays one, so two words become four tokens — that is why token count never equals word count.
  • Right, the bars — next-token probabilities sorted tallest-first. The dashed vertical line is the top-p cutoff: the green bars on the left are the "nucleus" whose probabilities add up to at least p (0.9); the short grey tail on the right is discarded.
  • The payoff — after dropping the tail you renormalize the kept bars back to sum to 1, then sample from just those. That keeps a truly unlikely token from ever being chosen.

In short: tokenization splits words into sub-word chunks (count ≠ words); top-p samples only from the smallest set of top tokens that covers probability mass p.

Top-p (nucleus) sampling
pythonimport numpy as np

def top_p_sample(probs, p=0.9):
    idx = np.argsort(-probs)                 # high→low
    sorted_p = probs[idx]
    cum = np.cumsum(sorted_p)
    cutoff = np.searchsorted(cum, p) + 1    # smallest set with mass ≥ p
    keep = idx[:cutoff]
    renorm = probs[keep] / probs[keep].sum()
    return int(np.random.choice(keep, p=renorm))   # sample from the nucleus
# temperature: divide logits by T before softmax — higher T = flatter = more random
▶ How this works

When a model picks the next word, it doesn't always take the single most likely one — that would be repetitive. Top-p (nucleus) sampling keeps just enough of the most likely words to cover a probability mass of p (say 90%), then picks randomly from that shortlist.

  1. np.argsort(-probs) orders the words from most to least likely (the minus sorts high→low). sorted_p is those probabilities in that order.
  2. np.cumsum(sorted_p) makes a running total: 0.5, then 0.75, then 0.9… np.searchsorted(cum, p) finds where that running total first reaches p, so cutoff is the size of the shortlist (the "nucleus").
  3. keep = idx[:cutoff] is that shortlist of word indices. Dividing their probabilities by their own sum (renorm) rescales them back to add up to 1, since we threw the rest away.
  4. np.random.choice(keep, p=renorm) then picks one word at random, weighted by those probabilities. The trailing comment notes temperature: dividing logits by a number T before softmax flattens the probabilities — higher T = more surprising word choices.

What the output means: The function returns a single integer — the index of the chosen next word. Run it many times and you'll get different words, but always ones from the high-probability shortlist.

Try this: Set p=0.1 and the shortlist shrinks to basically the single top word (nearly greedy, very repetitive). Set p=1.0 and every word is eligible (more creative, more random). That dial is exactly what "top-p" controls in a real API.

🔗 Builds onBPE tokenization and token counting from A5 §3–4. Follow-ups: greedy vs top-k vs top-p, temperature, repetition penalty, why streaming needs incremental detokenization.

5 · System design: "Design a RAG system" (A7) the design round expert

This is the round where fluent talkers fail and clear thinkers pass. "Design a question-answering system over ten million documents" invites you to name-drop — vector databases, embeddings, rerankers — and a surprising number of candidates do exactly that: a confident tour of buzzwords with no spine holding them together. The interviewer isn't grading vocabulary. They're listening for whether you can draw a system that would actually work, name the trade-off at each box, and — the part almost everyone forgets — say how it breaks. No production system is judged by its happy path.

The one insight that organizes the whole answer is that RAG has two separate lives, and confusing them is the tell of someone who's never built one. There's an offline path you run ahead of time, when documents change: break each document into bite-sized chunks, turn each chunk into an embedding vector, and load those vectors into a searchable index. This is the slow, expensive, batch work — and it's slow precisely so the other path can be fast. Then there's the online path you run for every single question: embed the query, ask the index for the handful of closest chunks, reorder them best-first (rerank), staple them into the prompt as context, and let the model answer from those documents instead of from its fuzzy memory. The analogy is an open-book exam: offline you write and tab your notes; online you flip to the right page and answer from it. Say "offline vs online" out loud and you've already out-structured half the room.

Now the part that separates senior from mid-level: the two failure modes, because they are different bugs with different fixes. A retrieval miss means the answer-bearing chunk never got fetched — the model was set up to fail, and no amount of prompt-tweaking helps; you fix it upstream in chunking, embeddings, or top-k (it shows up as low context recall). A hallucination is the opposite: the right chunk was fetched and the model ignored it, inventing an answer anyway — you fix that downstream with reranking, a tighter grounding prompt, and forced citations (it shows up as low faithfulness). The common mistake is treating "the answer was wrong" as one problem; a strong candidate immediately asks "wrong because we didn't retrieve it, or wrong because we didn't use what we retrieved?" — because those are two entirely different repairs, and knowing which one you're looking at is the whole job.

Asked as: "design a question-answering system over 10M company documents." The interviewer wants the pipeline, the scaling choices, and the failure modes — not code.

offline: ingest → chunk → embed → index · online: retrieve → rerank → generate docs chunk (A5) embed (A4) vector DB (A7) query retrieve k rerank LLM + context answer RAG has an offline and an online path. Offline: chunk → embed → index (rebuild when docs change). Online: embed the query → ANN retrieve top-k → rerank → stuff context → generate with citations. Name the trade-offs at each box.
🗺️ How to read this diagram

This is the whiteboard answer to "design a question-answering system over millions of documents." The trick is that RAG (retrieval-augmented generation) has two separate paths — one you run ahead of time, one you run per question. Read the top row first, then the bottom row.

  • Top row (offline, run once when documents change): docs → chunk → embed → vector DB. You break documents into small pieces (chunk), turn each piece into a vector of numbers (embed), and store those vectors in a searchable vector database. This is slow, so you do it in advance.
  • Bottom row (online, run for every question): query → retrieve k → rerank → LLM + context → answer. The user's question is embedded, the DB returns the k closest chunks (retrieve), a rerank step reorders them best-first, and those chunks are handed to the LLM as context so it answers from real documents instead of guessing.
  • The dashed arrow from the vector DB down to retrieve k shows the link between the two paths: the online search reads the index the offline path built.
  • The (A4), (A5), (A7) tags point at earlier lessons where each box is built from scratch — chunking (A5), embedding (A4), the vector index (A7).

In short: RAG = look it up, then answer. Offline you build a searchable index of your documents; online you fetch the few most relevant pieces and let the model write an answer grounded in them. In the interview, name the trade-off at every box.

What to say in the RAG design roundChunking (size vs recall, overlap — A5). Embeddings (model choice, dimension, cost — A4). Index (exact vs HNSW/IVF; when to shard — A7). Retrieval (hybrid dense+BM25, RRF, metadata filters — A7). Generation (context budget, citations, grounding check — Ch 3/6). Failure modes (stale index, hallucination when retrieval misses, cost blow-up) and evals (retrieval recall@k + answer quality — Ch 5). Mention caching and latency budget (A3/A8).

6 · System design: "Design an LLM agent / tool-use system" (A6, Ch 4) the design round expert

The word "safely" in this question is not decoration — it's the entire exam. "Design an assistant that can call tools and take actions" sounds like a fun architecture puzzle until you remember what "take actions" means: a probabilistic text model, which sometimes confidently makes things up, is now allowed to delete files, send emails, or move money. A candidate who designs a slick agent and forgets that it's an unpredictable model wired to real buttons has failed the round no matter how elegant the rest is. This is the "would I trust your system near production?" question.

The first thing to get right is that an agent is not a single clever prompt — it's a loop. The model doesn't solve the task in one shot; it takes a step, sees what happened, and decides the next step, over and over, the way a person debugging works: try something, read the error, try the next thing. The plain shape is propose → act → observe → repeat. The model proposes an action ("call the search tool with this query"), the action runs, the result (the "observation") is fed back into the model's context, and it proposes again — until it decides the task is done. The mistake that sinks people here is forgetting the loop can run forever; a model that keeps not-quite-finishing will spin until it drains your budget, which is why a hard MAX_STEPS cap is non-negotiable, not an optimization.

Then comes the safety spine, and the mental model to reach for is a bouncer between the model and the real world. The model never gets to act directly — it only ever proposes. Every proposal passes two checkpoints. First, validate: does this tool call even have the right shape and arguments? A malformed request dies here before it can do anything (this is where the structured-output work from A6 pays off). Second, the gate: a policy that classifies the action's risk and decides allow, ask a human, or block — reading a file is fine, deleting a production database asks for a human, exfiltrating secrets is blocked outright. The senior signal is insisting these guardrails live in code and IAM permissions, not in the prompt — because a prompt is a polite request a model can ignore, while a permission boundary is a wall it cannot cross. Round it out with reliability (retries with backoff, idempotency keys so a retried action doesn't fire twice) and observability (trace every step so you can see what it did), and you've described a system, not a demo.

Asked as: "design an assistant that can call tools / take actions safely." They want the loop, the safety gate, and how you keep it reliable.

the agent loop: model proposes → validate → gate → execute → feed result back LLM validate (A6) gate tool exec observation fed back → repeat until done (MAX_STEPS cap) An agent is a loop with guardrails. The model emits a tool call → validate it (Pydantic/discriminated union, A6) → a policy gate decides allow/ask/block → execute → feed the result back. Bound it with MAX_STEPS, idempotency keys, and a human gate on risky actions.
🗺️ How to read this diagram

This shows how an AI agent that can take actions (call tools) stays safe. An agent isn't one call — it's a loop that repeats until the task is done. Follow the arrows left to right, then notice the arrow that curves back.

  • LLM box — the model proposes an action, e.g. "call the delete-file tool." It only suggests; it doesn't get to run anything directly.
  • validate box — check the proposed action has the right shape and arguments (the A6 tag points to the validation lesson). A malformed request is rejected here before anything happens.
  • gate box — a safety policy decides allow, ask a human, or block. Risky or irreversible actions get stopped or require approval; safe ones pass through.
  • tool exec box — the approved action actually runs, and its result (the "observation") is fed back to the LLM via the curving arrow. The caption's MAX_STEPS cap is what stops the loop from running forever.

In short: An agent is a loop with guardrails: propose → validate → gate → execute → feed the result back → repeat. The gate and the step cap are what make it safe to let a model take real actions.

What to say in the agent design roundLoop (propose → act → observe, step cap). Structured tool calls validated before execution (A6 discriminated unions). Safety gate (risk classes, allow/ask/block, human-in-the-loop for irreversible ops — Ch 8). Reliability (retries/backoff, circuit breaker, idempotency — A6). Memory (bounded message window — D2 deque). Observability & evals (trace every step, hard-fail safety evals — Ch 5/A8). This is exactly the capstone in Ch 8.

7 · "Make 1000 LLM calls efficiently" (A3) common

The naive answer to this question — "loop over them and await each one" — is technically correct and will get you a polite pass to the next candidate. Making a thousand API calls one after another works; it's also brutally slow, because each call spends almost all its time waiting on the network while your CPU sits idle. This round tests whether you understand that waiting is not working, and whether you can speed things up without swinging to the opposite failure: firing all thousand at once and getting every one rejected.

The core idea is concurrency for I/O-bound work, and the analogy is a coffee shop. A one-at-a-time loop is a single barista who takes an order, stands and watches the espresso machine until it's done, hands it over, and only then takes the next order — most of the day spent staring at a machine. Concurrency is that barista starting the next order while the first is brewing. Nothing runs faster individually; you just stop wasting the waiting. Because LLM calls are almost entirely waiting (the network round-trip dwarfs your local work), overlapping them is a massive win — this is exactly the async/await machinery from A3, and it's why "is this task I/O-bound or CPU-bound?" is the first thing to say out loud.

But unlimited overlap is its own disaster, which is where the real judgment shows. Every API enforces a rate limit, and a naive "launch all 1000 concurrently" trips it instantly — the provider starts rejecting you with HTTP 429s and you've made things worse than the slow loop. So you need a governor. A semaphore is a bag of a fixed number of permits (say 10): a task must grab a permit before it runs and returns it when done, so at most 10 are ever in flight while the other 990 politely queue. That's the "how many at once" control. The mistake that costs the check-mark is stopping there — a semaphore alone isn't enough, because you'll still occasionally get a transient 429, so you pair it with retry and exponential backoff: on a rejection, wait a moment and try again, waiting longer each time. Concurrency capping and rate-limit recovery are two different controls, and the senior answer names both plus streaming results as they finish so you're not holding a thousand responses in memory at once.

Asked as: "you need to embed a million documents / evaluate 5000 prompts — how, without hitting rate limits?" Tests concurrency + backpressure judgment.

1000 tasks · semaphore(limit=3) caps how many run at once 1000 tasks semaphore call · in flight call · in flight call · in flight 997 wait in queue 429? result on 429 → wait (exponential backoff) → retry the same call two controls: semaphore caps concurrency · backoff recovers rate limits Two different controls, both needed. A semaphore bounds how many calls are in flight (here 3 of 1000; the rest queue) — that is concurrency capping. A transient 429 still happens, so you retry it with exponential backoff — that is rate-limit recovery.
🗺️ How to read this diagram

Follow the arrows left to right; the one arrow that curves back is the retry path.

  • 1000 tasks box — all the calls you want to make. You do not fire them all at once (that trips the rate limit instantly).
  • semaphore box — the governor. It holds a fixed number of permits (limit=3 here); a task must grab a permit to proceed, so only 3 are ever "in flight" (the green boxes) while the other 997 wait in the queue.
  • 429? box — every finished call is checked. A clean call flows right to result; a 429 ("too many requests") takes the curving-back arrow: wait a bit (longer each time — exponential backoff) and retry the same call.
  • The payoff — the bottom line names the two controls that must travel together: the semaphore caps concurrency, backoff recovers from the rejections that slip through anyway.

In short: cap the in-flight count with a semaphore, and recover from the occasional 429 with retry + exponential backoff — naming both is what earns the check-mark.

Bounded async fan-out (semaphore + gather)
pythonimport asyncio

async def bounded_map(coro_fn, items, limit=10):
    sem = asyncio.Semaphore(limit)         # cap concurrency → respect rate limits
    async def worker(x):
        async with sem:
            return await coro_fn(x)
    return await asyncio.gather(*[worker(x) for x in items])
# pair with token-bucket rate limiting + retry/backoff on 429s (A6)
▶ How this works

The interview question is "make 1000 LLM calls efficiently without hitting rate limits." You want many calls happening at once (fast), but not too many at once (or the API rejects you). This code runs them in parallel with a fixed ceiling.

  1. asyncio.Semaphore(limit) is a permit counter — it hands out at most limit permits (here 10) at a time. It's the "only 10 at once" rule that keeps you under the rate limit.
  2. Each worker does async with sem: — it waits until a permit is free, takes one, runs its call with await coro_fn(x), then releases the permit so a waiting worker can go. await means "pause here without blocking everyone else."
  3. asyncio.gather(*[worker(x) for x in items]) launches a worker for every item and waits for them all to finish. The semaphore quietly ensures only 10 are actually in flight at any moment; the rest queue up.
  4. The final comment is the senior add-on: pair this with a rate limiter and retry/backoff (wait-and-retry on HTTP 429 "too many requests") for a production-grade fan-out.

What the output means: It returns a list of all the results, in the same order as items — but the calls ran concurrently (up to 10 at a time), so 1000 calls finish far faster than doing them one by one.

Try this: Change limit=10 to limit=1 and the calls run strictly one after another (slow but gentle). Raise it to 100 and they flood out at once — which is exactly what trips a rate limit. The semaphore is the knob that balances speed against the API's limits.

🔗 Builds onAsync, semaphores & backpressure from A3 §4; retry/backoff from A6 §6. Say: I/O-bound → async or threads (GIL released — A2); cap concurrency with a semaphore; add a rate limiter + exponential backoff; stream results as they finish.

8 · "Productionize an LLM feature" (A8) the wrap-up

This is the round where the interviewer stops asking "can you build it?" and starts asking "would I let you own it?" Anyone can demo a chatbot that works on Tuesday. The senior question is what happens on the next Tuesday — when the model provider ships a new version, when traffic triples, when a user finds the prompt injection, when the monthly bill arrives. "Productionize an LLM feature" is deliberately open-ended because they want to see whether you think in features to add or in a system that stays healthy over time. The gap between those two mindsets is the gap between mid-level and senior.

The trap is to answer with a shopping list — "add caching, add monitoring, add guardrails" — which sounds competent and misses the point. Those are ingredients; the interviewer wants the recipe. The organizing idea is a closed loop, and the analogy is the difference between building a car and running a racing team: shipping the feature is building the car, but keeping it winning means measuring every lap, catching problems the moment they appear, and feeding what you learn back into the next build. Concretely: ship behind an evaluation gate (a golden set of cases that must still pass, checked in CI, so a "small prompt tweak" can't silently regress quality) → monitor quality, cost, latency, and drift in production → when something fails, capture it as a new golden case → iterate. That loop is what turns a demo into a system that survives contact with reality.

Then layer the specifics onto that spine, and know why each one matters rather than just listing it. Cost doesn't take care of itself: prompt caching for repeated prefixes, routing easy requests to a cheaper model, and token budgets are the levers, and they compound. Latency is a product feature, not a nicety — streaming tokens so the user sees words immediately, async so slow steps overlap, a cache for repeated questions. Safety lives in code and IAM, never in the prompt (the same senior tell as the agent round) plus hard-fail safety evals that block a deploy outright. And versioning is the one people forget until it burns them: pin the exact model and prompt version, and roll out changes as shadow or canary deploys so a regression hits 1% of traffic, not 100%. The common mistake is treating the model as a fixed dependency; it isn't — it changes underneath you, so the system has to be built to notice and absorb that.

Asked as: "we shipped a chatbot — how do you make it reliable, cheap, and safe over time?" The senior-signal round.

not a feature list — a closed loop that stays healthy over time eval gate (CI) ship monitor prod cost · latency · quality · drift a prod failure becomes a NEW golden case → back into the gate Productionizing is a loop, not a checklist. Ship behind an eval gate (a golden set that must pass in CI) → monitor cost, latency, quality and drift in prod → capture each failure as a new golden case → back into the gate. The loop is the senior signal.
🗺️ How to read this diagram

Read the three boxes left to right, then follow the long arrow that curves back — that return path is the whole point.

  • eval gate (CI) box — before anything ships, a "golden set" of known cases must still pass in continuous integration, so a small prompt tweak cannot silently regress quality.
  • ship box — the feature goes out (ideally as a shadow or canary deploy so a regression hits 1% of traffic, not 100%).
  • monitor prod box — in production you watch the four things that actually decay: cost, latency, quality, and drift (the model or inputs changing underneath you).
  • The payoff — the curving-back arrow: when monitoring catches a failure, you turn it into a new golden case and feed it back into the gate. That closed loop is what separates a demo from a system that survives.

In short: ship → monitor → feed failures back as golden cases → iterate. Describe the loop, not a shopping list of features.

ConcernWhat to sayRef
Quality doesn't regressgolden-set evals + LLM-judge, gate deploys in CICh 5, A8
Cost controlprompt caching, model routing, token budgetsA8 §8
Latencystreaming, async, cache, smaller model for easy stepsA3
Safetyguardrails in code/IAM, hard-fail safety evals, human gateCh 6, Ch 8
Versioning & rollbackpin model + prompt versions, shadow/canary deployA8 §3
Monitoringlatency/cost/quality/drift dashboards + alertsA8 §7
The senior answerDon't just list features — describe the loop: ship behind an eval gate → monitor quality/cost/latency in prod → feed failures back as new golden cases → iterate. Guardrails live in code and IAM, not the prompt. That closed loop is what separates a demo from a system.

AI-interview question → what they're testing expert

QuestionThey're testingSection
"Implement softmax (stably)"numerical care§1
"Implement attention"transformer fundamentals§2
"Cluster / kNN these embeddings"vector math → code§3
"Implement top-p sampling"decoding knowledge§4
"Design a RAG system"retrieval architecture§5
"Design a tool-using agent"loop + safety design§6
"Make 1000 LLM calls efficiently"concurrency & limits§7
"Productionize an LLM feature"MLOps maturity§8

Checkpoint expert

  • Implement stable softmax, scaled dot-product attention, and cosine kNN / k-means from scratch.
  • Explain tokenization and code a sampling strategy (top-p / temperature).
  • Whiteboard a RAG pipeline and an agent loop, naming trade-offs and failure modes.
  • Answer the "productionize / make it reliable + cheap + safe" round with the eval→monitor→iterate loop.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Implement softmax (numerically stable)Beginner

Context: Softmax is the classic ML-interview warm-up, and the point being tested is the numerical-stability trick: subtracting the max before exponentiating avoids exp overflow on large logits.

Your task: Implement a numerically stable softmax(logits) and confirm its output is a valid probability distribution.

Requirements:

  • Subtract the max logit before exponentiating so all exponents are ≤ 0
  • Return the same result mathematically as the naive version, without overflow
  • The returned values are all non-negative and sum to 1.0
  • Pure stdlib — math.exp is enough, no numpy
  • Print the distribution and its sum to verify it totals 1

💡 Hint: Compute m = max(logits), exponentiate x - m for each, then divide by the sum of those exponentials.

Show solution

Stable softmax subtracts the max before exponentiating (pure stdlib):

import math

def softmax(logits):
    m = max(logits)                       # stability: shift so the max is 0
    exps = [math.exp(x - m) for x in logits]
    s = sum(exps)
    return [e / s for e in exps]

out = softmax([2.0, 1.0, 0.1])
print([round(p, 3) for p in out])         # [0.659, 0.242, 0.099]
print(round(sum(out), 6))                 # 1.0

Without the - m shift, large logits overflow exp; subtracting the max is mathematically identical but keeps exponents ≤ 0. The interviewer is checking you know that stability trick and that softmax yields a probability distribution (sums to 1).

Exercise 2 · Cross-entropy lossIntermediate

Context: Cross-entropy is the standard classification and language-model training loss because it punishes confident mistakes hardest. Pairing it with softmax is the natural follow-up to the softmax warm-up.

Your task: Implement cross_entropy(logits, target_index) as the negative log of the softmax probability of the true class, and show it's low when confident-correct.

Requirements:

  • Turn logits into probabilities with a stable softmax first
  • Return -log(p) of the probability assigned to the target class
  • Loss is near 0 when the model is confidently correct
  • Loss is large when the model is confidently wrong
  • Demonstrate both the correct-target and wrong-target cases numerically

💡 Hint: Reuse your stable softmax, index out the target class's probability, and take -math.log of it.

Show solution

Cross-entropy penalizes low probability on the true class (runnable):

import math

def softmax(logits):
    m = max(logits); exps = [math.exp(x-m) for x in logits]; s = sum(exps)
    return [e/s for e in exps]

def cross_entropy(logits, target):
    p = softmax(logits)[target]
    return -math.log(p)

print(round(cross_entropy([5.0, 0.0, 0.0], 0), 4))   # 0.0135  confident & correct -> low loss
print(round(cross_entropy([5.0, 0.0, 0.0], 1), 4))   # 5.0136  confident & WRONG   -> high loss

Loss is -log of the probability assigned to the true class: near 0 when the model is confidently correct, large when it is confidently wrong. That asymmetry — punishing confident mistakes hardest — is why cross-entropy is the standard classification/LM training loss.

Exercise 3 · Scaled dot-product attentionAdvanced

Context: Scaled dot-product attention is the transformer core, and the detail interviewers probe is the 1/√d scaling — omit it and scores grow with dimension, saturating softmax and killing gradients.

Your task: Implement single-query scaled dot-product attention: score the query against keys, scale, softmax, then take the weighted sum of the values.

Requirements:

  • Score by dotting the query with each key
  • Divide the scores by √d before the softmax
  • Softmax the scaled scores into weights that sum to 1
  • Return the weighted sum of the value vectors (and optionally the weights)
  • Show the output attends most to the keys most aligned with the query

💡 Hint: Three steps — scaled scores, softmax weights, weighted sum — each a small comprehension; d is the query/key dimension.

Show solution

score → scale → softmax → weighted sum of values (pure stdlib):

import math

def softmax(xs):
    m = max(xs); e = [math.exp(x-m) for x in xs]; s = sum(e); return [v/s for v in e]

def attention(q, keys, values):
    d = len(q)
    scores = [sum(qi*ki for qi, ki in zip(q, k)) / math.sqrt(d) for k in keys]  # scale by 1/sqrt(d)
    weights = softmax(scores)
    dim = len(values[0])
    return [sum(w*v[j] for w, v in zip(weights, values)) for j in range(dim)], weights

q      = [1.0, 0.0]
keys   = [[1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
values = [[10.0, 0.0], [0.0, 10.0], [5.0, 5.0]]
out, w = attention(q, keys, values)
print([round(x, 2) for x in out])         # attends most to key[0] and key[2]
print([round(x, 3) for x in w])           # attention weights sum to 1

The query dots each key to get relevance scores; dividing by sqrt(d) keeps scores from growing with dimension (which would saturate softmax); softmax turns them into weights that mix the values. That scaling factor is the detail interviewers probe — omit it and gradients vanish at large d.

Exercise 4 · Temperature & top-p samplingExpert

Context: Temperature and nucleus (top-p) sampling are the decoding knobs that control how an LLM picks tokens, and interviewers want you to explain what each does. They compose: temperature reshapes the distribution, then top-p truncates its tail.

Your task: Implement temperature scaling in softmax and a top_p nucleus filter on a probability distribution, and explain each knob.

Requirements:

  • Temperature < 1 sharpens toward the argmax; > 1 flattens toward uniform
  • Top-p keeps the smallest set of tokens whose cumulative mass reaches p
  • The kept 'nucleus' probabilities are renormalized to sum to 1
  • Low-mass tail tokens outside the nucleus are dropped
  • Show a sharpened distribution and a top-p filtered one on the same logits

💡 Hint: Divide logits by the temperature before softmax; for top-p, sort by probability descending and accumulate until the running mass crosses p.

Show solution

Temperature reshapes the distribution; top-p truncates its tail (runnable):

import math

def softmax(logits, temp=1.0):
    z = [x/temp for x in logits]          # temp<1 sharpens, temp>1 flattens
    m = max(z); e = [math.exp(x-m) for x in z]; s = sum(e); return [v/s for v in e]

def top_p(probs, p=0.9):
    ranked = sorted(enumerate(probs), key=lambda t: t[1], reverse=True)
    kept, cum = [], 0.0
    for idx, pr in ranked:
        kept.append((idx, pr)); cum += pr
        if cum >= p: break                # smallest set whose mass >= p (the "nucleus")
    z = sum(pr for _, pr in kept)
    return {idx: pr/z for idx, pr in kept}  # renormalize over the nucleus

logits = [3.0, 2.0, 1.0, 0.1]
print([round(x,3) for x in softmax(logits, temp=0.5)])  # sharper
print(top_p(softmax(logits), p=0.9))                    # drops the low-mass tail

Temperature <1 sharpens toward the argmax (more deterministic), >1 flattens (more random). Top-p keeps only the smallest set of tokens whose probability mass reaches p and renormalizes, cutting the unreliable tail. They compose: temperature reshapes, then top-p truncates.

Exercise 5 · Design a RAG system (A7 system design)Professional

Context: 'Design a RAG system' is a staple system-design interview, and a defensible answer names the pipeline and the two ways it breaks — a retrieval miss versus a hallucination — because they need different metrics and different fixes.

Your task: Produce the component design (ingestion → retrieval → generation → eval) and name the two failure modes you'd be graded on.

Requirements:

  • Ingestion: chunk (with situating context) → embed → store in a vector DB (ANN like HNSW/IVF at scale)
  • Retrieval: embed query → ANN top-k → hybrid + RRF → cross-encoder rerank → precise top-k
  • Generation: system prompt + retrieved context + question, with source citations
  • Eval: context recall/precision, faithfulness, answer relevance on a curated set, gated in CI
  • Name failure mode 1 (retrieval miss → low context recall) and its fix
  • Name failure mode 2 (hallucination → low faithfulness) and its fix

💡 Hint: The grader is listening for the distinction: a missed chunk is a recall problem you fix in retrieval, a fetched-but-ignored chunk is a faithfulness problem you fix in grounding.

Show solution

A defensible RAG design names components and failure modes (worked design):

Ingestion:  chunk docs (with a situating context prefix per chunk) -> embed ->
            store vectors in a vector DB (HNSW/IVF for ANN at scale).
Retrieval:  embed query -> ANN top-k -> hybrid (dense + BM25) + RRF fusion ->
            cross-encoder rerank -> precise top-k into the prompt.
Generation: prompt = system + retrieved context + question; cite sources.
Eval:       context_recall / context_precision / faithfulness / answer_relevance
            on a curated eval set; gate changes in CI.

Failure mode 1: RETRIEVAL miss -> answer-bearing chunk never fetched
                (low context_recall). Fix chunking/embeddings/top-k, not the prompt.
Failure mode 2: HALLUCINATION -> answer not grounded in retrieved context
                (low faithfulness). Fix grounding: rerank, tighter prompt, cite.

The grader wants the pipeline (ingest → retrieve → generate → eval) and the two ways it breaks: a retrieval miss (the chunk was never fetched — a recall problem) versus a hallucination (the chunk was fetched but the model didn't ground on it — a faithfulness problem). Different metrics, different fixes.

Exercise 6 · Make 1000 LLM calls efficiently (A3 scale)Industry scenario

Context: The scale question — run 1000 independent LLM calls for the least cost and wall-clock — wants you to name the four levers and show they multiply, not just 'call it in a loop'.

Your task: Write a decision function for 1000 calls that combines batching, caching, concurrency, and model choice, and quantifies the resulting cost.

Requirements:

  • Use the Batches API (≈50% off) when latency doesn't matter; concurrency when it does
  • Prompt-cache a shared prefix so repeated tokens cost a fraction
  • Pick the cheapest model that clears the quality bar
  • Compute a rough per-call cost from input/output tokens and multiply out to 1000
  • Show the levers stack (multiply) rather than being either/or

💡 Hint: Model each lever as a multiplier or an added step; start from a per-call token cost, then apply the batch/cache discounts to get the fleet total.

Show solution

Combine the four levers and quantify the win (runnable):

def plan_1000_calls(latency_sensitive, shared_prefix, in_tok, out_tok):
    steps, mult = [], 1.0
    if not latency_sensitive:
        steps.append("Batches API — 50%% off, async"); mult *= 0.5
    else:
        steps.append("bounded concurrency (async) — overlap the 1000 calls")
    if shared_prefix:
        steps.append("prompt-cache the shared prefix — repeated tokens ~0.1x")
    steps.append("cheapest model that clears the quality bar (Haiku for simple)")
    # rough per-call cost on Haiku ($1/$5 per MTok) before caching/batch:
    base = in_tok/1e6*1.0 + out_tok/1e6*5.0
    return steps, round(base * 1000 * mult, 2)

steps, cost = plan_1000_calls(False, True, in_tok=800, out_tok=200)
for s in steps: print("-", s)
print(f"~est batch cost for 1000 calls: ${cost}")

The four levers stack: batch for 50% off when latency doesn't matter, cache the shared prefix (~0.1x on repeated tokens), concurrency to overlap calls when it does matter, and pick the cheapest model that clears the bar. The grader wants you to name all four and show they multiply, not just "call it in a loop."

Knowledge check check yourself

✓ Knowledge check

For "implement scaled dot-product attention," the lesson gives softmax(QKᵀ/√d)·V. What is the role of the √d scaling, and what does the optional mask do?

Show answer
Dividing by the square root of the key dimension keeps the dot-product scores from growing large as dimension grows, keeping softmax out of its saturated region (stable gradients); the causal mask sets disallowed/future positions to -inf so softmax drives their weight to ~0.
✓ Knowledge check

The "make 1000 LLM calls efficiently" answer pairs a semaphore with retry/backoff on 429s. Why does the lesson insist you name both to earn the check-mark?

Show answer
The semaphore bounds concurrency (how many run at once), but you still hit transient rate-limit rejections; retry with exponential backoff on HTTP 429 handles those — concurrency capping and rate-limit recovery are two different controls a production fan-out needs together.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in