In production systems, "hallucination" is a catch-all term for when Large Language Models (LLMs) generate plausible-sounding but incorrect or unintended output.

No matter how well you write your system prompts, prompt engineering alone cannot eliminate non-determinism. To build reliable AI applications, engineering teams must treat raw LLM output as untrusted data that must pass through deterministic validation gates before reaching end-users or internal APIs.

Here is what is actually happening when an LLM hallucinates, along with a complete walkthrough of how to build automated validation layers in Python from scratch.

1. What is an LLM Hallucination?

At their core, LLMs are probabilistic text generators. They predict the next most likely token based on training patterns rather than retrieving verified facts or executing logical calculations. The model creates output based on input data but does not inherently "know" if its output is correct.

When an output strays from reality, it typically falls into one of three buckets:

  • Factual Hallucinations: Confidently asserting false data (invented dates, fake API endpoints, fabricated cited sources).

  • Logical Hallucinations: Using correct premises to arrive at incorrect mathematical or causal conclusions.

  • Constraint Hallucinations: Failing to follow JSON schemas, length limits, or specific formatting instructions.

Flow diagram of an LLM Hallucination Decision Tree
Flow diagram of an LLM Hallucination Decision Tree

2. Setting Up Your Development Environment

Before writing validation scripts, let's set up a clean Python environment step-by-step.

Step 1: Install Python.

Ensure you have Python 3.10 or newer installed. You can check your version in your terminal or command prompt:

python --version

Step 2: Create a Project Directory

Open your terminal and create a dedicated folder for this project:

mkdir llm_guardrails
cd llm_guardrails

Step 3: Set Up a Virtual Environment & Install Dependencies

Create an isolated environment so your dependencies don't conflict with other projects, then install Pydantic (used for schema validation):

# Create virtual environment
python -m venv venv

# Activate it (Mac/Linux)
source venv/bin/activate

# Activate it (Windows)
# venv\Scripts\activate

# Install required package
pip install pydantic

3. Building Deterministic Guardrails (Code Walkthrough)

Instead of asking a second LLM to "judge" the first LLM (which increases latency, cost, and non-determinism), we use deterministic Python scripts to detect failures.

Flow diagram of Multi-Layer Deterministic Validation Architecture for LLMs
Flow diagram of Multi-Layer Deterministic Validation Architecture for LLMs

Create a file named guardrails.py in your llm_guardrails directory and follow along with the implementations below.

Layer A: Detecting Constraint Failures with Pydantic

The Aim: Ensure the LLM returns JSON with exact field names and data types.

When asking an LLM to generate data for an API call, it might output a string instead of an integer. We use Pydantic to detect this error instantly.

import json
from pydantic import BaseModel, Field, ValidationError

# 1. Define the exact contract expected from the LLM
class ActionContract(BaseModel):
    action: str
    user_id: int
    amount: float

def validate_schema(raw_json: str):
    """Parses raw text and validates structure against the contract."""
    try:
        data = json.loads(raw_json)
        validated = ActionContract(**data)
        return f"SUCCESS: Valid payload for user {validated.user_id}"
    except (json.JSONDecodeError, ValidationError) as e:
        return f"CONSTRAINT ERROR: Output violated schema.\n{e}"

# --- PRACTICE REPLICATION ---
# Test Case 1: Bad output (amount is a string "fifty", action type missing)
bad_llm_output = '{"user_id": 101, "amount": "fifty"}'
print(validate_schema(bad_llm_output))

# Test Case 2: Valid output
good_llm_output = '{"action": "refund", "user_id": 101, "amount": 50.0}'
print(validate_schema(good_llm_output))

How to replicate:

Run python guardrails.py in your terminal.

Modify ActionContract to add a new required field (e.g., email: str) and watch how bad_llm_output gets rejected.

Layer B: Stopping Logical Failures with Code Execution

The Aim: Prevent math and multi-step calculation mistakes by forcing the LLM to write code instead of raw answers, then evaluating that code.

LLMs fail at multi-step math because they guess tokens. The fix is to let the LLM output a Python snippet, then run it in an isolated scope to extract the true result.

def validate_logic(python_code: str):
    """Executes generated code in an isolated scope to extract the computed output."""
    local_scope = {}
    try:
        # Execute the code snippet safely within local_scope dictionary
        exec(python_code, {}, local_scope)
        return f"SUCCESS: Calculated Result = {local_scope.get('result')}"
    except Exception as e:
        return f"LOGIC ERROR: Code failed to execute. {e}"

# --- PRACTICE REPLICATION ---
# LLM generated code to calculate compound interest
llm_generated_code = """
principal = 1000
rate = 0.05
years = 3
result = principal * ((1 + rate) ** years)
"""

print(validate_logic(llm_generated_code))

How to replicate: Append this snippet to your script. Try changing llm_generated_code to introduce a syntax error (like missing a parenthesis) to observe how the execution guard catches the logical failure before it contaminates your application.

Layer C: Verifying Factual Grounding with Regex

The Aim: Confirm that referenced entity identifiers (e.g., order IDs, support tickets) match exact company standards rather than being fabricated.

import re

# Expected format: 3 uppercase letters, a hyphen, and 4 digits (e.g., ORD-1234)
TICKET_PATTERN = r"\b[A-Z]{3}-\d{4}\b"

def verify_factual_ids(llm_response: str):
    """Scans response text to confirm entity identifiers follow standard formats."""
    matches = re.findall(TICKET_PATTERN, llm_response)
    if "ticket" in llm_response.lower() and not matches:
        return "FACTUAL ERROR: Found ticket reference, but format is hallucinated."
    return f"SUCCESS: Valid entity reference found: {matches}"

# --- PRACTICE REPLICATION ---
print(verify_factual_ids("I checked your issue in ticket #99-ABC."))  # Invalid
print(verify_factual_ids("Your issue is tracked under ticket SUP-8821."))  # Valid

How to replicate: Run the script and experiment with different text inputs. Try modifying TICKET_PATTERN to match your own format (e.g., social security numbers, tracking numbers, or internal product IDs).

4. Advanced Hallucination Mitigation Strategies

Once basic validation layers are in place, production systems scale reliability by incorporating advanced architectural patterns.

Automated Retry Loops with Feedback Prompts

When a validation gate fails, do not return an error to the user immediately. Instead, feed the validation error message directly back into the LLM as a self-correction prompt:

Flow diagram of Automated LLM Self-Correction Loop
Flow diagram of Automated LLM Self-Correction Loop

By providing the model with exact error tracebacks (e.g., "ValidationError: amount field must be a float"), LLMs successfully self-correct on the second attempt over 80% of the time.

Conclusion

Building resilient, enterprise-grade AI systems requires a fundamental shift in technical strategy: moving away from viewing Large Language Models as autonomous oracle systems and towards treating them as non-deterministic compute components. Prompt engineering alone cannot guarantee operational reliability. By establishing rigorous, deterministic validation architectures enforcing strict schemas, offloading complex execution to sandboxed environments, and implementing continuous verification layers engineering teams can effectively eliminate the operational risks associated with model drift and non-determinism.

Ultimately, mitigating hallucinations is not about achieving hypothetical perfection within the language model itself, but about engineering robust systems around it. Organizations that embrace automated guardrails, adaptive self-correction loops, and structured data validation will maintain the velocity advantages of generative AI without compromising on application security, accuracy, or compliance.