Title: Lecture 10 — From Vibes to Verdicts: Unit Testing LLM Apps with DeepEval
Course: AIINFRA 301: Production RAG & LLMOps — Observability and Evaluation > Section: Week 10 — Deep Evaluation and Failure Analysis with DeepEval

📖 Lecture — From Vibes to Verdicts: Unit Testing LLM Apps with DeepEval

Last week you learned to evaluate RAG outputs by hand — reading transcripts, spotting hallucinations, forming a judgment call. That works for a handful of examples, but it does not scale, and it does not repeat reliably across a team. This week's tool, DeepEval, closes that gap by giving LLM applications something software engineers already trust: unit tests. Just like Pytest asserts that a function's output matches an expectation, DeepEval asserts that an LLM output meets a quality bar. Each DeepEval metric runs locally in your Python process, evaluates a test case, and returns a score between 0 and 1. You set a threshold (say, 0.7), and the test passes or fails exactly like any other assertion. This is the bridge between the ad hoc evaluation you practiced in Week 9 and the automated CI gates you'll build in Week 11 — once evaluation is a passing/failing test, it can run on every pull request. Why not just use BLEU, ROUGE, or exact string match? This is the most common shortcut newcomers reach for, and it is the wrong tool for the job. BLEU and ROUGE count overlapping n-grams (words or short phrases) between a generated answer and a reference answer. That works reasonably well for tasks with a narrow band of acceptable phrasing, like machine translation. It fails badly for open-ended RAG and chat outputs. A student's answer that correctly paraphrases the source material — different words, same meaning — scores low on n-gram overlap, even though it is exactly right. Meanwhile, a response that reuses a lot of the reference's vocabulary but subtly gets the meaning wrong scores high, because the metric never checks meaning, only surface overlap. This is precisely backwards for grading correctness. The fix is LLM-as-judge: use a capable LLM to read the question, the generated answer, and (for RAG) the retrieved context, and render a judgment the way a human grader would. DeepEval's metrics correlate far better with human judgment than n-gram overlap because they evaluate semantics, not string shape. G-Eval is DeepEval's flagship LLM-as-judge metric, and it is worth understanding in detail because it generalizes to almost any quality dimension you care about. You give G-Eval a criteria string in plain language — for example, "the answer should be technically accurate and should not contradict the retrieved context" — and G-Eval automatically generates a chain-of-thought (CoT) sequence of evaluation_steps from that criteria. Alternatively, if you already know exactly how you want the judgment made, you can supply evaluation_steps directly yourself. You use one or the other, never both — G-Eval will not merge them. Whichever you choose, those steps are then "form-filled" against your actual test case: the judge LLM works through each step against the real input, actual output, and context, arriving at a raw score from 1 to 5. DeepEval then normalizes that into the familiar 0-1 range using the underlying token probabilities of the judge's output rather than just its final text answer, which reduces variance and bias in how the score comes out. Misconception check: it is tempting to think that because G-Eval uses an LLM and produces a number, that number is objective and perfectly reproducible — and that a lazy, vague criteria like "is this a good answer?" is good enough. It is not. G-Eval is fundamentally non-deterministic: it depends on a generative model's reasoning, and vague criteria give that model too much latitude to weigh things inconsistently from run to run. A rubric like "is this a good answer?" leaves "good" undefined — good for whom, on what dimension, compared to what? Two runs against the identical test case can land on different scores simply because the model's chain-of-thought wandered differently. The fix is discipline in rubric-writing: name the specific dimension (factual grounding, tone, completeness, formatting), state what a 5 looks like and what a 1 looks like, and keep the criteria narrow enough that the judge doesn't have to invent its own definition of quality. If your project genuinely needs deterministic, fully reproducible scoring — for compliance-adjacent checks, for instance — DeepEval also offers a DAG (deterministic decision-graph) metric, which encodes an explicit tree of rule-based decisions rather than leaving judgment to free-form LLM reasoning. Reach for G-Eval when you need judgment on a nuanced, human-like quality dimension; reach for DAG when you need an auditable, repeatable yes/no path. Beyond G-Eval, DeepEval ships a RAGAS-equivalent composite metric and several dedicated metrics purpose-built for retrieval pipelines, all runnable locally against a range of judge model providers, including local models via Ollama (useful when you cannot send data to a hosted API). The table below summarizes the core metrics you'll use in the lab.

Metric | What it checks | Failure it catches
Answer Relevancy | Does the generated answer actually address the question asked? | Off-topic or evasive answers
Faithfulness | Is every claim in the answer supported by the retrieved context? | Hallucinated facts not present in context
Contextual Precision | Are the most relevant retrieved chunks ranked near the top? | Good context buried under noise
Contextual Recall | Does the retrieved context contain everything needed to answer? | Missing-context gaps in retrieval
Hallucination | Does the output contradict or invent facts beyond given context? | Confident fabrication
Bias / Toxicity | Does the output show unfair skew or harmful language? | Safety and fairness regressions

The last teaching point ties all of this together operationally: hierarchical failure analysis. When a RAG pipeline produces a bad answer, the first question is not "what's wrong with the model?" — it's "where in the trace did it go wrong?" Score the whole trace first with an end-to-end metric like Answer Relevancy or your custom G-Eval criteria, to confirm there is a real problem. Then descend into the trace's individual spans: score the retrieval span with Contextual Precision/Recall to check whether the right information was even fetched, and score the generation span with Faithfulness to check whether the model used what it was given honestly. This hierarchical approach — whole trace, then span-by-span — is exactly how you'll debug failing lab cases this week, and it is the same mental model production observability tools use to localize failures before someone has to read raw logs.