ESSAY / AI Evaluation & Safety / Detection & Control Validation

A Recipe for Shipping AI Guardrails (without experimenting on your users)

A guardrail can be completely broken while the app looks completely fine; the bad output slips through, nothing crashes, the dashboard stays green. Here is a recipe for measuring a detection-and-control protocol the way you would measure a model, by baselining the unguarded behavior, naming the threat vectors, and proving the catch rate and the over-refusal rate against golden, adversarial, and shadow-replay sets, all without running the experiment on live users.

Guardrails fail silently

Guardrails are a tricky thing to monitor and assess, because a guardrail can break completely while it simultaneously looks like it isn’t just functioning well, it is doing an awesome job; a team bolts a safety filter onto their LLM app to block the toxic outputs, or the medical advice, or the “sure, here is my read on whether you should hire this person” judgments that are illegal to make, they ship it, and then nobody can say whether it actually works, which is a strange place to land when the whole reason the thing exists is to reliably not do one specific bad thing. The mechanism is straightforward; a guardrail can be fully broken while the app looks fine, because the bad output slips through two percent of the time and nobody notices, the other ninety-eight percent is great, nothing throws an exception, and the dashboard stays a calm, reassuring green. A working safety control and a broken one produce the same demo, so “it seems fine” is just how a guardrail that does not work looks from the dashboard, and it can’t be your evidence for anything. The job, then, is to measure, and to measure without ever running the experiment on your users; the clean version of that experiment, the one where you turn the filter off for half of them and watch who gets hurt, is the one I tend to rule out, since letting a harm reach real people in order to count it is a strange thing to schedule on purpose, so the whole craft becomes recovering the numbers that experiment would have given you, without the experiment.

There is a second trap that sits next to the first one, which is firing rate; if a guardrail is firing all the time, that makes it look like your model has gone rogue and almost everything it is saying is awful and the guardrail is the only thing left to “hold the door” (you are welcome to name this kind of guardrail “Hodor,” I do). The problem with all of this is that the rate at which a guardrail fires tells you very little about whether it is doing a good job or a bad job, so that is where this article comes in; it is all about how you define “good” and “bad” and actually measure it in production, and what that means not just academically but operationally.

What follows is a recipe in four steps, and the steps are on the plain side by design, since a plain step is one that tends to give the same answer when you run it again next quarter.

Step one, become one with your traffic

Before anything gets built, it helps to measure how often the bad behavior already happens in the logs you already have; the move is to pull a sample of the conversations the system has actually had and label each one for the behavior you care about (a language model acting as judge will do this at a scale hand-labeling cannot reach), and report a rate with a confidence interval instead of an anecdote, because that rate is the denominator every later claim about your filter will divide by. There is a trick worth knowing, which is to stratify the sample toward the slice where the behavior is actually likely, the sessions that already reached for the records lookup, or the printenv, or whatever your particular version of danger looks like, since a uniform sample of mostly-normal traffic will spend its whole labeling budget proving that normal traffic is normal. When this step gets skipped, a specific and entirely understandable failure tends to follow, the one where a careful filter ships, violations come back at a tenth of a percent, the team celebrates, and nobody ever learned that the rate was a tenth of a percent before the filter existed too; the filter changed nothing, and that is an easy mistake to walk into. There is a lot to be said for measuring the thing before you touch it.

Step two, write the attacks down

A defense tends to cover only the attacks that were actually named, and a defense assembled from intuition covers the attacks that happened to come to mind, which is a narrower set than it usually feels like in the moment. So it helps to actually write the list. Most of the surface is covered by five families:

On top of all five sits a mutation layer that exists only to beat keyword matching, the base64 and unicode encodings, and the low-resource-language trick of asking in Zulu or Scots Gaelic for the thing that would be refused in English; the catalog is what does the work here, since a red team can only report coverage against the vectors that someone wrote down, so the list is the difference between a coverage number and a feeling.

Step three, put the controls where they can act

A control can only stop a harm that has not happened yet, so the useful way to organize a defense is by the moment each control runs. At the perimeter, a blunt input-size cap and a session-velocity counter remove a whole class of attack for almost nothing, since a jailbreak needs hundreds of words to build its frame while a real request seldom runs past twenty; at the input gate, a fast intent classifier routes the request before any sensitive resource is touched; and before the model acts at all, an authorization step decides whether the use is permitted and fails closed, which is the reason it belongs ahead of generation, since you cannot un-emit a token once the model has produced it, and every filter downstream of that point is really just writing an incident report. After a candidate response exists, an output scanner runs a cheap keyword tripwire alongside a slower entailment check that asks whether the draft actually crosses the line the policy draws.

The one design decision I will defend at length is the whitelist, not the blacklist. A blacklist of forbidden phrasings is a game of whack-a-mole that automated fuzzing wins something like ninety-nine percent of the time, because the ways to phrase a prohibited result are effectively unbounded, whereas enumerating the permitted uses and refusing everything outside that set converts an open-ended problem into a closed one that a red team can actually finish covering. A whitelist will, of course, occasionally refuse a legitimate request that nobody thought to enumerate, and the answer to that is a graceful escalation path for the person on the other end; a silent wall is the kind of bug worth fixing, and there is a lot to be said for having the path in place before the wall ships.

Step four, prove it offline, before a user ever meets it

A control’s effectiveness is two numbers, and both of them tend to matter, the fraction of real attacks it catches, and the fraction of legitimate requests it wrongly refuses; the temptation is to fixate on the first number and ship something that blocks half of everything, which is also a failure, just a slower and more annoying one. Both numbers can be recovered before a single user ever meets the control, from three sets you build yourself:

When a model does the grading on these runs, it helps to give the judge a different model family than the system under test, since models lean generous toward outputs that resemble their own, and it is worth freezing the whole pipeline once judge-to-human agreement slips below a substantial bar, somewhere around a Cohen’s kappa of 0.75, because a judge that has not been checked against people is just a second opinion you also cannot trust. The numbers these three sets return are how you decide you have reached a release threshold; a week in production with no incident is merely the absence of evidence, whereas these metrics let you actually judge and measure what is happening before you deploy.

Why not just ship it and watch

There is a real reason to keep all of this offline, beyond convenience. When the failure you are testing for is a harmful output, an online test that splits traffic into a guarded arm and an unguarded arm is, by construction, a decision to let the harm reach some fraction of real people so that you can count it, and the offline sets give you the same three numbers (catch rate, refusal rate, and in-the-wild coverage) without nominating anyone to be the test case. Measuring on history and on synthetic adversarial data also keeps the audit itself from becoming a second harm, since it reads aggregate and structural signals off the past instead of poking live users to see what breaks. The one limitation here is that your golden and adversarial sets contain only the attacks somebody thought to add, and that is the exact reason the recipe leans on step one and the shadow replay, since they are the parts of the method that listen to what your traffic actually did rather than to what you predicted it would do. A protocol built this way can still be surprised by something new, and eventually it will be; what it gives you in return is a measured floor under its catch rate and a measured ceiling over its refusals, which is a great deal more than a control trusted on faith has ever been able to offer.

For all the families and sets and thresholds, the method underneath stays small; it looks at the data it already has, fixes a baseline, leans toward enumerating what is permitted, and holds the result up against the adversarial set before trusting it. A guardrail measured that way is the only kind that has ever earned the green dashboard it shows you.

References

  • Perez, E., et al. (2022). Red teaming language models with language models. arXiv:2202.03286.
  • Chao, P., et al. (2024). JailbreakBench: An open robustness benchmark for jailbreaking large language models. arXiv:2404.01318.
  • Röttger, P., et al. (2024). XSTest: A test suite for identifying exaggerated safety behaviors in large language models. arXiv:2308.01263.
  • Zheng, L., et al. (2024). Judging LLM-as-a-judge with MT-Bench and Chatbot Arena. arXiv:2306.05685.
  • Yong, Z. X., Menghini, C., & Bach, S. H. (2023). Low-resource languages jailbreak GPT-4. arXiv:2310.02446.
  • Cohen, J. (1960). A coefficient of agreement for nominal scales. Educational and Psychological Measurement, 20(1), 37-46.

Related Works

Next Publication
Loading...