# Designing an AI Answer Engine like Perplexity

## Blog Details

- **Author**: Naveen R.
- **Date**: October 18, 2026
- **Tags**: AI, search engine, system design, LLM, information retrieval
- **Read Time**: 20 mins

When I design an AI answer engine like Perplexity, I start from what the user actually sees: they type a question in plain language, and a few seconds later a paragraph of prose appears with small numbered citations, each linking to the page the claim came from. No ten blue links, no scrolling through ads, no opening six tabs. The product promise is "read the web for me, and show your work".

Behind that paragraph sits a pipeline that is part search engine, part crawler, part LLM platform. One question can become several search queries, a dozen page fetches, a re-ranking pass over many text chunks, and a model call with thousands of tokens of evidence. Every stage adds latency, costs money, and can quietly inject wrong information. The engineering problem is to keep the answer **fast, grounded and cheap at the same time**, when each of those goals pulls the design in a different direction.

In this post I walk through how I would build it: scoping, requirements with capacity estimates, a high-level architecture, and four deep-dives:

- Query understanding and multi-step search planning
- Retrieval, fetching and re-ranking
- Grounded generation with citations
- Freshness, caching and cost per answer

## Scoping the Problem and Clarifying Assumptions

I am building the **answer path**: the system that takes a question (plus the conversation so far) and returns a streamed, cited answer.

**What I'm including:**
- A consumer product (web and mobile) with threaded conversations and follow-up questions
- A developer API that returns the same cited answers as JSON or a stream
- Two modes: a fast default answer and a slower "research" mode that runs more search steps
- Source cards and feedback signals (ratings, "this source is wrong")

**What I'm NOT building here:**
- Training foundation models. I call frontier models through providers and host smaller open-weight models myself.
- A full web-scale crawler and link graph. I crawl high-value domains and use third-party search APIs for the long tail.
- Ads, shopping, image generation, and LLM serving internals (I treat model pools as dependencies with SLOs).

**Key assumptions:**
- Tens of millions of queries per day. I plan for **30 million questions per day**.
- Most questions are informational ("how does X work", "compare A and B"), and a meaningful share are about recent events.
- One or more web search API providers return ranked URLs and snippets, priced per call.
- I host an embedding model, a re-ranker and a small LLM on my own GPUs.

**How this differs from a classic search engine:** a classic engine's job ends at ranking. It returns ten links in a few hundred milliseconds and the user does the reading. An answer engine does the reading itself. The hard work moves from ranking documents to **reading documents**: fetching pages in real time, extracting text, choosing passages, and writing only what those passages support. An answer also costs far more than a search query, so unit economics drive the design almost as much as latency. And unlike a plain chatbot, which answers from stale weights it cannot cite, an answer engine treats the model as a **reader and writer over retrieved evidence**, not as the source of truth.

## Functional and Non-Functional Requirements

### Functional Requirements

1. **Ask**: accept a natural-language question, optionally inside an existing thread, and return a synthesized answer.
2. **Cite**: every factual sentence carries inline citation markers that map to specific source pages.
3. **Stream**: show source cards first, then stream the answer text as it is generated.
4. **Follow up**: resolve follow-up questions ("what about its battery life?") using the thread's context.
5. **Research mode**: run a multi-step search plan for complex questions, showing progress ("searching for X", "reading 12 sources").
6. **Feedback**: record ratings and source complaints, and feed them back into ranking and evaluation.

### Non-Functional Requirements

| Requirement | Target | Why |
|-------------|--------|-----|
| Time to first source card | p50 under 1 s | Shows the system is working before the answer starts |
| Time to first answer token | p50 under 2.5 s, p99 under 6 s for default mode | Users compare it with a search results page |
| Groundedness | nearly every factual sentence supported by a cited source | The product is worthless if citations lie |
| Freshness | news answers reflect pages from the last hour or so | "Latest" questions are a core use case |
| Availability | 99.9%, degrade to fewer sources rather than fail | A partial answer beats an error |
| Cost per answer | tracked per mode and tier, with a budget | Search APIs and model tokens dominate spend |

Time to first answer token is dominated by planning, retrieval and fetching, which all happen before the model writes a word. That is where most of the latency engineering goes.

### Back-of-the-Envelope Capacity Estimation

These are my planning assumptions, not measurements. The point is the method: turn questions into downstream calls, then size each stage.

**Traffic:**
- 30 million questions per day / 86,400 s ≈ **350 questions per second on average**
- Peak at about 3x: **roughly 1,000 questions per second**

**Fan-out per question (default mode, assumed):**
- About 3 sub-queries per question: **~1,000 search calls per second** on average, ~3,000 at peak
- About 8 pages read per question: **~2,800 page reads per second** on average, ~8,400 at peak
- If I assume half of those hit my page cache, live fetches are **~1,400 per second** average, ~4,200 at peak

**Re-ranking:**
- 8 pages, each extracted to roughly 2,000 tokens and chunked into ~300-token passages, gives about 50 to 60 chunks per question, plus search snippets
- 350 × 60 ≈ **21,000 query-passage pairs per second** on average, ~60,000 at peak: a real GPU workload.

**Answer model tokens:**
- Context of about 8,000 tokens (instructions, thread summary, evidence), output of about 400 tokens
- Input: 350 × 8,000 ≈ **2.8 million input tokens per second** on average
- Output: 350 × 400 ≈ **140,000 output tokens per second** on average

The ratio matters: an answer engine is **input-heavy**, reading about twenty tokens per token written. Prefill throughput, prompt caching and the evidence budget matter more than decode speed.

**Storage:**
- Each answer with its sources, citation map and metadata is roughly 10 KB
- 30M × 10 KB ≈ **300 GB per day**, about 110 TB per year before compression
- Page cache: extracted text (say 20 KB) for tens of millions of URLs is on the order of a terabyte, which fits in a blob store with a key-value index.

## High-Level Architecture and Core Components

![The request path for the answer engine: clients reach an API gateway that streams responses, an answer orchestrator calls a query planner model and a retrieval layer that combines web search APIs, its own index, a page fetcher and a re-ranker into evidence, then streams grounded tokens from the answer model while saving the thread and logging events.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/01-high-level-architecture.png)

### Component Walkthrough

**API Gateway.** Terminates TLS, authenticates the user or API key, applies per-user rate limits, and holds the Server-Sent Events (SSE) connection that carries progress events, source cards and answer tokens back to the client.

**Answer orchestrator.** The center of the system. A stateless service that runs one answer as a small workflow (plan, retrieve, re-rank, pack, generate, verify, store). It owns each stage's latency budget and proceeds with fewer sources rather than waiting on a slow stage.

**Query planner.** A small, fast model that rewrites the question, classifies intent and freshness, and produces sub-queries.

**Retrieval layer.** Queries third-party web search APIs and my own index (BM25 plus vectors over crawled high-value domains), fetches and extracts pages through a page cache, and uses a cross-encoder re-ranker to choose the passages that reach the answer model.

**Answer model.** One of several LLM pools, from a small self-hosted model to a frontier model, writing the answer with citation markers.

**Thread store and event stream.** A NoSQL store keyed by thread id holds answers, sources and citation maps. Every answer also emits an event (plan, sources, latencies, tokens, cost, feedback) for dashboards and offline evaluation.

### Request Flow

1. The client sends `POST /v1/answer` with the question and optional `thread_id`.
2. The gateway authenticates, rate-limits and opens an SSE stream.
3. The orchestrator loads the last few turns of the thread and calls the planner, which returns a standalone query, an intent, a freshness label and two to four sub-queries.
4. The orchestrator checks the answer cache. On a miss, it fans sub-queries out to search APIs and the own index in parallel.
5. Results are fused, the top URLs go to the fetcher (most are page-cache hits), and extracted pages are chunked.
6. The re-ranker scores candidate chunks. The top passages are packed into a token budget and numbered. Source cards stream to the client now.
7. The answer model streams tokens with markers like `[2]`. The orchestrator maps markers to source cards and relays text.
8. After the stream ends, a verifier checks sentences against their cited passages asynchronously, the answer is stored, and a usage event is emitted.

### Scaling It Out

![The scaled-out answer engine: a global load balancer spreads users across regions, each with its own edge gateway and pool of stateless orchestrators that share answer and retrieval caches, call model pools by tier and the sharded web index, and send page fetches through a fetcher fleet with dedicated egress IPs, while a crawl and index pipeline refreshes index shards in the background.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/02-scalable-architecture.png)

- **Stateless async orchestrators per region.** They mostly wait on network calls, so I scale them on answers in flight, not CPU.
- **Model pools by tier.** Small models run in every region. Frontier models may sit in a few regions or behind a provider, so I route on latency and capacity and fall back to a smaller model under pressure.
- **Isolated fetcher fleet.** Fetching arbitrary URLs is a security risk, so fetchers run in a segment that cannot reach internal services (blocking server-side request forgery), exit through dedicated egress IPs, and enforce per-domain politeness limits.
- **Sharded own index.** BM25 and vector shards by document id, with replicas, refreshed continuously for news and slowly for everything else. Answer and retrieval caches are regional; the page cache is global.

## Deep-Dive: Query Understanding and Multi-Step Search Planning

![Query planning: the user's question and recent thread history go to a contextual rewriter that produces a standalone query, an intent and complexity classifier routes simple questions to a single-hop plan and complex ones to a multi-hop agent loop, and both feed sub-queries into a fan-out queue for the retrieval layer.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/03-query-planning.png)

### Why the Raw Question Is a Bad Search Query

Users do not type search queries. They type "is it worth upgrading?" in a thread about a phone, or paste a two-paragraph bug description. Before retrieving anything, I turn the question into **queries a search engine can answer**.

### Contextual Rewriting for Follow-Ups

The first step is to make the question **standalone**. After a question about the battery of a (fictional) Acme Nova 3 phone, the follow-up "how does that compare to the Zenith X12?" becomes "Acme Nova 3 battery capacity vs Zenith X12 battery capacity". The rewriter sees only the last two or three questions and a short summary of the answers, not full answers with sources. That keeps the call fast and stops it copying stale facts from earlier answers into the new query.

### Classifying Intent, Freshness and Complexity

In the same planner call, I ask for a small structured output:

```json
{
  "standalone_query": "Acme Nova 3 battery capacity vs Zenith X12 battery capacity",
  "intent": "comparison",
  "freshness": "months",
  "complexity": "single_hop",
  "needs_search": true,
  "sub_queries": [
    "Acme Nova 3 battery capacity mAh",
    "Zenith X12 battery capacity mAh",
    "Acme Nova 3 vs Zenith X12 battery life test"
  ]
}
```

Each field drives a later decision:

- **`needs_search`**: "convert 30 C to F" does not need the web, and skipping retrieval saves the whole fetch budget.
- **`freshness`** (`none` to `hours`): sets date filters, decides whether the answer cache is allowed, and sets cache TTLs.
- **`intent`**: selects the prompt template and preferred sources (documentation for code, recognized outlets for news).
- **`complexity`**: decides between one hop and multi-hop.

This runs on a small model with constrained JSON output and must return in a few hundred milliseconds. If the output fails schema validation, I fall back to the raw question as a single sub-query. A planner failure should cost quality, never availability.

### Decomposition into Sub-Queries

Comparison and multi-part questions retrieve far better as separate queries: one per side plus a direct comparison. I cap fan-out at three or four in default mode because each sub-query costs a search call. Sub-queries run **in parallel**, so decomposition adds cost but little latency.

### One Hop vs Agentic Multi-Hop

Some questions cannot be planned up front. "What did the CEO of the company behind the most popular open-source vector database say about pricing last month?" needs a hop to find the company, another for the CEO, and a third for the statements. For these I run a bounded **agent loop**: the planner sees a summary of the evidence so far and either declares it sufficient or emits the next sub-queries.

```python
async def research(question, ctx, max_hops=4, budget=CostBudget(cents=20)):
    evidence = []
    plan = await planner.initial_plan(question, ctx)
    for hop in range(max_hops):
        results = await gather(*(retrieve(q, plan.freshness) for q in plan.sub_queries))
        evidence = merge_and_dedupe(evidence, results)
        stream_progress(hop, plan.sub_queries, len(evidence))
        if budget.exhausted() or hop == max_hops - 1:
            break
        plan = await planner.next_step(question, summarize(evidence))
        if plan.sufficient:
            break
    return evidence
```

One hop is fast and cheap and handles most questions. Each extra hop adds a planner call, a retrieval round and fetches, so latency grows from seconds to tens of seconds. Default mode is therefore one hop, plus at most one gap-filling hop if a sub-query found no strong passages. Research mode, which the user opts into, gets the full loop with hard limits on hops, cost and wall-clock time, and visible progress events so the wait feels purposeful.

### Failure Modes and What I Would Do First

- **Over-decomposition.** Planners love six sub-queries for simple questions. I cap the count and check offline whether citations ever come from the later ones.
- **Query drift.** A rewrite can change the question's meaning. I log it next to the original, and my evaluation sets include ambiguous follow-ups.
- **Runaway loops.** Hop, cost and time limits are mandatory, not tuning knobs.

What I would build first: one planner call that rewrites, classifies and decomposes, feeding a one-hop path. Multi-hop comes second, behind research mode.

## Deep-Dive: Retrieval, Fetching, and Re-Ranking

![The retrieval pipeline: sub-queries go in parallel to third-party search APIs, a BM25 keyword index and an HNSW vector index; reciprocal rank fusion merges the lists, the top URLs are fetched in parallel with a two-second timeout, pages are extracted and chunked, a cross-encoder re-ranks candidate chunks, and a packer fits the best passages into the model's token budget.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/04-retrieval-reranking.png)

### Two Kinds of Sources

**Third-party search APIs** give me the whole web, ranked with signals I could never rebuild. The downsides are cost per call, rate limits, short snippets and no control over ranking.

**My own index** covers the domains that matter most, crawled and chunked ahead of time and indexed two ways: **BM25** for exact terms (product names, error messages, versions) and **dense embeddings** in an HNSW index for paraphrases. It is cheaper per query, returns passages without a live fetch, and gives me control of freshness. I would start with search APIs for everything, measure which domains get cited most, and crawl those first.

### Fusing Ranked Lists with RRF

Each source returns a ranked list, but their scores are not comparable: a BM25 score, a cosine similarity and a search API's position mean different things. **Reciprocal rank fusion (RRF)** sidesteps this by using only ranks:

```python
def rrf(ranked_lists, k=60):
    scores = defaultdict(float)
    for results in ranked_lists:            # one list per source per sub-query
        for rank, doc in enumerate(results, start=1):
            scores[doc.url] += 1.0 / (k + rank)
    return sorted(scores, key=scores.get, reverse=True)
```

A URL that ranks well in several lists rises to the top, and `k` (60 is the common default) damps the advantage of the first positions. RRF needs no training, which is why I start with it; a learned ranker using domain quality and freshness can replace it later. After fusion I **dedupe**: canonicalize URLs, collapse syndicated copies, and cap results per domain.

### Parallel Fetching with Hard Timeouts

For top URLs not in my own index I need the page text. This is the least predictable stage: some pages return in 100 ms, some never. My rules:

- **Fetch in parallel** with a per-fetch timeout (around 2 seconds in default mode) and a stage deadline. When the deadline hits, I proceed with whatever has arrived.
- **Over-fetch slightly.** If I want 8 pages, I fetch 10 or 12, so a few slow or failing sites do not starve the answer.
- **Page cache first**, keyed by canonical URL, and **fall back to the search snippet** as weak evidence if a page fails.
- **Respect the web**: robots.txt, a clear user agent, per-domain limits, nothing behind logins or paywalls.

**Extraction** strips navigation, ads and comments, keeps headings, tables and code, and pulls out title, date and author. PDFs and JavaScript-heavy pages take a slower path (a small, expensive headless browser pool), used only for highly ranked URLs.

### Chunking

I cut passages of roughly 200 to 400 tokens on headings and paragraphs, with small overlap, and prefix each with its page title and section heading so "it supports up to 128 GB" still says what "it" is. Too small loses context; too large wastes budget and makes citations vague.

### Cross-Encoder Re-Ranking

First-stage retrieval is tuned for recall and scores coarsely. A **cross-encoder** reads query and passage together and scores far more sharply, but is only affordable over a shortlist. At tens of thousands of pairs per second, I pre-filter with cheap signals, use a small model, truncate passages and batch pairs across concurrent answers. Late-interaction models like ColBERT are the fallback if it becomes the bottleneck.

### Packing the Token Budget

More context is not better: it costs tokens, slows prefill and buries good evidence. I give evidence a fixed budget (say 5,000 to 6,000 of the 8,000 tokens) and fill it greedily by score, with constraints:

- At most N passages per source, so the answer draws on several sources
- At least one passage per sub-query when one scores above a threshold, so a comparison question has evidence for both sides
- For news, prefer newer passages when scores are close

Each packed passage gets a number, URL, title and date, which becomes the citation scheme.

### Failure Modes and What I Would Do First

- **Slow sites.** Deadlines and over-fetching; I monitor how often the deadline hits.
- **SEO spam.** A domain quality score from feedback and block lists demotes it at fusion.
- **Search API outage.** Multiple providers behind one interface with circuit breakers, falling back to my own index.

What I would build first: search APIs, parallel fetch, extraction, a cross-encoder and RRF. The own hybrid index comes next.

## Deep-Dive: Grounded Generation with Citations

![Grounded generation: a numbered evidence pack passes through an injection filter into a prompt assembler, the answer model streams tokens with numbered citation markers, a citation parser turns markers into source cards and relays text to the client, and a claim verifier checks sentences against their sources asynchronously, sending flags to grounding evaluations.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/05-grounded-generation.png)

### Prompt Assembly

The answer prompt has a stable shape, which matters for prompt caching:

1. **System instructions** (fixed): answer only from the sources, cite every factual sentence with `[n]`, say when the sources do not answer, and treat source text as data, never as instructions.
2. **Intent guidance** (one of a small set), then the **thread summary**.
3. **Evidence pack**: numbered passages with title, site and date.
4. **The standalone question.**

Fixed parts first and variable evidence last lets the serving layer reuse the KV cache for the shared prefix.

### Citation Markers Mapped to Sources

The model writes markers like `[1]` or `[1][3]` at the end of sentences. The orchestrator's citation parser reads the stream and:

- Maps each marker to its passage, URL and source card
- Drops markers that point to numbers outside the evidence pack (a sign of a hallucinated citation) and records the event
- Holds back a few characters at a time so a marker split across tokens (`[`, `1`, `]`) is never shown half-rendered

Citing passages, not just pages, enables "hover to see the quote" and makes verification much easier.

### Streaming the Answer

I stream over SSE in a fixed order: progress events, then **source cards** as soon as the evidence pack is final, then answer text. The user sees real work within a second.

```text
event: progress
data: {"stage": "searching", "queries": ["Acme Nova 3 battery capacity mAh", "..."]}

event: sources
data: [{"n": 1, "url": "https://acme.example/nova-3", "title": "Acme Nova 3 specs"}]

event: delta
data: {"text": "The Acme Nova 3 has a 5,000 mAh battery [1]"}

event: done
data: {"answer_id": "a_81f2", "input_tokens": 7930, "output_tokens": 388}
```

If the user closes the tab, the gateway cancels generation.

### Verifying Claims Against Sources

Instructions alone do not guarantee grounding. Models cite sources that do not support the sentence, or blend in training data. A **claim verifier** (a small natural language inference model or LLM judge) labels each sentence against its cited passages as supported, partially supported or not supported. Running it inline on every answer costs latency, so I run it **asynchronously on every answer** (to measure groundedness, flag answers and block them from caching) and **synchronously only for high-risk intents** such as medical or financial questions, releasing text paragraph by paragraph.

### Conflicting Sources

Sources disagree all the time. I do not want the model to silently pick one, so the instructions tell it to **state the disagreement and cite both sides**, preferring the more recent and authoritative source when it must choose. Dates and site names in the evidence pack are what make this possible.

### Refusing When Evidence Is Weak

The most damaging failure is a confident answer built on nothing. If the best re-ranker score is below a threshold, the orchestrator flags weak evidence in the prompt, and the model says what it found, what it could not confirm, and suggests a narrower question.

### Prompt Injection from Retrieved Pages

Every fetched page is untrusted and may hide text like "ignore previous instructions". I strip hidden elements during extraction, run an injection classifier over passages, wrap passages in clear delimiters, and give the answer model no tools on this path, so an injection can only produce bad text, which the verifier can catch.

### Failure Modes and What I Would Do First

- **Citation drift** (right sentence, wrong number) and **answering from memory**: both caught by the verifier, tracked per model version.
- **Stale facts presented as current**: dates in the evidence plus freshness-aware packing.

What I would build first: the fixed prompt, passage numbering, a robust streaming citation parser and an offline groundedness eval set. The async verifier comes next.

## Deep-Dive: Freshness, Caching, and Cost per Answer

![Caching and cost: a normalized query passes through a freshness classifier, stable-topic questions check the answer cache, misses and news questions use the retrieval cache, then the page cache, before a model tier router sends simple questions to a small model and hard ones to a frontier model, with every answer's cost recorded in a warehouse.](https://d5osvdbc8um23.cloudfront.net/static-asset/blog_images/system-design-ai-answer-engine/06-caching-and-cost.png)

### Three Caches with Different Risks

I keep three caches because they go wrong in very different ways.

| Cache | Key | Stores | Main risk |
|-------|-----|--------|-----------|
| Answer cache | normalized query + locale + mode | full answer with citations | serving a wrong or stale answer to many people |
| Retrieval cache | sub-query + freshness bucket | fused, ranked URLs and passages | stale search results |
| Page cache | canonical URL | extracted text + metadata | outdated page content |

**The page cache** is the safest and most valuable: a popular article is read for thousands of questions. TTLs follow content type (minutes for news, days for reference), with ETag revalidation. **The retrieval cache** saves search calls because many questions share sub-queries; its TTL follows the freshness label. **The answer cache** skips everything, so it has the highest payoff and risk. I use it only for popular, non-personal, first-turn questions with slow freshness, and never for answers the verifier flagged.

### Semantic Caching Risks

Matching the answer cache on embedding similarity is tempting, and it fails badly: "is ibuprofen safe during pregnancy" and "... after pregnancy" sit very close in embedding space and have different answers. So I use **exact match on a normalized query** for answers, and semantic matching only at the retrieval layer, where a near-miss costs a little relevance rather than a wrong answer.

### Freshness for News Queries

For `hours` or `days` freshness, I change the whole pipeline, not just one TTL:

- Skip the answer cache entirely
- Add date filters to search API calls and query the own index's news shards, which a streaming crawl updates every few minutes from feeds and sitemaps of news domains
- Shorten page cache TTLs, and boost recency in packing
- Tell the model the current date and require it to mention when a fact is reported "as of" a time

The planner detects freshness need, with a backstop: when many users suddenly ask about the same entity, a trending signal from my query stream switches that entity to fresh mode, even if the question looks timeless.

### Model Tiering

I route the answer model by the planner's intent and complexity, and by evidence quality:

- **Small self-hosted model** for single-hop factual questions with strong evidence. The evidence does the hard work, and the model mostly summarizes and cites.
- **Frontier model** for comparisons, multi-step reasoning, research mode, code, and paid tiers that ask for it.
- **Escalation**: if the small model's answer fails a quick grounding check, or the user asks to "try a better model", I re-run on the larger model.

I evaluate routing offline: a change that saves money but lowers groundedness is a regression.

### Cost Breakdown per Answer

The dashboard number is **cost per answer**, per mode and tier:

```text
cost_per_answer =
    n_search_calls x price_per_search_call
  + n_live_fetches x cost_per_fetch            (fetcher compute + egress)
  + rerank_pairs x cost_per_pair               (GPU time)
  + planner_tokens x planner_price
  + input_tokens x input_price x (1 - cached_share x cache_discount)
  + output_tokens x output_price
```

To see which term dominates, I plug in placeholder prices that I would replace with real contract numbers: assume a search API call costs half a cent, the answer model costs $1 per million input tokens and $5 per million output tokens. A default answer with 3 search calls, 8,000 input tokens and 400 output tokens then costs about 1.5 cents in search, 0.8 cents in input tokens and 0.2 cents in output tokens, with fetching, re-ranking and planning adding small amounts. Under those assumptions, **search calls are the largest line item, then input tokens**. Output tokens, which dominate cost in a chatbot, are a minor term here.

The exact split depends entirely on real prices, but the method shows where to push:

1. **Fewer paid search calls**: the retrieval cache, a growing own index, and not decomposing simple questions.
2. **Fewer input tokens**: a tighter evidence budget, better re-ranking so fewer passages are needed, and prompt caching for the fixed instruction prefix.
3. **Cheaper models**: tiering and escalation.
4. **Skipping work**: `needs_search: false`, answer cache hits on stable popular questions, and cancellation when users leave.

Research mode has a different budget: many hops, many fetches and a frontier model can cost many times a default answer, so it gets a hard cost cap per run and is a natural paid-tier feature.

### Failure Modes and What I Would Do First

- **A cached wrong answer spreads.** Feedback ("this is wrong") on a cached answer invalidates it immediately, and verifier flags block caching.
- **A breaking story is served stale.** The trending signal flips topics to fresh mode, and news never uses the answer cache.
- **Cost creep.** Every answer's cost is logged with its plan, so I can explain each week's change in cost per answer by stage.

What I would build first: the page cache (safe and high value) and per-answer cost accounting. Then the retrieval cache, then model tiering, and only then a carefully scoped answer cache.

## Conclusion: Bringing It All Together

An AI answer engine looks like a search box on the outside and like a small research team on the inside. The orchestrator plans, a fleet of fetchers reads, a re-ranker decides what matters, and a model writes, with a verifier checking its work. The core shift from a classic search engine is that **the system does the reading**, so latency, cost and correctness are all decided by how well it chooses what to read.

If I had to summarise the design in four decisions:

1. **Plan before retrieving.** Rewrite follow-ups into standalone queries, decompose comparisons, and keep agentic multi-hop behind an explicit research mode with hard limits.
2. **Retrieve for recall, re-rank for precision, and pack a budget.** Search APIs plus an own hybrid index, RRF fusion, parallel fetch with deadlines, and a cross-encoder choosing a small number of passages.
3. **Ground every sentence.** Number passages, cite at passage level, parse citations in the stream, verify claims, surface conflicts, and say so when evidence is weak.
4. **Cache by risk and meter cost per answer.** Page cache first, retrieval cache second, answer cache only for stable popular questions, and model tiering driven by evidence quality.

Related reading: [Designing an LLM Inference Platform](/blog/system-design-llm-inference-platform), [AI Agents and Agentic Workflows](/blog/ai-agents-agentic-workflows), [Designing a Distributed Cache](/blog/system-design-distributed-cache), and [Designing Search Autocomplete](/blog/system-design-search-autocomplete).
