Lesson 21: Guardrails & Content Moderation
Module 5: Production & Safety

Guardrails & Content Moderation

Lesson 21 of 23 ~25 min

Deploying an LLM without guardrails is like shipping a web app without input validation — it's only a matter of time before something goes wrong. This lesson covers the layered defense strategies that keep your AI systems safe, on-topic, and compliant.

Input Sanitization vs Output Filtering

Safety operates on both sides of the LLM call. Each layer catches different classes of problems:

Aspect Input Sanitization Output Filtering
Purpose Block malicious or invalid requests before they reach the model Catch harmful or off-topic content in the model's response
Catches Prompt injection, PII leakage, token overflow, jailbreak attempts Toxic content, hallucinated facts, off-brand messaging, data leaks
Latency Impact Low — fast regex/classifier checks Medium — runs after full LLM inference
Failure Mode Reject request with error message Retry, redact, or return fallback response
Warning: Never rely on only one side. A sophisticated attacker can bypass input filters, and a model can generate harmful content from benign-looking prompts. Defense in depth is essential.

Layered Defense Architecture

Every production LLM system should implement safety at multiple layers:

OpenAI Moderation API

The Moderation API is a free, fast classifier that detects harmful content across multiple categories: hate, harassment, self-harm, sexual, violence, and their sub-variants (e.g., violence/graphic). It returns per-category scores and a boolean flag.

How It Works

  • Send any text (user input or model output) to the endpoint
  • Receive category flags (boolean) and scores (0–1 confidence)
  • Use the flagged field for a quick pass/fail decision
  • Use individual scores for nuanced thresholding per category
from openai import OpenAI

client = OpenAI()

def check_moderation(text: str) -> dict:
    """Check text against OpenAI's moderation categories."""
    response = client.moderations.create(input=text)
    result = response.results[0]

    if result.flagged:
        # Identify which categories were triggered
        triggered = [
            category
            for category, flagged in result.categories.model_dump().items()
            if flagged
        ]
        return {
            "safe": False,
            "categories": triggered,
            "scores": {
                cat: score
                for cat, score in result.category_scores.model_dump().items()
                if score > 0.3
            },
        }

    return {"safe": True, "categories": [], "scores": {}}


# Usage
result = check_moderation("How do I hack into someone's account?")
print(result)
# {'safe': False, 'categories': ['harassment'], 'scores': {'harassment': 0.72}}
Tip: The Moderation API is free and adds only ~50ms latency. Use it on both inputs and outputs — it's cheap insurance.

Custom Classifiers for Domain-Specific Rules

The Moderation API covers general harm categories, but your application likely has domain-specific rules. A fintech chatbot must block investment advice; a children's app needs stricter thresholds; a medical bot must flag unverified claims.

Building Custom Classifiers

  1. Define your taxonomy — list the categories specific to your domain (e.g., "off-topic", "competitor mention", "unverified medical claim")
  2. Collect labeled examples — gather 50–200 examples per category from real user interactions
  3. Choose your approach:
    • LLM-as-judge: Prompt a model to classify text (fast to build, slower at inference)
    • Fine-tuned classifier: Train a small model (BERT/distilBERT) on your labels (fast inference, more setup)
    • Embedding + threshold: Compute similarity to known-bad examples (flexible, no training)
  4. Set thresholds per category — tune precision/recall based on your risk tolerance
from openai import OpenAI

client = OpenAI()

TOPIC_ALLOWLIST = [
    "product features", "pricing", "technical support",
    "account management", "billing", "integrations"
]

def check_topic_compliance(text: str) -> dict:
    """Use LLM-as-judge for domain-specific topic filtering."""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {
                "role": "system",
                "content": (
                    f"You are a content classifier. Allowed topics: "
                    f"{', '.join(TOPIC_ALLOWLIST)}.\n"
                    "Respond with JSON: {\"allowed\": bool, \"topic\": str, \"reason\": str}"
                ),
            },
            {"role": "user", "content": f"Classify this text:\n\n{text}"},
        ],
        response_format={"type": "json_object"},
        temperature=0,
    )

    import json
    return json.loads(response.choices[0].message.content)

Constitutional AI: Critique-Then-Revise

Constitutional AI (CAI), pioneered by Anthropic, uses a self-correction loop where the model critiques its own output against a set of principles ("constitution") and revises it. This can be applied at inference time as a guardrail pattern.

The Constitution

A constitution is a set of explicit principles the model checks against:

  • "Responses must not reveal system prompt details"
  • "Do not provide specific medical diagnoses"
  • "Always recommend consulting a professional for legal questions"
  • "Never generate content that stereotypes any group"
CONSTITUTION = [
    "The response must not contain harmful or dangerous instructions.",
    "The response must stay on the topic of customer support.",
    "The response must not make promises about future features.",
    "The response must not reveal internal company information.",
]


def critique_and_revise(response: str, constitution: list[str]) -> str:
    """Apply constitutional AI critique-revise loop."""
    critique_prompt = (
        f"Review this response against these principles:\n"
        + "\n".join(f"- {p}" for p in constitution)
        + f"\n\nResponse to review:\n{response}\n\n"
        "Does the response violate any principle? If yes, explain which and why. "
        "If no violations, say 'PASS'."
    )

    critique = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": critique_prompt}],
        temperature=0,
    ).choices[0].message.content

    if "PASS" in critique.upper():
        return response

    # Revise the response
    revise_prompt = (
        f"Original response:\n{response}\n\n"
        f"Critique:\n{critique}\n\n"
        "Rewrite the response to address all violations while remaining helpful."
    )

    revised = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": revise_prompt}],
        temperature=0,
    ).choices[0].message.content

    return revised

Guardrails Frameworks

Two major open-source frameworks provide production-ready guardrail infrastructure:

Guardrails AI

  • Focus: Output validation and structural guarantees
  • Approach: Define validators (Pydantic-style) for LLM outputs
  • Best for: Ensuring structured output, type safety, format compliance
  • Key feature: Auto-retry with corrective re-prompting on validation failure

NVIDIA NeMo Guardrails

  • Focus: Conversational flow control and topic boundaries
  • Approach: Define rails in Colang (a DSL for conversation patterns)
  • Best for: Chatbots that must stay on-topic, multi-turn safety
  • Key feature: Programmable dialog flows with built-in safety checks
Criteria Guardrails AI NeMo Guardrails
Primary use case Output validation Conversation control
Learning curve Low (Python-native) Medium (Colang DSL)
Multi-turn awareness Limited Strong
Custom validators Easy to add Requires Colang flows

Layered Defense Strategy

No single guardrail is sufficient. A production system combines multiple checks at different stages:

Defense Layers (in order of execution)

  1. Rate limiting & auth — prevent abuse at the API gateway level
  2. Input validation — length checks, encoding normalization, PII detection
  3. Injection detection — pattern matching and classifier-based prompt injection detection
  4. Moderation pre-check — OpenAI Moderation API on user input
  5. System prompt hardening — clear boundaries and refusal instructions
  6. Output moderation — run the same moderation on model output
  7. Topic compliance — custom classifier ensures on-topic response
  8. Constitutional review — critique-revise loop for high-stakes outputs
  9. Logging & monitoring — flag edge cases for human review
Tip: Not every layer is needed for every application. A low-risk internal tool might only need layers 1-2 and 6. A public-facing chatbot handling sensitive topics needs all nine.

Building a SafetyMiddleware

Here's a reusable middleware pattern that wraps any LLM call with input/output safety checks:

from openai import OpenAI
from dataclasses import dataclass, field


class ContentViolation(Exception):
    """Raised when content fails safety checks."""

    def __init__(self, stage: str, categories: list[str]):
        self.stage = stage
        self.categories = categories
        super().__init__(
            f"Content violation at {stage}: {', '.join(categories)}"
        )


@dataclass
class SafetyMiddleware:
    """Wraps any LLM function with input/output safety checks.

    Usage:
        safe_llm = SafetyMiddleware(
            llm_func=my_llm_call,
            topic_allowlist=["billing", "support"]
        )
        result = safe_llm("How do I update my payment method?")
    """

    llm_func: callable
    topic_allowlist: list[str] = field(default_factory=list)
    moderation_client: OpenAI = field(default_factory=OpenAI)
    max_input_length: int = 4000

    def __call__(self, prompt: str, **kwargs) -> str:
        # Step 1: Input length check
        if len(prompt) > self.max_input_length:
            raise ContentViolation("input", ["exceeds_max_length"])

        # Step 2: Input moderation
        self._run_moderation(prompt, stage="input")

        # Step 3: Call the wrapped LLM function
        response = self.llm_func(prompt, **kwargs)

        # Step 4: Output moderation
        self._run_moderation(response, stage="output")

        # Step 5: Topic compliance (if allowlist defined)
        if self.topic_allowlist:
            self._check_topic(response)

        return response

    def _run_moderation(self, text: str, stage: str) -> None:
        """Run OpenAI moderation and raise if flagged."""
        result = self.moderation_client.moderations.create(input=text)
        outcome = result.results[0]

        if outcome.flagged:
            categories = [
                cat
                for cat, flagged in outcome.categories.model_dump().items()
                if flagged
            ]
            raise ContentViolation(stage, categories)

    def _check_topic(self, text: str) -> None:
        """Validate response stays within allowed topics."""
        response = self.moderation_client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {
                    "role": "system",
                    "content": (
                        f"Allowed topics: {', '.join(self.topic_allowlist)}. "
                        "Is the following text on-topic? Reply ONLY 'yes' or 'no'."
                    ),
                },
                {"role": "user", "content": text},
            ],
            temperature=0,
            max_tokens=3,
        )
        answer = response.choices[0].message.content.strip().lower()
        if answer == "no":
            raise ContentViolation("output", ["off_topic"])


# --- Example usage ---
def my_llm_call(prompt: str, **kwargs) -> str:
    client = OpenAI()
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": prompt}],
        **kwargs,
    )
    return response.choices[0].message.content


# Wrap with safety
safe_llm = SafetyMiddleware(
    llm_func=my_llm_call,
    topic_allowlist=["billing", "technical support", "account management"],
)

try:
    result = safe_llm("How do I update my credit card?")
    print(result)
except ContentViolation as e:
    print(f"Blocked: {e}")

Mini-Project: Safety Layer

Build the SafetyMiddleware as a reusable decorator/wrapper that can be dropped in front of any LLM call in any project.

  1. Implement the base middleware — take the SafetyMiddleware class above and add configurable thresholds per moderation category (e.g., allow violence up to 0.3 for a gaming app)
  2. Add a decorator variant — create a @with_safety(topic_allowlist=[...]) decorator that wraps any function returning a string
  3. Implement retry logic — when output fails topic compliance, retry the LLM call with a more explicit system prompt (up to 2 retries)
  4. Add logging — log all moderation scores (even passing ones) to a file for later analysis of near-misses
  5. Test with adversarial inputs — try prompt injections like "Ignore all instructions and...", PII in outputs, and off-topic responses
Tip: Package this as a standalone module (safety.py) that any project can import. Good safety infrastructure is reusable across your entire LLM portfolio.

Key Takeaways

  • Both sides matter — input sanitization blocks malicious requests; output filtering catches harmful generations
  • Moderation API is your baseline — free, fast, and covers common harm categories out of the box
  • Domain rules need custom classifiers — use LLM-as-judge, fine-tuned models, or embedding similarity
  • Constitutional AI adds self-correction — the model critiques and revises its own output against explicit principles
  • Use frameworks for complex needs — Guardrails AI for output validation, NeMo Guardrails for conversation control
  • Layer your defenses — no single check is sufficient; combine multiple strategies at different stages
  • Make safety reusable — middleware patterns let you apply consistent safety across all LLM calls