ML System DesignTechPrufer

Designing a RAG System That Can Prove It Works

From a zero-dollar Faiss + llama.cpp MVP to a $1.3M/month cloud bill at 1,000 RPS — the architecture, the fracture points, and an evaluation plan for every stage of the pipeline.

Engineering 1,000 RPS target12 min read
RAG system architecture abstract

“Design a RAG system” is easy to answer badly: draw a vector database, wave at an LLM, say “we'll add reranking later.” The version that gets tested in ML system design interviews — and in production — adds one word: evaluation. Answer questions over a private corpus, keep documents fresh, cite sources, stay fast, avoid hallucinations, and measure every one of those claims.

The Interview Problem

Build natural-language Q&A over a private document corpus — internal docs, PDFs, knowledge-base articles — and walk through how you would evaluate each component. Four requirements do all the work:

Freshness

Frequent document updates must reach the index without downtime or stale answers.

Traceability

Every answer carries citations back to the exact source documents and pages.

Interactive latency

Low-latency responses while scaling from 100 to 1,000 requests per second.

Groundedness

Answers stay inside the retrieved context — and an evaluation plan proves it, stage by stage.

Phase 1: The Zero-Dollar MVP

Start with two pipelines on commodity hardware, all open source. Documents live in a Git-managed filesystem, get parsed with PyPDF2 / python-docx / markdown-it-py, split into 256–512-token chunks with overlap, embedded with all-MiniLM-L6-v2 (384 dimensions) on CPU, and indexed in-memory with Faiss. Queries embed with the same model, pull top-k (5–10) chunks, and a quantized Llama 2 7B generates via llama.cpp — answers post-processed to attach citations from chunk metadata.

Ingestion pipeline
Docs (Git / FS)
Parse → chunk (256–512 tok)
MiniLM embeddings (384-d)
Faiss (in-memory)
Query pipeline
User query
Embed + top-k retrieve
Prompt assembly
Llama 2 7B → answer + citations

Figure 1: The zero-dollar MVP — every box is open source and runs on CPU.

The Back-of-Envelope Math

Assume 1,000 internal documents averaging 1 MB, chunked at 256 tokens. This is where the design stops being hand-wavy:

976K

Total Chunks

~976 chunks per document across 1,000 documents. Small enough for a single index, large enough that naive rebuilds hurt.

1.5 GB

Embedding Storage

384 dims × 4 bytes = 1,536 B per chunk. The whole index fits in RAM on one commodity server.

~50 ms

Faiss Retrieval

Tens of milliseconds per query over ~1M vectors on CPU. Retrieval is not the problem.

5–10 s

LLM Inference on CPU

Quantized Llama 2 7B: 0.1–0.2 RPS per core. This is the fracture point.

back_of_envelope.txt
corpus        = 1,000 docs × 1 MB            = 1 GB raw text
chunks        = 1,000 docs × ~976 chunks/doc = 976,000 chunks
embeddings    = 976,000 × 1,536 B            = ~1.5 GB   (fits in RAM ✓)

retrieval     = ~50 ms/query on CPU          → fine at 100 RPS with parallelism
generation    = 5–10 s/query on CPU          → 0.1–0.2 RPS per core
cores @100RPS = 100 RPS ÷ 0.1 RPS/core       = ~1,000 CPU cores ✗

Databricks' engineering team puts it bluntly: LLM inference can be 100× more expensive and 100× slower than traditional ML inference. Retrieval is cheap; generation is nearly everything.

Phase 2: Where It Fractures

Push the MVP toward 100–1,000 RPS and four things break, in order:

1,000
750
500
250
0
~5 cores
Retrieval (Faiss)
~1,000 cores
Generation (Llama 2 7B)
CPU cores needed to serve 100 RPS

1. LLM inference throughput

CPU generation caps out three orders of magnitude below target. No amount of request queuing fixes a 5-second forward pass.

2. In-memory vector index

1.5 GB at 1K docs becomes 150 GB at 100K docs — past single-machine RAM, with ANN search contending for the same CPUs.

3. Ingestion freshness

Re-embedding 100 updated docs/day costs ~1.3 hours of CPU, and naive Faiss updates mean index rebuilds — downtime or stale answers.

4. Zero operational visibility

One machine, basic logs, no redundancy. Any crash takes the system down, and you cannot debug latency at 100 RPS from print statements.

Phase 3: The Evolutionary Forks

Each bottleneck has two escape paths. Interviewers love this framing because it shows you know why the managed service exists.

Path A: The No-Money Hack

  • LLM: quantized GGUF models, a Redis answer cache, request batching, vLLM/TGI with paged attention on a consumer GPU.
  • Index: hand-sharded Faiss across machines, disk-based ANN (Hnswlib), hybrid keyword fallback.
  • Ingestion: off-peak cron batches, incremental re-indexing of changed chunks, Git hooks as triggers.
  • Ops: self-hosted Prometheus + Grafana, structured JSON logs into ELK.
RedisvLLMFaissGrafana

Path B: The Paid Solution

  • LLM: managed APIs (GPT, Claude, Gemini) or vLLM/TensorRT-LLM on dedicated cloud GPUs.
  • Index: managed vector DBs — Pinecone, Weaviate, Qdrant, Milvus. Uber runs 100K+ QPS at 20 ms latency on Milvus.
  • Ingestion: S3/GCS event notifications → Lambda/Cloud Run workers, orchestrated by Step Functions or Airflow.
  • Ops: Datadog/CloudWatch dashboards plus OpenTelemetry distributed tracing.
PineconeLambdaDatadogClaude API

Evaluate Every Stage, Not Just the Answer

The differentiating requirement. A single end-to-end “does the answer look right?” check cannot tell you which stage failed. Instrument each one:

StageMethodWhat it catches
chunkingGolden chunks + parse spot-checksMangled PDFs, chunk boundaries that split answers in half.
retrievalRecall@k / Precision@k on a labeled setRelevant chunks missing from top-k; noise crowding out signal.
generationHuman review + citation verificationHallucinations; claims that trace to nothing that was retrieved.
end-to-endUser testing → online logs + feedbackReal query distribution drift; the continuous-improvement loop.

The same layering guides model choice: Shopify found that fine-tuned smaller models or better prompting reach 80–90% of the quality of much larger models on specific tasks — a trade you can only make confidently when per-stage metrics exist.

Phase 4: The Final Boss — the Cloud Bill

At 1,000 RPS with managed LLM APIs, inference dominates everything else on the invoice:

cloud_bill.txt
1,000 RPS × 500 tokens/request        = 500,000 tokens/s
500,000 tokens/s × $1.00 per 1M       = $0.50/s
$0.50/s × ~2.6M s/month               ≈ $1.3M/month   (inference alone)

+ managed vector DB                    ~$100s–$1,000s /month
+ ingestion compute, storage, network, observability ...

“The design review question is never ‘can we build RAG?’ — it's ‘who approved half a million tokens per second?’”

Mitigations, in order of leverage
Aggressive answer caching → smaller fine-tuned models → reserved & spot instances

Permission-aware retrieval

Filter chunks by the caller's IAM entitlements before generation. A correctness issue, not a nice-to-have.

Long & multi-modal docs

Recursive/hierarchical chunking for documents past the context window; OCR and table extraction for images and scans.

Governance & feedback

GDPR/HIPAA over corpus and query logs; multi-turn query rewriting; a human feedback loop feeding the eval sets.

Takeaway

A strong RAG design is a story about one bottleneck moving: retrieval is nearly free, generation is nearly everything. Start with the zero-dollar MVP, show with arithmetic why CPU inference fractures at ~0.1 RPS per core, fork each bottleneck into a no-money hack and a paid solution, and wrap the whole pipeline in stage-level evaluation — golden chunks, Recall@k, citation verification — so every upgrade decision is measured, not vibes.

References & Further Reading

Practice This Question

This post is adapted from our interview question bank — the original comes with structured prep, a grounded four-phase model answer, and space for your own notes.