AI LLM ARCH

Building Resilient LLM Chains in Production

2024.02.28 | 8 min read | Bhavesh Kumar Parmar
AI LLM Architecture

Everyone talks about chaining LLMs. Few talk about what happens when chain link #3 hallucinates a JSON key that crashes link #4, at 3 AM, with 10,000 requests in the queue. This is what production LLM infrastructure actually looks like.

The Failure Taxonomy

After running multi-agent LLM systems in production for 8 months, I've categorized failures into 4 types. Understanding these shapes your entire defensive architecture:

Type 1: Schema Drift

LLM outputs valid JSON but with unexpected keys, missing fields, or wrong types. Happens 15-20% of the time.

Type 2: Hallucination Loops

Agent A asks Agent B for clarification, B hallucinates context, A accepts it, loop amplifies. Catastrophic at scale.

Type 3: Context Overflow

Accumulated chain context exceeds token limits. The model silently drops early instructions, changing behavior.

Type 4: Latency Cascade

One slow inference (8s instead of 2s) causes timeouts downstream. The chain collapses like dominoes.

Defense Layer 1: Structural Validation

Never trust an LLM's output structure. Every chain link should validate its input and output against a strict schema. We use Pydantic models with fallback defaults:

from pydantic import BaseModel, Field, validator
from typing import Optional

class AnalysisResult(BaseModel):
    severity: str = Field(default="unknown")
    root_cause: str = Field(default="unidentified")
    confidence: float = Field(default=0.0, ge=0.0, le=1.0)
    suggested_action: Optional[str] = None

    @validator('severity')
    def validate_severity(cls, v):
        allowed = ['critical', 'high', 'medium', 'low', 'unknown']
        return v.lower() if v.lower() in allowed else 'unknown'

def safe_parse(raw_output: str) -> AnalysisResult:
    """Parse LLM output with graceful degradation."""
    try:
        return AnalysisResult.model_validate_json(raw_output)
    except Exception:
        # Extract what we can, default the rest
        return AnalysisResult()

Defense Layer 2: Circuit Breakers

Borrowed from microservices architecture, circuit breakers prevent cascade failures. If an LLM endpoint fails 3 times in 60 seconds, we trip the breaker and route to a fallback (smaller model, cached response, or human escalation).

class LLMCircuitBreaker:
    def __init__(self, failure_threshold=3, reset_timeout=60):
        self.failures = 0
        self.threshold = failure_threshold
        self.reset_timeout = reset_timeout
        self.last_failure = 0
        self.state = "CLOSED"  # CLOSED -> OPEN -> HALF_OPEN

    async def call(self, func, *args, **kwargs):
        if self.state == "OPEN":
            if time.time() - self.last_failure > self.reset_timeout:
                self.state = "HALF_OPEN"
            else:
                return await self.fallback(*args, **kwargs)

        try:
            result = await asyncio.wait_for(func(*args, **kwargs), timeout=10)
            self.failures = 0
            self.state = "CLOSED"
            return result
        except (asyncio.TimeoutError, Exception):
            self.failures += 1
            self.last_failure = time.time()
            if self.failures >= self.threshold:
                self.state = "OPEN"
            return await self.fallback(*args, **kwargs)

Defense Layer 3: Context Windowing

Instead of passing the entire chain history forward, we implemented a sliding context window with semantic compression. At each chain step, we summarize the previous context to fit within 30% of the model's token budget, preserving the remaining 70% for the current task.

The compression itself is done by a fast, small model (Haiku) — it costs pennies and saves dollars by preventing the expensive model from receiving degraded prompts.

Results After 6 Months

94%
Chain Success Rate
3.2s
Avg. E2E Latency
0
Cascade Failures/Mo

The key insight: treat LLM chains like distributed microservices, not like function calls. Every link can fail. Design for it.

End of Transmission