Guardrails vs evaluators: what runs inline and what runs later
Guardrails block clear failures before users see them, and evaluators measure quality after a response is made. Guardrails run inside the request with a tight latency budget, so they're fast fixed rules: regexes, block lists, schema checks and small classifiers. Evaluators run afterwards, often in batches, so they can use slower methods like LLM judges, and their results feed dashboards and your improvement loop instead of blocking the answer.
Two jobs
The evals FAQ puts the difference simply: guardrails block clear failures before users see them, and evaluators measure quality after a response is made.
| Guardrail | Evaluator | |
|---|---|---|
| When | Inside the request | After the response |
| Latency budget | Typically a few milliseconds | Seconds to minutes |
| Methods | Regex, block lists, schema checks, small classifiers | LLM judges, code checks, human review |
| Catches | Clear, high-impact failures | Correctness, completeness, policy |
| Action | Redact, refuse, regenerate | Score, alert, add to test set |
| Runs on | Every request | Every request or a sample |
Guardrails
Guardrails run before the answer reaches the user, on the input, the output or both. The FAQ describes their latency budget as typically a few milliseconds, and your own latency target sets the exact number.
Because of that budget, guardrails are fast, fixed rules that catch clear failures with high impact:
- Personal data in the output (card numbers, phone numbers, ID numbers).
- Profanity or content from a block list.
- Broken JSON or missing required fields.
- SQL injection patterns or dangerous code in generated output.
import re
RULES = {
"card_number": re.compile(r"\b(?:\d[ -]?){12,15}\d\b"),
"email": re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+"),
"dangerous_call": re.compile(r"\b(os\.remove|shutil\.rmtree|eval|exec)\s*\("),
}
def output_guardrail(text: str) -> tuple[str, list[str]]:
fired = []
for name, pattern in RULES.items():
if pattern.search(text):
fired.append(name)
text = pattern.sub(f"[{name} removed]", text)
return text, fired
safe, fired = output_guardrail("Charge 4111 1111 1111 1111 and email ravi@example.com")
print(safe)
print("fired:", fired)
When a guardrail fires, it redacts, refuses or regenerates. Every false alarm hits a real user, so version your rules, log every trigger with the trace ID, and watch the trigger rate. A sudden jump usually means a rule broke or the traffic changed.
Evaluators
Evaluators run after the response, on every request or a sample, often in batches. They can afford slower methods like an LLM judge, because nobody is waiting.
They check things a rule can't:
- Is the answer correct?
- Is it complete?
- Does it follow the policy?
- Did it hand off when it should have?
Evaluator results feed dashboards, regression tests and your improvement loop. They don't block the answer.
Cascades: when a judge needs to run inline
Sometimes a failure is serious enough that you want a judge before the user sees the answer, for example an explanation of a refund policy. An LLM judge can run inline only if latency allows.
The sensible pattern is a cascade:
- A cheap check handles most traffic.
- Only borderline or high-risk cases go to a slower judge.
- If the judge fails the answer, regenerate or hand off to a person.
The guardrail sandwich pattern shows where these checks sit in the architecture.
Read ready-made guardrails before using them
Don't use ready-made LLM guardrails without reading their prompts and rules. You'd be putting someone else's idea of "unsafe" in front of your users, with false alarms you didn't choose.
A real incident
In February 2024, a Canadian tribunal ruled on Air Canada's chatbot, which had told Jake Moffatt he could claim a bereavement discount within 90 days of buying his ticket. That contradicted the airline's real policy. Air Canada argued the chatbot was responsible for its own actions, and the tribunal rejected that and ordered the airline to pay $812.02. A millisecond guardrail can't judge whether a policy explanation is right. An evaluator sampling chatbot answers would have flagged it, and for refund questions, a cascade is worth the extra latency.
From the floor
In DevFix, the safety guard was a set of rule-based filters on the generated code: it shouldn't contain calls that remove files, execute arbitrary commands or evaluate strings, and I said these rules could later be replaced with real compliance APIs. A human approval step came at the end, so the run only moved forward when we said yes.
Related
- Guardrail sandwich pattern
- Prompt injection
- CI gates and production monitoring
- Latency and streaming