Design a ChatGPT-style assistant¶
At a glance
Priority: P0 · Complexity: ⅘ · Est. time: 3 h · Phase: 3 · Prereqs: Framework, Context engineering, Memory systems, Cost & latency optimization You're done when: you can design a multi-tenant, streaming, tool-using chat assistant for millions of users — covering conversation storage, context-window management, memory, tool execution via MCP, streaming transport, GPU/API capacity math and safety — and explain how it differs from the classic chat-system case study.
Problem¶
"Design a ChatGPT-like assistant: users chat with an LLM in multi-turn conversations, upload files, and the assistant can use tools (web search, code execution, company connectors). Web + mobile. Scale: 5M DAU."
Enterprise variant worth practising: an internal "company GPT" for a 100k-employee logistics group with connectors (MCP) to shipment tracking, SharePoint and ticketing, SSO and per-user permissions.
Clarifying questions¶
| Question | Why it matters |
|---|---|
| Consumer or enterprise? | Enterprise → SSO, per-user tool auth, data retention/eDiscovery, no training on data |
| Which capabilities in v1: files, web search, code execution, image generation, voice? | Each adds a subsystem (sandbox, file parsing, search provider) |
| Models: one or many, user-selectable? | Routing layer, per-model quotas, pricing tiers |
| Long-term memory across chats? | Memory extraction/storage, privacy controls |
| Latency expectations? | Streaming mandatory; TTFT target |
| Free vs paid tiers? | Rate limits, model access, priority queuing |
| Regions / residency? | Multi-region deployment, region-pinned models |
Requirements¶
Functional: create/list/rename/delete conversations; send message and stream response; stop generation; regenerate/edit (branching); attach files; tool use with visible progress; memory on/off and view/delete; share conversation link; feedback.
| NFR | Target (assumed) |
|---|---|
| Quality | Tool-call success ≥ 97%; factual-error rate on eval set below threshold; refusal rate on benign prompts < 1% |
| Latency | TTFT p95 < 1 s (non-reasoning), streaming ≥ 30 tokens/s perceived; tool-using turns show progress within 1 s |
| Cost | Free tier < $0.30/user/month; paid tier margin-positive at $20–30/month |
| Safety | Content policy enforcement, injection-safe tools, no cross-user data leakage, PII handling, abuse detection |
| Availability | 99.9% chat; graceful degradation to smaller model on provider/GPU saturation |
| Durability | No lost messages once acknowledged |
Estimation¶
5M DAU × 8 turns/day = 40M turns/day ≈ 460 turns/s avg, ~1,400/s peak (3× factor)
Tokens per turn: system+tools 2k, history (truncated/summarised) 3k, user 150 → ~5k input; output 400
Daily: 40M × 5k = 200B input tokens; 40M × 400 = 16B output tokens
Output tokens/s at peak: 1,400 × 400 / ~10 s generation ≈ 56k tokens/s being streamed concurrently
Concurrent streams at peak ≈ 1,400 turns/s × ~10 s = 14k open streaming connections (plus idle websockets)
API cost (assumed $3/M in, $15/M out, 70% cached prefix at 10% price):
input effective ≈ 200B × (0.3 + 0.7×… only the 2k prefix is cacheable) → treat 2k/5k cacheable:
cacheable 80B × $0.3/M = $24k/day; uncached 120B × $3/M = $360k/day; output 16B × $15/M = $240k/day
≈ $620k/day → ~$19M/month (≈ $3.7/DAU/month) → need routing: most chat is easy
Route 70% of turns to a small model (~1/10 price) → ~$7M/month (~$1.4/DAU)
Self-host angle: a 70B-class model on 8×H100 with vLLM might sustain ~several thousand output tok/s under
batching; 56k tok/s peak → tens of nodes for the big model (validate with benchmarks, not this estimate).
Storage: 40M turns × ~4 KB (text + metadata) = 160 GB/day → ~58 TB/year before compression.
The estimate reveals the design driver: token economics and routing, not message storage.
Architecture¶
flowchart TB
C[Web / mobile client] <-->|SSE or WebSocket stream| EDGE[Edge / API gateway<br/>auth, rate limit, tier]
EDGE --> CS[Chat service<br/>stateless]
CS --> CONV[(Conversation store<br/>messages, branches)]
CS --> ORCH[Turn orchestrator<br/>agent loop, max steps, budget]
ORCH --> CTX[Context builder<br/>system prompt, memory, history window, files]
CTX --> MEM[(Memory store<br/>user facts, preferences)]
CTX --> FILES[(File store + per-chat index)]
ORCH --> TOOLS[Tool router]
TOOLS --> MCP1[MCP: web search]
TOOLS --> MCP2[MCP: enterprise connectors<br/>per-user OAuth]
TOOLS --> SBX[Code sandbox<br/>ephemeral microVM]
ORCH --> LGW[LLM gateway<br/>routing, prompt cache, fallback, quotas]
LGW --> P1[(Frontier API)]
LGW --> P2[(Self-hosted vLLM pool)]
ORCH --> GR[Guardrails<br/>input/output moderation, injection checks]
CS --> BUS[(Event bus)]
BUS --> ANA[Analytics, eval sampling, memory extraction, titles]
ORCH -. OTel GenAI .-> OBS[(Tracing)]
Key idea: the chat service is stateless; the conversation store is the source of truth; the orchestrator runs a bounded agent loop per turn; everything slow and non-critical (titles, memory extraction, eval sampling, analytics) goes async via the event bus.
Component deep dives¶
1. Streaming transport¶
| Option | Pros | Cons |
|---|---|---|
| SSE over HTTP | Simple, works through proxies/CDNs, native in browsers; each turn is a request | One-directional; reconnection must resume by offset |
| WebSocket | Bidirectional (stop, voice, typing), one connection | Sticky connections, LB/idle timeouts, harder to scale horizontally |
| AG-UI protocol over SSE/WS | Standard event types for agent state, tool progress, shared state; adopted by major agent frameworks in 2026 | Another abstraction to adopt |
Design: SSE for token streams (resume via Last-Event-ID and a per-turn token log in Redis/stream store), a separate cancel endpoint that signals the orchestrator (which aborts the upstream model call to stop paying for tokens). Persist the assistant message incrementally (or at completion with a checkpoint) so a reconnecting client can recover.
2. Conversation storage & branching¶
Messages form a tree (edit/regenerate creates siblings). Schema: messages(conversation_id, message_id, parent_id, role, content_parts[], model, token_counts, created_at); conversations(id, user_id, title, current_leaf_id, …). Partition by user_id / conversation_id (Cassandra/DynamoDB/Cosmos-style wide-column or sharded Postgres). Reads are "load the path from leaf to root" — cache the active path. Retention and deletion must cascade to derived stores (memory, file indexes, traces, eval samples) — a GDPR requirement people forget.
3. Context-window management¶
The context builder is the heart of quality and cost.
| Technique | Use | Trade-off |
|---|---|---|
| Sliding window of last N turns | Default | Loses early facts |
| Rolling summary of older turns | Long chats | Summarisation cost; summary drift |
| Retrieval over conversation history | Very long chats, "what did I say earlier about X" | Extra infra; misses implicit context |
| Tool-result compaction / clearing | Agentic turns with big tool outputs | Must keep references to re-fetch |
| Prompt caching friendly ordering | Always | Static first: system → tool defs → memory → summary → recent turns |
Budget example for a 128k-window model: system + tools 3k, memory 1k, summary 1k, recent turns up to 8k, files/tool results up to 20k, reserve output 4k. Just because the window is 200k–1M doesn't mean you should fill it — cost is linear in input tokens and quality degrades with distractors ("context rot").
4. Memory¶
| Type | Example | Store |
|---|---|---|
| Session (working) | Current task state | Conversation itself |
| Long-term semantic | "User works in customs brokerage, prefers metric units" | Memory store (Mem0/Letta/Zep-style or custom table + vector index) |
| Episodic | Summaries of past chats | Vector index per user |
| Procedural | Custom instructions | User profile |
Write path: async extractor after each turn proposes memory ops (add/update/delete) → dedupe/conflict resolution → store with provenance. Read path: retrieve top-k relevant memories into context. Privacy: user-visible, editable, deletable; never memorise secrets/sensitive categories; memory is a poisoning vector (OWASP Agentic: memory & context poisoning) — content from tools/web pages should not write memory without user action.
5. Tools via MCP¶
- Tool router exposes a curated subset of tools per turn (too many tool definitions hurt accuracy and burn tokens; use tool search/lazy loading for large catalogs).
- Enterprise connectors run as MCP servers using per-user OAuth (the user's delegated token, not a service super-account) — the 2025-11-25 spec's authorization model and the 2026-07-28 revision's OAuth/OIDC hardening make this the standard pattern.
- Code execution: ephemeral sandbox (Firecracker/gVisor microVM), no network by default, CPU/memory/time limits, files mounted per conversation.
- Web search results are untrusted: never let content from a fetched page trigger another side-effecting tool without confirmation.
6. Model routing¶
| Strategy | How | Risk |
|---|---|---|
| User-selected model | UI picker | Users pick the biggest model for everything |
| Rule-based | Attachments/code/long context → big model | Brittle |
| Learned router (RouteLLM-style) | Classifier predicts if small model suffices | Needs preference data; monitor quality per route |
| Cascade | Small model answers; verifier escalates | Double latency on escalations |
Plus reliability routing: provider fallback on 429/5xx, region failover, priority tiers for paid users.
7. Guardrails & abuse¶
Input moderation (fast classifier, parallel with the first model call where possible), output moderation on streamed chunks with ability to retract, jailbreak/injection classifiers, per-user and per-IP rate limits, anomaly detection for scraping/automation, and account-level abuse scoring. For enterprise: DLP on outbound content, tenant data never used for training, audit logs.
Evaluation strategy¶
- Offline suites per capability: general chat quality (pairwise preference judged by LLM, calibrated to humans), tool-use correctness (did it call the right tool with right args — deterministic checks), long-context recall, memory correctness (right memory retrieved, no stale facts), safety (red-team prompts, over-refusal set).
- Regression gates on prompt/model/router changes, sliced by language, capability and tier.
- Online: A/B tests on thumbs rate, regenerate rate, conversation length, return rate; sampled LLM-judge on production traces with human review of disagreements.
- Error analysis: weekly trace review of regenerates and thumbs-down; taxonomy like wrong tool, tool args malformed, ignored file, memory wrong, over-refusal, verbosity.
Observability¶
Per turn: trace with spans for context build, each model call (model, tokens in/out/cached, TTFT, TPOT), each tool call (name, latency, error), guardrail decisions. Metrics: TTFT/TPOT p50/p95 by model/region, stream disconnect rate, cancel rate, tokens/turn, cost per DAU by tier, tool error rate, router distribution, moderation block rate. Sample traces into the eval pipeline (with consent/redaction rules).
Failure modes¶
| Failure | Mitigation |
|---|---|
| Hallucinated facts | Encourage tool use (search) for factual queries, citations, calibrated uncertainty language, evals on factuality slices |
| Indirect prompt injection via web/file content | Treat tool outputs as data, confirm side-effects, restrict tools in turns with untrusted content (lethal trifecta), injection classifiers |
| Tool misuse / runaway loops | Max steps per turn, per-turn token and $ budget, duplicate-call detection, timeouts |
| Provider outage / 429 storms | Gateway fallback chain, degrade model, queue with backpressure, status messaging |
| Stream drop mid-answer | Resume by event ID, server-side persistence of partial output |
| Memory leak across users | Memory keyed by user, isolation tests, no shared semantic cache for personalised content |
| Cost spike (abuse, long contexts) | Tier quotas, context caps, anomaly alerts, bot detection |
Scaling & cost optimization¶
- Stateless chat/orchestrator pods autoscale on concurrent streams, not CPU.
- Prompt caching: keep system prompt and tool definitions byte-stable; version them deliberately.
- Routing and cascades: largest lever at consumer scale.
- Self-host the high-volume small model on vLLM/SGLang with prefix caching and continuous batching; keep frontier APIs for hard queries — hybrid fleet.
- Async/batch for non-interactive work (titles, memory extraction, summaries) via cheaper models and batch APIs.
- Output length control via instructions and
max_tokensper tier.
What a Staff-level answer adds¶
- Distinguishes the data plane (turn execution) from the control plane (model catalog, prompts, tool registry, policies, quotas) and versions everything in the control plane.
- A capacity plan with provider rate-limit contracts (TPM/RPM) and reserved/provisioned throughput vs pay-as-you-go.
- Privacy architecture: deletion propagation, retention, residency, "no training" guarantees, audit.
- Product-quality loop: which online metric is the north star (e.g., successful sessions/week) and how offline evals predict it.
- Protocol strategy: MCP for tools, AG-UI for front-end events, A2A if other teams' agents are invoked.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| Effective context engineering for AI agents (Anthropic) | article | Compaction, tool-result clearing, memory — the context builder's playbook | intermediate | free |
| Prompt caching (Claude docs) | docs | Exact prefix-caching semantics to design prompt ordering | intermediate | free |
| Prompt caching (OpenAI docs) | docs | Automatic caching behaviour for comparison | intermediate | free |
| AG-UI docs |
docs | Standard event model for streaming agent UIs | intermediate | free |
| MCP specification | docs | Tool/connector protocol and auth model | advanced | free |
| Mem0 docs | docs | Concrete memory extraction/update design | intermediate | freemium |
| RouteLLM (LMSYS blog) |
article | Learned routing with cost/quality curves | advanced | free |
| Musings on building a GenAI product (LinkedIn Eng) |
article | Honest production lessons on latency, evals, capacity | intermediate | free |
| Beyond the sandbox: Claude Code sandboxing (Anthropic) | article | Filesystem/network isolation pattern for code tools | advanced | free |
Follow-up questions¶
L2 — Apply¶
Q1. A user clicks Stop mid-stream. Walk through what must happen server-side.
Answer
Client calls cancel endpoint (or sends WS message) with turn ID → chat service publishes a cancel signal (Redis pub/sub or orchestrator control channel) → orchestrator aborts the in-flight model request (closing the upstream stream stops token billing at most providers) and any running tools (sandbox kill) → persists the partial assistant message with finish_reason=cancelled and token counts → emits a final stream event. Idempotent: repeated cancels are no-ops. Metrics: cancel rate by model (high rates signal verbosity or slowness).
Q2. A conversation has grown to 180k tokens. The model supports 200k. What does your context builder send?
Answer
Not 180k. Keep static prefix (system + tools, cacheable), user memory, a rolling summary of old turns (refreshed every N turns), the last ~10–20 turns verbatim, and retrieved snippets from older turns relevant to the current message; clear stale tool outputs, keeping references. Target maybe 15–30k tokens. Rationale: cost is linear in input tokens, TTFT grows with prefill, and accuracy drops with distractors. Evaluate with a long-conversation recall suite.
Q3. Estimate the concurrent streaming connections and pods needed at 1,400 turns/s peak with 10 s average stream duration.
Answer
Little's law: 1,400 × 10 = 14,000 concurrent streams. An async (event-loop) service can hold ~5–10k idle-ish streaming connections per pod, but orchestration work (JSON parsing, guardrail calls) limits it; plan ~1–2k active turns per pod → 10–15 pods plus 50% headroom and multi-AZ → ~24 pods. Scale on active-stream count. The real limiter is upstream model capacity (TPM limits), not pods.
Q4. How would you implement 'edit a previous message' without losing the old branch?
Answer
Messages as a tree: editing message M creates a new sibling M' with the same parent; generation continues from M'. current_leaf_id on the conversation points to the active branch. Loading = walk parent pointers from the leaf (cache the path). UI shows "< ⅔ >" sibling navigation. Context building uses only the active path. Deletion policies must handle whole subtrees.
L3 — Design & trade-offs¶
Q5. SSE or WebSocket for streaming? Decide for a web + mobile assistant with a voice feature planned next year.
Answer
SSE now for text streaming: simple, HTTP-native, CDN/proxy-friendly, easy resume with event IDs, and stateless LB. Cancel via a separate POST. For voice, use a dedicated realtime channel (WebRTC or WebSocket to a realtime speech service) rather than forcing all chat over WebSockets. Keep the event schema transport-agnostic (e.g., AG-UI events) so switching transports doesn't change the client model.
Q6. Should the free tier use a self-hosted open model while paid uses frontier APIs?
Answer
Plausible, with evidence. Free-tier traffic is huge, price-sensitive and mostly simple; a self-hosted mid-size model at high utilisation can cut cost per token substantially. Risks: quality gap hurts conversion; GPU capacity planning and on-call; safety tuning of the open model. I'd A/B: free tier on open model vs small API model, measuring retention and upgrade rate, not just eval scores. A hybrid fleet with a learned router often wins: open model for easy turns, API for hard ones regardless of tier.
Q7. Where do you enforce content moderation for streamed output without killing TTFT?
Answer
Input moderation runs in parallel with starting the model call (cancel generation if input is flagged). Output moderation runs on buffered windows (e.g., every sentence/200 tokens) asynchronously; if flagged, stop the stream and replace/retract the message. For high-risk categories, delay display by a small buffer. Accept a tiny exposure window as a documented trade-off, or for enterprise/regulated contexts, moderate before display at the cost of latency.
Q8. How do you prevent a malicious web page (fetched by the search tool) from making the assistant email the user's files to an attacker?
Answer
Break the lethal trifecta: in any turn where untrusted content is in context, disable or require explicit user confirmation for exfiltration-capable tools (email, HTTP POST, file share). Tools use per-user scoped tokens with least privilege. Show the full action (recipient, attachments) in a confirmation UI rendered from structured tool args — not model prose. Add injection classifiers on tool outputs, egress allow-lists, and red-team evals with poisoned pages (promptfoo OWASP agentic preset).
L4 — Staff-level ambiguity¶
Q9. Leadership wants to launch the company-internal assistant to 100k employees in 8 weeks. What do you cut and what don't you cut?
Answer
Cut: custom model hosting, long-term memory, code execution, broad connector catalog, fancy routing. Use a managed platform (Microsoft Foundry Agent Service or Bedrock AgentCore) or a thin custom stack over a gateway. Keep (non-negotiable): SSO + per-user permissions on the 2–3 connectors shipped, audit logging and retention policy, DLP/PII controls, basic moderation, eval suite for top intents, cost quotas per user, OTel tracing. Launch to a 2k pilot in week 6, measure, then expand. Document deferred items with trigger criteria.
Q10. Cost per DAU is 3× the business plan after launch. You have one quarter. Plan?
Answer
Decompose cost by driver from traces: model mix, input vs output tokens, cached vs uncached, tool loops, long-context turns, heavy users. Typical plan: (1) fix prompt ordering to maximise cache hits (days); (2) cap history/tool-output tokens via summarisation/compaction (weeks); (3) introduce routing with eval gates per slice (weeks); (4) move background work to batch + small models; (5) quotas and fair-use limits for extreme users; (6) negotiate provisioned throughput. Track weekly cost/DAU and quality metrics side by side; publish a burn-down to leadership.
Q11. Legal asks for a guarantee that deleted conversations are gone everywhere within 30 days. What's your design?
Answer
Maintain a data lineage map of every store derived from conversations: primary store, search/vector indexes, memory store, file store, traces/observability backend, eval datasets, analytics warehouse, backups, provider-side retention (zero-data-retention agreements). Deletion emits an event consumed by each store's deleter with idempotent, verifiable completion; a reconciliation job audits orphans. Backups: crypto-shredding (per-user keys) to avoid restoring deleted data. Evals built from prod must reference traces by ID and be purged or anonymised accordingly.
Real-world use cases¶
- Company GPT for a logistics group: SSO, MCP connectors to shipment tracking and document stores, per-user OAuth, audit — the enterprise flavour of this design.
- Customer-facing assistant on a carrier's portal: booking questions, tracking, invoices; tools restricted to the customer's own account; strict rate limits.
- Developer assistant in an IDE: repository context, tool execution in sandboxes — overlaps with Coding agent.
- Consumer assistant at scale: routing + self-hosted small model to hit unit economics.
Checklist¶
- I can draw the architecture and explain why the chat service is stateless
- I can compute concurrent streams, token volumes and $/DAU with routing and caching
- I can explain context-window budgeting, summarisation and cache-friendly ordering
- I can describe memory write/read paths and their privacy/poisoning risks
- I can defend SSE vs WebSocket and the stop/resume mechanics
- I answered all L3/L4 questions out loud in < 3 min each