← AI Engineering Interview Prep
23 min

Coding Drills & Take-Home Exercises

Six worked coding drills (retries, similarity, RRF, dispatcher, chunker, rate limiter) and how take-homes are judged.

Lesson 10 of 11
0 done
In plain English

Coding tests are normal programming wearing AI clothes: retries, search by similarity, checking inputs, splitting text and rate limits. Practise writing them from memory with tests. Say your assumptions aloud and be ready to explain every line.

Backoff
Waiting longer between each retry.
Cosine similarity
A score for how close two meanings are.
Rate limiter
A rule that caps how fast something can be used.
Picture it
Common coding drills

Practise from memory, with tests. Say your assumptions aloud.

1
Retries
With waiting between tries
2
Search by similarity
3
Checking inputs
4
Splitting text
5
Rate limits

Many AI engineering interviews include a live coding or take-home exercise. They're usually ordinary software problems wearing AI clothes: retries, similarity search, validation, chunking, rate limiting, evaluation harnesses. Interviewers watch for correctness, edge cases, tests and clear reasoning. Below are six drills with solutions to study — then close the page and write them yourself.

Drill 1: retry with exponential backoff and jitter

retry.py
import random, time

class Transient(Exception): ...
class Permanent(Exception): ...

def call_with_retry(fn, *, attempts=4, base=0.5, cap=20.0, sleep=time.sleep):
    for attempt in range(1, attempts + 1):
        try:
            return fn()
        except Permanent:
            raise                                   # never retry bad arguments / auth / policy
        except Transient:
            if attempt == attempts:
                raise
            delay = random.uniform(0, min(cap, base * 2 ** attempt))   # full jitter
            sleep(delay)

# What interviewers look for: classify errors, bound attempts, jitter, injectable sleep for tests,
# and a note that non-idempotent side effects need idempotency keys before retrying.

Drill 2: top-k cosine similarity

topk.py
import heapq, math

def normalize(v):
    n = math.sqrt(sum(x * x for x in v)) or 1.0
    return [x / n for x in v]

def top_k(query, docs, k=5):
    """docs: list of (doc_id, vector). Returns [(score, doc_id)] best first."""
    q = normalize(query)
    scored = ((sum(a * b for a, b in zip(q, normalize(vec))), doc_id) for doc_id, vec in docs)
    return heapq.nlargest(k, scored)

# Discuss: pre-normalize vectors at index time so cosine == dot product; ANN indexes for large corpora;
# filter by tenant BEFORE scoring; handle zero vectors; k larger than the corpus.

Drill 3: reciprocal rank fusion for hybrid search

rrf.py
def rrf(rankings: list[list[str]], k: int = 60) -> list[str]:
    """rankings: several best-first lists of doc ids (e.g. keyword results, vector results)."""
    scores: dict[str, float] = {}
    for ranking in rankings:
        for rank, doc_id in enumerate(ranking, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)

# Why rank-based? BM25 and cosine scores aren't on comparable scales; ranks are.
assert rrf([["a", "b", "c"], ["c", "a", "d"]])[0] == "a"

Drill 4: a validated tool dispatcher with a call budget

dispatcher.py
import json

class BudgetExceeded(Exception): ...

class Dispatcher:
    def __init__(self, tools, max_calls=10):
        self.tools, self.max_calls, self.calls = tools, max_calls, 0   # tools: name -> (validate_fn, run_fn)

    def run(self, name: str, raw_args: dict, principal) -> str:
        self.calls += 1
        if self.calls > self.max_calls:
            raise BudgetExceeded("tool-call budget exhausted")
        if name not in self.tools:
            return json.dumps({"error": "unknown_tool", "available": sorted(self.tools)})
        validate, run = self.tools[name]
        try:
            args = validate(raw_args)                      # raises ValueError on bad input
        except ValueError as e:
            return json.dumps({"error": "invalid_arguments", "detail": str(e)})
        if not principal.can(name, args):                  # authorization is code, not prompt
            return json.dumps({"error": "forbidden"})
        try:
            return json.dumps(run(**args))[:8000]          # bound the result size
        except Exception as e:
            return json.dumps({"error": type(e).__name__})

# Talk through: errors returned as tool results so the model can adapt; idempotency keys for writes; logging.

Drill 5: structure-aware chunker

chunker.py
import re

def chunk_markdown(text: str, max_chars=1200, overlap=100):
    sections, path, buf = [], [], []
    def flush():
        if buf:
            sections.append((" > ".join(path), "\n".join(buf).strip())); buf.clear()
    for line in text.splitlines():
        m = re.match(r"^(#{1,6})\s+(.*)", line)
        if m:
            flush()
            level = len(m.group(1))
            path[:] = path[: level - 1] + [m.group(2).strip()]
        else:
            buf.append(line)
    flush()
    chunks = []
    for heading, body in sections:
        start = 0
        while start < len(body):
            piece = body[start : start + max_chars]
            chunks.append({"heading": heading, "text": f"{heading}\n\n{piece}"})
            if start + max_chars >= len(body):
                break
            start += max_chars - overlap
    return chunks

# Discuss: tokens vs characters, never splitting code fences/tables, metadata (source, page, ACL), parent-child retrieval.

Drill 6: a per-tenant token-bucket rate limiter

ratelimit.py
import time

class TokenBucket:
    def __init__(self, rate_per_sec: float, capacity: float, now=time.monotonic):
        self.rate, self.capacity, self.now = rate_per_sec, capacity, now
        self.tokens, self.last = capacity, now()

    def allow(self, cost: float = 1.0) -> bool:
        t = self.now()
        self.tokens = min(self.capacity, self.tokens + (t - self.last) * self.rate)
        self.last = t
        if self.tokens >= cost:
            self.tokens -= cost
            return True
        return False

buckets: dict[str, TokenBucket] = {}
def allow(tenant_id: str, tokens_requested: int) -> bool:
    b = buckets.setdefault(tenant_id, TokenBucket(rate_per_sec=1000, capacity=20000))
    return b.allow(tokens_requested)     # meter LLM tokens, not just requests

# Discuss: distributed limiting (Redis), burst vs sustained, graceful degradation, fairness between tenants.

How take-home exercises are judged

Check yourself
Q1What do reviewers look for first in a take-home AI project?show answer

In plain words: Clear notes about your choices. Tests. A small quality check with numbers. Safe handling of secrets. And honesty about what is missing.

Full answer · A README that states requirements, assumptions and trade-offs; clean structure; tests (especially for validation, authorization and failure paths); evidence of evaluation (even a small labelled set with numbers); sensible handling of secrets and errors; and honesty about what you'd do with more time. A modest system with rigor beats an ambitious demo with no tests.

Q2How should you use AI tools during a live coding interview?show answer

In plain words: Follow the interviewer's rules. If allowed, say what you use, check it and own the design. Be able to explain every line.

Full answer · Follow the interviewer's rules; if tools are allowed, say what you're doing and why, verify everything, and keep ownership of design decisions and tests. If they aren't allowed, don't use them. In either case you should be able to explain every line you submit and its failure modes.

Q3You have 60 minutes and the task is 'build a RAG Q&A over these docs'. How do you spend it?show answer

In plain words: Plan for a few minutes. Build the simplest version that works from start to end. Add checks and tests. Finish with a short write-up.

Full answer · Ten minutes clarifying and designing (interfaces, tenant/ACL, evaluation approach); twenty building the thinnest end-to-end path (ingest, chunk with metadata, hybrid or vector retrieval, grounded answer with citations); ten on validation and failure handling; ten writing a small labelled eval and tests; and ten on the README stating limits and next steps. Finish end to end before polishing any part.

Key takeaways
  • ✓AI coding drills are classic engineering: retries, similarity, validation, chunking, rate limiting, evaluation.
  • ✓Show error classification, bounds, tests and edge cases — and say your assumptions aloud.
  • ✓In take-homes, tests, evals and a trade-offs README beat extra features.
  • ✓Be able to explain every line you submit, however it was produced.
Hands-on exercise

Write them from memory

  1. Close this page and implement drills 1, 2 and 4 from scratch with unit tests, timing yourself.
  2. Add three edge-case tests to each (zero vector, k > corpus, budget exhausted, permanent error, huge result).
  3. Implement drill 5 for a real markdown document of yours and inspect the chunks for broken tables or code fences.
  4. Extend drill 6 to share state across processes (describe the Redis approach and its failure modes).
← Staff, Architect & Leadership QuestionsMock Interview Playbook →
0 / 11 finished0%