Guardrails are the controls that sit around a model rather than inside it. Training changes what a model tends to do, and a guardrail is a separate component inspecting what went in and what came out, which is why it can be changed without retraining anything.
On the way in
Request classification decides what kind of request this is before the model sees it, meaning support question, code request, medical advice or something outside the product. Routing out of scope requests to a fixed response is cheaper and more reliable than a paragraph of prompt asking the model to stay on topic.
Topic and jailbreak detection looks for categories you have decided not to serve and shapes that try to override your instructions, meaning roleplay framings, encoded text and instructions buried in a pasted document. It is pattern matching against somebody who can rewrite their attempt, so treat it as reducing volume rather than closing a door.
Rate limiting belongs here too and gets forgotten, because it is not about content. It bounds how many attempts one caller gets, which makes an attack needing a hundred rephrasings not worth mounting.
On the way out
Content filtering applies the same categories in reverse, assuming something got through.
Schema validation checks the response is the shape your code expects. If you asked for an object with three fields, parse it, confirm the fields exist and hold the types you assumed, and reject anything else.
Grounding checks compare the response against the sources it was meant to use. Where every claim should trace to a retrieved passage, verifying that citations exist and contain what was attributed to them catches confident invention no content filter can see.
Refusal of malformed responses is the step teams skip. What happens when validation fails is part of the design, whether that means one retry, a fixed fallback or escalation to a person. A system with nothing decided here has undefined behaviour rather than none.
Improper output handling
This is the failure practitioners get wrong most often. It happens when model output reaches something that executes it, meaning a template that renders it, a parser, a database driver, a shell, a browser or a tool call.
Treat model output as untrusted user input, because that is what it is. It was produced from text including a user's request and probably a retrieved document, so anybody who can influence either can influence the output.
In practice that means escaping before rendering, so markup in a response is displayed rather than executed. It means validating before parsing rather than parsing and hoping, never concatenating a response into a query, a shell command, a file path or a request URL, and using parameterised calls instead. And it means constraining the model to a schema and checking the parsed values against an allowed set, rather than trusting free text that looked sensible in testing.
Layers, and where the boundary actually is
Guardrails are probabilistic. Each has a threshold somebody tuned, and tightening it trades false negatives for false positives on real users. Layering helps, since an attack has to survive every layer, and it still does not turn a probability into a boundary.
The boundary is permissions. What the system can do when a guardrail fails is decided by the credentials the code runs with, the scope of the tools an agent may call, and whether its account can write as well as read. A model talked into requesting a deletion deletes nothing if its token is read only. Design so the worst case of a guardrail failure is a bad answer rather than a bad action.
Managed guardrail services exist on every platform, including Guardrails for Amazon Bedrock, Azure AI Content Safety, and Model Armor alongside the safety filters on Google's Agent Platform, with open source options such as Llama Guard and NeMo Guardrails running wherever you host. None does the escaping, the schema checks or the permission scoping, which live in your application.
Practise this
You need one real response from a feature you run, fifteen minutes, and Python for the second half.
Take that response and trace it. Write down every place your application passes it onward unchecked, including rendering it in a page, writing it to a log, using a field as a key, and handing it to a tool. Then run this and watch a polite response fail three checks.
import html, json, re
raw = '{"answer": "<b onmouseover=x>hello</b>", "ticket_id": "42 or 1=1"}'
ALLOWED = {"answer", "ticket_id"}
try:
data = json.loads(raw)
except json.JSONDecodeError:
raise SystemExit("malformed, refuse")
if set(data) != ALLOWED:
raise SystemExit("unexpected fields, refuse")
if not re.fullmatch(r"[0-9]{1,10}", data["ticket_id"]):
raise SystemExit("ticket_id is not an id, refuse")
print(html.escape(data["answer"]))
What to look for. The payload is inert and the response is inoffensive, so no content filter would have objected. Every check that fires is about shape and destination rather than tone, and the one that matters on your own list is wherever a response reaches something that runs, queries or renders.
It teaches that the useful question is never whether the output was offensive but what your code does with it next.