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.

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 --versionStep 2: Create a Project Directory
Open your terminal and create a dedicated folder for this project:
mkdir llm_guardrails
cd llm_guardrailsStep 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 pydantic3. 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.

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.")) # ValidHow 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:

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.




