Question presented to candidate:
"We have a RAG assistant over our internal handbook and an end-to-end accuracy of about 64% on our test questions. Leadership wants it higher, but nobody can say whether the retriever or the model is the problem. Design the evaluation you would build for this pipeline, end to end."
What a strong answer should cover:
Evaluate stages separately as well as end to end: a labelled retrieval set scored with recall@k, MRR or nDCG; the generator scored with the correct context supplied (oracle context); then the full pipeline against reference answers
A golden set with evidence labels (which passage answers each question), slices by topic and difficulty, and deliberately unanswerable questions to test abstention
Failure attribution per question: retrieval miss, right chunk retrieved but wrong one used, over-abstention, answering the unanswerable; the counts tell you which stage to invest in
Generation-side scoring: answer correctness against a reference, faithfulness to the retrieved context, citation correctness, using code checks where possible and a calibrated LLM judge where not
Operational metrics alongside quality (latency, tokens and cost per query), CI gating on every chunking, embedding, prompt or model change, and online signals after release
Clarifying questions expected:
How big and how fast-changing is the corpus, and is there a per-user permission model on documents?
Do we have real user questions to build the test set from, and who can label the correct evidence?
What does a wrong answer cost compared with "I could not find that"?
Code / implementation expected: Optional โ a small harness that computes stage metrics and attributes each failure to a stage.
rag-evaluation
recall
faithfulness
evals
2
Try it yourself
Sketch your approach before you read the solution. That is what the interview tests.
Stuck? Get a hint.
A short nudge toward the approach. It will not give the answer away.
3
Solution
Target Audience: Engineers responsible for a RAG system in production who need an evaluation that says where it fails, not only how often, and candidates facing a design-style evaluation question.
Difficulty: Hard
Verified with: Node 22.22.0 and Python 3.12.3 (the stage-by-stage harness below). Framework descriptions (TruLens RAG triad, Ragas) were checked against their docs via web search in October 2026.
How to read this doc: Every idea is explained in plain language first, then tagged with ๐ Interview term: so you also learn the words an interviewer expects. All program output shown was produced by actually running the example; the Verified section quotes it in full.
1. Why This Even Matters โ A Story First
A pizza delivery company gets complaints: 36% of pizzas arrive wrong. The manager's first instinct is to retrain the drivers. But a wrong pizza can
The solution is hidden so you can try first
14 min read ยท 10 sections
4
Read the code
End-to-end RAG eval harness: stage metrics plus failure attribution
// End-to-end RAG eval harness on a tiny handbook. Retriever = word-overlap// ranking (a stand-in for BM25 or vectors); generator = deterministic stub that// answers from the first retrieved chunk sharing >= 2 words with the question,// else says "not found". The harness scores each stage separately, then attributes failures.constSTOP = newSet("a an the is are be can do does i to of in on for and or with have has it you my what how many when long often each get".split(" "));
conststem = (w) => (w.length > 4 && w.endsWith("ed") ? w.slice(0, -2) : w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w);
constwords = (s) => newSet(s.toLowerCase().replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((w) => w && !STOP.has(w)).map(stem));
constoverlap = (a, b) => { let n = 0; for (const w of a) if (b.has(w)) n++; return n; };
constDOCS = [
["d1", "Employees accrue 20 vacation days per year."],
["d2", "Up to 5 unused vacation days roll over into the next year."],
["d3", "Parental leave is 16 weeks paid for all employees."],
["d4", "Sick leave needs a doctor note after 3 consecutive sick days."],
["d5", "The office opens at 8 am and closes at 7 pm."],
["d6", "Remote work is allowed up to 3 days per week with manager approval."],
["d7", "Expense reports must be filed within 30 days of purchase."],
["d8", "Laptops are refreshed every 3 years by the IT team."],
].map(([id, text]) => ({ id, text, w: words(text) }));
// gold = the chunk that answers it; null = not answerable from the corpus.constQUERIES = [
["q01", "How many vacation days do employees get per year?", "d1"],
["q02", "Can unused vacation roll over to next year?", "d2"],
["q03", "How long is paid parental leave?", "d3"],
["q04", "When do I need a doctor note for sick days?", "d4"],
["q05", "What time does the office close?", "d5"],
["q06", "How many days per week can I work remotely?", "d6"],
["q07", "What is the deadline to submit receipts?", "d7"],
["q08", "How often are laptops replaced?", "d8"],
["q09", "How many vacation days can I carry forward each year?", "d2"],
["q10", "How many sick days per year do employees get?", null],
["q11", "What is the dress code?", null],
].map(([id, q, gold]) => ({ id, q, gold, w: words(q) }));
functionretrieve(qw, k) {
returnDOCS.map((d, i) => ({ d, s: overlap(qw, d.w), i }))
.filter((x) => x.s > 0)
.sort((a, b) => b.s - a.s || a.i - b.i)
.slice(0, k).map((x) => x.d);
}
functiongenerate(qw, ctx) {
for (const d of ctx) if (overlap(qw, d.w) >= 2) return d.id; // answers from this chunkreturnnull; // "not found in the handbook"
}
const K = 3;
const answerable = QUERIES.filter((q) => q.gold);
let r1 = 0, r3 = 0, mrr = 0, oracleOk = 0, e2eOk = 0;
const buckets = {};
const rows = [];
for (const q ofQUERIES) {
const ctx = retrieve(q.w, K);
const ids = ctx.map((d) => d.id);
const rank = q.gold ? ids.indexOf(q.gold) + 1 : 0;
if (q.gold) { if (rank === 1) r1++; if (rank >= 1) { r3++; mrr += 1 / rank; } }
// Stage 2 in isolation: give the generator the gold chunk only (oracle context).const oracle = q.gold ? generate(q.w, DOCS.filter((d) => d.id === q.gold)) : null;
if (q.gold && oracle === q.gold) oracleOk++;
const ans = generate(q.w, ctx);
let label;
if (!q.gold) label = ans ? "answered unanswerable" : "correct abstain";
elseif (ans === q.gold) label = "correct";
elseif (rank === 0) label = "retrieval miss";
elseif (ans === null) label = "over-abstain (gold retrieved)";
else label = "wrong chunk used (gold retrieved)";
if (label === "correct" || label === "correct abstain") e2eOk++;
buckets[label] = (buckets[label] || 0) + 1;
rows.push(`${q.id} top${K}=[${ids.join(",")}] gold=${q.gold || "-"} answer=${ans || "not found"} -> ${label}`);
}
rows.forEach((r) =>console.log(r));
const n = answerable.length, f = (x) => x.toFixed(3);
console.log(`retrieval (${n} answerable): recall@1=${f(r1 / n)} recall@${K}=${f(r3 / n)} MRR@${K}=${f(mrr / n)}`);
console.log(`generation with oracle context: ${oracleOk}/${n} = ${f(oracleOk / n)}`);
console.log(`end to end (${QUERIES.length} queries incl. unanswerable): ${e2eOk}/${QUERIES.length} = ${f(e2eOk / QUERIES.length)}`);
console.log("failure attribution:");
for (const k ofObject.keys(buckets).sort()) console.log(` ${k.padEnd(34)}${buckets[k]}`);
5
Discussion
Other approaches, follow-ups and how it went in real interviews.