Why a Test Set?
A system based on a language model is not tested like a classic function: the same question can receive different phrasings, and a prompt change that improves one case can degrade ten others. Trying three questions by hand before going to production gives a misleading impression of quality.
The basis of any evaluation is a test set (also called a golden dataset): a versioned list of cases, each with an input and what is expected. For AI it plays the role that unit tests play for code.
Structure of a Case
CASES = [
{"id": "T01", "categorie": "frequent", "input": "Où est ma facture de mars ?", "expected": "facturation"},
{"id": "T02", "categorie": "limite", "input": "factur mars ???", "expected": "facturation"},
{"id": "T03", "categorie": "hors_perimetre", "input": "Quelle est la capitale du Pérou ?", "expected": "hors_sujet"},
]
A good test set:
- starts from real requests (anonymized) or very realistic ones, not only from cases imagined by the team;
- covers frequent cases, edge cases (typos, ambiguous requests), out-of-scope requests, and manipulation attempts;
- indicates each case’s category, to track results by type;
- is versioned with the code, and enriched with each incident found in production.
A few dozen well-chosen cases are worth more than hundreds of redundant ones. Avoid keeping personal data you do not need.
Checking Automatically: Normalized Matching
When the expected output is short (a category, a date, an identifier), you compare it to the expected result after normalization: lowercase, accents removed, extra whitespace removed.
import unicodedata
def normalize(text):
text = unicodedata.normalize("NFKD", text)
text = "".join(c for c in text if not unicodedata.combining(c))
return " ".join(text.lower().split()).strip(" .!")
Without normalization, “Facturation.” and “facturation” would be counted as different, and the score would reflect formatting rather than correctness.
Checking Automatically: Rules
When the answer is free text, there is no single right answer. You then check criteria:
- required elements: the “30 days” return period must appear;
- forbidden elements: no promise of a refund, no card number;
- format: maximum length, valid JSON, language.
These rules are simple, fast, and free. They do not measure everything (tone, relevance), which you will complement with a judge model in the next lesson.
Score and Failure Analysis
You compute a success rate, but above all you look at the failing cases. A score of 90% can hide 0% on out-of-scope requests if there are few of them. Hence the value of the score by category.
Regression Testing
The test set is used at every change: new prompt, new model, new quantization, new documents in the knowledge base. The process:
- run the test set on the current version and on the candidate version;
- compare the global and per-category scores;
- examine each case that used to pass and now fails;
- decide, and keep the results with the version.
Also think about variability: with a non-zero temperature, the same case can pass and then fail. Set the temperature to 0 for evaluation when possible, or run each case several times.
Key Takeaways
- A versioned, representative, continuously enriched test set is the basis of evaluation.
- Short outputs: normalized matching; free-form outputs: rules.
- You track the score by category and read the failures.
- Every change goes through the test set before production.
Exercises
Solutions and tests stay on the server. In the student space, your code is run and checked, and the tutor gives hints before the solution.
Evaluate a simulated classifier
Write
normalize(text)(accents removed, lowercase, multiple spaces reduced, spaces and.!removed at the ends), thenevaluate(model, cases)which returns a dictionary:total: number of cases;correct: number of cases wherenormalize(model(input)) == normalize(expected);accuracy:correct / total;failures: list of the failingids, in the order of the cases.
import unicodedata CASES = [ {"id": "T01", "input": "Où est ma facture de mars ?", "expected": "facturation"}, {"id": "T02", "input": "Je n'arrive plus à me connecter", "expected": "accès"}, {"id": "T03", "input": "factur mars ???", "expected": "facturation"}, {"id": "T04", "input": "Quelle est la capitale du Pérou ?", "expected": "hors_sujet"}, ] def fake_model(text): """Classifieur simulé.""" t = text.lower() if "facture" in t: return "Facturation." if "connecter" in t: return " ACCES " return "facturation" def normalize(text): ... def evaluate(model, cases): ...Check rules on a free-form answer
Write
check_answer(answer, case)which returns the list of failing criteria, in this order:"manque: <text>"for each element ofcase["must_include"]that is absent (case-insensitive comparison);"interdit: <text>"for each element ofcase["must_not_include"]that is present (case-insensitive);"trop long"if the number of words (split()) exceedscase["max_words"].
The three keys are optional. Then write
pass_rate(answers, cases): the proportion of answers with no failure.def check_answer(answer, case): ... def pass_rate(answers, cases): ...Compose the test set
Your assistant classifies customer requests. Your test set contains 200 cases, all drawn from the most frequent requests. What is the main risk?
- The test set is too big to run
- Edge cases, out-of-scope requests, and manipulation attempts are not evaluated
- The score will necessarily be too low
End-of-lesson quiz
What does normalization before comparison allow?
- To improve the model
- To avoid counting differences in case, accents, or whitespace as errors
- To reduce the cost of evaluation
When should you rerun the test set?
- Once a year
- At every change of prompt, model, parameters, or data
- Only after a user complaint
Why track the score by case category?
- To have more numbers in the report
- Because a good global score can hide a complete failure on a rare category
- Because it is legally required
The rest of the course
- Evaluation by a Judge Model20 min
- Measuring Quality, Cost, and Latency20 min
- Drift and User Feedback20 min
- Building a Monitoring Dashboard20 min