Skip to the solution
EasyAI Engineering15 min read

What is RAG and why use it instead of fine-tuning for knowledge?

600 views

The problem

Question presented to candidate: "Our support bot needs to answer from our own help-centre articles and policies, which change every week. One engineer wants to fine-tune a model on the articles; another wants RAG. Explain what retrieval-augmented generation is, how it works, and why you would or would not use it instead of fine-tuning for this."

What a strong answer should cover:

  • The definition: RAG retrieves relevant passages from an external index at question time and puts them in the prompt, so the model answers from that evidence (non-parametric memory) instead of only from what is stored in its weights (parametric memory)
  • The two halves: an offline indexing path (load, chunk, embed, store) and an online query path (embed or parse the query, retrieve top-k, build the prompt, generate with citations)
  • Why RAG wins for knowledge: an update is a re-index of one document instead of a training run, deletions really delete, answers can cite sources, retrieval can be filtered per user or tenant, and the model can be told to say "I do not know" when nothing relevant is found
  • Evidence beyond intuition: published comparisons (for example Ovadia et al., EMNLP 2024) found RAG beat unsupervised fine-tuning for injecting both existing and new facts
  • What fine-tuning is still for: behaviour, format, tone and domain style, not facts that change; the two combine
  • What RAG does not fix: answers are only as good as retrieval (chunking, hybrid search, reranking), and retrieved text is untrusted input (prompt injection)

Clarifying questions expected:

  • How often does the content change, and must deletions take effect immediately?
  • Does every user see every document, or does access depend on the user or customer?
  • How big is the corpus: could it simply fit in the context window?

Code / implementation expected: Optional โ€” sketching the retrieve, build-prompt, generate loop (with a citation and an "I do not know" path) is a strong signal.

  • rag
  • retrieval
  • fine-tuning
  • architecture

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.

Solution

The solution is hidden so you can try first

15 min read ยท 10 sections

Read the code

A minimal RAG loop vs a frozen fine-tuned stub: update, refusal, tenant filter and citation
// RAG vs "knowledge baked into weights", with deterministic stubs (no model is called).
// The retriever is a tiny keyword index; the "generator" answers only from retrieved text.
const STOP = new Set("a an the is are do does i my of to in on for what how many can you we our with be it".split(" "));
const stem = (w) => (w.length > 4 && w.endsWith("ed") ? w.slice(0, -2) : w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w);
const words = (s) => s.toLowerCase().replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((w) => w && !STOP.has(w)).map(stem);

const docs = new Map(); // id -> { tenant, text }  (stands in for a vector store)
const upsert = (id, tenant, text) => docs.set(id, { tenant, text });

function retrieve(question, tenant, k = 2) {
  const q = new Set(words(question));
  return [...docs.entries()]
    .filter(([, d]) => d.tenant === tenant) // scope: a tenant only sees its own docs
    .map(([id, d]) => ({ id, text: d.text, score: words(d.text).filter((w) => q.has(w)).length }))
    .filter((h) => h.score > 0)
    .sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : 1))
    .slice(0, k);
}

function buildPrompt(question, hits) {
  const ctx = hits.map((h) => `[${h.id}] ${h.text}`).join("\n");
  return `Answer only from the sources. Cite the source id. If the sources do not answer, say you do not know.\nSources:\n${ctx}\nQuestion: ${question}`;
}

// Stub generator: copies the best-matching source sentence and cites it; refuses when nothing matches well.
function generate(question, hits) {
  const best = hits[0];
  if (!best || best.score < 2) return "I do not know based on the provided documents.";
  return `${best.text} [${best.id}]`;
}

// Stub "fine-tuned" model: facts memorised at training time, no sources, never says "I do not know".
const trainedSnapshot = {
  "return": "You have 30 days to return an item.",
  "ship": "We ship to every country within 5 business days.",
};
function fineTuned(question) {
  const ws = words(question).join(" ");
  if (ws.includes("return")) return trainedSnapshot.return;
  return trainedSnapshot.ship; // closest memory, stated confidently
}

const ask = (label, question, tenant = "acme") => {
  const hits = retrieve(question, tenant);
  console.log(`${label}`);
  console.log(`  retrieved : ${hits.map((h) => `${h.id}(${h.score})`).join(", ") || "(none)"}`);
  console.log(`  RAG       : ${generate(question, hits)}`);
  console.log(`  fine-tuned: ${fineTuned(question)}`);
};

upsert("returns-v1", "acme", "Items can be returned within 30 days of delivery for a full refund.");
upsert("shipping", "acme", "We ship to the UK and the EU; delivery takes 3 to 5 business days.");
upsert("warranty", "acme", "Electronics carry a 2 year warranty covering manufacturing defects.");
upsert("returns-b", "globex", "Globex accepts returns within 60 days of delivery.");

console.log("== day 1: both systems know the return window ==");
ask("Q: How many days do I have to return an item?", "How many days do I have to return an item?");

console.log("\n== day 2: policy changes to 14 days; RAG re-indexes one document ==");
docs.delete("returns-v1");
upsert("returns-v2", "acme", "Items can be returned within 14 days of delivery for a full refund.");
ask("Q: How many days do I have to return an item?", "How many days do I have to return an item?");

console.log("\n== a question the documents do not answer ==");
ask("Q: Do you ship to Mars?", "Do you ship to Mars?");

console.log("\n== same question, another tenant (metadata filter) ==");
const hb = retrieve("How many days do I have to return an item?", "globex");
console.log(`  globex RAG: ${generate("x", hb)}`);

console.log("\n== the prompt the generator actually receives ==");
const q = "How many days do I have to return an item?";
console.log(buildPrompt(q, retrieve(q, "acme")));

// Cost of a knowledge update, illustrative: re-embed 1 doc vs a fine-tuning run over the whole corpus.
const corpusTokens = 2_000_000, docTokens = 400, epochs = 3;
console.log("\n== tokens processed to apply one policy change (illustrative) ==");
console.log(`  RAG re-embed one doc : ${docTokens.toLocaleString("en-US")} tokens`);
console.log(`  fine-tune, ${epochs} epochs : ${(corpusTokens * epochs).toLocaleString("en-US")} tokens (${(corpusTokens * epochs / docTokens).toLocaleString("en-US")}x)`);

Discussion

Other approaches, follow-ups and how it went in real interviews.

Sign in to join the discussion.

No comments yet. Share how you would answer it.

Question 6 of 83 in AI engineering

Keep your momentum going.

Mark this question solved to track your progress, then take the next one while it is fresh.

Up nextEasy

What are tokens and why do they matter for cost and limits?

Continue