Skip to content
ExponaBLOG
PRACTICAL AI & MACHINE LEARNING · DAY 097 / 100

Guardrails for AI output: a AI & ML guide for developers

Keep AI features safe and on-topic.

Expona Admin9 min read

Guardrails for AI output: a AI & ML guide for developers

As AI features move into production, we face a familiar problem: garbage in, garbage out. Without proper safeguards, your LLM-powered chatbot might leak sensitive data, hallucinate convincingly, or go completely off-topic—all while your users watch in real time. Guardrails for AI output are the validation layer between what a model generates and what actually reaches your users, and they're non-negotiable in any serious deployment.

Over the past 96 days, we've covered prompt engineering, fine-tuning, retrieval augmentation, and cost optimization. Today we're addressing the gatekeeping function: how to keep AI features safe, on-topic, and fit for production.

Why guardrails matter in production

AI models are probabilistic engines. They don't promise correctness; they predict plausible text. When you plug one into a customer-facing feature or an internal workflow, you inherit both its capabilities and its failure modes. A language model trained on internet text will occasionally:

  • Generate content that contradicts your brand or policy
  • Produce outputs in the wrong format or language
  • Hallucinate facts with unwarranted confidence
  • Encode biases or toxic language
  • Exceed length or token budgets

Guardrails catch these failures before they reach users. They're your last line of defense, and they come in four flavors: input moderation, output moderation, topic restriction, and format validation.

Moderate inputs and outputs

Input moderation filters what users send to your model. Output moderation filters what the model sends back. Both are essential.

Input moderation screens for obvious red flags: prompt injection attempts, profanity, requests for illegal content, or personally identifiable information that shouldn't be there. A customer service chatbot, for example, shouldn't process requests asking it to ignore its system prompt or generate instructions for harmful activities.

Output moderation checks the model's response before you return it. Did it produce hate speech? Did it leak confidential data? Did it refuse to answer when it shouldn't have? These are rare with well-tuned models, but they happen.

Most teams start with a moderation API—OpenAI Moderation, Perspective API, or similar managed services. These are low-friction for proof-of-concept work:

python
import openai

def moderate_input(user_message):
    response = openai.Moderation.create(input=user_message)
    flagged = response["results"][0]["flagged"]
    categories = response["results"][0]["categories"]
    
    if flagged:
        return False, categories
    return True, None

def process_user_request(user_message):
    is_safe, flags = moderate_input(user_message)
    
    if not is_safe:
        return {"error": "Your message was flagged for policy violation", "details": flags}
    
    # Proceed to model call
    completion = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": user_message}]
    )
    
    model_output = completion.choices[0].message.content
    
    # Also moderate the output
    is_safe_output, output_flags = moderate_input(model_output)
    
    if not is_safe_output:
        return {"error": "Model output failed safety check"}
    
    return {"response": model_output}

For higher volumes, you might move to batch moderation or self-hosted classifiers trained on your own datasets. Managed services are convenient but add latency and cost; self-hosted models give you control and speed at the cost of maintenance.

Restrict to allowed topics

A common failure mode: your AI feature works brilliantly on its intended topic and terribly everywhere else. An e-commerce product search assistant should answer questions about inventory, pricing, and shipping—not provide medical advice or explain quantum physics.

Topic restriction uses a second, lightweight classifier to ensure the user's question (and sometimes the model's answer) falls within scope. You can build this with a few labeled examples and a small model:

python
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfVectorizer
import pickle

# Training data: (text, label) pairs
training_data = [
    ("What colors does this shirt come in?", "in_scope"),
    ("How do I track my order?", "in_scope"),
    ("What's your return policy?", "in_scope"),
    ("Can you help me with my taxes?", "out_of_scope"),
    ("Tell me about climate change", "out_of_scope"),
    ("Do you deliver to Canada?", "in_scope"),
]

texts = [t[0] for t in training_data]
labels = [t[1] for t in training_data]

vectorizer = TfidfVectorizer(max_features=100)
X = vectorizer.fit_transform(texts)

classifier = MultinomialNB()
classifier.fit(X, labels)

# Pickle for production use
with open("topic_classifier.pkl", "wb") as f:
    pickle.dump((vectorizer, classifier), f)

# At inference time
with open("topic_classifier.pkl", "rb") as f:
    vectorizer, classifier = pickle.load(f)

user_question = "Do you have this in size XL?"
X_test = vectorizer.transform([user_question])
prediction = classifier.predict(X_test)[0]

if prediction == "out_of_scope":
    return {"response": "I can only help with product and order questions."}

You can also use a zero-shot classifier with a larger model if you want more flexibility without retraining:

python
from transformers import pipeline

classifier = pipeline("zero-shot-classification", model="facebook/bart-large-mnli")

user_question = "What's your favorite movie?"
candidate_labels = ["product inquiry", "order tracking", "policy question", "out of scope"]

result = classifier(user_question, candidate_labels)

if result["labels"][0] == "out of scope":
    confidence = result["scores"][0]
    if confidence > 0.9:
        return {"response": "I'm here to help with orders and products. How can I assist?"}

The key is setting a confidence threshold. If your classifier isn't sure, escalate rather than guess.

Validate formats and lengths

Many AI features need to produce output in a specific format: JSON, structured fields, a particular language, or a bounded length. A model can hallucinate or drift; validation ensures consistency.

Format validation is straightforward. If you ask a model to output JSON, parse it and reject invalid responses:

python
import json
import openai

def extract_structured_data(user_input):
    prompt = f"""Extract the following from this text and return valid JSON:
- name (string)
- email (string)
- phone (string, optional)

Text: {user_input}

Return only valid JSON, no markdown or explanation."""
    
    completion = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        temperature=0
    )
    
    raw_output = completion.choices[0].message.content
    
    try:
        parsed = json.loads(raw_output)
        
        # Validate required fields
        if not all(k in parsed for k in ["name", "email"]):
            return None, "Missing required fields"
        
        # Validate email format (basic)
        if "@" not in parsed["email"]:
            return None, "Invalid email format"
        
        return parsed, None
    
    except json.JSONDecodeError:
        return None, f"Invalid JSON: {raw_output}"

Length validation prevents token overruns and keeps responses concise. Set a maximum length and truncate or re-prompt if needed:

python
import tiktoken

def generate_with_length_limit(prompt, max_tokens=500):
    completion = openai.ChatCompletion.create(
        model="gpt-4",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=max_tokens
    )
    
    response = completion.choices[0].message.content
    
    # Count actual tokens used
    encoding = tiktoken.encoding_for_model("gpt-4")
    token_count = len(encoding.encode(response))
    
    if token_count > max_tokens * 0.95:
        # Log for review; response was cut off by the model
        print(f"Response hit token limit: {token_count} tokens")
    
    return response

Language validation ensures responses are in the right language. Use a language detection library:

python
from langdetect import detect, LangDetectException

def validate_response_language(response, expected_lang="en"):
    try:
        detected_lang = detect(response)
        if detected_lang != expected_lang:
            return False, f"Response in {detected_lang}, expected {expected_lang}"
        return True, None
    except LangDetectException:
        return False, "Could not detect language"

Escalate uncertain cases to humans

Perfect automation is impossible. Some requests are genuinely ambiguous—they could be legitimate or they could be edge cases your guardrails didn't anticipate. When confidence is low, escalate to a human.

This is where you build your escalation workflow. A request fails one of your guardrails, or passes all checks but scores below a confidence threshold? Route it to a queue for manual review:

python
import uuid

def process_with_escalation(user_input, confidence_threshold=0.85):
    # Step 1: Input moderation
    is_safe, flags = moderate_input(user_input)
    if not is_safe:
        return {"error": "Request flagged", "escalated": False}
    
    # Step 2: Topic classification
    topic_prediction, topic_confidence = classify_topic(user_input)
    
    if topic_confidence < confidence_threshold:
        escalation_id = str(uuid.uuid4())
        escalate_to_queue(
            escalation_id,
            user_input,
            reason="Low topic confidence",
            confidence=topic_confidence,
            assigned_to="support_queue"
        )
        return {
            "escalated": True,
            "escalation_id": escalation_id,
            "message": "Your request is being reviewed by our team."
        }
    
    # Step 3: Generate response
    response = generate_ai_response(user_input, topic_prediction)
    
    # Step 4: Validate output
    is_valid, validation_error = validate_response(response)
    
    if not is_valid:
        escalation_id = str(uuid.uuid4())
        escalate_to_queue(
            escalation_id,
            user_input,
            reason=f"Output validation failed: {validation_error}",
            model_response=response,
            assigned_to="ml_review_queue"
        )
        return {
            "escalated": True,
            "escalation_id": escalation_id,
            "message": "Response validation pending."
        }
    
    # All checks passed
    return {"escalated": False, "response": response}

Track escalations as metrics. A high escalation rate signals that your guardrails are too strict, your model is underperforming, or your training data doesn't cover the actual use cases. Use this signal to iterate.

Common mistakes

Skipping input moderation. Teams often focus on output but forget that garbage input is caught earlier and more cheaply at the door. Always moderate input.

Hardcoding thresholds. Confidence thresholds and escalation rules should be configurable, not baked into code. What works for 100 users rarely scales to 100,000.

Over-filtering. If your guardrails reject 20% of legitimate requests, they're too aggressive. Start permissive and tighten based on real incidents, not speculation.

Trusting external APIs blindly. Moderation services have latency, cost, and occasional downtime. Always have a fallback—either cached results or a degraded but functional mode.

Ignoring escalation queues. If escalations pile up unreviewed, your safety layer becomes theater. Treat escalations as data; review them weekly and feed insights back into your guardrails.

Testing only the happy path. Adversarial testing—deliberately trying to break your safeguards—is essential. Ask a few users to try to game the system; you'll find gaps your automated tests miss.

Key takeaways

  • Guardrails are a multi-layer defense: moderate inputs, restrict topics, validate formats, and escalate edge cases.
  • Start with managed moderation APIs for proof-of-concept; move to self-hosted or hybrid approaches as you scale.
  • Topic classification with a lightweight model catches off-topic requests before they waste tokens and confuse users.
  • Enforce format, length, and language constraints to keep outputs predictable and production-ready.
  • Escalation workflows are not failures—they're learning opportunities. High escalation rates indicate where your system needs improvement.
  • Measure guardrail performance like any other system: track false positives, false negatives, latency, and cost.
  • Test adversarially. Ask colleagues or beta users to try breaking your safeguards; real attackers will find what your tests miss.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles