DEV Community

Mohit Bajaj
Mohit Bajaj

Posted on

What Makes an AI System Production-Ready? Part 4: Evaluation

Part of a series on building production-grade AI systems. Part 3 ended
with the input-stage guardrail gate built and the threat model reasoned
through. With that in place, the next question isn't "does the system
respond" — it's "how do we know if a response is actually good?" This
post covers wiring up RAGAS
to answer that with numbers instead of a gut check.

Why "it looks right" isn't evaluation

For a while, checking whether the system worked meant asking it a
question and reading the answer. If it sounded right, it shipped.

The problem with that approach is specific to RAG: a hallucinated answer
and a correct one can look identical. Both are fluent, both are
confident, both are formatted the same way. The only way to actually
tell them apart is to check the answer against the context the system
retrieved to produce it — not against how convincing the sentence
sounds. That check is what an eval suite does that a spot-check reading
can't.

What RAGAS is actually scoring

RAGAS breaks a RAG pipeline into the two places it can fail — retrieval
and generation — and scores each side independently, rather than giving
one blended "quality" number that hides which half is broken:

  • Faithfulness — is the generated answer actually grounded in the retrieved context, or did the model add something that isn't supported by it? This is the hallucination check, specifically scoped to "relative to what was retrieved," not "relative to general truth."
  • Answer relevancy — does the answer address the question that was asked, rather than something adjacent to it? A faithful answer can still be relevancy-poor if it's grounded but off-topic for the actual question.
  • Context precision — of everything retrieved, how much of it was actually useful? A retriever that pulls in five chunks where only one is relevant scores low here even if generation handles it gracefully.
  • Context recall — did retrieval pull in everything needed to answer correctly in the first place? This is the one metric that has nothing to do with the LLM's generation — it's purely "did the retriever do its job."

The split matters more than any single score. Low faithfulness with high
context recall means the model is hallucinating despite having good
context in front of it — a generation problem. Low context recall means
retrieval itself is the bottleneck, and no amount of prompt tuning fixes
that; the fix is upstream, in chunking or embeddings.

A golden dataset, with a guardrail of its own

RAGAS needs something to compare answers against — real question and
ground-truth pairs pulled from the project's own ingested documents,
not synthetic examples.

app/evals/golden_dataset.py holds those pairs. One deliberate rule:
the runner refuses to execute if it finds placeholder entries still
sitting in the dataset. Small guardrail, but it closes off the failure
mode where "I'll fill in real questions later" quietly turns into an eval
suite scoring against fake data indefinitely.

How RAGAS actually computes these — and why that matters for trust

None of these four numbers come from a single "rate this 0-1" prompt.
That's worth knowing, because an LLM asked to holistically grade its own
output is exactly the kind of eval nobody should trust blindly. RAGAS
instead breaks each metric into smaller, checkable steps:

  • Faithfulness is computed by first decomposing the generated answer into individual factual claims, then checking each claim separately against the retrieved context — can this specific claim be inferred from what was retrieved, yes or no. The score is the fraction of claims that pass. A hallucinated answer isn't judged as "sounds ungrounded overall" — it's judged claim by claim, which is what makes the number mean something more specific than a vibe.
  • Answer relevancy works in reverse: RAGAS generates several plausible questions from the answer, then measures how semantically similar those generated questions are to the original question that was actually asked. An answer that wandered off-topic produces reverse-engineered questions that don't match the original — that mismatch is the score.
  • Context precision and context recall both use an LLM to judge, chunk by chunk, whether each piece of retrieved context was actually relevant to answering the ground-truth question — precision penalizes retrieved noise, recall penalizes what should have been retrieved but wasn't. The honest caveat: every one of these steps still runs on an LLM as the judge — here, the same Groq model already in the stack. The scores inherit whatever blind spots that judge model has, and aren't perfectly reproducible run to run, since the judge itself has some variance. That's not a reason to distrust the number entirely — claim-level and chunk-level decomposition is a real improvement over one holistic "rate this" prompt — but it's why a single run isn't gospel. Trust the direction a score moves across a code change more than the third decimal place of any one run, and re-run before concluding a regression is real rather than judge noise.

Running it against the real system, not a mock

app/evals/run_eval.py sends every golden question to the running
backend's POST /query — the same endpoint the actual UI hits, not a
separate test-only path. That matters: the eval is scoring the system as
users would actually experience it, not a stubbed-out version of it.

make eval
# or, with arguments passed through:
make eval ARGS="--output custom_results.csv"
Enter fullscreen mode Exit fullscreen mode

Day to day, that's two terminals — make dev running the app,
make eval running the suite against it.

terminal 1 - showing progress

Each question gets scored across the four metrics above, using the same
Groq LLM and Gemini embeddings already running in production — acting as
the judge, not a separate evaluation-only model.

terminal showing 100% completion — answer_relevancy 0.8531, context_recall 0.9411, results saved to CSV

On this run: answer_relevancy came out at 0.8531, context_recall
at 0.9411. Read against what each metric actually checks — recall
this high means the retriever is doing its job, pulling in what's
actually needed to answer correctly. Relevancy in the 0.85 range means
most answers stay on-target, with room to tighten before it's tight
enough to trust without spot-checking.

That gap between the two — recall stronger than relevancy — is exactly
the kind of thing a single blended score would hide. It says the
bottleneck right now isn't retrieval, it's generation: the right context
is showing up, the answers built from it aren't always landing as
precisely on the question as they could.

Results land in eval_results.csv, one row per question, so a specific
low-scoring question is something you can go read — not just a number
that dropped.

What this actually buys

Before this, "does it work" meant reading a handful of outputs and
guessing. Now it means re-running make eval and diffing two CSVs.
"Did context recall drop after I changed the chunking strategy" is a
question with an actual answer, on demand, instead of a feeling.

None of this is complicated engineering — it's a golden dataset, a
compatibility shim, and a CSV. But it's the difference between believing
the system got better and being able to check.

Next

With evaluation in place, next on we will be deploying this rag pipeline with BYOK(Bring Your Own Key) concept.

Top comments (0)