Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

AI System Design

Design a Conversational AI

Learn AI system design by building a production conversational AI like ChatGPT step by step.

The numbers to beat128kwindow (tokens)~4 chars≈ 1 tokenin + outboth billed

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

What is a conversational AI, really?

Strip away the chat bubble and a system like ChatGPT does one deceptively simple thing: take your message plus everything said so far, run it through a language model, and stream back an answer — for millions of people at once.

Userweb / app
New in this step: User.

That sentence hides the hard parts. What exactly do we feed the model (and how much fits)? How does it remember earlier turns? How do we serve a giant model fast and affordably? And how do we keep the output safe?

What the new pieces do

Userclient
A person typing a message. Sends a turn, then watches the answer stream back token by token.

Step 1 · The skeleton

A client, a gateway, a model

Your browser can’t hold a 100-billion-parameter model, and it can’t be trusted with the keys, the rate limits, or other users’ data. So where does the request go?

Userweb / appInference Gatewayentry point
New in this step: Inference Gateway.

You type a message. Where does the thinking happen?

  1. Frontier models are tens to hundreds of gigabytes and need GPUs. Even if it fit, you’d ship the weights to every user and lose all control over cost and safety.

  2. A neutral backend holds the keys, enforces limits, assembles the prompt and routes to the GPU fleet. This is the spine of every LLM product.

  3. Letting clients hit inference directly means no auth boundary, no rate limiting, no prompt assembly, and your model endpoint exposed to the world.

We put an Inference Gateway in the middle. The browser sends a turn; the gateway authenticates, rate-limits, and orchestrates everything behind it. This is the client → gateway → model backbone under every chat product.

Why this piece earns its place

The gateway earns its keep the moment you try to limit anything. Rate limiting by requests is the reflex, and here it is close to meaningless: a limiter that counts requests treats a one-line question and a turn carrying a pasted-in document as the same unit of work, and they are nowhere near the same cost. The limiter has to count tokens, which it can only do once the prompt is assembled and the answer is finished. So the gateway reserves a pessimistic estimate at the door and settles the real number when the turn completes, which is a different shape of limiter from the one every other service on your estate runs. The second thing it owns is the request contract: which knobs a caller may set, and which the server pins. Let a client choose a model or ask for a shorter answer, but keep the ceiling on output length on the server, because that one field multiplies what a turn costs and a caller has no reason to be modest with it. Defaults belong here too. A default that lives in client code is a default you cannot change — old clients keep sending the old value long after you changed your mind.

What the new pieces do

Inference Gatewaybackend
The single front door for every chat request. Authenticates, rate-limits, then orchestrates context, guardrails and model serving.

Step 2 · What the model sees

Assemble the context window

A language model has no memory between calls — it only sees the text you hand it this request. So what exactly do we put in front of it for each turn?

Inference Gatewayentry pointContext Builderprompt assembly
New in this step: Context Builder.

The model is stateless. What do you send it for each new message?

  1. Then it forgets the system instructions and everything said earlier — every turn starts from scratch. The "conversation" disappears.

  2. Right instinct, wrong limit. Conversations grow past the context window and cost scales with every token sent. You need to assemble and budget.

  3. A Context Builder composes the model input each turn — the system prompt, the recent/relevant history, and the new message — shaped to fit the window.

A Context Builder composes the model input for every turn: the system prompt (who the assistant is), the conversation history, and the new message. The model is stateless, so the full context is rebuilt and resent on each request.

Why this piece earns its place

Treat the assembled prompt as a build artifact, not a string. The system prompt is the clearest case: because the builder reassembles everything on each turn, editing it silently changes the behavior of every conversation already in flight, including ones a user started yesterday and will carry on tomorrow. There is no migration, no marker anyone can see, and nothing in a log that says why quality moved. Version the system prompt and stamp that version onto the turn, and you can at least tell a prompt change apart from a model change when the numbers shift. The other thing that belongs here is that token counting is model-specific. Each model tokenizes with its own vocabulary, so the same history measures differently depending on which model the turn is routed to, and a budget computed against one model does not automatically hold for another. Count with the tokenizer of the model you are actually about to call, which means the target model is an input to the builder, not something it assumes.

What the new pieces do

Context Builderservice
Assembles the model input: system prompt + recent history + the new message, trimmed to fit the context window.

Step 3 · Budget the tokens

It won’t all fit — now what?

The window is finite (say 128k tokens) and every token costs money and latency. A long chat — or a big document — blows past it. How do you decide what makes the cut?

Context Builderprompt assemblyVector / KBretrieval
New in this step: Vector / KB.

The conversation is longer than the context window. What gets sent?

  1. Overflowing the window errors or silently drops the start — usually the system prompt, the most important part. You must decide what to keep on purpose.

  2. Allocate the token budget deliberately — pin the system prompt, keep recent turns, compress older ones, and pull in only the relevant facts (RAG) instead of everything.

  3. Recency usually matters most in a conversation, and the oldest turns are often stale. Blindly keeping the start wastes budget on what no longer matters.

The builder works to a token budget. It pins the system prompt, keeps the most recent turns, and summarizes or drops older ones. For knowledge beyond the chat, it retrieves only the relevant passages from a vector store (RAG) instead of stuffing everything in.

Why this piece earns its place

Two things about the budget are far easier to get right now than to retrofit. The first is order. Whatever is constant across turns belongs at the front and whatever changes every turn belongs at the back, because the serving tier keeps the prefixes it has already computed and reuses one whenever the leading tokens of a new request match it — across concurrent users, not merely against the request it happened to handle last. One shared system prompt at the front is therefore paid for once and reused by everybody chatting at the same time. Put the current date or a freshly retrieved passage in the second line and the match ends there, so you have invalidated everything after it, on every turn, for every user. Prompt layout looks like a formatting decision and is really a serving decision. The second is that summarizing older turns is itself a model call, with its own cost, latency and failure mode. Run it inline and you have put a generation in front of a turn someone is waiting on; run it in the background once a turn completes and the cost lands where no cursor is blinking. Keep the raw turns either way. The summary is derived and can be rebuilt with better rules later, and the day it becomes authoritative is the day you quietly discard detail no future turn can recover.

  • 128kwindow (tokens)
  • ~4 chars≈ 1 token
  • in + outboth billed

What the new pieces do

Vector / KBindex
Optional knowledge source. The context builder can retrieve relevant facts to ground the answer (RAG).

Back of the envelope

system prompt
pinned — always sent
recent turns
kept verbatim while they fit
older turns
summarized into a running recap
external knowledge
retrieved on demand (RAG), not preloaded

Step 4 · Make it remember

Where does history live?

The model is stateless and the browser can’t be trusted to hold the true history. Yet tomorrow the user reopens the chat and expects it all to be there. Where does the conversation actually live?

Memory ServiceconversationConversation DBturns & threads
New in this step: Memory Service, Conversation DB.

The model forgets between calls. Where is the real conversation stored?

  1. Inference doesn’t update the weights. The model never remembers anything between requests; memory must live outside it.

  2. Close the tab and it’s gone; switch devices and it’s gone; you can’t rebuild context server-side. The client is a cache, not the source of truth.

  3. Persist every turn server-side. The builder loads history from it each request and saves the new turn — survives reloads, devices and crashes.

A Memory Service reads and writes the Conversation DB — the durable source of truth for every turn. Each request, the builder loads recent history from it; after each answer, the new turn is appended. The browser is just a fast cache of what the database already knows.

Why this piece earns its place

The interesting part of memory is the write, not the read. Append the user message before generation and a crash leaves a turn with a question and no answer; append after and a crash loses the message the user actually typed. Write first and mark the assistant turn incomplete until generation finishes, then decide deliberately what a reopened conversation shows and whether a half-generated answer is fed back as context next turn. Nobody specifies this, and every chat product ends up answering it by accident. Retries need the same care. A client resending after a timeout appends a duplicate unless the turn carries an id the client generates, so the write is idempotent on that id rather than on arrival order. And history stops being one table the moment the product succeeds: the same text now sits in the database, inside the running summary, and in whatever prompt logging you added to debug bad answers. A deletion that only touches the first of those is not a deletion.

What the new pieces do

Memory Serviceservice
Loads and saves conversation history so a chat remembers earlier turns across requests.
Conversation DBstore
Durable storage of every message in every conversation — the source of truth for history.

Step 5 · The real cost

Inference is the expensive part

Assembling a prompt is cheap. Running a hundred-billion-parameter model to generate each token is not — it needs GPUs, and a single request can hold one for seconds. How should the system treat this?

UserInference GatewayContext BuilderMemory ServiceConversation DBVector / KB
The system as it stands at this step. · swipe to pan the diagram

What dominates the cost and latency of a chat turn?

  1. A few small reads are microseconds. Real, but a rounding error next to generation.

  2. Each output token is a full forward pass through the model on scarce GPUs. This dominates both cost and latency, so it gets its own carefully-managed tier.

  3. Bytes are tiny; latency here is milliseconds. The seconds you wait are the model thinking, not the wire.

Generation runs on a dedicated Model Servers tier — a fleet of GPUs holding the model weights. Because each output token is a full pass through the model, this tier is the bottleneck we design everything else around: it gets its own queue, its own scaling, and its own failure handling.

Why this piece earns its place

What separates this tier from the stateless services around it is how slowly it grows. Adding a replica means pulling tens of gigabytes of weights and loading them into GPU memory, so new capacity is minutes away, not seconds. Autoscaling on queue depth therefore reacts well after the queue that triggered it has already hurt somebody — and the reaction can fail outright, because scarce accelerators are a reservation with a lead time, not an API call that always succeeds. Capacity here is a forecast you commit to ahead of the traffic, closer to planning a purchase than to tuning a control loop. The economics invert too. Over-provisioning a stateless tier is cheap enough to be standard advice; over-provisioning GPUs is the largest line on the bill, and an idle GPU costs exactly what a busy one does. So the number you manage is utilization, and that is a scheduling problem rather than a scaling one. Isolating serving also buys optionality you will want later: once it is its own tier behind the gateway, several pools — different model sizes, different versions, a canary — sit behind one front door, and no caller has to know which one answered.

Step 6 · Serve it efficiently

Batching and the KV cache

GPUs are most efficient when fully fed, but chat requests arrive one at a time and each generates tokens one at a time. Run them naively and the GPUs sit half-idle while users wait. How do you serve a giant model fast?

Model ServersGPU inferenceModel Weightssharded · GPUEvent Streamusage · evals
New in this step: Model Servers, Model Weights, Event Stream.

How do you keep scarce GPUs busy across many concurrent chats?

  1. A single chat can’t saturate a GPU, and others queue behind it. You pay for hardware that mostly idles while users wait.

  2. Pack concurrent requests into shared batches and cache the attention state (KV cache) so each new token is cheap. This is how real serving stacks hit high throughput.

  3. Re-running attention over the entire context for each output token is quadratic waste — exactly what the KV cache exists to avoid.

The model servers use continuous batching — packing many in-flight chats into shared GPU passes — and a KV cache that stores attention state so each new token reuses prior work instead of recomputing the whole prompt. Every turn also emits usage events (tokens, latency) to the stream for billing and evals.

Why this piece earns its place

The ceiling on batch size is memory, not compute. Every sequence in the batch holds its own KV cache, and that cache grows with each token generated, so a replica’s real capacity is how many concurrent contexts of this length fit in GPU memory — and it shrinks as the conversations in flight get longer. A few very long chats can squeeze everyone else out of the batch, which surfaces as latency variance rather than as an error, spread across users who did nothing unusual. Two consequences are worth saying out loud. First, a request’s latency depends on what else is batched with it, so a latency target only means something at a stated concurrency, and a p99 measured on an idle fleet is a number about nothing. Second, when memory runs short a serving stack preempts sequences and recomputes their cache later, so a request can pay for its prompt twice with no failure recorded anywhere. It also turns the context budget from Step 3 into a capacity decision. Long contexts do not merely cost more per turn; they cost the fleet concurrency.

What the new pieces do

Model Serversservice
The GPU fleet running the LLM. Batches concurrent requests and reuses the KV cache to generate tokens efficiently.
Model Weightsstore
The model parameters, loaded across the GPU fleet. Large models are sharded over many GPUs.
Event Streambus
Every turn emits events — tokens used, latency, safety flags — feeding billing, evals and abuse detection.

Back of the envelope

continuous batching
merge concurrent requests into shared GPU passes
KV cache
reuse attention state — no recompute per token
weights sharded across GPUs
big models span many devices
emit usage events
tokens + latency → billing, evals, abuse detection

Step 7 · Don’t make them wait

Stream the tokens back

A long answer can take many seconds to finish. If the user stares at a blank screen until it’s done, the product feels broken — even when it’s working perfectly. How do you make it feel instant?

UserInference GatewayMemory ServiceModel Servers
New in this step: Model Servers → Inference Gateway, Inference Gateway → User. · swipe to pan the diagram

A full answer takes 6 seconds to generate. What does the user see?

  1. Six seconds of blank screen reads as "frozen." You’re hiding progress the model is already making.

  2. Stream over a persistent connection (SSE/WebSocket) so words appear as they’re produced. Time-to-first-token is what users actually feel.

  3. Polling adds latency and load and still lags the model. A push stream delivers each token the moment it exists.

The model generates tokens incrementally, and the gateway streams them straight to the browser over a persistent connection (server-sent events or WebSocket). The user sees the answer appear word by word. The metric that matters is time-to-first-token, not total time.

Why this piece earns its place

Streaming moves most error handling out of HTTP. The status code is committed the instant the first token leaves, so everything that goes wrong afterwards — a model server dying, a guardrail firing, a timeout — has to be expressed inside the stream and handled by a client that is already showing text. There is no transparent retry once bytes are on the wire. You either resume from where it stopped or you visibly start over, and which one you pick is more a product decision than an engineering one. For the same reason the stream needs an explicit end-of-answer event: with a 200 long since sent, a connection that simply closes looks exactly like an answer that finished. Cancellation is the other half of it. Close the tab and nothing stops generation unless the gateway notices the dropped connection and propagates a cancel to the model server — and whether it can notice at all depends on what sits in front of it, since any proxy or CDN that buffers responses will hold that connection open, collect the whole answer, and hand it over in one lump long after the browser is gone. Tokens generated for a reader who left are the only work in this system with nobody at the end of it, and somebody still has to decide who pays for them.

Step 8 · Keep it safe

Guard the input and the output

Users will send prompts that try to jailbreak, extract secrets, or produce harmful content — and even a well-behaved model can generate something unsafe. Where do you put the checks?

Inference Gatewayentry pointGuardrailsin + outModel Serversbatch + stream
New in this step: Guardrails.

Where do safety checks belong in the request path?

  1. Models can be jailbroken and make mistakes. Safety can’t be the model’s job alone — you need an independent gate.

  2. Input filtering catches malicious prompts but not unsafe generations. A clean prompt can still yield output you must block. You need both sides.

  3. A Guardrails layer checks the prompt before generation and the answer before it reaches the user — defense on both edges, independent of the model.

A Guardrails layer screens both edges: the incoming prompt (block jailbreaks, abuse, PII leaks) before it reaches the model, and the outgoing answer before it streams to the user. Safety flags are emitted to the event stream so abuse can be detected and rate-limited over time.

Why this piece earns its place

Output screening and streaming pull against each other, and this is where the design is actually decided. Checking an answer the user has already read is not a check, so you either hold a chunk at a time before releasing it — spending the exact metric the previous step optimized — or you screen as you go and accept that you will occasionally retract text already on screen. Choose one on purpose; the default is to stumble into the second. It is also worth naming that a guardrail is a model. It has its own latency, it sits on the critical path twice, and it has its own failure modes, so it needs its own eval set and its own thresholds rather than being tuned by eye. The failure nobody sees is the false positive. A wrongly blocked answer looks identical in your metrics to a correctly blocked one, and the refused user just leaves. Reviewing those needs the flagged text, which is precisely the content you are least free to retain — so sample it, keep categories and scores in the clear, and put the text itself behind tighter access and a shorter window.

What the new pieces do

Guardrailsservice
Screens the incoming prompt and the outgoing answer for unsafe or disallowed content before either is acted on.

The payoff

You built a conversational AI

From one box to a production LLM system: a gateway that orchestrates, a context builder that budgets tokens, durable memory, a batched GPU serving tier, streaming, and guardrails on both edges.

UserAPI GatewayContext BuilderMemory ServiceGuardrailsModel ServersConversation DBModel WeightsVector / KBEvent Stream
The finished design, end to end. · swipe to pan the diagram

Now overload the model servers and watch how the serving tier — isolated behind its own queue — sheds load and degrades to a smaller model instead of taking the whole product down.

Everything you assembled, in order

  • Inference Gateway — one front door — auth, rate limits, orchestration
  • Context Builder — assembles system prompt + history + message
  • Token budget — pin, keep recent, summarize, retrieve (RAG)
  • Memory Service + DB — history lives outside the stateless model
  • Model Servers — batched GPU inference + KV cache
  • Streaming — tokens pushed as generated — fast time-to-first-token
  • Guardrails — screen both the prompt and the answer
  • Event stream — usage + safety events → billing, evals, abuse detection

Deep cut · 13:08

The form never got bigger. It got chosen.

The interactive build above lays out a conversational AI: a gateway, a context builder working to a token budget, a conversation store, a model tier with batching and a KV cache, streaming, and guardrails. The film zooms into the one part that decides whether the bot seems to remember: the context window. Maya writes to Pim, a travel agent who keeps nothing between telegrams, so before every message Otto the clerk writes Pim one fresh form with a fixed number of boxes. At telegram three Maya says she does not fly. At telegram forty Pim books her the six a.m. flight.

  • See the wreck: a sliding window drops the oldest telegrams first, and a summary turns “I don’t fly” into “discussed a trip to Portugal”. Nothing crashes, nothing errors, every box on the form is true, and the one rule Maya set loses a fight for space because it was said early and said once.
  • Take it into the interview: pack the context on purpose. A system prompt nothing pushes off, recent turns word for word, older ones summarised, a token budget per part, retrieval by meaning for old turns the new question needs, and user rules pinned so they never depend on a search going right. Then name the bill: summary calls, a search per turn, and every token paid for on every message.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why does the Context Builder rebuild the full prompt on every single turn instead of somehow keeping the model "warm" with prior context?

    Inference is stateless by construction — a model server has no persistent memory of a specific user's session between HTTP requests, and even if a specific GPU replica happened to still hold state from the last turn, the NEXT turn could easily be routed to a different replica by the load balancer. Rebuilding the full context every request is what makes the system horizontally scalable: any replica can serve any turn, because everything the model needs is self-contained in that one request's prompt. The cost is resending tokens every turn (which the token budget in Step 3 manages); the benefit is a serving tier with no per-user sticky state to coordinate.

  2. Why summarize or drop older turns instead of just always retrieving relevant history via RAG, the same way external knowledge is handled?

    RAG retrieval works well for FACTS — discrete, indexable pieces of information you can search for by similarity. Conversation history is different: it's sequential, and often what matters is the THREAD of reasoning (what was already tried, what was already rejected) rather than any single retrievable fact. A running summary preserves that narrative continuity in a compact form; pure retrieval could pull an isolated old message without the surrounding context that made it make sense. Most production systems use a combination: recent turns verbatim, a running summary for the middle, and RAG only for genuinely fact-like content (documents, a knowledge base) rather than for the conversation itself.

  3. The chaos scenario shows model-server overload causing a "busy, retry" response. Why not just queue every request indefinitely instead of shedding load?

    An unbounded queue during sustained overload means EVERY request — including ones from users who'd be happy to retry in a few seconds — waits behind an ever-growing backlog, and the wait time itself becomes unpredictable and can exceed what any client-side timeout tolerates anyway. Shedding load (explicitly telling some requests "busy, retry shortly" rather than silently queuing them) keeps the SYSTEM's behavior predictable and lets the client decide how to handle the delay (retry with backoff, show a friendly message) — an unbounded queue just defers the failure to an unpredictable later moment instead of communicating it honestly now.

  4. Why do both the incoming prompt AND the outgoing answer need separate guardrail checks — couldn't one sufficiently strict input filter prevent unsafe outputs entirely?

    A clean, entirely reasonable-looking prompt can still produce an unsafe generation — the model might hallucinate harmful content, leak something from its training data, or simply make a mistake that has nothing to do with anything malicious in the input. Conversely, an input filter alone catches attempts to elicit bad behavior but can't catch bad behavior that emerges without an adversarial prompt at all. The two checks are looking for genuinely different things (is the REQUEST trying to misuse the system vs. is the RESPONSE actually safe to show), which is why defense needs to sit at both edges of the model, independently.

  5. How would this design need to change to support multiple simultaneous conversations per user (e.g. several open chat tabs), rather than one linear thread?

    The Conversation DB already models history as discrete turns rather than one global blob, so the core schema doesn't need to change much — each conversation just needs its own thread/session identifier, and the Memory Service loads and saves against that specific thread id rather than a single per-user history. The more interesting design question is at the Context Builder: for a genuinely multi-thread product, you'd want to make sure a message sent in thread A never accidentally pulls context from thread B (a scoping discipline similar to tenant isolation elsewhere in this series, just scoped to conversation-id instead of tenant-id) — the gateway and context builder both need to consistently thread that id through every step, not just the database schema.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. The Inference Gateway sits between the client and the model servers mainly to…

    A browser can't be trusted with keys or limits — the gateway is the neutral backend every chat product needs.

  2. The Context Builder assembles a fresh prompt every turn because…

    Inference never updates the weights, so anything the model needs to "remember" has to be re-included in every request.

  3. When a conversation exceeds the context window, the right approach is to…

    Treating the window as a budget to allocate on purpose avoids losing the system prompt or wasting tokens on stale content.

  4. Continuous batching and the KV cache exist together to…

    Batching raises throughput across the fleet; the KV cache makes each new token cheap by reusing prior attention state.

  5. Guardrails screen both the incoming prompt and the outgoing answer because…

    Input filtering and output filtering are independent defenses against different risks, not redundant copies of the same check.

Worked example: turn 40 of Maya’s chat, from send to the last streamed token

At turn 3 Maya said she does not fly. At turn 40 she types “get me to Lisbon by Friday”, about 160 characters, so roughly 40 tokens at ~4 characters a token. The window is 128k. Every token count except that one is illustrative.

StepWhereWhat happensTokens
1Inference GatewayAuthenticates and reserves a pessimistic token estimate, settled when the turn completes.reserve, settle later
2Memory Service → Conversation DBWrites her message first, with the assistant turn marked incomplete, then loads the running summary, the last 10 turns and her pinned rules.1 write, 1 read
3Context BuilderConstant parts first (system prompt, pinned rule “does not fly”), then summary, recent turns, new message, counted with the target model's tokenizer.1,500 + 50 + 800 + 6,000 + 40
4Vector / KBRetrieval by meaning pulls 2 old turns about Lisbon dates. The pinned rule never depends on this search.+600 → 8,990 in (7.02% of 128k)
5Guardrails (in)Screens the prompt before generation. It passes.1 check
6Model ServersJoins a running batch; the shared system-prompt prefix is already computed, so only Maya's part is new work. Generates a train itinerary.400 out (illustrative)
7Guardrails (out) → Gateway → UserChunks are screened and streamed over SSE as they arrive, followed by an explicit end-of-answer event.time-to-first-token is what she feels
8Memory Service + Event StreamMarks the turn complete, refreshes the summary in the background, and emits tokens and latency for billing.9,390 billed (in + out)

The trace shows where the 6-second answer goes. Not the database or the wire: 400 sequential decode passes on the GPU tier, which is why streaming and time-to-first-token matter more than total time (Step 7). Maya got a train because the rule was pinned. A sliding window over 40 turns would have dropped turn 3 first (Step 3). Of the 8,990 tokens in, 1,500 (16.7%) are a system prompt shared by every user, which is why it goes first: put a timestamp above it and that reuse breaks for everyone. At 10× concurrency, compute is not what runs out first. KV-cache memory does: every in-flight context holds its cache, so long chats shrink how many fit in a batch (Step 6). And new GPU replicas are minutes away, so capacity has to be forecast ahead of traffic (Step 5).

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Answer a turn: take the user’s message plus the conversation so far, run the model, and stream back a reply.
  • Remember the chat: persist every turn so a conversation survives reloads, new devices, and crashes.
  • Fit the window: assemble system prompt + relevant history + the new message within a fixed token budget.
  • Ground with knowledge: retrieve only the relevant passages from a vector store (RAG) instead of stuffing everything in.
  • Stay safe: screen the incoming prompt and the outgoing answer for unsafe or disallowed content.

The qualities that shape everything

Each one names the mechanism that buys it.

Cost and latency don’t blow up per turn
The context builder works to a token budget — pin the system prompt, keep recent turns, summarize the rest — since cost and latency both scale with tokens in + out.
A stateless model that still remembers
History lives in a durable Conversation DB, reloaded per turn — the model is fed its memory as text each request rather than holding it.
Isolate the expensive bottleneck
GPU inference gets its own queued, independently-scaled tier so it can be batched, autoscaled, and protected from the cheap stateless work around it.
Serve a giant model economically
Continuous batching packs concurrent chats into shared GPU passes and a KV cache reuses attention state, so each new token is cheap.
Feel instant even on a slow answer
Tokens stream to the browser over a persistent connection as they’re generated — time-to-first-token, not total time, is what the user feels.
Safe on both edges of the model
An independent guardrails layer screens the prompt before generation and the answer before it streams — a clean prompt can still yield unsafe output.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Budget the context window over send the whole conversation every time

Overflowing the window errors or silently drops the start — usually the system prompt, the most important part — and cost scales with every token. Pin, keep recent, summarize or retrieve the rest.

A durable Conversation DB over keep history only in the browser tab

Close the tab and it’s gone; switch devices and it’s gone. The client is a fast cache, not the source of truth; the stateless model needs history reloaded from a durable store each turn.

Continuous batching + KV cache over one request per GPU, start to finish

A single chat can’t saturate a GPU and others queue behind it. Batching raises throughput across the fleet, and the KV cache avoids re-running attention over the whole prompt per token.

Stream tokens as generated over wait for the whole answer

Six seconds of blank screen reads as frozen. Streaming doesn’t speed up generation — it shows progress immediately, and optimizing time-to-first-token beats optimizing total time.

Guardrails on both edges over filtering only the user’s input

Input filtering catches malicious prompts but not unsafe generations — a clean prompt can still produce output you must block. Safety is an independent layer around the model, not a property of it.

The answer, out loud

What a strong answer to “Design a Conversational AI (ChatGPT-style)” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Say plainly what the product does

    Let me state the product plainly first, because it keeps the rest of this honest. Somebody types something, we put that in front of a language model along with whatever has already been said, and the reply comes back as it is written — for a very large number of people at once. Everything hard hides inside that sentence: what goes into the model, how much of it fits, how it remembers earlier turns, how we serve something this expensive, and how we keep the output safe. I’d build it in roughly that order.

  2. 3–6 min

    The skeleton: client, gateway, model

    The skeleton is three boxes. A browser can’t hold the model and can’t be trusted with the keys or the rate limits, so I’d put an inference gateway in the middle — it authenticates, applies limits, assembles the prompt, and routes to the GPU fleet. The alternative people reach for is calling the model endpoint straight from the client, and that gives away the auth boundary and the limits together. I’d keep the three roles separate, because every later decision then lands cleanly on one of them.

    Built in step 1: A client, a gateway, a model
  3. 6–11 min

    The model is stateless, so rebuild the prompt

    The thing to get straight early is that the model has no memory between calls — it sees the text we hand it this request and nothing else. So there’s a context builder whose whole job is to write that text fresh every turn: who the assistant is, what’s been said, what was just asked. All of it goes over the wire again each time, and that repetition isn’t waste — it’s what keeps the serving tier interchangeable, because nothing is sitting on one machine that the next request depends on.

    Built in step 2: Assemble the context window
  4. 11–17 min

    Budget the window, don’t fill it

    The window is finite — call it 128k tokens — and cost and latency both scale with tokens in plus tokens out, so I’d allocate it rather than fill it. Concretely: pin the system prompt so it can never be the thing that falls off the front, keep the recent turns verbatim while they fit, summarize the older ones into a running recap, and bring in outside knowledge by retrieving only what’s relevant instead of preloading it. The failure I’m designing away is sending everything and letting it overflow, because what gets dropped is the start — usually the system prompt, the part I care most about.

    Built in step 3: It won’t all fit — now what?
  5. 17–22 min

    Memory lives outside the model

    Then: where does the conversation actually live? Not in the model — inference doesn’t update the weights — and not only in the browser tab, because closing it or switching devices loses everything and I can’t rebuild context server-side. So a memory service reads and writes a durable conversation DB, and that is the source of truth. Each turn loads recent history from it, and after each answer the new turn is appended. The client is a fast cache of what the database already knows.

    Built in step 4: Where does history live?
  6. 22–29 min

    Isolate serving, then make it affordable

    Now the expensive part. Building a prompt costs almost nothing; every token of the answer is another full pass through a very large model on hardware there isn’t much of. So generation gets its own tier — its own queue, its own scaling, its own failure handling — and two things inside it make the economics work. Continuous batching: rather than give a GPU one conversation at a time, the server keeps a rolling batch moving through the same pass and frees a slot the moment an answer finishes, instead of waiting on the slowest. Then the KV cache: the attention already worked out for earlier tokens is kept, so the next one is a little work on top rather than a fresh read of the whole prompt. Batching costs any single request some latency to buy throughput across the fleet, and I’d take that trade.

    Built in step 6: Batching and the KV cache
  7. 29–34 min

    Stream, and optimize the first token

    A long answer takes several seconds, and if nothing appears for six of them people assume it’s broken. So the connection stays open and the gateway pushes each token down it as the model produces it — server-sent events or a WebSocket. Polling would add load and still trail the model by whatever the interval is. None of this makes the answer arrive sooner; it stops us hiding progress that already exists. The number I’d hold the team to is time-to-first-token, not total time.

    Built in step 7: Stream the tokens back
  8. 34–40 min

    Guardrails on both edges

    Safety I’d put in a service of its own that the gateway calls, not inside the model tier. Both edges get checked; I’d take that as read rather than argue it. What I’d argue is where the check lives. Policy changes far more often than the model does, and if it ships with the weights, tightening a rule means a GPU deploy — so it won’t happen on the day you need it. The line isn’t a constant either: the same answer can be fine in a developer tool and wrong in a product built for teenagers, so I’d make the threshold a per-surface parameter on the request, with one service enforcing all of them.

    Built in step 8: Guard the input and the output
  9. 40–45 min

    Close on the trade-off

    So, in one breath: a gateway out front, a context builder working to a token budget, memory in a durable store, a batched GPU tier with a KV cache, streaming, and guardrails on both edges. The trade-off underneath all of it: every token I keep buys context and costs money, latency and fleet capacity, and every safety or quality layer sits in front of the first token. Two things I’d want more time for. Nothing here tells me whether a prompt change or a new model made answers better — the event stream carries the raw material, but that eval loop is its own design. And memory that outlives one conversation: what to remember about a person across chats is a different question from what belongs in this turn’s window.

What this teaches

Learn AI system design by building a production conversational AI like ChatGPT step by step. An interactive guide covering the inference gateway, context-window assembly and token budgeting, conversation memory, model serving with batching and KV cache, token streaming, and prompt/output guardrails.

Key takeaways

  • Inference Gateway — one front door — auth, rate limits, orchestration
  • Context Builder — assembles system prompt + history + message
  • Token budget — pin, keep recent, summarize, retrieve (RAG)
  • Memory Service + DB — history lives outside the stateless model
  • Model Servers — batched GPU inference + KV cache
  • Streaming — tokens pushed as generated — fast time-to-first-token
  • Guardrails — screen both the prompt and the answer
  • Event stream — usage + safety events → billing, evals, abuse detection

Concepts covered

  • What is a conversational AI, really?
  • A client, a gateway, a model
  • Assemble the context window
  • It won’t all fit — now what?
  • Where does history live?
  • Inference is the expensive part
  • Batching and the KV cache
  • Stream the tokens back
  • Guard the input and the output
RUN IT YOURSELF

The text the reader already had when the guardrail said no

Step 7 pushes every token out the moment it exists; step 8 has to screen that same answer before anyone reads it. This file streams three model answers through both release policies — a fixed HOLD of N tokens, and release-on-sentence-boundary — and prints, for each, the tick the first token appears and the characters the reader saw and had taken back. Change HOLD, hit Run, and watch the two costs trade.

HOW TO READ THE CODE — 5 IDEAS
  1. Streaming commits the answer before it is screened, so the guardrail is always late by one sentence — checking text the user has read is not a check (step 8).
  2. At HOLD=4 the first token lands on tick 5 and 97 characters are shown then retracted. At HOLD=0 it is tick 1 and 146.
  3. Retraction only reaches zero at HOLD=13, which puts the first token on tick 14 — the exact metric step 7 exists to protect, spent.
  4. No constant wins: every hold that beats the boundary on first token still retracts, and every hold that retracts nothing is slower than it.
  5. The boundary does both — tick 6, nothing retracted — because a constant must cover the longest sentence (14 tokens) while a signal waits out only the opening one (6).
CPython · WebAssembly
built to be reasoned about, not memorized — make the calls, trip the guardrail, run the quiz.
Finished this one? 0 / 65 AI System Designs done
NextDesign a RAG Pipeline Closest AI system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs