LLMModule 08 · Stage 10

Evaluation-driven development & red teaming

Decide how you'll measure success before you build, check that your test questions haven't leaked into training, and attack your own model on purpose so you find its weak spots before users do.

PrerequisitesModule 01 (thresholds, calibration), Module 04's evaluation checklist, basic Python
Time20–35 h: ~8 h reading and tools, 12–25 h on the harness
HardwareLaptop for the classifier version. Colab GPU or API credits for an LLM attacker and judge
Key papersZheng et al. 2023 (LLM-as-a-judge), Perez et al. 2022, TAP (Mehrotra et al. 2023), HarmBench (Mazeika et al. 2024), GSM1k (Zhang et al. 2024)

Source: Suraj Sharma, X post, stage 10

Start here (no maths needed)

Good AI builders test their models like tough teachers: with fresh questions the model has never seen, and with tricky questions designed to catch it out.

Think of it like this

Imagine a spelling test where the teacher uses the same list every year. Older kids pass the list around, so scores go up even though nobody spells better. A good teacher writes new words each year and checks if anyone has seen the list. A great teacher also asks a friend to play "trickster" and try to fool the marking, so the teacher can fix the marking before the real test.

How it works

  1. Write down what "good" means and build a test set before changing the model.
  2. Check the test questions don't appear in the training data (that would be like seeing the answers).
  3. Keep making new test questions so old ones can't be memorised.
  4. Run a "trickster" program that keeps changing inputs to find ones your model gets wrong.
  5. Add every mistake it finds to your tests, fix the model, and test again on tricks it hasn't seen.

Why robots care

A robot that only ever practised in one kitchen may look perfect there and fail in yours. Testing it in new rooms, with odd lighting and objects in strange places, finds the problems before it drops a plate, or worse.

Try it

Words to know

Eval
A test that measures how well a model does a job.
Contamination
When test questions leak into the training data.
Red teaming
Attacking your own system on purpose to find its weak spots.
Judge
A model used to mark another model's answers.
Attack success rate
How often the attacker manages to fool the system.

Going deeper: everything below is the full version, with some maths. On a first read, skim the headings and the "Why it matters for robotics" box, and come back for the rest later.

1. What this stage covers

TL;DR

Evaluation-driven development means the eval comes first and every change is judged against it. Four skills make the evals trustworthy: contamination detection (has the model seen the test?), dynamic evals (fresh items so it can't have), adversarial robustness (does it hold up when someone searches for failures?) and judge calibration (can you trust the model doing the marking?). The post's project is an automated red-teaming agent that uses tree search to find inputs your safety classifier misses. On this page that becomes a sandboxed harness against your own classifier: attack categories, a query budget, attack success rate per category, and a loop that turns failures into training and regression data.

2. Concepts, explained

Contamination detection

A benchmark is contaminated when its items, or near-copies, are in the training data. Then the score measures memory, not skill. There are three families of check:

overlap(item, corpus, n) = |ngrams_n(item) ∩ ngrams_n(corpus)| / |ngrams_n(item)|
flag if any shared n-gram (n = 13 in GPT-3), after lowercasing and stripping punctuation
gap = acc(public benchmark) − acc(fresh mirror set)     # large gap ⇒ suspect memorisation

Dynamic evals

A static test set slowly leaks: into web crawls, into synthetic data made by other models, and into your own decisions as you tune against it. Dynamic evals keep the test fresh:

The cost: scores from different months aren't directly comparable, because the questions changed. Keep a frozen regression set alongside the fresh one.

Adversarial robustness

Average-case accuracy says little about worst-case behaviour. Adversarial evaluation asks: if someone searches for a failure, how quickly do they find one?

ASR(category) = #goals where ≥1 attack within budget B succeeds / #goals
success = judge says the target failed  AND  the attack kept the test goal's intent

ASR is only meaningful with its budget, judge and goal set stated. The same attacker can show 10% at B = 5 queries and 70% at B = 100.

LLM-as-a-judge calibration

Judges mark open-ended answers, and in red teaming they decide whether an attack "worked". Zheng et al. (2023) found strong judges agreed with human preferences over 80% of the time, about the level at which humans agree with each other. They also documented position bias (preferring the first or second answer), verbosity bias (preferring longer answers) and self-enhancement bias (preferring answers from the same model). A practical calibration routine:

  1. Hand-label 100–300 examples, stratified by category. Keep a held-out set.
  2. Measure agreement with humans: precision and recall on "failure", plus Cohen's κ. Use Module 01's cross-validated threshold tuning to pick a cut-off that meets minimum precision and recall.
  3. Check the score itself with a reliability diagram and Platt scaling (Module 01, calibration) if you'll use it as a probability.
  4. Run bias probes: swap answer order, pad an answer with filler, and swap which model wrote it. A calibrated judge's verdict shouldn't move.
  5. Re-check on attack outputs. Adversarial text is out of distribution for the judge too, so a judge tuned on normal traffic can be fooled by the same tricks as the target.

3. The practice project

Scope and safety. Attack only models and classifiers you own or are authorised to test, in a sandbox with no real users. Use test goals from your own policy. Keep generated attacks in an access-controlled store, don't publish working attacks against other people's systems, and report anything you find in third-party models through their disclosure process. This page deliberately contains no jailbreak prompts.

(a) Laptop version: red-team your own contact-details filter

Use a deliberately harmless policy so the whole loop is safe to run and share: a classifier that should flag messages containing contact details (an email address or phone number), for example a filter in a marketplace chat. Use made-up data only (example.com, 555 numbers).

  1. Target: start with a regex, then a small fine-tuned classifier (a DistilBERT-sized model trains in minutes on CPU or Colab).
  2. Seed goals: 100+ messages that must be flagged, plus 200+ benign messages that mustn't (to track false positives).
  3. Attack categories: formatting (spacing, separators), spelling-out (" at ", " dot "), character substitution (look-alike Unicode), context wrapping (burying it in a long message), and splitting across turns. Each mutation must keep the details readable to a human. That's the "intent preserved" check.
  4. Tree search: from each seed, branch width mutations, score with the target, keep the lowest-scoring children, go depth levels. Count queries.
  5. Report: ASR per category at a fixed budget, queries to first success, and the false-positive rate on the benign set.
  6. Close the loop: add successful attacks to training, retrain, then measure ASR on a held-out set of attacks from a fresh search. Freeze old successes as a regression suite.
  7. Upgrade the attacker: swap the hand-written mutators for a small local instruct model that proposes rewrites (TAP-style), and add a judge that checks the rewrite still contains the details. Calibrate that judge on 100 hand-labelled rewrites first.
import random, re

def detector(t):  # stand-in for YOUR classifier: P(contains contact details)
    return 1.0 if re.search(r"[\w.]+@[\w-]+\.\w+|\d{3}[- ]?\d{3}[- ]?\d{4}", t) else 0.0

MUT = {  # each keeps the details readable to a human
    "spell-out": lambda t: t.replace("@", " at ").replace(".com", " dot com"),
    "spacing":   lambda t: re.sub(r"(?<=\d)(?=\d)", " ", t),
    "fullwidth": lambda t: t.translate({ord(c): ord(c) + 0xFEE0 for c in "0123456789@"}),
    "wrapper":   lambda t: f"reminder for later, {t}, thanks",
}

def search(seed, depth=3, width=2, rng=None):
    frontier, q = [(seed, [])], 0
    for _ in range(depth):
        kids = []
        for t, path in frontier:
            for name in rng.sample(sorted(MUT), width):
                c = MUT[name](t); q += 1
                s = detector(c)
                if s < 0.5:
                    return path + [name], q
                kids.append((s, c, path + [name]))
        kids.sort(key=lambda k: k[0])
        frontier = [(c, p) for _, c, p in kids[:width]]
    return None, q

rng = random.Random(0)
seeds = [f"email me at user{i}@example.com" for i in range(50)] + \
        [f"call me on 555-{100 + i}-{1000 + i}" for i in range(50)]
hits = [search(s, rng=rng) for s in seeds]
print(f"ASR {sum(p is not None for p, _ in hits) / len(seeds):.0%}")

Expected outcome: the regex falls to almost every seed (this exact script prints 97%), which is the point: it shows the harness works. The interesting numbers come once you swap in a trained classifier and a model-driven attacker: per-category ASR before and after one round of adversarial training, measured on held-out attacks.

(b) Full version from the post

An attacker LLM running TAP-style tree search against your own safety classifier (for example a guard model you fine-tuned), inside a sandbox. Use a written policy with categories, a goal set per category, a calibrated judge, a fixed query budget, and a non-tree baseline at the same budget so you can show the tree search earns its cost. Frameworks help: PyRIT orchestrates attacker, target and scorer; garak runs libraries of known probes; Inspect makes the whole thing a reproducible eval. Cost: each query involves the attacker, the target and usually the judge. 200 goals × 15 target queries each (width 3, depth 5, keeping one branch per level) is 3,000 target queries per run, wider trees multiply that, and attacker and judge calls come on top, so use small open models locally or budget API spend before you start. A 7–8B attacker runs on one 24 GB GPU.

(c) Pitfalls

Interactive: what an n-gram contamination check catches

Top: a made-up benchmark item. Bottom: a training document. Highlighted words belong to an n-word sequence that appears in both, after lowercasing and removing punctuation. A GPT-3-style check flags the item if any n-gram matches (n = 13 there).

Training document:

Shared n-grams
–
Item covered
–
Longest shared run
–
Flagged?
–

Try this: at n = 13 only the verbatim copy is flagged. "Numbers changed" is the same question with new numbers: its longest shared run is 10 words, so it slips through until n ≤ 10. The paraphrase is missed at every n. That's why mirror sets and dynamic evals exist.

4. Reality check

The claim: static benchmarks are easily gamed, so you must build adversarial harnesses that continuously attack your own models.

5. In this course

6. Why it matters for robotics

7. Self-check

Why does a 13-gram overlap check miss many contaminated items?

It only catches long verbatim runs. Changed numbers break the item into short runs, and paraphrases or translations share almost no long n-grams. Mirror sets, order-based statistical tests and fresh items cover those cases.

How can you test for contamination without the training data?

Oren et al.'s exchangeability test: compare the model's likelihood of the benchmark in its canonical order with shuffled orders. A significantly higher canonical likelihood suggests the file was memorised. Or build a fresh mirror set and compare accuracy.

An attacker reports 60% ASR. What else do you need to know?

The query budget, the number and kind of goals, the judge and its measured agreement with humans, and whether successes kept the test goal's intent. Also the target's false-positive rate on benign inputs.

Name three LLM-judge biases and a probe for each.

Position bias: swap answer order. Verbosity bias: pad one answer with filler. Self-enhancement bias: swap which model wrote the answer. A calibrated judge's verdict should stay the same.

You retrain on found attacks and ASR drops from 70% to 5% on those same attacks. Done?

No. That measures memorisation of the attacks. Run a fresh search (new seeds, same budget) against the new model and report ASR on those, with the benign false-positive rate.

What does TAP add over a single attacker refining one candidate?

A tree: it branches several refinements, prunes candidates that drift off the goal before spending target queries, and keeps the most promising branches.

8. Courses & content

ResourceTypeUse it for
Hamel Husain & Shreya Shankar · AI Evals for Engineers & PMsPaid cohort courseError analysis, LLM judges and evals that drive product decisions (the authors behind Module 01)
UK AISI · InspectEval framework + docsWrite reproducible evals, including agentic ones
EleutherAI · lm-evaluation-harnessEval frameworkStandard benchmark runs
Hugging Face · Evaluation GuidebookFree guidePractical evaluation, including contamination and judges
Microsoft PyRITRed-teaming toolkitAutomated adversarial testing of your own GenAI system
NVIDIA garakLLM vulnerability scannerProbe your own model for known failure categories

Start with the free Hugging Face Evaluation Guidebook for contamination and judges, then build the harness in Inspect. Add PyRIT when you get to the red-teaming loop.

Full course map for all 12 stages →

9. Papers & docs