Coding Drills & Take-Home Exercises
Six worked coding drills (retries, similarity, RRF, dispatcher, chunker, rate limiter) and how take-homes are judged.
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.
Practise from memory, with tests. Say your assumptions aloud.
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
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
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
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
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
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
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
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.
- ✓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.
Write them from memory
- Close this page and implement drills 1, 2 and 4 from scratch with unit tests, timing yourself.
- Add three edge-case tests to each (zero vector, k > corpus, budget exhausted, permanent error, huge result).
- Implement drill 5 for a real markdown document of yours and inspect the chunks for broken tables or code fences.
- Extend drill 6 to share state across processes (describe the Redis approach and its failure modes).