Skip to content
learn
  1. Learn
  2. Artificial Intelligence
  3. Building AI Products

AI AI-14.01

Anatomy of an LLM application

The parts of a real LLM product and how a request flows through them: UI, orchestration, model API, retrieval, tools, storage and observability.

Beginner 12 min Last reviewed

The big idea

A weekend demo of an AI feature is one function: take the user's text, send it to a model, print the answer. A product that thousands of people use every day has a lot more around that call. It has to remember the conversation, look up your company's documents, check an order in a database, stream the answer to a phone, survive the model provider having a bad hour, and let you find out later why a customer got a wrong answer.

So the model is one component among many. Think of a restaurant. The chef is the star, but diners never talk to the chef. A waiter takes the order (the UI), the head of the kitchen decides what gets cooked and in what order (orchestration), the pantry supplies ingredients (retrieval), the ovens and grills do work the chef asks for (tools), the order book remembers every table (storage), and the manager's log records what went out and what came back (observability). A brilliant chef in a badly run kitchen still serves cold food.

This lesson names those parts, follows one request through them, and shows what each part is responsible for. The rest of the module zooms in on the parts that make or break a product: the interface, tracing, budgets and shipping changes safely.

How it works

Layered architecture of an LLM application with one request path A user talks to the UI, which sends the request to the orchestration layer in the middle. Step 1: the request arrives from the user through the UI. Step 2: orchestration loads the conversation from storage and fetches relevant passages from retrieval. Step 3: orchestration builds the prompt and calls the model API through a gateway. Step 4: the model asks for a tool; orchestration runs the tool and sends the result back to the model. Step 5: the answer streams back to the UI, the new turn is saved to storage, and every step is recorded by observability. request history query prompt tool request stream user UI chat, streaming orchestration prompts, tool loop model API via a gateway retrieval search index tools APIs, databases, code storage chats, state observability traces, tokens, latency, cost Figure 1. The model is one box among many. Step through a single request: the glowing path is where the work is at each stage.
  1. Request arrives The user types in the UI. The UI sends the message, the session id and who the user is to your backend.
  2. Gather context Orchestration loads recent turns from storage and asks retrieval for passages that match the question.
  3. Call the model Orchestration assembles the prompt (instructions, history, passages, tool definitions) and calls the model API through a gateway.
  4. Run tools The model asks for a tool. Your code checks the request, runs it, and sends the result back for another model call.
  5. Respond and record The answer streams to the UI. The turn is saved, and every step, with its tokens, latency and cost, lands in the trace.

UI: where expectations are set

The user interfaceThe screens, chat boxes, buttons and notifications through which a person uses the product. does more than show text. It streams tokens as they arrive so a 6-second answer feels quick, shows sources, offers retry and undo, and asks for confirmation before anything with consequences. These patterns are the subject of UX patterns for AI . Streaming usually rides on server-sent events or WebSockets ( Real-time: polling, long polling, SSE and WebSockets ).

Orchestration: the part you actually write

The orchestration layerThe application code that turns a user request into model calls: it gathers context, builds the prompt, runs the tool loop, validates outputs and decides what to return. is your backend code for the AI feature, and it holds most of the product logic. It decides which context to fetch, assembles the prompt from instructions, history and retrieved passages, calls the model, runs the tool loop from The agent loop (ReAct) , validates the output against a schema, and handles errors. It can be a few hundred lines of plain code or built on a framework. Either way, keep prompts as versioned files, not strings scattered through the code, because you will change them often.

Model API, ideally behind a gateway

The model itself is usually a call over HTTP to a provider, or to a model you host. Production apps often put a thin model gatewayA shared service in front of model providers that handles API keys, retries, fallbacks to other models, rate limits, caching and usage accounting. in front of it: one place for keys, timeouts and retries ( Timeouts, retries, backoff and jitter ), fallback to a second model when the first is down, rate limits, prompt caching ( Prompt caching and cost control ) and per-team usage tracking. It is the same idea as an API gateway in classic systems ( API gateway and backend-for-frontend ).

Retrieval: your data, at question time

RetrievalFinding the passages from your own documents or databases that are relevant to the current question, so they can be added to the prompt. gives the model facts it was never trained on: your help center, contracts, tickets. It is the RAG pipeline from Why RAG? , with an ingestion job that keeps the index fresh as documents change, and permission filters so a user can only retrieve what they are allowed to read.

Tools: actions with real effects

Tools are ordinary functions the model can ask for, such as looking up an order, issuing a refund or running code. The model only proposes; your code checks the arguments and permissions, runs the call and returns the result. Anything that changes the world needs limits and, for risky actions, a human approval step ( Coding agents and computer-use agents ).

Storage: conversations and state

LLM APIs are stateless: each call sees only what you send. So your app stores conversations, user settings, long-term memories ( Memory for agents ), uploaded files and the state of long agent runs. Keeping state in storage, not in the server's memory, also lets you run many identical servers behind a load balancer ( Stateless vs stateful services ).

Observability: what actually happened

ObservabilityRecording what a system did, for every request, in enough detail to explain its behavior afterwards: logs, metrics and traces. for an LLM app means recording the exact prompt, retrieved passages, tool calls, output, token counts, latency and cost of every step. Without it, a user report like "the bot told me the wrong refund window" cannot be debugged, and you cannot build evals from real failures ( Why evals are the core skill ). LLM observability and tracing covers tracing in depth.

Try it

Which part of the app is responsible for each job?

Show answers
  • Show the answer word by word as it is generated: UI. Streaming is a display concern; the backend forwards tokens and the UI renders them as they arrive.
  • Decide which passages and which past turns go into the prompt: Orchestration. Assembling context is the orchestration layer's main job.
  • Switch to a second provider when the first returns errors: Model gateway. Fallbacks, retries and keys live in one shared place, so every feature gets them.
  • Find the three help-center paragraphs that mention refunds: Retrieval. Searching your own documents at question time is retrieval.
  • Look up the delivery status of order B42: Tools. Live data from another system comes through a tool call that your code executes.
  • Remember what the user said two messages ago: Storage. The model API is stateless, so the app keeps the history and sends it again.
  • Record that a request used 4,100 input tokens and took 3.2 seconds: Observability. Per-step token, latency and cost records are what tracing captures.
  • Check that the model's JSON has every required field before using it: Orchestration. Validating outputs and deciding what to do when they are wrong is application logic.
  1. Which job did you hesitate on? Most confusion is between orchestration and the gateway: the gateway is shared plumbing for every feature, orchestration is the logic of one feature.
  2. Now read the same layers as code. Every part is a few lines here, with a fake model so it runs anywhere:
anatomy.pyruns with python3 (standard library)
# Every layer of an LLM app in one file, with a fake model so it runs offline.
import json, re

DOCS = {  # retrieval: a tiny knowledge base
    "returns": "You can return items within 30 days for a full refund.",
    "shipping": "Each order ships in 3 to 5 business days.",
}
ORDERS = {"A17": "shipped", "B42": "delivered"}  # data behind a tool
STORE = {}   # storage: conversation history per session
TRACE = []   # observability: one record per step

def log(step, **info):
    TRACE.append({"step": step, **info})

def retrieve(q):
    words = set(re.findall(r"\w+", q.lower()))
    best = max(DOCS, key=lambda d: len(words & set(re.findall(r"\w+", DOCS[d].lower()))))
    log("retrieve", query=q, hit=best)
    return DOCS[best]

def order_status(order_id):  # a tool: ordinary code that the model can ask for
    log("tool", tool="order_status", order_id=order_id)
    return ORDERS.get(order_id, "not found")

def fake_model(prompt):  # stand-in for the model API
    log("model", prompt_tokens=len(prompt) // 4)  # rough: 4 characters per token
    parts = dict(line.split(": ", 1) for line in prompt.splitlines())
    ids = re.findall(r"\b[A-Z]\d+\b", parts["USER"])
    if "TOOL RESULT" in parts:
        return {"text": f"Your order is {parts['TOOL RESULT']}."}
    if ids:
        return {"tool": "order_status", "args": {"order_id": ids[-1]}}
    return {"text": parts["CONTEXT"]}

def handle(session, msg):  # orchestration: build prompt, call model, run tools
    history = STORE.setdefault(session, [])
    prompt = f"SYSTEM: be brief\nHISTORY: {history[-2:]}\nCONTEXT: {retrieve(msg)}\nUSER: {msg}"
    reply = fake_model(prompt)
    if "tool" in reply:
        prompt += f"\nTOOL RESULT: {order_status(**reply['args'])}"
        reply = fake_model(prompt)
    history += [msg, reply["text"]]
    log("store", session=session, messages=len(history))
    return reply["text"]

print(handle("s1", "Where is my order B42?"))  # the UI would show these
print(handle("s1", "Can I return it for a refund?"))
for record in TRACE:
    print(json.dumps(record))

Output

Your order is delivered.
You can return items within 30 days for a full refund.
{"step": "retrieve", "query": "Where is my order B42?", "hit": "shipping"}
{"step": "model", "prompt_tokens": 27}
{"step": "tool", "tool": "order_status", "order_id": "B42"}
{"step": "model", "prompt_tokens": 32}
{"step": "store", "session": "s1", "messages": 2}
{"step": "retrieve", "query": "Can I return it for a refund?", "hit": "returns"}
{"step": "model", "prompt_tokens": 45}
{"step": "store", "session": "s1", "messages": 4}
  1. Look at the trace. The first question needed a tool and two model calls; the second needed one. Notice that the second prompt is longer (45 rough tokens versus 27) because it carries the history from storage. That growth is why long conversations get slower and more expensive.

Going deeper

Where the complexity actually lives

Teams new to AI products expect the model call to be the hard part. In practice it is one line. The work is in the layers around it: getting the right context into the prompt, handling the model's mistakes, keeping the index fresh, and seeing what happened. A useful rule: if a behavior must always hold, put it in code, not in the prompt. "Never refund more than 200 dollars" belongs in the refund tool, where a confused or manipulated model cannot talk its way past it.

One request, many model calls

A single user message often triggers several model calls: a cheap one to classify the request or rewrite the search query, the main answer call, perhaps a tool round trip, and a check on the output. Each adds latency and cost, which is why Latency and cost budgets breaks a request into its parts and budgets each one.

Frameworks or plain code

Frameworks give you retrieval, tool loops and tracing out of the box, which is fast for a prototype. They also add layers between you and the prompt that is actually sent. Many teams start with a framework, then move the core loop to plain code once they need to see and control every token. Whatever you choose, make sure you can print the exact final prompt.

Trade-offs

OptionWins whenLoses when
Single model call, no extra layers A demo or internal tool with one simple job Users need history, your data, actions or debugging
Orchestration in plain code You need full control and visibility of every prompt You rebuild retrieval, tool loops and tracing by hand
Orchestration framework Fast prototypes with standard RAG and tool patterns Abstractions hide the real prompt; upgrades break things
Shared model gateway Many features and teams share keys, fallbacks and cost tracking One more service to run; a gateway outage hits every feature

Common mistakes

  • Treating the model as the whole product, then discovering memory, permissions and logging after launch.
  • Building prompts from strings spread across the codebase, so nobody knows which prompt is live.
  • Putting hard business rules only in the prompt instead of enforcing them in tools and validation code.
  • Keeping conversation state in one server's memory, so a restart or a second server loses it.
  • Shipping without per-request traces, so a bad answer cannot be explained or turned into an eval case.

In the wild

GitHub

The architecture of today's LLM applications (opens in a new tab)

GitHub's engineering blog walks an internet-provider support assistant through the components of an LLM app. It groups them into user input tools (a UI, the LLM and an app hosting platform), input enrichment and prompt construction (data sources, an embedding model, a vector database, a data filter for unauthorized data and prompt construction), and efficient and responsible AI tooling (an LLM cache, a content classifier and telemetry). It also separates offline evaluations before launch from online evaluations with real users, such as Copilot's acceptance rate.

Check yourself

  1. Which component's job is to assemble the prompt from instructions, history and retrieved passages?

    Show answer

    B. The orchestration layer Orchestration is the application logic of the feature: it decides what context to gather and builds the prompt.

  2. A chatbot forgets what the user said earlier whenever a request lands on a different server. What is wrong?

    Show answer

    B. Conversation history is kept in server memory instead of shared storage Model APIs are stateless, so the app must store history. Keeping it in one server's memory breaks as soon as there are two servers.

  3. A support bot must never refund more than 200 dollars. Where should that rule live?

    Show answer

    B. In the refund tool's code, with the prompt also mentioning it Code enforces the limit even if the model is confused or manipulated. The prompt helps the model behave, but it is not a guarantee.

  4. Your primary model provider has an outage. Which component lets every AI feature fail over to another model without code changes in each feature?

    Show answer

    A. A shared model gateway A gateway centralizes keys, retries and fallbacks, so one configuration change protects every feature that calls models through it.

Recap

  • Around the model API (often behind a gateway), an LLM application has a UI, orchestration, retrieval, tools, storage and observability.
  • Orchestration is the code you write: it gathers context, builds prompts, runs the tool loop and validates outputs.
  • Model APIs are stateless, so conversations and agent state live in your storage.
  • Rules that must always hold belong in code and tools, not only in prompts.
  • Trace every step of every request, so wrong answers can be explained and turned into eval cases.

Get new lessons by email

Get new posts on backend and AI internals. No spam, unsubscribe anytime.

Your email is only used to send new posts. Privacy