Skip to main content
If you train the model on the same tasks you test it on, the score goes up and means nothing. It is handing out the exam a week early. So before any training, you split the rows. One part trains. The other part is locked away, and the only number you ever report comes from it.

The mechanism

The locked part is the held-out set. Three rules for it.
  1. Never train on it.
  2. Never use it to pick between models while you are still building. Every peek makes it a little less held out.
  3. Measure before and after on it, and nowhere else.
Simulated asks repeat, and production traffic repeats too, so a training row can be a near copy of a test row without anyone meaning it to. The check for that is decontamination: compare every training row’s prompt against every test row’s, and drop the training row when they overlap. The library counts shared runs of eight words and drops a row when 80% of its runs also appear in a test row.

Run it

The setup is lesson 2’s run and lesson 3’s judge. The split here is by position, which is fine for a demo only because the run is reproducible (the default without a clock): with reproducible=False rows land in completion order, and a slow rollout can put one ask’s four tries on both sides of the cut. For real rows, split by task so all four tries of an ask land on the same side. Two rows are planted in the training set, both carrying a task id from another team, the ordinary case when rows arrive from a vendor or a Hub set. One is a copy of a held-out ask. The other asks the same thing in different words.
Two planted rows, one dropped. The copy was caught by the exact rule, the one input the check catches every time. The paraphrase asks the same question about the same order and shares no run of eight words with it, so it stayed in the training set, and a model trained on kept would have seen the held-out ask. Word overlap does not see a paraphrase. Three things follow. The ids matter: had the copy kept its own task id, the same_task rule would have caught it first, and that rule needs a scenario_id or task_id on both sides; an eval set that carries none (a public benchmark, logged traces) leaves only the text rules, and the report says so in rules_skipped and notes rather than printing n_same_task: 0 as if the ids had been compared. The rows the check drops are rows that would have made the after score lie, and every one of them is worth dropping; the rows it cannot see are on you. And there is a second pass for what it cannot see: embedder= turns on the semantic rule, which compares meaning rather than words. It needs an embedding model, so it is off by default, and the one-line call is:
A semantic hit is a question to check, not a verdict: two asks about different orders can read alike. kept still carries the run’s system prompt and tools, so the next lesson’s select(kept, mode="sft").export(...) writes both.
Public benchmarks have the same problem at scale: their questions are on the internet, so they are in the pretraining data. That is one reason a test built from your own traffic, checked for overlap, says more about your agent than a leaderboard does.

Where it comes from

  1. Lambert, N. Reinforcement Learning from Human Feedback. arXiv:2504.12501, 2025. Chapter Evaluation: contamination, and why held-out sets decay.
  2. Touvron, H. et al. Llama 2: Open Foundation and Fine-Tuned Chat Models. arXiv:2307.09288, 2023. The 8-gram, 80% coverage rule.
  3. Miller, E. Adding Error Bars to Evals. arXiv:2411.00640, 2024. The before-and-after on the same questions is a paired test.

Next

Train on what the model gets right sometimes: which of the training rows are worth a gradient.
Last modified on September 21, 2026