Your model just summarized a support ticket. Is the summary any good? You cannot run accuracy_score on free-form text: there is no single right answer, and two good summaries can use completely different words. LLM evaluation in Python exists to put a number on quality anyway, and this post builds the tools for it, from a hand-rolled grader up to RAGAS and DeepEval.
The trick the whole industry settled on is almost funny: use another language model as the grader. You hand a stronger model the question, the answer, and the source text, and you ask it to score how faithful the answer is. Frameworks like RAGAS and DeepEval wrap that idea in clean application programming interfaces (APIs). But before we reach for a framework, we are going to build a tiny grader by hand so you can see exactly what is happening under the hood. No magic, no black box.
Think of it like grading an essay exam instead of a multiple-choice test. A multiple-choice answer is either A or B, so a scanner checks it in a millisecond. An essay needs a human reader holding a rubric: did the student answer the question, did they back up claims with the source material, did they stay on topic? LLM evaluation is the same shift, from scanner to rubric. The catch is that the “reader” holding the rubric is now a second model.
“A year spent in artificial intelligence is enough to make one believe in God.”
Alan Perlis, Epigrams on Programming
Last Updated: July 2026 | Tested on: Python 3.14.6, ragas 0.4.3, deepeval 4.0.6, anthropic 0.111.0 | Difficulty: Advanced | Reading Time: 18 minutes
- MLOps tutorial (for running evals on a schedule)
- An LLM API key if you want to run the RAGAS and DeepEval sections yourself
LLM evaluation is the practice of measuring how good a language model’s outputs actually are. Traditional ML compares predictions against labeled answers with metrics like accuracy or F1. LLM outputs are free-form text, so there is rarely one correct string to match. Instead you measure things like faithfulness (is the answer grounded in the source?), relevancy (does it answer the question asked?), and safety (is the tone acceptable?). The tools to measure these are automated metrics, human review, and the LLM-as-judge approach you will build in this post.
Table of Contents
The Map: Three Ways to Score an LLM
Before any code, look at the shape of the field. There are three broad families of LLM evaluation, and most real systems use a mix of all three. Automated metrics are cheap and fast but shallow. Metrics specific to Retrieval-Augmented Generation (RAG), like the ones RAGAS computes, target retrieval quality. Human evaluation is the gold standard but slow and expensive. The diagram below lays out where each piece fits.
If retrieval and chunks are new terms, here is the short version: chunks are the pieces each document is split into ahead of time, and retrieval means fetching the chunks most relevant to a question from that store. The full RAG walkthrough comes in the later posts, so this quick picture is all you need to follow along here.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
Read the diagram left to right. Automated metrics like BLEU and ROUGE just count overlapping words, so they are fast but blind to meaning. BERTScore goes one step deeper and compares embeddings for semantic similarity. The RAGAS column is the interesting one: faithfulness, answer relevancy, and context precision are all judged by an LLM, not by string matching. Human evaluation on the right is still the ground truth everything else tries to approximate. We are going to walk up this ladder, starting at the cheapest rung.
One box in that automated column deserves its own note: perplexity, the classic language-model metric. It measures how surprised a model is by a piece of held-out text (formally the exponential of the average negative log-likelihood per token), so a lower number means the text matched the model’s expectations better, and it is genuinely useful for comparing a base model against a fine-tuned version on the same corpus. What it is not is a task-quality metric: a model can post a low perplexity and still hand you a fluent, confident answer that is unhelpful or plain wrong, so never read it as a measure of whether the output actually did the job.
Build a Grader by Hand
The fastest way to demystify RAGAS and DeepEval is to write a miniature version yourself. No API key, no install, just standard-library Python. The core question for catching hallucinations is simple: does every claim in the answer trace back to the retrieved context? If the answer mentions a fact the context never stated, that fact is a suspect. Let us code exactly that.
📄 scratch_metrics.py: a hallucination check in 20 lines
# A tiny, dependency-free evaluator you can run on your own machine.
# It shows the mechanic that every framework (RAGAS, DeepEval) wraps:
# check each claim word against the retrieved context. Words the context
# never mentions are your hallucination suspects.
def tokenize(text):
return [w.strip(".,!?;:").lower() for w in text.split()]
# stop words carry no factual claim, so we ignore them
STOP = {"a", "an", "the", "in", "at", "by", "and", "was", "is", "of", "to", "first"}
def unsupported_words(answer, context):
"""Return the answer words that never appear in the context."""
ctx = set(tokenize(context))
return [w for w in tokenize(answer) if w not in ctx and w not in STOP]
context = "Python was created by Guido van Rossum and first released in 1991 at CWI in the Netherlands."
grounded_answer = "Python was created by Guido van Rossum and released in 1991."
made_up_answer = "Python was created by Guido van Rossum in 1991 at Bell Labs."
for label, answer in [("grounded answer", grounded_answer), ("made-up answer", made_up_answer)]:
flagged = unsupported_words(answer, context)
verdict = "PASS (grounded)" if not flagged else f"FAIL (not in context: {flagged})"
print(f"{label}: {verdict}")
▶ Output
grounded answer: PASS (grounded) made-up answer: FAIL (not in context: ['bell', 'labs'])
What happened here: The grounded answer only used words the context already contains, so it passed. The made-up answer slipped in “Bell Labs”, which the context never mentions, and our checker flagged exactly those two words. That is the real fact, by the way: Guido van Rossum created Python at CWI in the Netherlands, not Bell Labs (Bell Labs is where C and Unix were born, which is probably why models confuse the two).
This twenty-line script is doing, in crude form, what a faithfulness metric does: line up the claims against the evidence and flag anything unsupported. The difference is that a real metric uses an LLM to compare meaning, so it catches a paraphrased hallucination that shares no exact words. Word matching is the training-wheels version. It is honest about its own limits, and that is the point.
LLM as Judge: The Real Engine
Here is the move that powers RAGAS, DeepEval, G-Eval, and most production eval stacks. You take a capable model, hand it a rubric in plain English, and ask it to grade. The grader reads the question, the answer, and the source context, then returns a score and a one-line reason. The reason matters as much as the number, because it tells you why something failed, which is what you need to fix it. Once this pattern clicks, the rest of LLM evaluation is mostly packaging around it.
The code below is a complete, working LLM-as-judge using the official Anthropic Software Development Kit (SDK). We did not execute it here because every call costs money and needs an API key, but the structure is exactly what you would run.
📄 judge.py: a faithfulness grader using Claude
# An LLM-as-judge scorer. A stronger model reads the question, the answer,
# and the source context, then returns a 1 to 5 faithfulness score with a reason.
# This is exactly what RAGAS and DeepEval do under the hood.
import os
from anthropic import Anthropic
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
JUDGE_PROMPT = """You are a strict grader. Score how faithful the ANSWER is to the
CONTEXT on a scale of 1 (invented facts) to 5 (fully supported).
Reply with only a number and a one-line reason.
QUESTION: {question}
CONTEXT: {context}
ANSWER: {answer}"""
def judge_faithfulness(question, answer, context):
msg = client.messages.create(
model="claude-opus-4-8", # current at the time of writing; models change fast, check the provider docs
max_tokens=100,
messages=[{
"role": "user",
"content": JUDGE_PROMPT.format(question=question, context=context, answer=answer),
}],
)
return msg.content[0].text.strip()
verdict = judge_faithfulness(
question="Where was Python created?",
answer="Python was created by Guido van Rossum at Bell Labs.",
context="Python was created by Guido van Rossum at CWI in the Netherlands.",
)
print(verdict)
▶ Output (illustrative: this call needs an API key, see note)
1 - The answer claims "Bell Labs" but the context says Python was created at CWI in the Netherlands.
client.messages.create), and the shown grade is the kind of response the rubric asks for. Your exact wording will vary because the model decides the phrasing. The hand-built grader in the previous section, by contrast, is real local output you can reproduce.What happened here: The judge spotted the same “Bell Labs” error our hand-built checker did, but it explained the problem in a sentence and gave it a 1 out of 5. That reason string is gold during debugging. The reason this approach scales is that one prompt template grades any rubric: swap “faithful” for “relevant” or “polite” and you have a new metric. This single pattern is what frameworks generalize. Now you understand the engine, so the framework code on top of it will read like a thin convenience layer rather than a mystery.
RAGAS: Evaluating RAG Pipelines
Think of RAGAS as a full blood-test panel for your RAG pipeline. Instead of one vague “it feels off” number, you get four separate readings, and each one points at a different organ that might be failing. RAGAS (Retrieval-Augmented Generation Assessment) is the go-to framework for scoring RAG systems, and it is built entirely on the LLM-as-judge idea you just saw. It bundles the rubrics for you. The four metrics worth knowing: faithfulness (is the answer grounded in the retrieved context?), answer relevancy (does it answer the question that was asked?), context precision (are the retrieved chunks actually relevant?), and context recall (did retrieval find everything the reference needed?).
📄 ragas_eval.py: scoring a RAG pipeline (ragas 0.4.3)
from ragas import evaluate, EvaluationDataset
from ragas.metrics import (
Faithfulness,
ResponseRelevancy,
LLMContextPrecisionWithReference,
LLMContextRecall,
)
# Aviraj, a backend engineer, evaluates his RAG pipeline. Each sample is one question the bot answered,
# the answer it gave, the chunks it retrieved, and the known-good reference.
samples = [
{
"user_input": "How do I deploy a Flask app to production?",
"response": "Deploy with Docker: build the image, push to a registry, run it on Kubernetes.",
"retrieved_contexts": ["Production deployment uses Docker and Kubernetes. Build with docker build."],
"reference": "Deploy using Docker containers on Kubernetes with health checks.",
},
{
"user_input": "What authentication does the API support?",
"response": "The API supports API key auth, JWT tokens, and session-based auth.",
"retrieved_contexts": ["API authentication supports API keys (X-API-Key header) and JWT bearer tokens."],
"reference": "API key auth via X-API-Key, JWT bearer tokens, and session auth.",
},
]
dataset = EvaluationDataset.from_list(samples)
result = evaluate(
dataset=dataset,
metrics=[
Faithfulness(),
ResponseRelevancy(),
LLMContextPrecisionWithReference(),
LLMContextRecall(),
],
)
print(result)
▶ Output (illustrative: RAGAS calls an LLM judge, see note)
{'faithfulness': 0.92, 'answer_relevancy': 0.88, 'llm_context_precision_with_reference': 0.85, 'context_recall': 0.78}
datasets.Dataset approach from older guides is gone, replaced by EvaluationDataset.from_list with user_input / response / retrieved_contexts / reference keys and class-based metrics. APIs in this space move fast, so check the RAGAS docs before pinning a version.What happened here: RAGAS scored the pipeline on four axes. In a result like this, faithfulness is high (the answers stay close to the retrieved text), but context recall is the weakest number. That gap is the diagnosis: a low recall means retrieval is missing relevant chunks, so the fix lives in your chunking strategy or a larger top_k, not in the model. This is the everyday workflow: low faithfulness points at a hallucinating generator, low recall points at a leaky retriever. You read the four numbers like a dashboard and they tell you which half of the pipeline to go fix.
DeepEval: Unit Tests for LLMs
Think of DeepEval as a quality-control inspector standing at the end of a food-packaging line: every item that fails the checklist gets pulled before it ever ships to a customer. If RAGAS is for RAG dashboards, DeepEval is for your test suite. It makes LLM checks feel like pytest assertions: define an input, the actual output, the context, a metric, and a threshold. Run it, and you get a pass or fail. The genius is that you can drop these straight into CI (continuous integration) and block a deploy when scores slip. That turns LLM evaluation from a report you glance at into a gate your code has to pass.
📄 deepeval_tests.py: LLM checks that read like pytest (deepeval 4.0.6)
from deepeval.test_case import LLMTestCase
from deepeval.metrics import (
AnswerRelevancyMetric,
FaithfulnessMetric,
ToxicityMetric,
)
# Anvay, a QA engineer, writes LLM checks that read like pytest assertions.
# Test 1: is the answer actually relevant to the question?
relevancy_case = LLMTestCase(
input="What is Python's GIL?",
actual_output="The Global Interpreter Lock lets only one thread run Python bytecode at a time.",
retrieval_context=["Python's GIL is a mutex that protects access to Python objects."],
)
relevancy = AnswerRelevancyMetric(threshold=0.7)
relevancy.measure(relevancy_case)
print(f"Relevancy: {relevancy.score:.2f} -> {'PASS' if relevancy.is_successful() else 'FAIL'}")
# Test 2: is the answer faithful to the source, or did the model invent a fact?
faithfulness_case = LLMTestCase(
input="Where was Python created?",
actual_output="Python was created by Guido van Rossum at Bell Labs.",
retrieval_context=["Python was created by Guido van Rossum at CWI in the Netherlands."],
)
faithfulness = FaithfulnessMetric(threshold=0.7)
faithfulness.measure(faithfulness_case)
print(f"Faithfulness: {faithfulness.score:.2f} -> {'PASS' if faithfulness.is_successful() else 'FAIL'}")
# Test 3: is the tone safe for users to read?
toxicity_case = LLMTestCase(
input="Review this pull request",
actual_output="The variable names are unclear and the function is doing too much. Worth splitting up.",
)
toxicity = ToxicityMetric(threshold=0.5)
toxicity.measure(toxicity_case)
print(f"Toxicity: {toxicity.score:.2f} -> {'PASS' if toxicity.is_successful() else 'FAIL'}")
▶ Output (illustrative: DeepEval calls an LLM judge, see note)
Relevancy: 0.95 -> PASS Faithfulness: 0.33 -> FAIL Toxicity: 0.04 -> PASS
What happened here: The faithfulness check did its job. The model claimed “Bell Labs” while the source says CWI, so the score lands below the 0.7 threshold and the test fails, exactly like a failing assert in pytest. The relevancy and toxicity checks pass because the Global Interpreter Lock (GIL) answer is on-topic and the code review is blunt but not abusive. Wire these into CI with the deepeval pytest plugin and a bad model update gets blocked before it ever reaches users. It is unit testing, just pointed at text instead of return values.
Common Misconceptions
- “BLEU and ROUGE are good enough.” They count word overlap, so they reward a wrong answer that happens to reuse the right words and punish a correct paraphrase. Use them as a cheap smoke test, never as your only signal.
- “The judge model is objective.” An LLM judge has biases. It tends to prefer longer answers and answers that sound confident, and it can favor outputs from its own model family. Use a strong judge, keep the rubric tight, and spot-check against humans.
- “Evaluate once at launch and you are done.” Behavior drifts when you tweak a prompt, swap a chunking strategy, or the provider ships a new model version. Run your eval suite on a schedule, not just on launch day.
- “No evaluation is fine for a small app.” Shipping an LLM feature with zero evals is shipping untested software. Even fifty hand-written question and answer pairs run before each deploy will catch the embarrassing regressions.
Try It Yourself
- Extend the hand-built grader: add a
recallfunction that measures how much of the reference answer’s content the model’s answer covered, then test it on a deliberately incomplete answer. - Write a relevancy rubric: change the
JUDGE_PROMPTso the judge scores whether the answer addresses the question, instead of faithfulness. Same code, new metric. - Build a tiny test suite: create ten question and answer pairs for a topic you know well, run them through your judge, and record the scores as your baseline for the next model update.
More in this series:
- Python Chatbot Project: Multi-Provider Chat App with Costs
- LLM Observability with Langfuse: Traces, Spans, and Cost
- Python: Vector Databases (ChromaDB, Pinecone, pgvector for AI Apps)
Frequently Asked Questions
Which metrics matter most when you evaluate LLM output?
It depends on the job. For RAG applications, start with faithfulness (is the answer grounded?) and context recall (did retrieval find enough?). For chatbots, watch answer relevancy and toxicity. For code generation, functional correctness (does it run?) beats everything. Pick the metric that catches your most dangerous failure mode first, then add the rest.
Can I use one LLM to evaluate another LLM?
Yes, and it is now standard practice. Capable models such as Claude and GPT-5.5 are commonly used as judges, and both RAGAS and DeepEval rely on this approach (current at the time of writing; models change fast, check the provider docs). Use a judge at least as strong as the model under test. In published studies like MT-Bench, strong LLM judges agreed with human raters roughly 80 percent of the time, but they carry biases toward longer and more confident answers, so spot-check against humans.
Can I evaluate without an API key?
Partly. Word-overlap metrics like BLEU and ROUGE and embedding metrics like BERTScore run locally with no API. The LLM-as-judge metrics inside RAGAS and DeepEval do call a model, so those need a key and spend tokens. A good pattern is to gate cheap local metrics on every commit and run the LLM-judge suite on a nightly schedule.
How do I add LLM evaluation to CI/CD?
Write a file of question and expected-answer pairs. Run DeepEval metrics through its pytest plugin and set pass/fail thresholds. Block the deploy when a score drops below threshold. It is the same idea as unit testing, just measuring text quality instead of return values. Keep the suite small and fast so it does not stall every pull request.
How do I detect hallucinations specifically?
Compare every claim in the answer against the retrieved context. A faithfulness metric does this with an LLM judge so it catches paraphrased inventions, while a quick word-overlap check (like the hand-built grader in this post) flags claims using words the context never mentioned. In production, log low-faithfulness answers and review them, because a hallucination is the failure mode most likely to lose user trust.
Interview Questions on LLM Evaluation
Interviewers rarely ask for definitions. They ask what happens in situations like these.
Q: Why can’t you use accuracy or F1 to evaluate free-form LLM output?
Accuracy and F1 need a single labeled ground-truth string to compare against, and they reward an exact match. LLM output is open-ended, so two equally correct answers can share almost no words. Matching against one “right” string would punish a valid paraphrase and reward a wrong answer that happened to copy the reference wording. That is why evaluation shifts to graded qualities like faithfulness and relevancy, usually scored by an LLM judge.
Q: What is the difference between faithfulness and answer relevancy?
Faithfulness asks whether every claim in the answer is grounded in the retrieved context, so it catches hallucinations. Answer relevancy asks whether the answer actually addresses the question that was asked, so it catches off-topic or evasive replies. An answer can be perfectly faithful yet irrelevant (it repeats the source but ignores the question), or relevant yet unfaithful (it answers the question but invents a fact). You usually need both.
Q: What are BLEU and ROUGE, and what is their main limitation?
BLEU and ROUGE are automated metrics that score how many words or n-grams the output shares with a reference answer, and they run locally with no API cost. Their main limitation is that they measure surface word overlap, not meaning: they punish a correct paraphrase that uses different words and can reward a wrong answer that reuses the right ones. Treat them as a cheap smoke test, never as your only quality signal.
Q: Why is the “reason” string from an LLM judge as valuable as the numeric score?
The number tells you that something failed; the reason tells you why, which is what you actually need to fix it. A bare “faithfulness: 0.3” leaves you guessing, but “the answer claims Bell Labs while the context says CWI” points straight at the broken claim. Logging the reason also lets you audit the judge itself and spot cases where it graded on the wrong basis. Always capture both the score and the explanation.
Q: Your RAGAS report shows faithfulness at 0.95 but context recall at 0.55. Where is the problem and what do you fix first?
High faithfulness with low recall means the generator is behaving (it stays close to whatever context it was given) but retrieval is missing relevant chunks, so the model simply never sees part of the answer. The fix lives in the retriever, not the prompt: revisit your chunking strategy, raise top_k, or improve the embedding or reranking step. Changing the generation prompt would waste time, because the model cannot ground an answer in text it was never handed.
Q: Your LLM-as-judge gives a confident 5 out of 5 to an answer you can see is wrong. What could be going on and how do you tighten it?
LLM judges carry known biases: they tend to favor longer, more confident-sounding answers and can go easy when the rubric is vague. Start by making the rubric explicit and forcing the judge to cite the exact context span that supports each claim, so it cannot hand-wave. Use a judge at least as strong as the model under test, and spot-check a sample of its verdicts against human labels to measure agreement. If it still over-scores, add a few adversarial examples to the prompt showing what a 1 and a 5 look like.
What Comes Next
You can now measure LLM quality instead of guessing at it. You built a hand grader from scratch, saw how the LLM-as-judge pattern powers RAGAS and DeepEval, scored a RAG pipeline on faithfulness, relevancy, precision, and recall, and wired faithfulness checks into a pytest-style suite. In the AI guardrails tutorial, we defend against the failure modes you just learned to detect: prompt injection, unsafe content, and the safety patterns that keep an LLM application trustworthy once real users start poking at it.
Want the bigger picture? Browse the complete Python + AI/ML tutorial series home to see where LLM evaluation fits among every other lesson, from your first script to production AI systems.
Want more? Hugging Face documentation documents everything this post could not fit.
Related Posts
Previous: LLM Cost Optimization: Tokens, Caching, and Model Tiers
Next: LLM-as-a-Judge: Online Evals, CI Gates, and Canary Sets
Series Home: Python + AI/ML Tutorial Series

No comment