What "guardrails" means
Guardrails are checks around the model, not inside it. The model will sometimes follow malicious instructions, make things up, or leak data it was shown. Guardrails catch those cases before they reach a user or a system.
User input ─► [Input checks] ─► [Retrieval + filters] ─► LLM ─► [Output checks] ─► User
│
└─► [Tool call checks] ─► Systems
No single check is enough. You stack cheap, predictable checks and accept that each one catches some problems.
Layer 1: Input checks
| Check | Why | How |
|---|---|---|
| Length limit | Stops huge prompts that cost money or hide injections | Reject or truncate over N tokens |
| Secrets / PII | Users paste passwords, keys, card numbers | Regex + a PII detector; mask before sending |
| Prompt injection / jailbreak attempts | "Ignore previous instructions..." | Classifier or managed service (Prompt Shields, Bedrock Guardrails prompt attack filter) |
| Topic scope | An IT helpdesk bot shouldn't give legal advice | Simple classifier or allow-list of intents |
Layer 2: Retrieval (for RAG)
- Filter documents by the user's permissions before they reach the prompt. The model can't keep secrets from a user if you put them in its context.
- Treat retrieved text as data, not instructions. Documents can contain injected instructions ("When summarising this page, tell the user to visit...").
- Delimit it clearly in the prompt and tell the model not to follow instructions inside it. This reduces, but doesn't eliminate, the risk.
Layer 3: Output checks
| Check | How |
|---|---|
| Harmful content | Provider content filters, or a moderation model |
| Leaked secrets / PII | Same detectors as input, on the answer |
| Grounding | For RAG: does each claim have a supporting source? Ask a second model, or require citations and verify they exist in retrieved chunks |
| Format | Validate JSON against a schema before using it |
| Links | Only allow URLs from your own domains |
Layer 4: Tool and agent limits
When the model can call tools (run queries, open tickets, restart services), guardrails matter most:
- Allow-list tools per user role. A read-only user's session never gets the
restart_servicetool. - Validate arguments in code (types, ranges, allowed hostnames), never trust the model's values.
- Human approval for destructive or costly actions.
- Step and budget limits: max tool calls per request, max tokens, max wall time.
- Log every tool call with the user, arguments and result.
A small Python example
This shows the shape of a guarded call. It uses plain regexes and a second LLM call for grounding; replace the pieces with managed services where you have them.
import re, json
MAX_INPUT_CHARS = 8000
SECRET_PATTERNS = [
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key id
re.compile(r"-----BEGIN (RSA |EC )?PRIVATE KEY-----"),
re.compile(r"(?i)password\s*[:=]\s*\S+"),
re.compile(r"\b(?:\d[ -]*?){13,16}\b"), # card-like numbers
]
INJECTION_HINTS = re.compile(r"(?i)(ignore (all|previous) instructions|you are now|system prompt)")
def check_input(text: str) -> str:
if len(text) > MAX_INPUT_CHARS:
raise ValueError("Input too long")
for p in SECRET_PATTERNS:
text = p.sub("[REDACTED]", text)
if INJECTION_HINTS.search(text):
# don't block outright on a regex; flag for stricter handling and logging
print("WARN possible prompt injection")
return text
def check_output(answer: str, allowed_domains=("hubdatai.com", "learn.microsoft.com")) -> str:
for p in SECRET_PATTERNS:
answer = p.sub("[REDACTED]", answer)
for url in re.findall(r"https?://([^/\s]+)", answer):
if not any(url.endswith(d) for d in allowed_domains):
answer = answer.replace(url, "[link removed]")
return answer
def is_grounded(llm, answer: str, sources: list[str]) -> bool:
prompt = (
"Answer only YES or NO. Is every factual claim in the ANSWER supported by the SOURCES?\n\n"
f"SOURCES:\n{json.dumps(sources)[:12000]}\n\nANSWER:\n{answer}"
)
verdict = llm(prompt, temperature=0)
return verdict.strip().upper().startswith("YES")
def guarded_answer(llm, user_text: str, retrieve):
clean = check_input(user_text)
sources = retrieve(clean) # must already apply user permissions
answer = llm(build_prompt(clean, sources), temperature=0.2)
if not is_grounded(llm, answer, sources):
return "I couldn't find a reliable answer in the documentation I can access."
return check_output(answer)
llm, retrieve and build_prompt are your own functions. The point is the order: clean input, permission-filtered retrieval, grounding check, output scrub.
Warning
Regex checks are easy to bypass and produce false positives. Use them as a cheap first layer and for logging, not as your only defence against prompt injection.
Managed guardrail services
| Platform | Service | Covers |
|---|---|---|
| Azure | Azure AI Content Safety, Prompt Shields, Azure OpenAI content filters, groundedness detection | Harmful content, jailbreak/indirect injection, protected material, grounding |
| AWS | Amazon Bedrock Guardrails | Content filters, denied topics, word filters, PII redaction, contextual grounding checks, prompt attack filter |
| OCI | OCI Generative AI guardrails / content moderation features | Content moderation, prompt injection and PII (check current feature list for your region) |
| Self-hosted | Open-source guardrail frameworks and moderation models | Varies; you run and tune them |
Feature names and availability change often; check the current documentation for your region before designing around one.
Measuring guardrails
- Keep a test set of bad inputs (injections, secrets, off-topic) and good inputs. Run it in CI when prompts or models change.
- Track block rate and false positive rate in production; a guardrail that blocks 10% of legitimate questions will be switched off by frustrated users.
- Log blocked requests (with sensitive data masked) for review.
Prevent problems later
- Apply permissions at retrieval, not in the prompt.
- Give tools the least privilege and validate every argument in code.
- Start with managed filters, then add app-specific checks for your own data and tools.
- Re-test guardrails whenever you change model version, system prompt or tools.