Back to Blog
This post is Part 4 of 13 in the series: AI Architect Foundations

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.

Share Editorial
Testing & Evaluating LLM Outputs with LLM-as-a-Judge

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.ipynb in 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.

diagram
Rendering diagram…

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.

python
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:

python
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 errors

Level 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.

diagram
Rendering diagram…

Implementing the Judge Evaluator

python
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

json
{
  "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:

  1. Pull Request Trigger: Any change to prompts, schemas, or models triggers the eval suite.
  2. Fast Unit Tests: Exact-match and schema validation run across hundreds of synthetic samples in seconds.
  3. Judge Suite: The LLM Judge runs on a curated set of 50+ challenging edge cases.
  4. Threshold Gate: If pass rate drops below 95% or average score drops below 8.5/10, the PR is blocked.

Key Takeaways

  1. Multi-Layered Testing: Combine fast unit tests for deterministic routing with LLM judges for subjective behavior.
  2. Deterministic Judges: Always run LLM judges at temperature=0.0 with structured JSON output.
  3. 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.