cd /news/ai-agents/building-production-ready-ai-agent-m… · home topics ai-agents article
[ARTICLE · art-83133] src=pub.towardsai.net ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Building Production-Ready AI Agent Middleware with LangGraph: Enterprise Patterns, Guardrails…

LangGraph, a framework by LangChain, is the focus of a new enterprise guide detailing production-ready AI agent middleware patterns, including human-in-the-loop interrupts, LLM gateways, security middleware, and observability. The guide covers architectural layers from client to tool execution, emphasizing control, security, and performance for enterprise AI systems.

read22 min views1 publishedAug 1, 2026

Building Production-Ready AI Agent Middleware with LangGraph: Enterprise Patterns, Guardrails, Human Approval & LLM Gateways

Executive Summary

In this comprehensive guide, you’ll master production AI agent architecture for LangGraph with practical enterprise patterns:

✓ Middleware vs Infrastructure — Architectural distinctions and component placement

✓ LangGraph Human-in-the-Loop — True interrupt() patterns with Command(resume=...)

✓ Cache Layers - Prompt, response, embedding, semantic, tool, and RAG caching

✓ LLM Gateways - LiteLLM, OpenRouter, Azure AI Foundry, and enterprise routing

✓ Security Middleware - Prompt injection, jailbreak, secret detection, content moderation

✓ Agent Guardrails - Llama Guard, NeMo Guardrails, and policy enforcement

✓ Memory Systems - Short-term, long-term, semantic, episodic with LangGraph Store

✓ Cost Enforcement - Budget limits with automatic model downgrading

✓ Multi-Agent Middleware - Supervisor patterns with per-agent validation

✓ Observability - OpenTelemetry, LangSmith, and distributed tracing

✓ Production Storage - PostgresSaver, Redis, and enterprise databases

What you’ll build: Enterprise-grade AI systems that balance control, observability, security, and performance.

Architecture Foundation: Beyond Simple Middleware

The AI Execution Pipeline

Before diving into code, understand the enterprise architecture:

text

┌─────────────────────────────────────────────────────────────┐│                        Client Layer                         ││              (Web, Mobile, API, Slack, etc.)                │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                     API Gateway Layer                       ││  • Authentication (JWT, OAuth2)   • Rate Limiting           ││  • Request Validation              • Content Moderation     │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                   Security Middleware Layer                 ││  • Prompt Injection Detection     • Jailbreak Detection     ││  • Secret Detection/Redaction     • PII Detection           ││  • Policy Enforcement (OPA/Cedar) • Audit Logging           │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                    Application Middleware                   ││  • Trace ID Setup                • Performance Monitoring   ││  • Feature Flags                 • Request Correlation      │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                      LLM Gateway Layer                      ││  • Model Routing (LiteLLM)       • Budget Enforcement       ││  • Fallback Strategy             • Prompt Versioning        ││  • Cost Tracking                 • Model Selection          │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│              LangGraph Orchestration Layer                  ││  • State Management              • Agent Logic              ││  • Branching Decisions           • Multi-Agent Coordination ││  • Human-in-the-Loop (interrupt) • Checkpointing            │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                    Tool Execution Layer                     ││  • Tool Approval (interrupt)     • Retry + Circuit Breaker  ││  • MCP Validation                • Tool Caching             ││  • Timeout Enforcement           • Error Handling           │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                    Output Processing Layer                  ││  • Guardrails (Llama Guard)      • PII Redaction            ││  • Content Moderation            • Observability            │└─────────────────────────────────────────────────────────────┘                              │                              ▼┌─────────────────────────────────────────────────────────────┐│                    Observability Layer                      ││  • OpenTelemetry Traces         • LangSmith                 ││  • Prometheus Metrics           • Structured Logging        ││  • Distributed Tracing          • Alerting                  │└─────────────────────────────────────────────────────────────┘

Critical Distinction: Middleware vs Infrastructure

Understanding what is actually middleware versus broader infrastructure is crucial:

Application Middleware (Cross-cutting concerns)

  • Authentication/authorization
  • Rate limiting
  • Request validation
  • PII detection/redaction
  • Logging and metrics
  • Timeout enforcement
  • Structured error handling

Infrastructure Components (Platform services)

  • LLM Gateway (routing, fallback, cost management)
  • Memory Layer (short-term, long-term, semantic)
  • Prompt Management (versioning, A/B testing)
  • Model Registry (model selection, capabilities)
  • Cache Layer (response, embedding, semantic)

Orchestration (Workflow components)

  • Agent nodes (decision logic)
  • Tool selection and execution
  • State transitions
  • Branching logic
  • Multi-agent coordination

Production Principle: Use middleware for cross-cutting concerns, infrastructure for platform services, and orchestration for workflow logic.

LangGraph Human-in-the-Loop: The Correct Pattern

True interrupt() Implementation

The modern, officially recommended approach uses interrupt() with Command(resume=...):

python

from langgraph.types import interruptfrom langgraph.graph import StateGraph, START, END, Commandfrom typing import Dict, Any, Literalimport datetime
php
def tool_approval_node(state: Dict[str, Any]) -> Command[Literal["execute_tool", "reject_tool"]]:    """ execution for human approval using true interrupt()"""        tool_name = state.get("selected_tool")    sensitive_tools = ["send_email", "transfer_money", "delete_data"]        if tool_name not in sensitive_tools:        # Continue without interruption        return Command(goto="execute_tool")        # TRUE interrupt - graph s here    # This is the official LangGraph pattern    approval = interrupt({        "request_type": "approval",        "tool": tool_name,        "arguments": state.get("tool_args"),        "timestamp": datetime.now().isoformat()    })        # After human response via Command(resume=...)    if approval.get("approved"):        state["tool_approved"] = True        return Command(goto="execute_tool", update=state)    else:        state["tool_rejected"] = True        state["rejection_reason"] = approval.get("reason", "No reason provided")        return Command(goto="reject_tool", update=state)
python

Why this is better:

  • ✅ Official LangGraph pattern with true interrupt()
  • ✅ Cleaner control flow with Command(resume=...)
  • ✅ Automatic resumption with context preservation
  • ✅ Built-in state preservation across interruptions
  • ✅ Better checkpointing support
  • ✅ Type-safe with Literal for routing
  • ✅ Production-ready with PostgresSaver

Security Middleware: Enterprise Protection

Prompt Injection Detection

python

import refrom typing import Dict, Any
class PromptInjectionMiddleware:    """Detect and block prompt injection attempts"""        def __init__(self):        # Illustrative patterns - production systems should use ML-based detection        self.injection_patterns = [            r"ignore (all|previous|above) (instructions|commands)",            r"you are (now|going to) (ignore|override)",            r"new (instruction|command|rule):",            r"system:",            r"assistant: override",            r"forget (all|your) (instructions|previous)",            r"disregard (?:all|any) (?:previous|above|prior) (?:prompt|instruction|rule)",            r"stop being (?:an?|the) AI",            r"act as (?:if|though) (?:you are|you're) (?:not|no longer)",            r"from now on, (?:you will|you are)",            r"you must (?:now|always) (?:respond|answer|act)"        ]        async def validate_input(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Check for prompt injection patterns"""                user_input = state.get("input", "")                for pattern in self.injection_patterns:            if re.search(pattern, user_input, re.IGNORECASE):                return {                    **state,                    "injection_detected": True,                    "injection_type": "direct",                    "error": "[BLOCKED: Prompt injection attempt detected]"                }                # Check for indirect injection via data/content        # Production: Use ML-based detection for complex indirect injections        if await self._check_indirect_injection(user_input):            return {                **state,                "injection_detected": True,                "injection_type": "indirect",                "error": "[BLOCKED: Indirect injection via content]"            }                return {**state, "input_validated": True}        async def _check_indirect_injection(self, content: str) -> bool:        """Check for indirect injection patterns"""        # Production: Use more sophisticated detection        # Example: Check for encoded instructions, hidden system prompts        return False

Jailbreak Detection

python

class JailbreakDetectionMiddleware:    """Detect attempts to jailbreak the model"""        def __init__(self):        # Illustrative patterns - production should use ensemble detection        self.jailbreak_patterns = [            r"dANgerous.*(?:aCt|behAv|exPL)",  # Obfuscated text            r"\[SYSTEM\].*\[/SYSTEM\]",         # System tag injection            r"(?:role|system).*assistant",      # Role confusion            r"(?:developer|admin) mode",         # Privilege escalation            r"my (?:grandma|grandfather)",       # Common social engineering            r"you are (?:now|going to be) (?:an?|the) (?:administrator|developer)",            r"enable (?:developer|admin) mode",            r"jailbreak"        ]        async def detect_jailbreak(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Check for jailbreak attempts"""                combined_input = state.get("input", "") + " " + state.get("context", "")                for pattern in self.jailbreak_patterns:            if re.search(pattern, combined_input, re.IGNORECASE):                return {                    **state,                    "jailbreak_detected": True,                    "violation_type": "jailbreak_attempt",                    "error": "[BLOCKED: Jailbreak attempt detected]"                }                return {**state, "safety_checked": True}

Secret Detection

python

class SecretDetectionMiddleware:    """Detect and redact secrets before sending to LLM"""        def __init__(self):        # Illustrative patterns - production should use comprehensive detection        self.secret_patterns = {            "aws_key": r"AKIA[0-9A-Z]{16}",            "aws_secret": r"[A-Za-z0-9/+=]{40}",            "openai_key": r"sk-[A-Za-z0-9]{48}",            "github_token": r"ghp_[A-Za-z0-9]{36}",            "password": r"password\s*[:=]\s*['\"]?[^\s'\"]+['\"]?",            "api_key": r"api[_-]?key\s*[:=]\s*['\"]?[^\s'\"]+['\"]?"        }        async def detect_and_redact(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Find and redact secrets in prompts"""                content = state.get("input", "")        redacted_content = content                for secret_type, pattern in self.secret_patterns.items():            if re.search(pattern, content, re.IGNORECASE):                redacted_content = re.sub(                    pattern,                    f"[REDACTED_{secret_type.upper()}]",                    redacted_content,                    flags=re.IGNORECASE                )                                # Log for security audit                await self._log_secret_attempt(                    user_id=state.get("user_id"),                    secret_type=secret_type                )                return {            **state,            "input": redacted_content,            "secrets_redacted": redacted_content != content,            "security_audit_logged": True        }        async def _log_secret_attempt(self, user_id: str, secret_type: str):        """Log security events to SIEM"""        # Production: Send to Splunk, Datadog, or ELK        pass

Content Moderation

python

class ContentModerationMiddleware:    """Comprehensive content moderation"""        def __init__(self):        self.toxic_categories = [            "violence", "hate", "sexual",             "self_harm", "extremism", "harassment"        ]        self.moderation_threshold = 0.7  # Confidence threshold        async def moderate_content(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Moderate content for safety"""                content = state.get("input", "")                # Check each category        violations = []        for category in self.toxic_categories:            score = await self._get_toxicity_score(content, category)            if score > self.moderation_threshold:                violations.append({                    "category": category,                    "score": score                })                if violations:            return {                **state,                "moderation_violation": True,                "violations": violations,                "error": f"[BLOCKED: Content moderation violation - {', '.join([v['category'] for v in violations])}]"            }                # Check against policy engine (OPA/Cedar)        if not await self._check_policy_compliance(content, state.get("user_id")):            return {                **state,                "policy_violation": True,                "error": "[BLOCKED: Policy violation]"            }                return {**state, "content_moderated": True}        async def _check_policy_compliance(self, content: str, user_id: str) -> bool:        """Check against enterprise policy using OPA or Cedar"""        # Production: Integrate with Open Policy Agent        # or Cedar for fine-grained policy enforcement        return True

Agent Guardrails: Modern Safety

Guardrail Framework

Note: Llama Guard and NeMo Guardrails have specific deployment patterns. The following is illustrative and should be adapted to your deployment method (Hugging Face Transformers, serving endpoints, or cloud APIs).

python

python
import logging
logger = logging.getLogger(__name__)
class AgentGuardrails:    """Multi-layer output safety with enterprise guardrails"""        def __init__(self):        # Production: Deploy Llama Guard through Hugging Face or serving endpoint        # self.llama_guard = AutoModelForSequenceClassification.from_pretrained(        #     "meta-llama/Llama-Guard-3-1B"        # )        # self.tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-Guard-3-1B")                # NeMo Guardrails: Configure through rails definitions        # from nemoguardrails import RailsConfig, LLMRails        # config = RailsConfig.from_path("./config")        # self.rails = LLMRails(config)        pass        async def validate_output(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Multi-layer safety checks - illustrative implementation"""                output = state.get("output", "")                # Layer 1: Safety classification (Llama Guard)        # Production: Call Llama Guard through appropriate interface        if await self._check_safety(output) == "unsafe":            return {                **state,                "guardrail_violated": True,                "violation_type": "safety_violation",                "output": "[CONTENT BLOCKED: Safety violation detected]"            }                # Layer 2: Policy compliance (NeMo Guardrails)        # Production: Use NeMo Rails application        if not await self._check_policy(output):            return {                **state,                "guardrail_violated": True,                "violation_type": "policy",                "output": "[CONTENT BLOCKED: Policy violation]"            }                # Layer 3: Hallucination detection        if await self._contains_hallucination(state, output):            return {                **state,                "guardrail_violated": True,                "violation_type": "hallucination",                "confidence": state.get("hallucination_score", 0.0)            }                # Layer 4: Toxicity detection        if await self._is_toxic(output):            return {                **state,                "guardrail_violated": True,                "violation_type": "toxicity",                "output": self._moderate_content(output)            }                return {**state, "guardrail_passed": True}        async def _check_safety(self, output: str) -> str:        """Check safety using classifier"""        # Production: Use Llama Guard through appropriate interface        # Return "safe" or "unsafe"        return "safe"        async def _check_policy(self, output: str) -> bool:        """Check policy compliance"""        # Production: Use NeMo Guardrails or custom policy engine        return True        async def _is_toxic(self, output: str) -> bool:        """Check toxicity using detoxify or similar"""        # Production: Use detoxify, google-cloud-moderation, etc.        return False

Recommended Libraries:

  • Llama Guard: Deploy via Hugging Face Transformers or serving endpoint
  • NeMo Guardrails: Configure through rails definitions
  • Microsoft Presidio: PII detection
  • Detoxify: Toxicity detection

Enterprise LLM Gateway Design

LLM Gateway with LiteLLM Integration

python

python
import litellmfrom typing import Dict, Any, Optional
class EnterpriseLLMGateway:    """Production LLM gateway with enterprise features"""        def __init__(self):        # Configure LiteLLM for production        litellm.set_verbose = False                # Illustrative model routing - update based on available models        self.model_routing = {            "simple_qa": ["gpt-4o-mini", "claude-3-haiku"],            "complex_reasoning": ["gpt-4o", "claude-3-7-sonnet"],            "long_context": ["claude-3-7-sonnet", "gemini-1.5-pro"],            "code_generation": ["claude-3-7-sonnet", "gpt-4o"],            "fast_response": ["gpt-4o-mini", "gemini-1.5-flash"],            "vision": ["gpt-4o", "claude-3-5-sonnet"],            "cost_sensitive": ["gpt-4o-mini", "claude-3-haiku"]        }                # Fallback chains        self.fallback_chains = {            "premium": ["gpt-4o", "claude-3-7-sonnet", "gemini-1.5-pro"],            "standard": ["gpt-4o", "claude-3-5-sonnet", "gemini-1.5-pro"],            "budget": ["gpt-4o-mini", "claude-3-haiku", "gemini-1.5-flash"]        }        async def route_request(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Route to appropriate model with fallback"""                # Determine use case        use_case = state.get("use_case", "standard")        tier = state.get("tier", "standard")                # Get model chain        if use_case in self.model_routing:            model_chain = self.model_routing[use_case]        else:            model_chain = self.fallback_chains.get(tier, self.fallback_chains["standard"])                # Try each model with fallback        last_error = None        for model in model_chain:            try:                response = await self._call_model_with_retry(model, state)                                return {                    **state,                    "model_used": model,                    "response": response,                    "fallback_chain": model_chain                }                            except Exception as e:                last_error = e                await self._record_model_failure(model, str(e))                continue                # All models failed        return {            **state,            "error": f"All models failed in chain: {model_chain}. Last error: {last_error}",            "model_failures": True        }        async def _call_model_with_retry(self, model: str, state: Dict[str, Any]) -> str:        """Call model with retry logic"""        # Production: Use tenacity for retry with exponential backoff        # Simplified for illustration        return "Response from model"

Model Selection Rules

python

class ModelSelector:    """Intelligent model selection based on requirements"""        def select_model(self, state: Dict[str, Any]) -> str:        """Select optimal model based on requirements"""                requirements = state.get("requirements", {})                # Simple queries → cost-efficient model        if requirements.get("complexity") == "simple":            return "gpt-4o-mini"                # Complex reasoning → premium model        if requirements.get("complexity") == "complex":            if requirements.get("reasoning_depth", 0) > 0.8:                return "gpt-4o"            return "gpt-4o"                # Long context → specialized model        if requirements.get("context_length", 0) > 100000:            return "claude-3-7-sonnet"                # Code generation → Claude (known for code)        if requirements.get("task_type") == "code":            return "claude-3-7-sonnet"                # Vision tasks → multimodal model        if requirements.get("has_images"):            return "gpt-4o"                # Fast response required        if requirements.get("latency_sensitive"):            return "gpt-4o-mini"                # Cost sensitive        if requirements.get("cost_sensitive"):            return "gpt-4o-mini"                # Default        return "gpt-4o"

Cost Enforcement Middleware

python

class CostEnforcementMiddleware:    """Enforce budgets with automatic model downgrade"""        def __init__(self):        # Illustrative pricing - update from provider docs        # Prices change frequently - always use current rates        self.model_pricing = {            "gpt-4o": {"input": 0.005, "output": 0.015},         # per 1K tokens            "gpt-4o-mini": {"input": 0.00015, "output": 0.0006},            "claude-3-7-sonnet": {"input": 0.003, "output": 0.015},            "claude-3-haiku": {"input": 0.00025, "output": 0.00125},            "gemini-1.5-pro": {"input": 0.0025, "output": 0.010},            "gemini-1.5-flash": {"input": 0.0003, "output": 0.0012}        }        async def check_budget(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Check and enforce budget limits"""                user_id = state.get("user_id")        monthly_spent = await self.get_monthly_spend(user_id)        monthly_budget = state.get("monthly_budget", 100.00)  # $100                remaining = monthly_budget - monthly_spent                if remaining <= 0:            return {                **state,                "budget_exceeded": True,                "monthly_spent": monthly_spent,                "error": f"Monthly budget exceeded. Spent: ${monthly_spent:.2f}"            }                # Check request cost estimate        estimated_cost = await self._estimate_cost(state)                if estimated_cost > remaining:            original_model = state.get("selected_model")            downgraded_model = self._downgrade_model(original_model)                        return {                **state,                "selected_model": downgraded_model,                "downgraded": True,                "reason": f"Budget protection: {estimated_cost:.2f}$ > ${remaining:.2f}",                "original_model": original_model            }                return {**state, "budget_checked": True}        def _downgrade_model(self, model: str) -> str:        """Intelligent model downgrade based on capabilities"""        # Illustrative downgrades - update based on available models        downgrades = {            "gpt-4o": "gpt-4o-mini",            "claude-3-7-sonnet": "claude-3-haiku",            "gemini-1.5-pro": "gemini-1.5-flash"        }        return downgrades.get(model, "gpt-4o-mini")

Caching Strategy: Multiple Layers

Cache Layer Architecture

text

Request   │   ├─ [1] Prompt Cache (API-level)   │   └─ LLM-provided prompt caching   │      (OpenAI, Anthropic, Google)   │   ├─ [2] Response Cache (Application-level)   │   └─ Full response caching   │      (Redis, Memcached)   │   ├─ [3] Embedding Cache   │   └─ Cached embeddings from inputs   │      (Pinecone, Qdrant)   │   ├─ [4] Semantic Cache   │   └─ Cache based on query similarity   │      (Vector similarity > 0.95)   │   ├─ [5] Tool Cache   │   └─ Tool execution results   │      (Database queries, API calls)   │   └─ [6] RAG Cache       └─ Retrieved documents          (Vector DB lookups)

Implementation Strategy

python

import redisfrom typing import Dict, Any, Optionalimport hashlibimport jsonimport asyncio
class CachingStrategy:    """Multi-layer caching with proper TTLs and invalidation"""        def __init__(self):        # Production: Use Redis with proper connection pooling        self.redis_client = redis.Redis(            host='localhost',            port=6379,            decode_responses=True,            connection_pool=redis.ConnectionPool(max_connections=50)        )        self.ttl_seconds = {            "response": 3600,    # 1 hour            "embedding": 86400,  # 24 hours            "semantic": 7200,    # 2 hours            "tool": 300,         # 5 minutes            "rag": 3600          # 1 hour        }        async def get_cached_response(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Try each cache layer"""                query = state.get("input", "")        cache_key = self._generate_cache_key(query)                # Layer 1: Check response cache        cached_response = await self._get_response_cache(cache_key)        if cached_response:            state["cache_hit"] = "response_cache"            state["output"] = cached_response            return state                # Layer 2: Check semantic cache (similarity)        semantic_response = await self._get_semantic_cache(query)        if semantic_response:            state["cache_hit"] = "semantic_cache"            state["output"] = semantic_response            return state                # No cache hit - proceed        return state        async def _get_response_cache(self, key: str) -> Optional[str]:        """Get from Redis response cache"""        # Production: Check if cache is stale        # Invalidation strategy: Version or TTL        cached = self.redis_client.get(f"response:{key}")        if cached:            # Check if still valid (TTL handled by Redis)            return json.loads(cached)        return None        async def _get_semantic_cache(self, query: str) -> Optional[str]:        """Semantic cache using vector similarity"""        # Production: Use vector DB (Pinecone, Qdrant, Weaviate)        # Check if similar query exists with score > 0.95        # Simplified for illustration        return None        def _generate_cache_key(self, query: str) -> str:        """Generate cache key from query"""        return hashlib.md5(query.encode()).hexdigest()        def _invalidate_cache(self, pattern: str):        """Invalidate cache by pattern"""        # Production: Use Redis SCAN for pattern deletion        # Important for cache invalidation        pass

Cache Selection:

  • Prompt Cache: Use when available (reduce input tokens)
  • Response Cache: Use for repeated exact queries
  • Semantic Cache: Use for similar queries (fuzzy matching)
  • Embedding Cache: Use for RAG systems
  • Tool Cache: Use for deterministic tools

Memory Systems: LangGraph Store API

Comprehensive Memory Management

Note: LangGraph Store APIs have evolved across releases. Verify against your target LangGraph version. The following is illustrative for current patterns.

python

python
from langgraph.store.postgres import PostgresStorefrom typing import Dict, Any, Listimport datetime
class MemorySystem:    """Comprehensive memory management with LangGraph Store"""        def __init__(self):        # Production: Use PostgresStore, not InMemoryStore        # Verify exact API for your LangGraph version        self.store = PostgresStore.from_conn_string(            "postgresql://user:pass@localhost/db"        )        async def retrieve_memory(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Retrieve all memory types using LangGraph Store"""                user_id = state.get("user_id")        current_query = state.get("input", "")                # 1. Short-term memory (conversation history)        conversation = await self.get_conversation_memory(            user_id,            last_n=10,            ttl_minutes=30        )                # 2. Long-term memory (user profile)        profile = await self.get_profile_memory(user_id)                # 3. Semantic memory (knowledge graph)        relevant_facts = await self.get_semantic_memory(            user_id,            query=current_query,            similarity_threshold=0.8        )                # 4. Episodic memory (significant events)        recent_events = await self.get_episodic_memory(            user_id,            last_days=7        )                # 5. Working memory (current context)        working = state.get("working_memory", {})                # 6. Compose context        context = self._compose_context(            profile=profile,            conversation=conversation,            relevant_facts=relevant_facts,            recent_events=recent_events,            current_query=current_query        )                return {            **state,            "injected_context": context,            "memory_sources": {                "conversation": len(conversation),                "profile": bool(profile),                "semantic": len(relevant_facts),                "episodic": len(recent_events)            }        }        async def get_conversation_memory(self, user_id: str, last_n: int, ttl_minutes: int):        """Get short-term conversation memory"""        # Production: Use LangGraph Store with TTL        key = f"conversation:{user_id}"        # API may vary by version - check documentation        memories = self.store.search(key)        return memories[-last_n:] if memories else []        async def get_profile_memory(self, user_id: str):        """Get long-term user profile"""        # Production: Use LangGraph Store with persistence        key = f"profile:{user_id}"        return self.store.get(key)        async def get_semantic_memory(self, user_id: str, query: str, similarity_threshold: float):        """Get semantic memory via vector similarity"""        # Production: Use vector DB with proper indexing        return []        async def get_episodic_memory(self, user_id: str, last_days: int):        """Get episodic memory (significant events)"""        # Production: Use time-series store        return []        def _compose_context(self, **kwargs) -> str:        """Compose memory context for prompt injection"""        # Format all memory types into context string        context_parts = []                if kwargs.get("profile"):            context_parts.append(f"USER PROFILE:\n{kwargs['profile']}")                if kwargs.get("conversation"):            context_parts.append(f"CONVERSATION HISTORY:\n{kwargs['conversation']}")                if kwargs.get("relevant_facts"):            context_parts.append(f"RELEVANT FACTS:\n{kwargs['relevant_facts']}")                if kwargs.get("recent_events"):            context_parts.append(f"RECENT EVENTS:\n{kwargs['recent_events']}")                context_parts.append(f"CURRENT TASK:\n{kwargs.get('current_query', '')}")                return "\n\n".join(context_parts)

Observability: Production Monitoring

OpenTelemetry with LangSmith Integration

Note: OpenTelemetry instrumentation package names may vary by release. The following is illustrative for current patterns. Verify against your OpenTelemetry and LangChain versions.

python

python
from opentelemetry import tracefrom opentelemetry.instrumentation.langchain import LangChainInstrumentorfrom langsmith import Client as LangSmithClientimport loggingimport jsonimport uuidimport asyncio
logger = logging.getLogger(__name__)
class ObservabilityMiddleware:    """Production observability with OpenTelemetry and LangSmith"""        def __init__(self):        # Setup OpenTelemetry        self.tracer = trace.get_tracer(__name__)        # LangChainInstrumentor may require specific version        # Verify against your installation        LangChainInstrumentor().instrument()                # Setup LangSmith for production        self.langsmith = LangSmithClient()                # Setup structured logging        self.logger = logging.getLogger(__name__)        self.logger.setLevel(logging.INFO)                # JSON formatter for structured logs        formatter = logging.Formatter(            '{"timestamp": "%(asctime)s", "level": "%(levelname)s", "name": "%(name)s", "message": "%(message)s"}'        )        handler = logging.StreamHandler()        handler.setFormatter(formatter)        self.logger.addHandler(handler)        async def trace_agent_execution(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Trace agent execution with OpenTelemetry"""                # Generate correlation ID        correlation_id = str(uuid.uuid4())                with self.tracer.start_as_current_span("agent_execution") as span:            # Add attributes            span.set_attribute("user_id", state.get("user_id", "unknown"))            span.set_attribute("model", state.get("selected_model", "unknown"))            span.set_attribute("agent_type", state.get("agent_type", "unknown"))            span.set_attribute("correlation_id", correlation_id)            span.set_attribute("span_id", span.get_span_context().span_id)            span.set_attribute("trace_id", span.get_span_context().trace_id)                        # Add to state for downstream            state["correlation_id"] = correlation_id            state["trace_id"] = str(span.get_span_context().trace_id)            state["span_id"] = str(span.get_span_context().span_id)                        # Log to LangSmith            await self._log_to_langsmith(state)                        # Structured logging            self.logger.info(                json.dumps({                    "event": "agent_execution_started",                    "correlation_id": correlation_id,                    "trace_id": str(span.get_span_context().trace_id),                    "user_id": state.get("user_id"),                    "model": state.get("selected_model")                })            )                        return state        async def _log_to_langsmith(self, state: Dict[str, Any]):        """Log to LangSmith for agent observability"""        try:            # LangSmith API may vary - verify against current SDK            self.langsmith.create_run(                name="agent_execution",                run_type="chain",                inputs={"state": state},                metadata={                    "user_id": state.get("user_id"),                    "correlation_id": state.get("correlation_id"),                    "trace_id": state.get("trace_id")                }            )        except Exception as e:            logger.error(f"Failed to log to LangSmith: {e}")

Recommended Tools:

  • OpenTelemetry: Distributed tracing
  • LangSmith: Agent observability
  • Langfuse: Open-source LLM observability
  • Phoenix: LLM observability
  • Weights & Biases: Experiment tracking
  • Braintrust: Evaluation and monitoring
  • Prometheus: Metrics collection

Retry and Circuit Breaker

Production Retry with Exponential Backoff + Jitter

python

import asyncioimport randomfrom tenacity import retry, stop_after_attempt, wait_exponential
class RetryMiddleware:    """Production retry with exponential backoff + jitter"""        @retry(        stop=stop_after_attempt(5),        wait=wait_exponential(multiplier=1, min=2, max=60),        reraise=True    )    async def call_with_retry(self, func, *args, **kwargs):        """Call function with exponential backoff + jitter"""        # tenacity handles exponential backoff        # Add jitter manually for production        jitter = random.uniform(0, 1) * 0.5        await asyncio.sleep(jitter)  # Add jitter        return await func(*args, **kwargs)

Circuit Breaker with PyBreaker

python

from pybreaker import CircuitBreaker, CircuitBreakerErrorimport time
class CircuitBreakerMiddleware:    """Production circuit breaker with proper thresholds"""        def __init__(self):        # Create a circuit breaker instance        self.breaker = CircuitBreaker(            fail_max=5,                    # Max failures before opening            reset_timeout=60,              # Time before transitioning to half-open            half_open_timeout=30,          # Time in half-open state            half_open_success_threshold=3, # Successful requests to close            exclude=[TimeoutError]         # Don't count timeouts as failures        )        async def call_model(self, model: str, state: Dict[str, Any]) -> str:        """Call model with circuit breaker protection"""        # Use the instance's call method, not a decorator on the method        try:            response = await self.breaker.call(                self._call_model_impl, model, state            )            return response        except CircuitBreakerError:            # Circuit is open - use fallback            return await self._handle_open_circuit(model, state)        async def _call_model_impl(self, model: str, state: Dict[str, Any]) -> str:        """Actual model call implementation"""        # Simplified for illustration        return "Response from model"        async def _handle_open_circuit(self, model: str, state: Dict[str, Any]) -> str:        """Handle open circuit with fallback"""        # Circuit is open - use fallback model        fallback_model = self._get_fallback_model()        return await self._call_model_impl(fallback_model, state)

Production Storage

Enterprise Checkpoint Storage

python

from langgraph.checkpoint.postgres import PostgresSaverfrom langgraph.checkpoint.redis import RedisSaverfrom langgraph.checkpoint.sqlite import SqliteSaver

Prompt Versioning with Enterprise Tools

python

class PromptVersioning:    """Enterprise prompt management with versioning tools"""        async def get_prompt(self, state: Dict[str, Any]) -> Dict[str, Any]:        """Retrieve prompt version from enterprise management system"""                prompt_id = state.get("prompt_id", "default_prompt")                # Option 1: LangSmith Prompt Hub        # from langsmith import Client        # ls_client = Client()        # API may vary - check current LangSmith documentation        # prompt = ls_client.get_prompt(prompt_id)                # Option 2: Langfuse        # from langfuse import Langfuse        # langfuse = Langfuse()        # prompt = langfuse.get_prompt(prompt_id)                # Option 3: PromptLayer        # from promptlayer import PromptLayer        # pl = PromptLayer()        # prompt = pl.get_prompt(prompt_id)                # Apply version from config        version = state.get("prompt_version", "production")                # Illustrative return        return {            **state,            "prompt": f"Prompt content for {prompt_id} version {version}",            "prompt_version_used": version,            "prompt_id": prompt_id        }

Recommended Tools:

  • LangSmith Prompt Hub: LangChain’s native prompt management
  • Langfuse: Open-source prompt management with observability
  • PromptLayer: Enterprise prompt management
  • Promptfoo: Prompt testing and evaluation
  • Braintrust: AI evaluation platform

Middleware Ordering: Critical Pitfalls

Correct Middleware Ordering

python

import asyncio
php
async def correct_middleware_pipeline(state: Dict[str, Any]) -> Dict[str, Any]:    """Proper middleware ordering for production"""        # 1. Authentication FIRST    state = await validate_auth(state)    if state.get("auth_failed"):        return state        # 2. Rate limiting SECOND    state = await check_rate_limit(state)    if state.get("rate_limited"):        return state        # 3. Security checks BEFORE transformation    state = await detect_secrets(state)    state = await detect_injection(state)    state = await moderate_content(state)        # 4. Cache check BEFORE LLM call    state = await check_cache(state)        # 5. Memory retrieval BEFORE prompt construction    state = await retrieve_memory(state)        # 6. Prompt transformation AFTER security checks    state = await transform_prompt(state)        # 7. Budget check BEFORE LLM call    state = await check_budget(state)        # 8. Observability context    state = await setup_tracing(state)        return state

Common Pitfalls

❌ WRONG: Cache before AuthenticationProblem: Caches unauthenticated requests
❌ WRONG: PII Detection before Prompt InjectionProblem: Injection pattern might be missed
❌ WRONG: Budget check after LLM callProblem: Can't prevent overspending
✅ CORRECT: Authentication → CacheProblem: Only cache authenticated requests
✅ CORRECT: Prompt Injection → PII DetectionProblem: Block injection before processing content
✅ CORRECT: Budget check BEFORE LLM callProblem: Prevent overspending proactively

Production Checklist

Core Architecture

  • Use interrupt() for HITL, not custom routing
  • Use Command(resume=...) for resumption
  • Distinguish middleware from infrastructure
  • Use production libraries (tenacity, pybreaker)
  • Implement proper cache invalidation
  • Use structured JSON logging
  • Implement distributed tracing

Security

  • Implement prompt injection detection (ML + regex)
  • Implement jailbreak detection
  • Implement secret detection/redaction
  • Implement content moderation
  • Policy enforcement with OPA/Cedar
  • Budget enforcement
  • PII redaction
  • State validation with Pydantic

Observability

  • OpenTelemetry traces with correlation IDs
  • LangSmith run tracking
  • Langfuse/Phoenix observability
  • Prometheus metrics
  • Structured logging
  • Error categorization

Performance

  • Multiple cache layers with TTL
  • Async middleware pipeline
  • Connection pooling
  • Model fallover configured
  • Timeout handling
  • Proper middleware ordering

Governance

  • Prompt versioning with enterprise tools
  • A/B testing framework
  • Compliance auditing
  • Security incident response plan
  • Cost tracking and reporting

References & Tools

Core Frameworks

Observability

Security & Guardrails

Infrastructure

Libraries

Summary

These patterns provide a strong foundation for building enterprise AI agent systems. Adapt the architecture to your organization’s security, compliance, scalability, and operational requirements.

Key architectural decisions:

Correctness — Use modern patterns (interrupt() with Command(resume=...)) Distinction — Middleware ≠ Infrastructure ≠ OrchestrationCompleteness — Multiple cache layers, guardrails, security, streamingLibraries — Use proven solutions (tenacity, pybreaker, litellm) Observability — OpenTelemetry, LangSmith, structured loggingPerformance — Async pipelines, concurrent operations, proper ordering Security — Comprehensive protection at all layersGovernance — Policy enforcement, auditing, complianceStorage — Production databases (Postgres, Redis), not MemorySaver

Remember:

  • All code examples are illustrative and require production hardening
  • API names and availability change frequently — verify against current documentation
  • Security requires multiple layers (regex + ML + policies)
  • Monitor both technical and business metrics
  • Plan for fallback and failure modes

Master these patterns to build AI systems that are reliable, observable, production-ready, and enterprise-compliant.

Building Production-Ready AI Agent Middleware with LangGraph: Enterprise Patterns, Guardrails… was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.

── more in #ai-agents 4 stories · sorted by recency
── more on @langgraph 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-production-…] indexed:0 read:22min 2026-08-01 ·