Curating a High-Quality Instruction Dataset
5 min read · updated August 3, 2026
“A thousand good examples beat fifty thousand mediocre ones” is repeated everywhere and attributed to nobody. It comes from specific experiments with specific setups, and knowing what they compared tells you when the advice transfers.
The results this rests on
| Study | Description |
|---|---|
| LIMA | Zhou et al., 2023 (arXiv 2305.11206), 'LIMA: Less Is More for Alignment'. Fine-tuned a 65B pretrained model on 1,000 carefully curated prompt–response pairs, with no reinforcement learning from human feedback at all, and reported responses competitive with far more heavily tuned systems. Their framing — the Superficial Alignment Hypothesis — is that instruction tuning mostly teaches format and style, while knowledge comes from pretraining. |
| AlpaGasus | Chen et al., 2023 (arXiv 2307.08701). Took Alpaca's 52,000 Self-Instruct examples, scored each one with a model against a quality rubric, kept roughly 9,000, and reported that the model trained on the filtered subset outperformed the one trained on the full set. Same generator, same seeds, same recipe — the only variable was the filter. |
| textbook-quality curation | Gunasekar et al., 2023, 'Textbooks Are All You Need' (arXiv 2306.11644), and the phi model series that followed. A small model trained on a modest volume of filtered and synthesised 'textbook-quality' material reported coding results far above what its parameter count and token budget would suggest. Applies to pretraining data rather than instruction data, and makes the same argument about selection. |
| Self-Instruct's own filter | Wang et al., 2022 (arXiv 2212.10560). Worth counting here because the pipeline everyone copies already contained a filter: new instructions were rejected when their ROUGE-L overlap with existing ones was too high. Diversity enforcement was part of the original recipe, not an addition to it. |
Read together, the useful claim is narrower and more reliable than the slogan: for teaching format, style and task framing, a small curated set is competitive with a large uncurated one, and filtering an existing set can improve it. The AlpaGasus result is the most directly actionable of the four, because filtering a set you already have is a cheap experiment with a published precedent.
Why fewer rows can win
It is not mysterious once you accept LIMA’s framing. If instruction tuning is largely teaching the model which of its existing behaviours to surface, then each example is a demonstration, and demonstrations are only useful if they demonstrate the right thing. A mediocre example is not neutral:
- A wrong answer teaches the wrong answer. Training does not know the row was a mistake. It fits it with the same enthusiasm as the good rows.
- A hedge teaches hedging. If a fifth of your responses open by restating the question and end with a caveat, you are training a verbal tic that will appear in every answer the model produces afterwards.
- A truncated or malformed response teaches truncation. Format is exactly the thing instruction tuning transfers most reliably, so format errors transfer most reliably too.
- A duplicate multiplies whatever it teaches. Near-duplicates concentrate mass on one pattern and are the easiest defect in the list to remove.
The corollary that governs practice: the marginal value of one more row is small and the marginal damage of one bad row is not, so the expected value of a filter is positive well before it is precise.
What “quality” means concretely
“High quality” is unusable as an instruction to a filter. Decomposed into properties you can test for, it becomes six things:
- Correct. The response answers the instruction and is factually right. The only property that requires either a verifier or a human.
- Complete. It does the whole task, including the part of the instruction that came last — the most commonly dropped one.
- Well formed. Valid JSON where JSON was asked for, correct language, no truncation, no scaffolding leaked from the generation prompt.
- Appropriately scoped. Not a three-paragraph essay in answer to a yes/no question, and not one word in answer to “explain”. Length mismatch is the most common quality defect in generated instruction data and the easiest to detect.
- Non-degenerate. No repetition loops, no self-reference to being an AI assistant, no refusal for a request that was perfectly reasonable.
- Distinct. Not a near-duplicate of another row.
Five of those six are checkable with code. Only the first needs judgement — which is exactly the right order to build the stack in.
One property deliberately absent from that list is length. Long responses are not better responses, and a scorer left to its own devices will rank them as though they were: verbosity correlates with apparent thoroughness in almost every rubric anyone writes quickly. If you filter on quality without controlling for length, you will produce a dataset of long answers and a model that cannot give a short one, which is a regression your evaluation is unlikely to catch because the long answers are also correct. Check the length distribution of the survivors against the length distribution of the input, and if the filter moved it, the filter is measuring the wrong thing.
The filter stack
Run in this order. Each stage is cheaper than the one below it, so every row killed early is money saved later.
import re, hashlib
from collections import Counter
BOILERPLATE = re.compile(
r"^(sure[,!]|certainly[,!]|of course[,!]|as an ai|i'm sorry, but)", re.I)
def stage1_structural(rows):
"""Free. Typically removes a surprising fraction of a generated set."""
keep = []
for r in rows:
resp, instr = r["response"].strip(), r["instruction"].strip()
if not resp or not instr: continue
if len(resp) < 10 or len(resp) > 8000: continue
if BOILERPLATE.match(resp): continue
if resp.rstrip().endswith((",", "and", "the")): continue # truncated
if r.get("format") == "json" and not valid_json(resp): continue
keep.append(r)
return keep
def stage2_dedupe(rows, threshold=0.85):
"""Exact hash, then near-duplicate by MinHash over 5-grams."""
seen, index, keep = set(), MinHashIndex(threshold=threshold), []
for r in rows:
h = hashlib.sha256((r["instruction"] + r["response"]).encode()).hexdigest()
if h in seen: continue
if index.query(r["instruction"]): continue # near-dup instruction
seen.add(h); index.add(r["instruction"]); keep.append(r)
return keep
def stage3_shape(rows):
"""Length mismatch: short question, essay answer, or the reverse."""
keep = []
for r in rows:
ratio = len(r["response"].split()) / max(len(r["instruction"].split()), 1)
asks_explain = bool(re.search(r"\b(explain|why|how)\b", r["instruction"], re.I))
if ratio > 60 and not asks_explain: continue
if ratio < 0.3 and asks_explain: continue
keep.append(r)
return keep
def stage4_score(rows, keep_fraction=0.5):
"""The AlpaGasus move: rubric-score every survivor, keep the top slice.
Use a DIFFERENT model than the generator, and treat the score as a rank."""
scored = [(rubric_score(r), r) for r in rows]
scored.sort(key=lambda t: t[0], reverse=True)
return [r for _, r in scored[: int(len(scored) * keep_fraction)]]One thing the code deliberately does not do is pick a numeric quality threshold. Scores from a rubric model are not comparable across models, across rubrics or across versions of the same model, so a fixed cutoff of “keep everything above 7” will silently keep a different fraction next month. Keeping a fraction is stable; keeping a threshold is not.
Coverage is the constraint filtering breaks
Filtering hard has one predictable side effect and it is worth designing around. Quality scores correlate with task type: short extraction answers score lower than well-structured explanations, terse-but-correct scores lower than verbose-and-fluent, and answers in less common languages score lower for reasons that have nothing to do with their correctness. Filter to the top half globally and you can delete an entire capability from the dataset without any single decision looking wrong.
The fix is mechanical: filter within buckets, not globally. Keep the top half of each task type, each language and each difficulty level separately, so the composition of the set is a decision you made rather than an artefact of the scorer’s preferences.
Then check the composition before you train. A histogram of task type, input length, response length and language across the final set takes two minutes and catches the failure that the filter stack cannot see: that after all that work, 70% of the surviving rows are the same kind of easy question. Diversity checks belong at the end of the filter stack, not only at the start of the generator.