Evaluating RAG with Ragas: faithfulness, answer relevancy, context precision?
323 views
49 of 83
1
The problem
Question presented to candidate:
"Our RAG assistant answers questions from the returns policy, and we have started logging Ragas scores. Explain what faithfulness, answer relevancy, context precision and context recall actually measure, how each is computed, and what you would change in the pipeline when each one drops."
What a strong answer should cover:
Faithfulness: the answer is split into claims, each claim is checked against the retrieved context, score = supported claims / total claims; it detects answers not grounded in the context, not answers that are wrong in the real world
Answer (response) relevancy: the judge writes questions back from the answer and Ragas averages their embedding cosine similarity to the real question; it penalises off-topic, incomplete or padded answers and says nothing about correctness
Context precision: are the relevant chunks ranked near the top, computed as the mean of precision@k at each relevant rank; context recall: how many claims of a reference answer are supported by the retrieved context, so it needs a labelled reference
Which stage each metric points at: precision and recall diagnose the retriever and reranker, faithfulness and relevancy diagnose the generator and prompt
Limits: every score is an LLM judge plus (for relevancy) an embedding model, so scores shift with the judge, should be validated against human labels, and are read together with claim-level verdicts; the Ragas API changed between versions (collections API in 0.4)
Code / implementation expected: Optional โ computing the four formulas on a small example, or a Ragas scoring call.
rag-eval
ragas
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 who run or are about to run Ragas on a RAG system and want to know what each number means, how it is computed, and what to fix when it moves.
Difficulty: Medium
Verified with: Node 22.22.0 and Python 3.12.3 (a hand-computed version of the four formulas below). Metric definitions were checked against the Ragas documentation pages for v0.2 to v0.4 via web search in October 2026; API details that only appeared in third-party guides are marked as such.
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
You ask a research assistant a question. They go to the library, pull four book
The solution is hidden so you can try first
14 min read ยท 13 sections
4
Read the code
The four Ragas formulas by hand: faithfulness, answer relevancy, context precision, context recall
// The four core Ragas formulas, computed by hand. Where Ragas would ask an LLM// (split into claims, judge support, write questions back from the answer) a// deterministic stub does it: "supported" means every content word appears in the text.constSTOP = newSet("a an the is are be can do i to of in on for and or with within have has it you your my this that how what".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) => s.toLowerCase().replace(/[^a-z0-9 ]/g, " ").split(/\s+/).filter((w) => w && !STOP.has(w)).map(stem);
constclaims = (s) => s.split(". ").map((x) => x.replace(/\.$/, "")).filter(Boolean);
constsupportedBy = (claim, text) => { const t = newSet(words(text)); returnwords(claim).every((w) => t.has(w)); };
constCHUNKS = {
R: "Laptops can be returned within 14 days of delivery.",
F: "Opened laptops carry a 15 percent restocking fee.",
S: "Standard shipping takes 3 to 5 business days.",
W: "Every laptop includes a one year limited warranty.",
};
constQUESTION = "How many days do I have to return a laptop?";
constREFERENCE = "Laptops can be returned within 14 days. Opened laptops carry a 15 percent restocking fee.";
// Each scenario: retrieved chunk ids in rank order, the answer, and the questions// the judge would generate back from that answer (for answer relevancy).constSCENARIOS = [
{ name: "1 good", ctx: ["R", "F", "S"], answer: "Laptops can be returned within 14 days. Opened laptops carry a 15 percent restocking fee.",
gen: ["How many days to return a laptop?", "Is there a fee to return an opened laptop?", "Can laptops be returned?"] },
{ name: "2 relevant ranked low", ctx: ["S", "W", "R", "F"], answer: "Laptops can be returned within 14 days. Opened laptops carry a 15 percent restocking fee.",
gen: ["How many days to return a laptop?", "Is there a fee to return an opened laptop?", "Can laptops be returned?"] },
{ name: "3 hallucinated claim", ctx: ["R", "F", "S"], answer: "Laptops can be returned within 14 days. Return shipping is always free.",
gen: ["How many days to return a laptop?", "Is return shipping free?", "Can laptops be returned?"] },
{ name: "4 fee chunk missing", ctx: ["R", "S", "W"], answer: "Laptops can be returned within 14 days.",
gen: ["How many days to return a laptop?", "Can laptops be returned?", "What is the laptop return window?"] },
{ name: "5 off-topic answer", ctx: ["R", "F", "S"], answer: "Standard shipping takes 3 to 5 business days.",
gen: ["How long does standard shipping take?", "How many business days is shipping?", "What is the shipping time?"] },
];
functionfaithfulness(answer, ctx) {
const text = ctx.map((id) =>CHUNKS[id]).join(" ");
const cs = claims(answer);
return cs.filter((c) =>supportedBy(c, text)).length / cs.length;
}
functionvec(s) { const m = newMap(); for (const w ofwords(s)) m.set(w, (m.get(w) || 0) + 1); return m; }
functioncosine(a, b) {
let dot = 0, na = 0, nb = 0;
for (const [w, x] of a) { na += x * x; if (b.has(w)) dot += x * b.get(w); }
for (const [, y] of b) nb += y * y;
return dot / Math.sqrt(na * nb);
}
functionanswerRelevancy(gen) { const q = vec(QUESTION); return gen.reduce((s, g) => s + cosine(vec(g), q), 0) / gen.length; }
// Context precision@K = sum over k of (precision@k * v_k) / (relevant items in top K).functioncontextPrecision(ctx) {
const refClaims = claims(REFERENCE);
const v = ctx.map((id) => (refClaims.some((c) =>supportedBy(c, CHUNKS[id])) ? 1 : 0));
let hits = 0, sum = 0;
v.forEach((rel, i) => { if (rel) { hits++; sum += hits / (i + 1); } });
return { score: hits ? sum / hits : 0, v: v.join("") };
}
// Context recall = reference claims supported by the retrieved context / all reference claims.functioncontextRecall(ctx) {
const text = ctx.map((id) =>CHUNKS[id]).join(" ");
const rc = claims(REFERENCE);
return rc.filter((c) =>supportedBy(c, text)).length / rc.length;
}
constf = (x) => x.toFixed(3);
console.log(`question: ${QUESTION}`);
console.log("scenario faithful ans_rel ctx_prec ctx_recall relevance-by-rank");
for (const s ofSCENARIOS) {
const cp = contextPrecision(s.ctx);
console.log(`${s.name.padEnd(24)}${f(faithfulness(s.answer, s.ctx)).padStart(8)}${f(answerRelevancy(s.gen)).padStart(8)}${f(cp.score).padStart(9)}${f(contextRecall(s.ctx)).padStart(11)}${cp.v}`);
}
// Show the claim-level verdicts for the hallucination case, which is what to read when the number drops.const s3 = SCENARIOS[2], text3 = s3.ctx.map((id) =>CHUNKS[id]).join(" ");
for (const c ofclaims(s3.answer)) console.log(` scenario 3 claim "${c}" -> ${supportedBy(c, text3) ? "supported" : "NOT supported"}`);
5
Discussion
Other approaches, follow-ups and how it went in real interviews.