Prompt Testing & Evaluation

Ma Mahalakshmi V Updated 19 Sep 2026
8 min read ·Lesson 152 of 224

Testing Prompts Against Representative Datasets

Unit 13 introduced how to build and grade an eval: assembling a dataset of representative inputs, defining a grading method, and running a model against the dataset to produce a score. This lesson applies that methodology to a specific, recurring engineering task raised repeatedly throughout this unit — deciding whether a prompt change (a new version, per Lesson 8; a tightened output requirement, per Lesson 7; a different set of few-shot examples, per Lesson 4) actually improved behavior, rather than guessing from a handful of manual spot checks.

Why Manual Spot Checks Are Not Enough

The natural way to check whether a prompt edit helped is to try it on two or three examples and read the output. This is a reasonable first pass during active editing, but it does not scale to a reliable engineering decision, for a specific reason: prompt changes rarely affect every input uniformly. A change that fixes a formatting issue on typical inputs might introduce a regression only on inputs with unusual structure, and a handful of manually chosen test cases — usually the easy, typical ones that come to mind first — are exactly the cases least likely to reveal that kind of regression. A representative dataset, run systematically, is what surfaces it.

Building a Representative Dataset

A dataset for prompt testing is a collection of realistic inputs paired with either an expected output or a way to grade the actual output's quality. It should reflect the diversity of real usage, not just the typical case:

from dataclasses import dataclass

@dataclass(frozen=True)
class TestCase:
    input_text: str
    expected_category: str

TICKET_TEST_CASES: list[TestCase] = [
    TestCase("I was charged twice this month.", "billing"),
    TestCase("The app crashes every time I upload a photo.", "technical"),
    TestCase("How do I change my account email?", "account"),
    TestCase("Can you recommend a good restaurant nearby?", "other"),
    TestCase("", "other"),  # edge case: empty input
    TestCase("My invoice shows a charge but the app also won't load.", "billing"),  # ambiguous, multi-topic
]

Three properties make this dataset useful rather than decorative. First, it includes an edge case (empty input) that a developer testing informally would likely never think to try, but that real production traffic will eventually send. Second, it includes an intentionally ambiguous case (a ticket that touches both billing and a technical symptom) precisely because ambiguous cases are where prompt changes most often cause visible behavior shifts — a change to the category list or the instructions wording can flip how these borderline cases resolve, even while easy cases stay stable. Third, each case's expected_category is a genuine judgment call the dataset's author made deliberately, which is itself worth documenting — for the ambiguous case above, "billing" was chosen because the invoice complaint was mentioned first and is the more actionable-sounding half.

Note: Sourcing test cases from real, anonymized production inputs (with appropriate handling of any sensitive data) generally produces a more representative dataset than inventing cases from imagination, for the same reason noted in Lesson 4 about few-shot examples — real usage patterns are harder to guess correctly than they are to observe.

Running a Prompt Against the Dataset

With a dataset defined, running the current prompt version against every case and grading the results follows directly from Unit 13's eval structure:

from openai import OpenAI

client = OpenAI()

def classify_ticket(ticket_text: str, instructions: str) -> str:
    response = client.responses.create(
        model="gpt-5.6-terra",
        instructions=instructions,
        input=ticket_text if ticket_text else "(empty message)",
    )
    return response.output_text.strip().lower()

def run_eval(test_cases: list[TestCase], instructions: str) -> dict:
    results = []
    correct = 0
    for case in test_cases:
        predicted = classify_ticket(case.input_text, instructions)
        is_correct = predicted == case.expected_category
        correct += is_correct
        results.append({
            "input": case.input_text,
            "expected": case.expected_category,
            "predicted": predicted,
            "correct": is_correct,
        })
    return {
        "accuracy": correct / len(test_cases),
        "results": results,
    }

run_eval returns both a single summary number (accuracy) and the per-case detail (results). The summary number is what you track over time and compare across prompt versions; the per-case detail is what you actually read when accuracy drops, because a single number cannot tell you which cases regressed or why — only the individual results can.

Comparing Two Prompt Versions

The direct application of this to Lesson 8's versioning: run the same dataset against both the current and candidate prompt versions, and compare accuracy and, critically, which specific cases changed:

def compare_prompt_versions(test_cases: list[TestCase], old_instructions: str, new_instructions: str) -> dict:
    old_eval = run_eval(test_cases, old_instructions)
    new_eval = run_eval(test_cases, new_instructions)

    regressions = []
    improvements = []
    for old_r, new_r in zip(old_eval["results"], new_eval["results"]):
        if old_r["correct"] and not new_r["correct"]:
            regressions.append(new_r)
        elif not old_r["correct"] and new_r["correct"]:
            improvements.append(new_r)

    return {
        "old_accuracy": old_eval["accuracy"],
        "new_accuracy": new_eval["accuracy"],
        "regressions": regressions,
        "improvements": improvements,
    }
OLD_INSTRUCTIONS = "Classify the ticket into billing, technical, account, or other. Respond with only the category."
NEW_INSTRUCTIONS = (
    "Classify the support ticket into exactly one of: billing, technical, "
    "account, other. If the ticket mentions multiple topics, choose the "
    "one that seems most urgent or actionable. Respond with only the "
    "category name, lowercase, no punctuation."
)

comparison = compare_prompt_versions(TICKET_TEST_CASES, OLD_INSTRUCTIONS, NEW_INSTRUCTIONS)
print(f"Old accuracy: {comparison['old_accuracy']:.2f}")
print(f"New accuracy: {comparison['new_accuracy']:.2f}")
print(f"Regressions: {len(comparison['regressions'])}")
print(f"Improvements: {len(comparison['improvements'])}")

The regressions and improvements lists are more decision-relevant than the two accuracy numbers alone. A new prompt version that raises overall accuracy from 0.80 to 0.83 sounds like an unambiguous win until you check regressions and find that it broke a case your team considers especially important — an accuracy improvement that trades away correctness on a high-value case is not automatically a good trade, and only inspecting the per-case detail reveals that tradeoff exists at all.

Grading Tasks Without an Exact Expected Output

Classification has a clean notion of "correct" — an exact string match. Summarization and transformation (Lesson 6) do not, since there is no single correct summary to match against. For these, grading needs either a rubric checked by a separate model call (a technique Unit 13 refers to as model-graded evaluation) or a set of mechanical property checks, as introduced in Lesson 7's check_output_shape:

@dataclass(frozen=True)
class SummaryTestCase:
    document: str
    max_sentences: int

SUMMARY_TEST_CASES = [
    SummaryTestCase("Long article text about a product launch...", 3),
    SummaryTestCase("Another article about a company earnings report...", 3),
]

def evaluate_summary_properties(summary: str, max_sentences: int) -> dict:
    sentence_count = summary.count(".") + summary.count("!") + summary.count("?")
    return {
        "within_length": sentence_count <= max_sentences,
        "non_empty": len(summary.strip()) > 0,
        "sentence_count": sentence_count,
    }

This does not verify that the summary is a good summary — that still requires either human review or a model-graded rubric, both covered in Unit 13 — but it verifies the mechanical properties (length, non-emptiness) cheaply and deterministically across every case in the dataset, catching a meaningful class of regressions (a prompt change that starts producing five-sentence summaries instead of three) without needing a second model call per case.

Building a Regression Test Suite From Prior Failures

A dataset should grow over time, specifically by adding every real failure encountered in production as a new permanent test case, so the same mistake cannot silently reappear in a future prompt version without being caught:

def add_regression_case(test_cases: list[TestCase], input_text: str, correct_category: str) -> list[TestCase]:
    return test_cases + [TestCase(input_text, correct_category)]
# A real misclassification found in production, on 2027-02-10:
# ticket was routed to "other" but should have been "technical"
TICKET_TEST_CASES = add_regression_case(
    TICKET_TEST_CASES,
    "Nothing happens when I click the export button.",
    "technical",
)

This is the same principle as regression testing in ordinary software: a bug found in production becomes a permanent test case precisely because prompt behavior is not guaranteed to stay stable across future edits, few-shot example changes, or even model version upgrades — the case that broke once can break again in a future change unless something is actively checking for it every time.

Testing the Evaluation Logic Itself

The comparison and grading functions are themselves plain Python and should be tested directly, using fake predictions rather than live model calls, to make sure the evaluation harness is trustworthy before relying on its verdicts:

def test_run_eval_computes_accuracy_correctly():
    def fake_classify(ticket_text: str, instructions: str) -> str:
        return "billing" if "charged" in ticket_text else "other"

    cases = [
        TestCase("I was charged twice.", "billing"),
        TestCase("Tell me a joke.", "other"),
        TestCase("I was charged incorrectly.", "technical"),  # will mismatch
    ]
    correct = sum(1 for c in cases if fake_classify(c.input_text, "") == c.expected_category)
    accuracy = correct / len(cases)
    assert abs(accuracy - (2 / 3)) < 1e-9
    print("PASS: accuracy computed correctly against known fake predictions")

def test_compare_detects_regression():
    old_results = [{"correct": True}, {"correct": True}]
    new_results = [{"correct": True}, {"correct": False}]
    regressions = [n for o, n in zip(old_results, new_results) if o["correct"] and not n["correct"]]
    assert len(regressions) == 1
    print("PASS: comparison correctly identifies a single regression")

test_run_eval_computes_accuracy_correctly()
test_compare_detects_regression()

Common Mistakes

Testing a prompt change against only a few manually chosen, typical examples. This reliably misses regressions on edge cases and ambiguous inputs, which is exactly where prompt wording changes tend to shift behavior the most.

Comparing only aggregate accuracy between prompt versions, without inspecting per-case regressions. An improved overall score can mask the loss of a specific, high-value case; always check which individual cases flipped, not just the summary number.

Never adding production failures back into the test dataset. Without this feedback loop, the same mistake can resurface silently in a later prompt version, because nothing in the test suite would catch it a second time.

Best Practices

Build the test dataset to include edge cases and ambiguous inputs deliberately, not just typical ones. These are the cases most likely to reveal a regression when a prompt changes.

Compare prompt versions on the same fixed dataset and inspect both aggregate accuracy and per-case regressions before rolling out a change. Follow the gradual rollout approach from Lesson 8 once the comparison looks favorable.

Turn every real production failure into a permanent regression test case. This is the mechanism that keeps a growing prompt codebase from repeating its own past mistakes as it evolves.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Prompt Testing & Evaluation and get answers drawn from it.

Signed-in readers only.