Course Wiki
Built-in Judge — LLM Zoomcamp Module 5
--- video_url: 'https://www.youtube.com/watch?v=YLOLQyrMDuY&list=PL3MmuxUbc_hLZFNgSad56pDBKK8KO0XIv' --- # Built-in Judge
Related Pages
LLM Zoomcamp › Module 5: Monitoring › Built-in Judge
Notes
video_url: “https://www.youtube.com/watch?v=YLOLQyrMDuY&list=PL3MmuxUbc_hLZFNgSad56pDBKK8KO0XIv”
Built-in Judge
In the previous module we used an LLM as a judge for offline evaluation. One LLM grades the output of another. We can run the same idea online.
After each answer, we ask a judge whether it’s relevant to the question. That gives us an automatic quality signal on every response. We don’t have to wait for anyone to click thumbs up or down.
There’s one real difference from the offline setup. Back then we had ground truth: a reference answer to compare against. The judge could check our answer against that known-good one. Online we don’t have it.
The judge now only sees the question and the answer. It has to make the call on its own. That’s a harder job, so in the instructions we describe more carefully what a good answer looks like.
Adding relevance evaluation
We use structured output so the judge returns a clean label instead of free text we’d have to parse.
We need evaluation_utils from module 04.
Download it if you don’t have it:
PREFIX=https://raw.githubusercontent.com/DataTalksClub/llm-zoomcamp/main
wget ${PREFIX}/cohorts/2026/04-evaluation/code/evaluation_utils.py
Create judge.py:
import json
from pydantic import BaseModel
from typing import Literal
from openai import OpenAI
from dotenv import load_dotenv
from evaluation_utils import llm_structured_retry
class RelevanceVerdict(BaseModel):
relevance: Literal["NON_RELEVANT", "PARTLY_RELEVANT", "RELEVANT"]
explanation: str
judge_instructions = """
You are an expert evaluator for a RAG system.
Analyze the relevance of the generated answer to the given question.
Classify the answer as:
- RELEVANT: the answer addresses the question
- PARTLY_RELEVANT: the answer partially addresses the question
- NON_RELEVANT: the answer does not address the question
""".strip()
judge_prompt = """
Question: {question}
Generated Answer: {answer}
""".strip()
The explanation field matters even though we don’t always do anything
with it. Asking the judge to explain forces it to reason about the answer
before it commits to a label. That tends to make the label better.
We call the judge through llm_structured_retry, the helper from the
previous module. It’s the same as llm_structured, but it retries if
something breaks.
Once in a while a model returns JSON that doesn’t quite match the structure we asked for. It’s rare with OpenAI’s small models, maybe once or twice in my experience. It’s more common with other providers, where it might be a handful out of a thousand calls. The retry covers those cases.
The evaluation function.
Add this to judge.py:
def evaluate_relevance(question, answer, client=None):
if client is None:
client = OpenAI()
prompt = judge_prompt.format(
question=question,
answer=answer
)
result, usage = llm_structured_retry(
client,
judge_instructions,
prompt,
RelevanceVerdict,
)
return result.relevance, result.explanation
Test it:
if __name__ == "__main__":
load_dotenv()
question = "Can I still join the course?"
answer = "Yes, you can still join. The course is self-paced."
relevance, explanation = evaluate_relevance(question, answer)
print(relevance)
print(explanation)
Run it:
uv run python judge.py
This judge is deliberately basic, so handle its verdicts with care. Sometimes it’ll call a weak answer relevant, or the other way around. When you build your own, spend time on the prompt until “relevant” means relevant.
The way to get there is alignment. You collect some labels from your users, or label a sample yourself. Then you tune the judge until it agrees with them. The team at Evidently has a good talk on this on our DataTalks Club channel. Look for automated prompt optimization.
Saving judge
Key concepts
Related notes
- llmz-m01-agents
- llmz-m01-building-the-prompt
- llmz-m01-data-ingestion
- llmz-m01-environment
- llmz-m01-function-calling
- llmz-m01-introduction
- llmz-m01-other-frameworks
- llmz-m01-quick-rag-revision-optional
- llmz-m01-rag
- llmz-m01-rag-helper
- llmz-m01-search
- llmz-m01-the-agentic-loop
- llmz-m01-the-course-faq-dataset
- llmz-m01-the-llm
- llmz-m01-toyaikit
- llmz-m01-wrap-up-of-part-1
- llmz-m02-embeddings
- llmz-m02-next-steps
- llmz-m02-rag-with-vector-search
- llmz-m02-vector-search
- llmz-m02-vector-search-with-minsearch
- llmz-m02-vector-search-with-pgvector
- llmz-m02-vector-search-with-sqlitesearch
- llmz-m03-ai-agents
- llmz-m03-ai-copilot
- llmz-m03-ai-orchestration
- llmz-m03-best-practices
- llmz-m03-context-engineering
- llmz-m03-multi-agent-systems
- llmz-m03-next-steps
- llmz-m03-retrieval-augmented-generation
- llmz-m03-setting-up-kestra
- llmz-m04-agent-evaluation
- llmz-m04-evaluation
- llmz-m04-generating-ground-truth-data
- llmz-m04-generating-ground-truth-for-all-documents
- llmz-m04-generating-rag-answers
- llmz-m04-llm-as-a-judge
- llmz-m04-next-steps
- llmz-m04-rag-and-agent-evaluation
- llmz-m04-search-evaluation
- llmz-m04-search-evaluation-metrics
- llmz-m04-search-parameter-tuning
- llmz-m05-assistant
- llmz-m05-capturing-metrics
- llmz-m05-chat-app
- llmz-m05-docker-compose
- llmz-m05-feedback-dashboard
- llmz-m05-grafana-dashboards
- llmz-m05-monitoring
- llmz-m05-next-steps
- llmz-m05-querying-data
- llmz-m05-storing-data-in-postgresql
- llmz-m05-streamlit-dashboard
- llmz-m05-synthetic-data-generation
- llmz-m05-user-feedback
- llmz-m06-best-practices-for-rag
- llmz-m06-document-reranking
- llmz-m06-next-steps
- llmz-m07-chunking-for-longer-texts
- llmz-m07-content-processing-cases-and-steps
- llmz-m07-end-to-end-project-example
- llmz-m07-evaluating-rag
- llmz-m07-evaluating-retrieval
- llmz-m07-interface-and-ingestion-pipeline
- llmz-m07-monitoring-and-containerization
- llmz-m07-summary-and-closing-remarks