Skip to content
AI360XpertBeta

RAG Architecture

Documents are indexed once ahead of time. Each query is embedded, matched against that index, and the best passages join the prompt before the model answers.

Three chunks are indexed once, a query is ranked against them by cosine, and the top two join the prompt so the answer cites the chunk it used
Three chunks are indexed once, a query is ranked against them by cosine, and the top two join the prompt so the answer cites the chunk it used

Why Does This Exist?

Your model's memory ends on the day its training data stopped. A handbook updated yesterday, a customer's private records, a price that changed this morning: none of it is in the weights, and retraining the model every time a fact changes is slow and expensive. Context windows keep growing, but pasting a whole knowledge base into every prompt still breaks down once the corpus is large, and you pay for those tokens on every single request.

Retrieval-augmented generation (RAG) keeps the weights frozen and changes what the model reads instead. For each question, the system fetches a few relevant passages from an external store and puts them in the prompt, so the model answers from material handed to it now rather than from memory of training. Closed book versus open book: that shift is the whole idea.

The one idea this page assumes is small. An embedding turns any passage into a list of numbers positioned so that closeness means relatedness, and cosine similarity scores that closeness from 0 to 1. If that sentence already clicks, you have everything you need; the full treatment with its own worked example is embeddings.

Scope, so you know where you are: this page is the pipeline itself. Slicing documents into passages lives in chunking strategies, measuring the system lives in RAG evaluation, and debugging it lives in RAG failure diagnosis.

Think of It Like This

An open-book exam, graded on the citations

A closed-book exam tests what you memorized months ago. An open-book exam tests something else entirely: with the reference material on your desk, can you find the right passage and use it to answer the question in front of you.

RAG turns every question into the second kind of exam. The document store is the book. Ranking is flipping to the right page. The generated answer is your response, and a good one names the page it used, which is why the worked answer below cites its chunk.

Where the analogy stops: exam books agree with each other, or at least the exam only asks about one of them. Real document stores contradict themselves, go stale, and bury the answer in near duplicates. Retrieval quality caps everything downstream, and no exam technique fixes the wrong page.

How It Actually Works

The two lanes: index once, retrieve per query

Indexing runs offline, whenever documents change. Each document is split into chunks, short passages sized so one embedding holds one idea, every chunk is embedded into a vector, and those vectors sit in an index. Once per document, no matter how many questions arrive later.

Retrieval runs per question. The question is embedded with the same model, that query vector is scored against every stored chunk vector, the top few chunks are pasted into the prompt next to the question, and the model generates from the combined text. The weights never move. That property should feel familiar: it is the same trick in-context learning uses, pointed at retrieved knowledge instead of examples.

How the ranking is scored

Write the embedding model as EE. It maps the question to a query vector q=E(question)q = E(\text{question}), where qq is just a list of numbers standing in for what you asked. It maps chunk ii to a chunk vector di=E(chunki)d_i = E(\text{chunk}_i), the same kind of list standing in for that passage. Same EE on both sides, or the comparison is meaningless.

The score for chunk ii is the cosine of the angle between the two lists:

si=q⋅di∥q∥ ∥di∥s_i = \frac{q \cdot d_i}{\lVert q \rVert \, \lVert d_i \rVert}

Here q⋅diq \cdot d_i is the dot product (multiply the lists element by element, then add), ∥q∥\lVert q \rVert and ∥di∥\lVert d_i \rVert are the lengths of the two lists (square each entry, add, take the square root), and sis_i is the resulting score: near 1 when the two point almost the same way, near 0 when they are unrelated. Keep the top kk, where kk is simply how many chunks survive into the prompt. Small kk keeps the prompt cheap and focused; larger kk covers more ground at the cost of tokens and noise.

Worked example: three chunks, one shipping question

Three chunks sit in the store:

  • C1: "Refunds are issued within 30 days." with d1=[0.9,0.1]d_1 = [0.9, 0.1]
  • C2: "Orders ship in 3 to 5 business days." with d2=[0.6,0.8]d_2 = [0.6, 0.8]
  • C3: "Support is open 9 to 5 on weekdays." with d3=[0.1,0.9]d_3 = [0.1, 0.9]

The question is "How long until my order ships?" and its embedding is q=[0.8,0.6]q = [0.8, 0.6]. Its length is ∥q∥=0.82+0.62=0.64+0.36=1.0=1\lVert q \rVert = \sqrt{0.8^2 + 0.6^2} = \sqrt{0.64 + 0.36} = \sqrt{1.0} = 1, which keeps the arithmetic honest and short.

Score each chunk:

  • C1: q⋅d1=0.8×0.9+0.6×0.1=0.72+0.06=0.78q \cdot d_1 = 0.8 \times 0.9 + 0.6 \times 0.1 = 0.72 + 0.06 = 0.78. Length ∥d1∥=0.81+0.01=0.82≈0.906\lVert d_1 \rVert = \sqrt{0.81 + 0.01} = \sqrt{0.82} \approx 0.906. So s1≈0.78/0.906≈0.86s_1 \approx 0.78 / 0.906 \approx 0.86.
  • C2: q⋅d2=0.8×0.6+0.6×0.8=0.48+0.48=0.96q \cdot d_2 = 0.8 \times 0.6 + 0.6 \times 0.8 = 0.48 + 0.48 = 0.96. Length ∥d2∥=0.36+0.64=1\lVert d_2 \rVert = \sqrt{0.36 + 0.64} = 1. So s2=0.96s_2 = 0.96.
  • C3: q⋅d3=0.8×0.1+0.6×0.9=0.08+0.54=0.62q \cdot d_3 = 0.8 \times 0.1 + 0.6 \times 0.9 = 0.08 + 0.54 = 0.62. Length ∥d3∥=0.01+0.81=0.82≈0.906\lVert d_3 \rVert = \sqrt{0.01 + 0.81} = \sqrt{0.82} \approx 0.906. So s3≈0.62/0.906≈0.68s_3 \approx 0.62 / 0.906 \approx 0.68.

Ranking: C2 at 0.96, then C1 at 0.86, then C3 at 0.68. With k=2k = 2 the prompt carries C2 and C1, and C3 stays in the store. The model answers "Orders ship in 3 to 5 business days [C2]." The bracket names the chunk the answer actually used, which is the thread you pull when an answer looks wrong.

What augmentation does to the prompt

Augmentation is prompt construction, nothing fancier. Instruction, then the surviving passages with their ids, then the question. No weights change, no retraining, nothing carries over to the next query. Keep the chunk ids visible in the prompt and ask the model to cite them, because those ids tell you exactly which evidence each answer rested on.

The price, and when to skip RAG

RAG buys freshness and grounding: new documents answer questions minutes after indexing, and answers can point at their sources. It pays in three currencies: latency from the extra lookup, tokens from stuffing passages into every prompt, and upkeep of the index itself, since everything must be re-embedded whenever the embedding model changes.

So skip it when the knowledge is stable and already in the model, or when the whole corpus fits in context and you can paste it directly (that tradeoff is long context versus RAG). Skip it too when the question needs math over the entire corpus, like counting or averaging across all documents: retrieval returns the most similar few passages, never a full table, so you can't build aggregates on them. The method assumes the answer sits inside a few passages and that similarity tracks relevance. Paraphrase gaps, multi-step questions, and contradictory documents break that assumption, which is where upgrades like hybrid search help.

Code

Two functions, two lanes. Indexing runs once; retrieval runs per question over the same three vectors from the worked example.

import numpy as np

def cosine_sim(a: np.ndarray, b: np.ndarray) -> float:    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))

chunks = {    "C1": np.array([0.9, 0.1]),    "C2": np.array([0.6, 0.8]),    "C3": np.array([0.1, 0.9]),}query = np.array([0.8, 0.6])  # question: how long until my order ships
sims = {cid: round(cosine_sim(query, vec), 2) for cid, vec in chunks.items()}print(sims)  # -> {'C1': 0.86, 'C2': 0.96, 'C3': 0.68}
ranked = sorted(sims, key=lambda c: sims[c], reverse=True)print(ranked[:2])  # -> ['C2', 'C1']

Both # -> lines print what the worked example derived by hand: C2 first at 0.96, C1 second at 0.86, C3 last at 0.68, and the top two are C2 and C1.

Watch Out For

You rewrote the prompt, but the chunk was never retrieved

Symptom: the answer stays wrong through several prompt rewrites, and the retrieval log (or a quick top-kk print like the snippet above) shows the gold chunk sitting at rank eight, or missing entirely. No wording fixes that, because the model never saw the passage, so don't start there.

Fix: read the retrieved chunk ids before touching the prompt. If the right chunk is absent, change the chunking, the embedding model, or kk first, and only then tune wording. The full stage-by-stage order is RAG failure diagnosis.

The query and the store were embedded by different models

Symptom: scores bunch together or look random right after an embedding model upgrade, and queries that used to work start missing. Vectors from two models live in different spaces, so a cosine between them means nothing.

Fix: re-embed the entire store with the exact model and version the queries use, and treat the embedding model as a locked dependency. Documents added after the last indexing run fail the same way, so check recency too: if it was never indexed, it can never be retrieved.

The Quick Version

  • RAG freezes the weights and answers from passages fetched at request time, not from training memory.
  • Indexing runs once per document. Retrieval, ranking, and generation run on every question.
  • Cosine ranking decides what the model sees: 0.96 beats 0.68, and the loser never reaches the prompt.
  • The prompt carries the winning passages plus the question, so the answer can cite the chunk it used.
  • Retrieval caps generation. A missing or misranked chunk fails no matter how good the prompt is.
  • Skip RAG for stable knowledge, tiny corpora that fit in context, or questions needing math over everything.