Stop Trusting One Model to Do Everything: A Practical Guide to Composite AI Systems

Learn how to architect composite AI systems that combine LLMs, reasoning engines, and retrieval layers into a reliable, cost-efficient production pipeline.

The single-model era of AI is ending. Not because large language models have failed, but because they have matured enough that teams can see exactly where they break: hallucinations on factual queries, ballooning costs when every request routes through the heaviest model, latency problems when complex reasoning holds up a simple request, and brittleness whenever the task drifts outside a model's core training distribution.

The answer is composite AI: architecting heterogeneous systems that combine specialized models, reasoning engines, retrieval layers, and external tools into a unified production pipeline. Gartner named it a top-ten strategic technology trend for 2026, and the enterprise data backs the hype. Organizations using composite patterns complete AI projects at 2.4 times the rate of those relying on monolithic setups. The shift is not about using more AI. It is about using the right AI for each subtask.

This guide walks through the full architecture using a single running example: an AI-powered customer support pipeline. By the end you will have a mental model of every layer, the failure modes that surface in production, and the design decisions that keep costs under control.


The Running Example: Customer Support as a Composite Pipeline

Imagine a support request arrives: "My invoice from last month shows a charge I don't recognize. I'm also having trouble connecting my device after the latest firmware update."

A monolithic approach pipes this entire query into a large model and hopes for the best. A composite system decomposes the request:

  1. Classify the query (billing issue, technical issue, or both)
  2. Route each thread to the right specialist component
  3. Retrieve relevant knowledge (past invoices, device firmware logs, known bug reports)
  4. Reason about the technical issue if diagnostic logic is required
  5. Validate every output before the next stage consumes it
  6. Aggregate responses into a coherent reply

Each step uses a different capability. Routing that entire job to Claude Opus or a comparable frontier model every time is expensive, slow, and unnecessary. Composing specialized pieces is how you get a system that is faster, cheaper, and more reliable all at once.


Layer 1: The Orchestration Architecture

The orchestrator is the conductor. It decides which component handles which subtask, manages context hand-offs between components, and coordinates retries when something fails. Three patterns dominate in production.

Centralized orchestrator-worker is the simplest: one orchestrator receives all requests, fans out tasks to worker agents, and collects results. Easy to reason about and easy to debug. The tradeoff is a single point of failure and a bottleneck at scale.

Decentralized peer-to-peer mesh has agents communicate directly with each other without a central coordinator. Highly resilient because there is no single failure point. The cost is complexity: debugging why agent B sent the wrong context to agent C when A was supposed to mediate is genuinely hard.

Hierarchical multi-tier balances both. A top-level orchestrator handles coarse routing and lifecycle management. Sub-orchestrators within each domain (billing, technical support) manage their own specialist workers. This maps cleanly onto how organizations already divide expertise, which makes it the most common pattern in enterprise systems.

For the customer support example, hierarchical is the right call. A top-level orchestrator routes billing threads to a billing sub-orchestrator and technical threads to a diagnostics sub-orchestrator. Each sub-orchestrator knows its own domain's components.


Layer 2: Output Schema Validation at Every Boundary

This is the piece that teams almost always skip and almost always regret. In a composite system, one component's output is the next component's input. One malformed output cascades failures throughout the entire pipeline.

The fix is enforcing a strict output schema at every component boundary. In Python, Pydantic is the standard choice.

from pydantic import BaseModel
from enum import Enum

class QueryCategory(str, Enum):
    BILLING = "billing"
    TECHNICAL = "technical"
    BOTH = "both"
    OUT_OF_SCOPE = "out_of_scope"

class ClassificationResult(BaseModel):
    category: QueryCategory
    confidence: float  # 0.0 to 1.0
    billing_summary: str | None = None
    technical_summary: str | None = None

def classify_query(raw_query: str) -> ClassificationResult:
    # Call a fast model (e.g. claude-haiku-4-5) here
    response_text = fast_model.call(CLASSIFIER_PROMPT, raw_query)
    # Parse and validate: Pydantic raises immediately if the schema is wrong
    return ClassificationResult.model_validate_json(response_text)

If the classification model returns a confidence score of "high" instead of a float, the pipeline catches it immediately at this boundary instead of propagating a string comparison error three components downstream. Validation frameworks eliminate the majority of production cascade failures.


Layer 3: Fast and Slow Reasoning (System 1 and System 2)

Not every task deserves the same depth of reasoning. Cognitive science offers a useful frame here.

System 1 reasoning is fast, intuitive, and pattern-matching. For AI systems, standard LLM inference embodies this mode. Given a simple billing question ("What is the cancellation policy?"), a fast model answers correctly in milliseconds at minimal cost. There is nothing to gain from deliberate step-by-step reasoning here.

System 2 reasoning is slow, deliberate, and sequential. Reasoning models (o1-style, chain-of-thought, and extended-thinking variants) write out intermediate steps, check their own logic, and iterate until they reach a confident answer. For the technical support thread in our example (diagnosing a firmware connectivity failure by correlating device logs, firmware release notes, and known bug patterns), System 2 reasoning is worth the cost.

The composite system adds a metacognition layer between the two: a lightweight decision component that looks at the incoming task and routes it.

def route_to_reasoning_tier(task: dict) -> str:
    complexity_score = estimate_complexity(task)

    if complexity_score < 0.4:
        return "system1"  # Fast model, cheap
    elif complexity_score < 0.75:
        return "system1_with_validation"  # Fast model + output check
    else:
        return "system2"  # Reasoning model, expensive but accurate

def estimate_complexity(task: dict) -> float:
    # Heuristics: multi-hop, contradictory constraints,
    # requires numeric reasoning, involves multiple data sources
    signals = [
        task.get("requires_multi_hop", False),
        task.get("data_sources_count", 1) > 2,
        task.get("contradictory_constraints", False),
    ]
    return sum(signals) / len(signals)

This routing logic does not need to be sophisticated. Simple heuristics cover most cases. The key insight: never default to System 2 for everything. The cost amplification on reasoning models is real, and most tasks do not need it.


Retrieval-Augmented Generation has evolved well past the pattern of "embed a query, find the nearest chunks, stuff them in the prompt." Production RAG is a modular architecture with a retriever, a reranker, and a generator, each independently testable and replaceable.

Three retrieval paradigms matter in practice.

Single-pass retrieval fires one query against a vector index and returns the top-k results. Fast and predictable, it is appropriate for narrow-domain knowledge bases where documents are relatively uniform in topic and length.

Iterative retrieval re-queries the corpus as generation progresses. The model retrieves a first set of documents, begins generating, recognizes a gap in its knowledge, and retrieves again. Accuracy improves; latency increases.

Multi-hop retrieval decomposes complex queries into sequential subquestions. "Why did my device fail after the firmware update?" becomes: (1) What changed in the latest firmware build? (2) Which device models are affected? (3) What are the known failure modes for this hardware and this firmware version? Each hop retrieves targeted context for the next reasoning step.

For technical support, multi-hop retrieval is the right choice. A two-stage pipeline (broad vector search to find candidate documents, followed by a cross-encoder reranker to filter to the top five) balances latency against quality.

When to add a knowledge graph layer. Vector search finds semantically similar text. It does not follow relationships. If your knowledge base contains structured relationships (this device model is compatible with this firmware version, this bug affects customers on this subscription tier), graph-enhanced RAG outperforms pure vector search. The vector layer handles semantic similarity; the graph layer traverses explicit connections between entities.


Layer 5: Agentic RAG (When Retrieval Becomes a Decision)

Classical RAG is a static pipeline: retrieve once, generate once. Agentic RAG treats retrieval as a dynamic decision that an agent makes mid-reasoning.

The agent decides what to retrieve, when to retrieve it, and whether the retrieved content is sufficient or whether another retrieval pass is needed. This mirrors how a skilled human researcher operates: you do not read every document before forming an opinion; you seek information iteratively, evaluate each piece against your current understanding, and stop when you are confident.

The practical architecture looks like this:

class AgenticRetriever:
    def __init__(self, vector_store, graph_store, reranker, tools):
        self.vector_store = vector_store
        self.graph_store = graph_store
        self.reranker = reranker
        self.tools = tools
        self.memory = []  # Short-term context for this session

    def research(self, question: str, max_hops: int = 4) -> str:
        context = []

        for hop in range(max_hops):
            # Agent decides what to search for next
            search_query = self.plan_next_retrieval(question, context)
            if search_query is None:
                break  # Agent decided it has enough context

            # Retrieve and add to context
            docs = self.vector_store.search(search_query, top_k=10)
            reranked = self.reranker.rank(docs, question)[:5]
            context.extend(reranked)

            # Agent evaluates sufficiency
            if self.is_context_sufficient(question, context):
                break

        return self.generate_answer(question, context)

    def plan_next_retrieval(self, question, current_context):
        # A fast model decides the next search query
        # Returns None if no further retrieval is needed
        ...

    def is_context_sufficient(self, question, context):
        # A fast model judges whether the current context
        # is enough to answer the question confidently
        ...

Frameworks like LangGraph, LlamaIndex, and LangChain all provide primitives for this pattern. LangGraph's graph-based state management is particularly well-suited for systems where the retrieval path branches or loops based on intermediate findings.


Layer 6: Tool Integration (Connecting Reasoning to Reality)

Retrieval gives the agent knowledge. Tools give it agency.

In the customer support example, the diagnostics agent needs more than retrieved documents. It may need to call a device telemetry API to pull actual error logs from the customer's device, query the billing system to confirm the charge in question, or check an internal knowledge base for known regressions in a firmware version. These are all tool calls.

The architecture defines tools as structured callable interfaces with validated inputs and outputs. The agent reasons about which tool to call and what arguments to pass. The tool executes, returns structured data, and the agent incorporates the result into its reasoning.

from pydantic import BaseModel

class DeviceTelemetryInput(BaseModel):
    device_id: str
    since_timestamp: str  # ISO 8601

class DeviceTelemetryOutput(BaseModel):
    error_codes: list[str]
    last_connected: str
    firmware_version: str

def fetch_device_telemetry(input: DeviceTelemetryInput) -> DeviceTelemetryOutput:
    # Call internal telemetry API
    raw = telemetry_api.get(input.device_id, input.since_timestamp)
    return DeviceTelemetryOutput(**raw)

Production considerations: tools fail. APIs time out, rate-limit, or return malformed responses. The orchestrator needs a timeout and retry policy for every tool. The agent also needs to handle the case where a tool returns an error and decide whether to retry, use a fallback source, or escalate to a human.


Layer 7: State and Memory Management

A composite system running multiple reasoning steps across multiple agents accumulates state: what has been retrieved, what tools have been called, what the agent has concluded so far, and what constraints still apply.

Short-term memory lives in the conversation context for the current task. It is fast, cheap, and ephemeral. The risk is context-window overflow: a long multi-hop reasoning chain with multiple retrieval results can easily exceed even a 200k-token context window if not managed carefully. Production systems summarize older context as the session progresses, pruning detail while preserving key conclusions.

Long-term memory bridges sessions. A vector database stores past interaction patterns, resolved issues, and customer-specific context. A knowledge graph stores structured facts and relationships that change slowly. Both are queried at the start of a new session to seed the short-term context.

LangGraph's checkpointer pattern is the standard approach for persisting state between agent decisions. Every time an agent takes an action (retrieving documents, calling a tool, or producing an intermediate conclusion), the state is written to a durable store. This enables two capabilities that production systems require: resumability (if the process crashes mid-reasoning, it can restart from the last checkpoint without losing work) and human-in-the-loop review (a human can inspect the agent's reasoning at any checkpoint and override or redirect it).

For storage backend selection: Redis is appropriate when you need sub-millisecond latency for hot state. PostgreSQL is appropriate when durability and queryability matter more than raw speed. Vector databases (Pinecone, Weaviate, pgvector) are appropriate for semantic retrieval over long-term memory. Most production systems use all three in combination.


Layer 8: Cost Optimization Through Intelligent Model Routing

This is the practical payoff of the entire composite approach. When you architect a system with multiple model tiers and explicit routing logic, you can cut LLM spend by 60 to 70 percent compared to routing everything through the heaviest model.

The routing decision is straightforward in principle. Use a fast small model for classification, filtering, and high-confidence simple responses. Use a mid-tier model for the majority of balanced reasoning tasks. Reserve the heavy model for genuinely difficult problems that smaller models get wrong.

class ModelRouter:
    TIERS = {
        "fast": "claude-haiku-4-5",       # Classification, filtering, simple Q&A
        "balanced": "claude-sonnet-4-6",   # Most reasoning tasks
        "heavy": "claude-opus-4-5",        # Complex multi-step, high-stakes decisions
    }

    def select_model(self, task: dict) -> str:
        # Start optimistic (cheapest model)
        if task["complexity"] < 0.3 and task["confidence_threshold"] < 0.9:
            return self.TIERS["fast"]

        if task["complexity"] < 0.7:
            return self.TIERS["balanced"]

        # Only invoke heavy model when truly necessary
        return self.TIERS["heavy"]

    def escalate_if_needed(self, result, task) -> str | None:
        """If the result fails quality checks, escalate to the next tier."""
        if result.confidence < task["confidence_threshold"]:
            current = result.model_used
            if current == self.TIERS["fast"]:
                return self.TIERS["balanced"]
            if current == self.TIERS["balanced"]:
                return self.TIERS["heavy"]
        return None  # No escalation needed

The routing logic can start as simple heuristics (complexity score, confidence threshold, task category) and evolve into a learned classifier as you accumulate data about which task types succeed at each tier. Production systems measure cost per task type continuously and adjust routing thresholds when the economics shift.


Putting It All Together

Return to the customer support request: "My invoice shows an unrecognized charge. I'm also having trouble connecting my device."

Here is how the composite pipeline processes it:

  1. The fast model classifies the request as BOTH with a confidence of 0.94. Schema validation confirms the output is well-formed.
  2. The orchestrator forks the task into two parallel threads: billing and technical.
  3. The billing thread routes to System 1. A balanced model retrieves the customer's recent invoices via tool call, identifies the charge (a prorated upgrade fee), and drafts an explanation.
  4. The technical thread routes to System 2 after the metacognition layer scores the complexity at 0.81. An agentic RAG loop queries the firmware release notes, calls the device telemetry API to fetch the actual error codes, and finds a known connectivity regression in firmware 3.2.1 affecting this device model. The reasoning model synthesizes a diagnostic explanation and recommends a rollback step.
  5. Both threads return validated structured responses.
  6. The aggregator merges the two responses into a single coherent reply, personalizes it with the customer's name from the session context, and surfaces it to the support agent (or sends it directly if confidence across both threads is high enough to skip human review).

Total latency: under three seconds for a response that would have taken minutes of human triage. The billing thread used a cheap model; only the technical thread invoked reasoning-tier inference. Cost per interaction is a fraction of what a monolithic heavy-model approach would cost.


Conclusion

Composite AI systems are not a new category of product. They are a design discipline: decomposing complex AI tasks into subtasks that match the right capability to the right component, validating every handoff, and routing work efficiently across a heterogeneous model fleet.

The patterns are not complicated in isolation. A classifier routes to a fast model. A reasoning task escalates to a slower one. RAG retrieves context in multiple hops. An agentic loop decides when retrieval is finished. State persists across steps. Each layer is individually understandable. The architectural challenge is composing them reliably and maintaining observability across the entire graph so that when something fails in production, you know exactly which component produced the bad output.

Start with the orchestration pattern that matches your team's operational maturity (centralized is easier to debug, hierarchical scales better). Add schema validation at every boundary before you add any new component. Layer in retrieval, reasoning tiers, and tool use as your use case demands. Measure cost per task type from day one. The systems that succeed are not the ones built on the most capable model available; they are the ones built on the most disciplined architecture around a portfolio of models.

Subscribe to Marvin G6 | Engineering, AI & Automation

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe