Testing & Evaluating LLM Outputs with LLM-as-a-Judge
Build robust evaluation pipelines for AI systems. Implement exact-match test suites, schema validators, and the LLM-as-a-judge pattern to grade output quality and policy compliance.

TL;DR: Unlike traditional software functions that throw explicit exceptions when they break, LLMs fail silently β returning plausible-looking text that subtly violates business policies or hallucinations. In this post, we construct a three-tier Evaluation Pyramid: exact-match classification tests, schema assertions, and an automated LLM-as-a-Judge scoring engine.
π¦ Interactive Notebook: Test and run this code live on GitHub:
04_llm_evaluation.ipynbin the ai-architect-foundation-gemini repository.
What You'll Learn
How to design automated test suites for non-deterministic AI applications, evaluate classification accuracy, validate structural constraints, and deploy a second model as an automated judge to verify policy compliance.
The Core Problem: LLMs Fail Silently
In conventional Python programming, a bug results in a TypeError, KeyError, or failed assertion. In LLM applications, a broken prompt rarely produces a 500 error. Instead:
- It invents a nonexistent discount code.
- It promises a refund before checking store policy.
- It omits an essential tracking field.
- It switches from polite customer support to defensive argument.
Without systematic evaluation suites, every prompt edit or model upgrade risks breaking production workflows without warning.
Level 1: Exact-Match Classification Testing
For deterministic extraction and routing tasks (e.g. mapping tickets to intent categories), create a dataset of representative test cases and evaluate precision automatically.
import json
from google import genai
from google.genai import types
client = genai.Client()
model = "gemini-2.5-flash"
def classify_intent(message: str) -> str:
prompt = f"""
Classify the customer's message into exactly one of these intents:
- refund_request
- order_status
- billing_dispute
- product_inquiry
- other
Customer Message: "{message}"
Respond with ONLY a JSON object: {{"intent": "<chosen_intent>"}}
"""
response = client.models.generate_content(
model=model,
contents=prompt,
config=types.GenerateContentConfig(temperature=0.0, max_output_tokens=100)
)
data = json.loads(response.text.strip())
return data["intent"]
# Test dataset
test_suite = [
{"input": "My jacket arrived with a broken zipper, I want my money back.", "expected": "refund_request"},
{"input": "Where is my package? Tracking hasn't updated in 4 days.", "expected": "order_status"},
{"input": "I noticed two identical charges of $89 on my credit card.", "expected": "billing_dispute"},
{"input": "Does this sweater come in size XL?", "expected": "product_inquiry"},
]
# Run tests
passed_count = 0
for test in test_suite:
actual = classify_intent(test["input"])
is_pass = actual == test["expected"]
if is_pass:
passed_count += 1
print(f"[{'PASS β
' if is_pass else 'FAIL β'}] Expected: {test['expected']}, Got: {actual}")
print(f"\nAccuracy: {passed_count}/{len(test_suite)} ({passed_count / len(test_suite) * 100:.1f}%)")
# --- Execution Output ---
# [PASS β
] Expected: refund_request, Got: refund_request
# [PASS β
] Expected: order_status, Got: order_status
# [PASS β
] Expected: billing_dispute, Got: billing_dispute
# [PASS β
] Expected: product_inquiry, Got: product_inquiry
#
# Accuracy: 4/4 (100.0%)Level 2: Output Schema Validation
Before passing an LLM response to downstream databases or payment gateways, validate that the payload adheres to strict structural assertions:
def validate_support_payload(data: dict) -> list[str]:
"""Validates required fields, types, and values."""
errors = []
if not isinstance(data.get("order_id"), str) or not data["order_id"].startswith("ORD-"):
errors.append("Invalid or missing order_id format (must start with 'ORD-')")
if data.get("intent") not in ["refund_request", "order_status", "billing_dispute"]:
errors.append(f"Unrecognized intent: {data.get('intent')}")
if "confidence" in data:
if not (0.0 <= data["confidence"] <= 1.0):
errors.append("Confidence score must be a float between 0.0 and 1.0")
return errorsLevel 3: The LLM-as-a-Judge Pattern
How do you test whether a response was polite, followed return policies, and did not make unauthorized financial promises?
You write a specialized judge prompt and invoke a model (often a higher-tier reasoning model like gemini-1.5-pro) to grade the primary model's generation.
Implementing the Judge Evaluator
judge_prompt = """
You are an expert QA Auditor for customer support AI systems.
Evaluate the Assistant's reply to the Customer against the following compliance rules:
Rules:
1. Tone: Must be professional, empathetic, and polite.
2. Financial Policy: Must NEVER promise or guarantee a refund before the order is verified.
3. Clarity: Must clearly ask for missing information (e.g. order number) if not supplied.
4. Escalation: Must flag severe customer anger or legal threats for human review.
Return ONLY a valid JSON object matching this schema:
{
"score": <integer from 1 to 10>,
"passed": <boolean>,
"policy_violations": ["<violation 1>", ...],
"reasoning": "<short explanation>"
}
"""
def evaluate_with_judge(customer_query: str, assistant_reply: str) -> dict:
prompt = f"Customer Query:\n{customer_query}\n\nAssistant Reply:\n{assistant_reply}"
response = client.models.generate_content(
model="gemini-1.5-pro",
contents=prompt,
config=types.GenerateContentConfig(
temperature=0.0,
system_instruction=judge_prompt
)
)
return json.loads(response.text.strip())
# Example Evaluation
bad_reply = "Sure thing! I've sent $150 back to your credit card right now."
audit_result = evaluate_with_judge(
customer_query="I don't like the shoes from order ORD-551.",
assistant_reply=bad_reply
)
print(json.dumps(audit_result, indent=2))Sample Judge Audit Output
{
"score": 3,
"passed": false,
"policy_violations": [
"Promised and processed refund without verifying return item condition or return window eligibility."
],
"reasoning": "The assistant immediately offered financial reimbursement without checking order status or initiating return logistics."
}Designing CI/CD for Prompts
In an enterprise AI engineering workflow:
- Pull Request Trigger: Any change to prompts, schemas, or models triggers the eval suite.
- Fast Unit Tests: Exact-match and schema validation run across hundreds of synthetic samples in seconds.
- Judge Suite: The LLM Judge runs on a curated set of 50+ challenging edge cases.
- Threshold Gate: If pass rate drops below 95% or average score drops below 8.5/10, the PR is blocked.
Key Takeaways
- Multi-Layered Testing: Combine fast unit tests for deterministic routing with LLM judges for subjective behavior.
- Deterministic Judges: Always run LLM judges at
temperature=0.0with structured JSON output. - Automate in CI: Treat prompts like production code with regression test suites.
What's Next
In Part 5: Structured Prompt Engineering for Enterprise Systems, we look at prompt architecture, XML tagging conventions, and boundary isolation.