TL;DR
-
Production AI systems are layered pipelines, not single API calls - ingestion, indexing, orchestration, retrieval, generation, and post-processing each have different scaling and failure profiles.
-
The orchestration layer is the control plane - it routes requests, manages state, invokes tools, enforces timeouts, and coordinates multi-step workflows without embedding business logic in prompt strings.
-
Separate offline indexing from online query paths - re-embedding documents should not require redeploying your API, and query latency should not depend on batch ingestion jobs.
-
Every layer boundary needs observability, caching, and fallbacks - trace IDs across retrieval and generation, degrade gracefully when a vector DB is slow, and never let one failed tool call hang the entire request.
-
Start with a thin orchestrator and explicit contracts - typed inputs/outputs between layers beat a monolithic LangChain chain that nobody can debug at 2 AM.
Why This Matters
Most AI demos are a single Python script: embed a query, call GPT, return text. Production systems look nothing like that. They serve thousands of concurrent users, enforce tenant isolation, integrate with CRMs and databases, survive provider outages, and must be debugged when a customer reports a wrong answer from three days ago.
Without deliberate architecture, teams accumulate spaghetti: prompts embedded in API handlers, retrieval logic duplicated across endpoints, no way to A/B test a new reranker, and incident response that starts with "which service even logged this?" The orchestration layer - often dismissed as "just glue code" - is where reliability, cost control, and security actually live.
If you're moving from prototype to production, or inheriting a system that's grown organically, understanding AI system architecture is how you avoid rebuilding from scratch after the first serious outage.
The Problem AI System Architecture Solves
LLM applications combine familiar backend concerns (APIs, databases, auth) with unfamiliar ones (non-deterministic outputs, context windows, embedding drift, prompt injection). Without structure, three problems compound:
Unbounded complexity. A support bot becomes a RAG pipeline, then adds tool calling, then multi-step agents, then human escalation - all in one 800-line file. Changes become risky; testing becomes impossible.
Operational blindness. When latency spikes or answers degrade, you need to know: Was retrieval slow? Did the wrong model get routed? Did a tool timeout? Monolithic designs make root cause analysis guesswork.
Scaling mismatches. Embedding 10 million documents is batch work. Answering a user query is latency-sensitive. Coupling them means ingestion jobs starve query traffic, or you over-provision everything.
Architecture solves this by defining layers with clear contracts, async boundaries, and operational hooks (tracing, caching, circuit breakers) at every seam.
What Is AI System Architecture?
AI system architecture is the structural design of a production LLM application: how data flows from sources to users, which components own which responsibilities, and how the system behaves under load, failure, and change.
It is not a model choice or a framework selection. It is the blueprint that answers:
- Where does knowledge live, and how is it updated?
- Who orchestrates multi-step reasoning - application code, a workflow engine, or an agent loop?
- What happens when OpenAI returns 503, the vector DB is degraded, or retrieval returns zero results?
- How do you deploy a new reranker without a full system redeploy?
The reference pattern for most production systems today:
Sources → Ingestion → Index → [Orchestration] → Retrieve → Generate → Post-process → Client
↑
Tools / APIs / Agents
The orchestration layer sits at the center. It is not the LLM. It is the code (or framework) that decides what to call, in what order, with what timeouts and fallbacks.
How AI System Architecture Works
A production AI system decomposes into layers that can be developed, scaled, and monitored independently.
Layer 1: Ingestion and Indexing (Offline)
Documents, tickets, code, or database rows are parsed, chunked, embedded, and stored. This path is throughput-oriented: batch jobs, queues, retries, idempotent upserts. Latency is measured in minutes or hours, not milliseconds.
Key properties: versioning (embedding model ID in metadata), incremental updates (CDC, webhooks), and separation of raw storage from vector indexes (re-embed without re-parsing).
Layer 2: Orchestration (Online Control Plane)
The orchestration layer receives user requests and coordinates execution. Responsibilities include:
-
Session and state management - conversation history, user context, tenant ID.
-
Routing - which pipeline, model, or agent handles this request.
-
Workflow execution - sequential or parallel steps with defined inputs/outputs.
-
Tool invocation - schema validation, permission checks, timeouts.
-
Error handling - retries, fallbacks, partial responses.
-
Observability emission - trace spans, cost attribution, structured logs.
Frameworks like LangChain and LlamaIndex provide orchestration primitives (chains, graphs, agents). In mature systems, teams often extract a thin custom orchestrator that calls framework components as libraries rather than owning the entire stack.
Layer 3: Retrieval (Online Data Plane)
Given a query (or agent-generated sub-query), retrieval returns relevant chunks from the index. Hybrid search, metadata filters, and reranking live here. Retrieval must enforce authorization at the database query level - never rely on the LLM to filter sensitive documents.
Layer 4: Generation (Online Compute Plane)
The LLM synthesizes output from prompt + context + tools. This layer is latency and cost sensitive. Streaming, model routing, and prompt caching are applied here.
Layer 5: Post-processing and Delivery
Output validation, citation formatting, guardrails, logging, and response streaming to the client. Failures caught here prevent bad output from reaching users.
RAG couples a dense vector index of external knowledge with a sequence-to-sequence generator. At query time, the retriever selects relevant passages and the generator conditions its answer on that evidence.
Architecture
| Layer | Owns | Does NOT Own | Scaling Pattern |
|---|---|---|---|
| Ingestion | Parse, normalize, enqueue | Query-time logic | Horizontal workers, job queues |
| Indexing | Chunk, embed, upsert | User sessions | Batch pipelines, scheduled jobs |
| Orchestration | Routing, state, workflows | Raw retrieval algorithms | Stateless pods, horizontal scale |
| Retrieval | Search, filter, rerank | Generation prompts | Read replicas, ANN indexes |
| Generation | LLM calls, streaming | Document storage | Provider API limits, regional routing |
| Post-process | Validation, citations, guardrails | Business workflows | Lightweight, co-located with API |
Orchestration Layer Deep Dive
The orchestration layer is where production systems diverge from tutorials. A well-designed orchestrator exposes:
@dataclass
class OrchestrationContext:
trace_id: str
tenant_id: str
user_id: str
session_id: str
metadata: dict
class Orchestrator(Protocol):
async def handle(self, request: UserRequest, ctx: OrchestrationContext) -> Response:
...
Concrete implementations might be:
-
Fixed pipeline - retrieve → rerank → generate (most RAG apps).
-
Workflow graph - DAG with conditional branches (support triage: classify → route).
-
Agent loop - plan → act → observe until stop condition (see AI Agents).
The orchestrator should be framework-agnostic at the interface. Swap LangChain for custom code without changing your API contract.
Step-by-Step Flow
Request Lifecycle (Production RAG + Tools)
Step 1: API gateway receives request. Authenticate, rate-limit, attach trace ID, resolve tenant. Never pass raw user input directly to retrieval without sanitization.
Step 2: Orchestrator loads session state. Fetch conversation history from Redis or DB. Truncate to context budget. Apply prompt injection defenses on new user message.
Step 3: Intent routing (optional). Classify query: FAQ lookup, account action, escalation. Route to appropriate pipeline or model tier (see Cost Optimization).
Step 4: Retrieval phase. Orchestrator calls retrieval service with tenant filter. If zero results, orchestrator decides: broaden search, ask clarifying question, or fallback response - not the LLM's job to silently invent.
Step 5: Generation phase. Build prompt from template + retrieved context + history. Stream tokens to client. Log prompt hash and model version.
Step 6: Tool calls (if agentic). Orchestrator parses tool requests, validates schema, checks permissions, executes with timeout, injects results back into agent loop. Cap iterations.
Step 7: Post-processing. Run guardrails, format citations, persist turn to session, emit observability spans with latency and token counts.
Step 8: Async side effects. Log to eval dataset, update analytics, trigger human review queue if confidence is low.
Real Production Example
A B2B SaaS company runs a multi-tenant assistant over product docs, API references, and account data. Architecture: FastAPI gateway → custom orchestrator → retrieval microservice (Qdrant) → OpenAI/Anthropic with model routing → NeMo-style guardrails on output.
import asyncio
import uuid
from dataclasses import dataclass
from typing import AsyncIterator, Optional
@dataclass
class RequestContext:
trace_id: str
tenant_id: str
user_id: str
session_id: str
class ProductionOrchestrator:
def __init__(
self,
retriever,
llm_router,
tool_registry,
guardrails,
tracer, # LangSmith / OpenTelemetry
):
self.retriever = retriever
self.llm_router = llm_router
self.tools = tool_registry
self.guardrails = guardrails
self.tracer = tracer
async def handle_query(
self,
query: str,
ctx: RequestContext,
stream: bool = True,
) -> AsyncIterator[str]:
with self.tracer.start_span("orchestrator.handle", trace_id=ctx.trace_id):
# Layer: routing
route = await self.llm_router.classify(query, ctx)
model = route.model # e.g. gpt-4o-mini vs gpt-4o
with self.tracer.start_span("retrieval"):
chunks = await self.retriever.search(
query=query,
filter={"tenant_id": ctx.tenant_id},
top_k=20,
)
chunks = await self.retriever.rerank(query, chunks, top_n=5)
if not chunks and route.requires_grounding:
yield "I couldn't find relevant documentation. Can you rephrase or specify a product area?"
return
# Layer: generation with streaming
prompt = self._build_prompt(query, chunks, ctx)
with self.tracer.start_span("generation", model=model):
buffer = []
async for token in self.llm_router.stream(model, prompt):
buffer.append(token)
if stream:
yield token
full_response = "".join(buffer)
# Layer: post-process
with self.tracer.start_span("guardrails"):
result = await self.guardrails.validate(
query=query,
response=full_response,
sources=chunks,
)
if not result.passed:
yield result.safe_fallback
return
if not stream:
yield result.text
async def handle_agent_turn(self, query: str, ctx: RequestContext, max_steps: int = 5):
messages = [{"role": "user", "content": query}]
for step in range(max_steps):
with self.tracer.start_span(f"agent.step.{step}"):
response = await self.llm_router.complete(
model="gpt-4o",
messages=messages,
tools=self.tools.schemas_for(ctx.tenant_id),
)
if response.tool_calls:
for call in response.tool_calls:
if not self.tools.is_allowed(call.name, ctx):
raise PermissionError(f"Tool {call.name} not permitted")
result = await asyncio.wait_for(
self.tools.execute(call.name, call.arguments),
timeout=10.0,
)
messages.append({"role": "tool", "content": result})
continue
return response.content
raise TimeoutError("Agent exceeded max steps")
The orchestrator owns flow control. Retrieval, LLM, and tools are injectable services. Every phase emits spans for LangSmith or OpenTelemetry.
Design Decisions
| Decision | Option A | Option B | When to choose |
|---|---|---|---|
| Orchestration | Custom code + thin wrappers | Full framework (LangChain/LlamaIndex) | Custom when you need strict control and testability; framework for speed to market |
| Deployment | Monolith (API + orchestrator) | Microservices (retrieval, generation separate) | Monolith until retrieval or indexing needs independent scaling |
| Agent vs workflow | Fixed DAG | Agent loop | DAG for predictable paths; agents when query decomposition is required |
| State store | Redis (sessions) | Postgres (durable history) | Redis for low-latency chat; Postgres when audit trail is mandatory |
| Model access | Direct provider API | Gateway (LiteLLM, Cloudflare AI Gateway) | Gateway when routing across providers, caching, and rate limits matter |
| Sync vs async indexing | Webhook-triggered upsert | Nightly batch | Webhook for freshness; batch for cost on large corpora |
⚠ Common Mistakes
-
No orchestration layer - logic in API handlers. Prompt assembly, retrieval, and tool calls scattered across routes. Extract an orchestrator early.
-
Coupling indexing and query deployments. Changing chunk size requires redeploying the API. Version indexes; run blue/green index migrations.
-
Trusting the LLM for authorization. Tenant filters must be enforced in retrieval queries, not mentioned in the system prompt.
-
Missing timeouts on every external call. Vector search, reranker, LLM, and tools each need independent timeouts. One hung tool should not block the fleet.
-
Framework as architecture. LangChain is a library, not a substitute for defining layers, contracts, and operational boundaries.
-
No idempotency on write tools. Agent retries can double-charge or duplicate records. Use idempotency keys.
-
Streaming without backpressure. Unbounded token buffers OOM your service under slow clients.
Where It Breaks Down
Cross-cutting concerns resist layering. A/B testing a new prompt while comparing retrieval configs requires coordinated experiments across layers - architecture helps but experiment infrastructure is still hard.
Agent architectures blur boundaries. When the LLM decides the next step, your clean pipeline becomes a loop with unpredictable cost and latency. Cap steps, budget tokens, and require human approval for destructive actions.
Multi-modal and real-time data break the simple ingest-index-query model. Live dashboards, video, and collaborative editing need streaming ingestion and different index types.
Org boundaries. Retrieval owned by search team, LLM by ML team, API by backend team - without shared trace IDs and contracts, incidents span three on-call rotations.
Running in Production
Best Practice
✅ Best Practices - Instrument every stage, version embedding models, enforce access control at retrieval time, and evaluate on a fixed golden set before shipping changes.
| Dimension | Consideration |
|---|---|
| Scaling | Scale orchestration statelessly. Scale retrieval read replicas separately. Indexing workers scale on queue depth, not request QPS. |
| Latency | Budget per layer: gateway 10ms, retrieval 100–300ms, generation 1–3s. Stream generation to mask LLM latency. See Latency Optimization. |
| Cost | Orchestrator is the injection point for model routing, caching, and request classification. Log cost per tenant. |
| Monitoring | Distributed traces across orchestration → retrieval → generation. Alert on p99 latency per layer, error rate, zero-result retrieval rate. |
| Evaluation | Golden tests at orchestration level (end-to-end), not just unit tests on prompts. Block deploys on eval regression. |
| Security | Input sanitization at gateway. Tool permission matrix in orchestrator. Secrets never in prompts. See AI Security. |
Important
The orchestration layer is where you enforce timeouts, tenant isolation, and fallbacks. If it's missing, every outage becomes an LLM outage.
Ecosystem
-
Orchestration: LangChain, LlamaIndex, Haystack, Semantic Kernel, custom FastAPI/Node services.
-
Workflow engines: Temporal, Inngest, Prefect - durable execution for long-running AI jobs.
-
Gateways: LiteLLM, Cloudflare AI Gateway - unified routing, caching, rate limits across providers.
-
Observability: LangSmith, Langfuse, Arize Phoenix, OpenTelemetry - see Observability.
-
MCP: Model Context Protocol standardizes tool servers; orchestrator remains the client.
Related Technologies
-
RAG: The dominant retrieval-generation pattern inside the architecture.
-
AI Agents: Agent loops as an orchestration pattern with higher autonomy.
-
Observability: Tracing and metrics across architectural layers.
-
Cost Optimization: Model routing and caching at the orchestration layer.
-
AI Security: Defense in depth across ingestion, orchestration, and output.
-
Model Context Protocol: Standardized tool integration layer.
Learning Path
Prerequisites: RAG · Large Language Models · AI Agents
Next topics: Observability · Cost Optimization · AI Security · Latency Optimization
Estimated time: 55 min · Difficulty: Advanced
FAQs
What is the orchestration layer in an AI system?
The orchestration layer coordinates request handling: routing, session state, retrieval calls, LLM invocation, tool execution, error handling, and observability. It is application control plane code, not the model itself.
Should I use LangChain or write custom orchestration?
Use LangChain (or LlamaIndex) to move fast on prototypes. Extract a custom orchestrator when you need deterministic tests, strict latency budgets, or framework upgrades are blocking releases. Many production teams use frameworks as libraries behind a thin custom layer.
How do I separate indexing from querying?
Run indexing as async jobs (queue + workers) writing to a versioned vector index. The query API reads from the active index alias. Promote new index versions without redeploying the API. Store raw documents separately for re-embedding.
When should retrieval be a separate microservice?
When retrieval latency or load diverges from API traffic, when multiple products share one index, or when the search team owns embedding/reranking lifecycle independently. Before that, a monolith module is fine.
How do agents fit into layered architecture?
Agents are an orchestration pattern: a loop of plan → tool call → observe. Keep the same layers (retrieval, generation, tools) but replace fixed DAG with a bounded loop. Add stricter timeouts, iteration caps, and approval gates.
What belongs in post-processing vs orchestration?
Orchestration decides what runs and in what order. Post-processing validates and formats output (guardrails, citations, PII redaction) before returning to the client. Both can async-log for eval.
How do I handle multi-tenancy?
Pass tenant ID through every layer. Enforce metadata filters at retrieval. Separate rate limits and cost attribution per tenant. Never mix tenant data in shared caches without tenant-scoped keys.
What's a reasonable latency budget?
Typical interactive RAG: 50ms embed + 100ms retrieve + 150ms rerank + 1500ms generation = ~1.8s. Stream after first token (~500ms). Budget 2–3s p95 for chat; sub-second for classification/routing steps.
How do I version prompts and models in architecture?
Store prompt templates in version control or a config service. Log prompt_version and model_id on every request. Run eval against new versions before traffic shift. Blue/green or canary at the orchestrator routing layer.
What fallbacks should the orchestrator implement?
Retrieval empty → clarifying question or search broadening. LLM 503 → secondary provider or cached response. Tool timeout → partial answer with disclaimer. Guardrail failure → safe static message, not raw model output.
How does MCP change architecture?
MCP standardizes how tools expose capabilities. Your orchestrator (or agent runtime) becomes an MCP client connecting to tool servers. It replaces ad-hoc tool wrappers but does not replace retrieval, guardrails, or observability layers.
When is a monolith enough?
Under ~100 RPS, single team, one product surface - a well-structured monolith with internal modules beats premature microservices. Split when operational or scaling boundaries are clear.
References
Further Reading
Summary
-
Production AI systems are layered: ingestion, indexing, orchestration, retrieval, generation, post-processing. - The orchestration layer is the control plane - routing, state, tools, timeouts, and observability. - Separate offline indexing from online queries; version indexes independently of API deploys. - Enforce authorization and tenant isolation in retrieval and tools, not in prompts.
-
Start with explicit contracts between layers; add agents and microservices when metrics justify the complexity.