Retrieval-Augmented Generation lets you ground LLM answers in your own data — no fine-tuning required. This lesson covers embeddings, vector stores, and the core RAG pipeline.
LLMs have three fundamental limitations that RAG addresses directly:
RAG is the most cost-effective way to give an LLM access to domain-specific knowledge.
An embedding model converts text into a dense numeric vector that captures semantic meaning. Similar texts produce similar vectors.
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode(["Python is great", "I love Python programming"])
# embeddings.shape == (2, 384)
Popular models: all-MiniLM-L6-v2 (fast, 384d), all-mpnet-base-v2 (better quality, 768d), OpenAI text-embedding-3-small (API-based, 1536d).
A vector store indexes embeddings for fast similarity search. ChromaDB is a lightweight, local-first option perfect for prototyping.
import chromadb
chroma_client = chromadb.Client() # in-memory
collection = chroma_client.create_collection("my_docs")
# Add documents (ChromaDB can auto-embed with a configured model)
collection.add(
ids=["doc1", "doc2"],
documents=["Python pathlib guide", "JavaScript async patterns"],
metadatas=[{"topic": "python"}, {"topic": "javascript"}]
)
# Query
results = collection.query(query_texts=["file handling in Python"], n_results=2)
print(results["documents"])
Pattern: Store → Query → Get top-k documents → Inject into prompt.
The complete pipeline has two phases: indexing (offline) and querying (online).
Critical rule: Always use the same embedding model for indexing and querying. Mixing models produces meaningless similarity scores.
How you split documents directly impacts retrieval quality. Chunks that are too large dilute relevance; chunks that are too small lose context.
| Chunk Size | Pros | Cons |
|---|---|---|
| 128 tokens | Precise retrieval | May miss surrounding context |
| 256–512 tokens | Good balance of precision and context | Standard choice for most use cases |
| 1024+ tokens | Rich context per chunk | Diluted relevance; fewer chunks fit in context |
Adding 10–20% overlap between consecutive chunks prevents information from being split across chunk boundaries. A sentence that spans two chunks would otherwise be lost to both.
# Simple chunking with overlap
def chunk_text(text, chunk_size=500, overlap=50):
words = text.split()
chunks = []
for i in range(0, len(words), chunk_size - overlap):
chunk = " ".join(words[i:i + chunk_size])
if chunk:
chunks.append(chunk)
return chunks
Live demonstration: build a RAG system over 10 Python documentation files.
demo/sample_docs/all-MiniLM-L6-v2 and store in ChromaDBrag_answer(question, collection, top_k=3)