Build the router that turns a pile of flagged outputs into clean labels — sorting true violations from over-refusals and sending the hard ones where they belong.
Day 53 of 66
You have a taxonomy (Day 51) and you understand the helpful/harmless tension (Day 52). Today you make them operate. A flagged-output queue is never uniform: some items are clear violations, some are over-refusals masquerading as violations, some are genuinely ambiguous, and a few are severe enough that no single annotator should rule on them alone. Triage is the routing logic that sorts them — and it's the difference between a safety queue that runs and one that drowns.
The full safety_triage.py lives in the "Try This" box below (injected for build days). Type it in rather than pasting; the point is to read every branch and ask "is this where I'd route that item?" The thinking is the exercise — the syntax is trivial.
Every flagged item exits triage as exactly one of these. If your router can't produce all four, it's not safe to run.
| Outcome | When | Why it matters |
|---|---|---|
| Label directly | Clear category, confident | The throughput case — most items, fast. |
| Over-refusal | Benign item the model refused | Captured as preference data, not discarded (Day 52). |
| Adjudicate | Ambiguous / low confidence | Goes to consensus, not a guess — disagreement is data (Week 3). |
| Escalate | Highest-severity categories | Senior review + annotator support; never a solo call (Day 51). |
Triage rules are checked top to bottom, so severity must be checked before confidence. A high-severity item that happens to be low-confidence should still escalate — not silently fall into the adjudication bucket. Get the order wrong and your most dangerous items take your least careful path. Read the branch order in the code and confirm it escalates first.
The triage step leans on a confidence score (from a model classifier, a first-pass annotator, or both). The threshold you set is a real decision with real costs.
Set the confidence cutoff high (say, only auto-label above 0.9) and you route lots of items to adjudication — cleaner labels, but a heavier, slower, more expensive human queue. Set it low (auto-label above 0.5) and you save human effort but let borderline calls through as automatic labels — cheaper, noisier. There's no universal right answer; the right cutoff depends on the cost of a wrong label in that category. High-severity categories get a stricter threshold because the cost of being wrong is higher.
A common rookie error is to treat "low confidence" and "high severity" as the same dial. They aren't. Severity is how bad it is if true; confidence is how sure you are it's true. A confident low-severity item labels directly. An unsure high-severity item escalates. The triage logic has to read both axes independently.
The code routes a severe item to "escalate," but a label string isn't a process. Behind that branch sits a standard operating procedure a lead writes once and the team follows every time.
Run safety_triage.py from the Try This box on the sample batch, then extend it with what you learned today.
Then test on five real flagged items from your own work (scrub anything sensitive first). Did any route to "adjudicate" that you'd have silently guessed on before? That's the queue catching a guess — exactly its job.
An annotator works items as they come. A lead designs the router that decides which path each item takes — and gets the branch order right so severity is checked before confidence, because a router that lets a high-severity item fall into adjudication sends your most dangerous data down your least careful path.
Say this in an interview: "My triage logic checks severity before confidence — they're different axes and collapsing them is how a dangerous item slips through. And 'escalate' isn't a label, it's an SOP: who reviews, how fast, what support, and what feeds back into the taxonomy."