Building Basic RAG
Build a simple Retrieval-Augmented Generation system step by step with Pythonβfrom documents and chunks to embeddings, retrieval, context, and a grounded answer.
By the end of this lesson, you will be able to:
- Build the core stages of a small RAG system.
- Prepare documents and split them into retrieval chunks.
- Create embeddings for chunks and a user query.
- Retrieve the most relevant chunks with similarity search.
- Construct a context prompt from retrieved evidence.
- Generate an answer that is grounded in the retrieved context.
- Understand which parts are simplified in this teaching example.
What We Are Building
A basic RAG application has two connected flows. First, we prepare knowledge. Then, when a question arrives, we retrieve relevant knowledge and give it to an LLM.
Step 1: Start With Small Documents
For learning, use a short list of documents instead of beginning with PDFs or a large database. Each document should contain information that can answer a realistic question.
Customers can request a refund within 30 days of purchase.
Standard shipping normally takes 3 to 5 business days.
Business plans include priority support during business hours.
These examples are intentionally small. The same architecture can later work with larger document collections.
Step 2: Represent the Knowledge
Store each piece of knowledge as text plus optional metadata. Metadata helps you trace a result back to its source and can later support filtering.
Step 3: Chunk the Documents
Real documents are usually too large to treat as one retrieval unit. Split them into chunks that preserve useful meaning.
Entire manual
A query about refunds may retrieve a huge document containing installation, shipping, troubleshooting, and warranty information.
Refund section
A focused chunk can be retrieved and placed into the model context without carrying unrelated content.
text = "Refunds are allowed within 30 days. Shipping takes 3 to 5 business days."
# Very simple teaching example: split into sentences.
chunks = [sentence.strip() for sentence in text.split(".") if sentence.strip()]
for chunk in chunks:
print(chunk)This is not a production-grade chunker. It is a simple way to see where chunking fits in the pipeline.
Step 4: Create Embeddings
An embedding model converts each chunk into a vector. Similar meanings should produce vectors that are useful for similarity search.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
chunks = [
"Refunds are allowed within 30 days.",
"Standard shipping takes 3 to 5 business days.",
"Business plans include priority support."
]
vectors = model.encode(chunks)
print(vectors.shape)The exact embedding model is a design choice. What matters here is the flow: text β embedding vectors β similarity search.
Step 5: Store the Vectors
A basic prototype can keep vectors in memory. A real application will usually use a vector index or vector database when the collection becomes larger or needs persistence.
Python memory
Simple and useful for understanding the algorithm.
FAISS
Useful for fast vector similarity search in many local applications.
Qdrant / Pinecone
Useful when persistence, filtering, scaling, or managed infrastructure matters.
Source mapping
Store the original chunk and metadata alongside its vector.
Step 6: Create the User Query
When the user asks a question, embed the question using the same embedding space used for the stored chunks.
The user may use different wording from the source document.
The vector lets us compare the query with stored chunk vectors.
Step 7: Retrieve Relevant Chunks
Compare the query vector with stored vectors and select the highest-scoring chunks. This is the retrieval step.
from sentence_transformers import SentenceTransformer
from numpy import dot
from numpy.linalg import norm
model = SentenceTransformer("all-MiniLM-L6-v2")
chunks = [
"Refunds are allowed within 30 days.",
"Standard shipping takes 3 to 5 business days.",
"Business plans include priority support."
]
chunk_vectors = model.encode(chunks)
query = "How long do I have to request a refund?"
query_vector = model.encode([query])[0]
def cosine(a, b):
return dot(a, b) / (norm(a) * norm(b))
scores = [cosine(query_vector, vector) for vector in chunk_vectors]
ranked = sorted(zip(chunks, scores), key=lambda item: item[1], reverse=True)
for text, score in ranked:
print(round(score, 3), text)The highest-scoring result should be the refund chunk for this simple example. In a production system, use an appropriate vector index or vector database rather than scanning every vector manually.
Step 8: Choose Top-K
Usually, you do not send every retrieved chunk to the LLM. Top-K controls how many candidates are selected for the next stage.
Very focused
Low context size, but the system can miss useful supporting evidence.
More evidence
Higher recall can help, but more context can increase cost and noise.
There is no universally correct K. Choose it using retrieval evaluation and the type of questions your application receives.
Step 9: Build the Context
The retrieved chunks become evidence for the LLM. A simple context builder can label each source and join the selected text.
retrieved = [
{"source": "refund-policy", "text": "Refunds are allowed within 30 days."},
{"source": "shipping-policy", "text": "Standard shipping takes 3 to 5 business days."}
]
context = "\n\n".join(
f"Source: {item['source']}\n{item['text']}"
for item in retrieved
)
print(context)Good context is relevant, concise, and traceable. Avoid dumping every available document into the prompt.
Step 10: Augment the Prompt
RAG adds the retrieved evidence to the user question before sending the request to the generation model.
Step 11: Generate a Grounded Answer
The LLM receives the question plus selected context. A strong prompt should tell the model to use the supplied evidence and avoid inventing facts that are not supported.
question = "How long do I have to request a refund?"
context = "Refunds are allowed within 30 days."
prompt = f"""Answer the question using only the context below.
If the context does not contain the answer, say that you do not have enough information.
Context:
{context}
Question:
{question}
"""
print(prompt)This example prepares the prompt but does not call a paid or hosted LLM. An application would send this prompt to its selected generation model.
Step 12: Return the Answer and Source
A useful RAG application should preserve source information so the application can show where the evidence came from.
Source labels or citations should correspond to the actual retrieved evidence. A citation by itself does not make an unsupported answer correct.
Putting the Basic RAG Loop Together
Here is the complete teaching model in one place.
A Compact Python Prototype
This prototype demonstrates the retrieval portion of a basic RAG system. It intentionally keeps the index in memory so the logic is easy to understand.
from sentence_transformers import SentenceTransformer
from numpy import dot
from numpy.linalg import norm
model = SentenceTransformer("all-MiniLM-L6-v2")
knowledge = [
{"source": "refund-policy", "text": "Refunds are allowed within 30 days."},
{"source": "shipping-policy", "text": "Standard shipping takes 3 to 5 business days."},
{"source": "support-policy", "text": "Business plans include priority support."},
]
vectors = model.encode([item["text"] for item in knowledge])
query = "How long can I request a refund?"
query_vector = model.encode([query])[0]
def cosine(a, b):
return dot(a, b) / (norm(a) * norm(b))
ranked = sorted(
zip(knowledge, vectors),
key=lambda item: cosine(query_vector, item[1]),
reverse=True,
)
top_k = ranked[:2]
context = "\n\n".join(item[0]["text"] for item in top_k)
print("Retrieved context:")
print(context)To turn this into a complete application, connect the selected context to an LLM API and return the generated answer with the retrieved source metadata.
What This Prototype Simplifies
A real system may use FAISS, Qdrant, Pinecone, or another index.
Production chunking should respect document structure and retrieval goals.
Real applications must choose and evaluate an embedding model for their data.
A second-stage reranker can improve ordering when basic retrieval is not enough.
Production systems must prevent users from retrieving documents they are not allowed to see.
You need representative questions and metrics to know whether changes improve the system.
Basic RAG vs Production RAG
- Small text collection
- In-memory vectors
- Simple chunking
- Basic similarity search
- Simple prompt
- Document ingestion pipeline
- Persistent vector database
- Metadata filtering
- Hybrid search and/or reranking
- Security, evaluation, monitoring, and cost controls
The prototype teaches the architecture. Production engineering adds reliability, scale, security, observability, and continuous evaluation.
When Basic RAG Works Well
Support assistant
Retrieve answers from a controlled FAQ or help center.
Knowledge assistant
Search internal documentation and explain relevant sections.
Policy Q&A
Retrieve the correct policy section before generating an answer.
Product information
Ground responses in current product documentation.
Common Mistakes
Retrieval exists to reduce the knowledge sent to the model.
Bad boundaries can make otherwise good documents difficult to retrieve.
Too few chunks can miss evidence; too many can add noise.
RAG reduces unsupported generation but does not eliminate model errors.
Without source mapping, debugging and user trust become harder.
Measure retrieval and answer quality instead of judging the system by a few examples.
How to Improve Your Basic RAG
Improve chunking
Use structure-aware boundaries and appropriate chunk sizes.
Improve embeddings
Evaluate models against your actual query and document distribution.
Add a vector index
Use an efficient searchable index as the collection grows.
Add filters
Restrict retrieval by tenant, product, language, date, or access scope.
Add reranking
Reorder candidates when first-stage retrieval is not precise enough.
Evaluate
Test retrieval recall, ranking, groundedness, answer quality, latency, and cost.
Practice: Build Your First RAG Flow
Imagine a company has 20 help-center articles. A user asks: βHow long does standard shipping take?β
Trace the seven key steps
Write down: 1) source article, 2) chunk, 3) chunk embedding, 4) query embedding, 5) Top-K retrieval, 6) context prompt, and 7) grounded answer. Also decide what source metadata you would return.
Quick Quiz
1. What is the first major task when building RAG?
Prepare the knowledge: collect, clean, structure, and chunk the source content.
2. Why are embeddings created?
They provide vector representations that can be compared for semantic similarity.
3. What does Top-K control?
It controls how many high-ranking retrieval candidates are selected for the next stage.
4. What does augmentation mean?
It means adding selected retrieved evidence to the model input along with the user question.
5. Does retrieval guarantee a correct answer?
No. The evidence can be wrong, incomplete, poorly ranked, or misunderstood by the model.
6. Why keep source metadata?
It supports traceability, filtering, citations, debugging, and access control.
30-Second Recap
Collect and clean knowledge.
Create useful retrieval units.
Convert chunks into vectors.
Find relevant candidates for a query.
Use Top-K, filters, or reranking.
Build concise evidence for the LLM.
Answer using the retrieved context.
Measure retrieval and answer quality.
Key Takeaways
- A basic RAG system connects a knowledge source to an LLM through retrieval.
- The core flow is documents β chunks β embeddings β index β query β retrieval β context β generation.
- Top-K controls how much retrieved evidence moves forward.
- Good context should be relevant, concise, and traceable to a source.
- A simple in-memory Python prototype is useful for learning, but production systems need stronger infrastructure.
- Vector databases, metadata filters, hybrid search, and reranking can improve larger RAG systems.
- RAG can reduce unsupported answers, but it does not guarantee correctness.
- Evaluation, security, monitoring, freshness, latency, and cost become important as the application moves toward production.