SynfraCore
Synfracore
Start Learning
Navigation

Academies

Platform

RoadmapsLabsCertificationsInterviewPYQsAI AssistantCareer
Start Learning Free🗺️ Learning Roadmaps

RAG SystemsOverview

What it is, why it matters, architecture and key concepts

✍️
Written by senior engineers. Reviewed for technical accuracy.· Updated 2025 · SynfraCore RAG Systems Team
Expert Content

RAG — Retrieval Augmented Generation

RAG is the most important pattern in production LLM systems. It solves the fundamental problem of LLMs: they have a knowledge cutoff and don't know about your private data. RAG lets LLMs answer questions about YOUR documents, databases, and real-time information.

The Problem RAG Solves

Without RAG:
User: "What is our Q3 deployment policy?"
LLM:  "I don't have access to your company's internal policies."

With RAG:
User: "What is our Q3 deployment policy?"
System: [searches policy docs] → [finds relevant policy] → [injects into prompt]
LLM:  "According to your deployment policy document, Q3 deployments require..."

LLMs are expensive to fine-tune and have training cutoffs. RAG is cheaper, more accurate for factual retrieval, and updateable in real-time.

RAG Architecture

Documents/Data
     │
     ▼
[Document Loader]
(PDFs, URLs, Confluence, Notion, S3...)
     │
     ▼
[Text Splitter]
(Chunk into smaller pieces)
     │
     ▼
[Embedding Model]
(text → vector, e.g. text-embedding-3-small)
     │
     ▼
[Vector Database]
(Pinecone, Chroma, Weaviate, pgvector)
     │
     │ ← At query time:
     │
[User Query] → [Embed query] → [Similarity Search] → [Top K chunks]
                                                            │
                                               [Inject into LLM prompt]
                                                            │
                                                       [LLM Response]

Chunking Strategies

How you split documents dramatically affects RAG quality.

python
from langchain.text_splitter import (
    RecursiveCharacterTextSplitter,
    MarkdownHeaderTextSplitter,
    TokenTextSplitter,
)

# Strategy 1: Recursive Character Splitting (most common)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=1000,        # Characters per chunk
    chunk_overlap=200,      # Overlap prevents cutting context
    separators=["\n\n", "\n", " ", ""],  # Split on these in order
)
chunks = splitter.split_documents(docs)

# Strategy 2: Split by Markdown headers (for structured docs)
md_splitter = MarkdownHeaderTextSplitter(
    headers_to_split_on=[
        ("#", "Header1"),
        ("##", "Header2"),
        ("###", "Header3"),
    ]
)
chunks = md_splitter.split_text(markdown_text)
# Each chunk keeps header context in metadata

# Strategy 3: Token-based (for LLM context window optimization)
token_splitter = TokenTextSplitter(
    encoding_name="cl100k_base",  # GPT-4 tokenizer
    chunk_size=512,               # Tokens, not chars
    chunk_overlap=50,
)

Chunking rules of thumb:

Technical docs: 500-1000 chars with 100-200 overlap
Legal/compliance: 1000-2000 chars (context matters)
Code: split by function/class, not arbitrary chars
Always include metadata: source file, page number, section

Vector Databases

python
# Chroma — local, great for development
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

embeddings = OpenAIEmbeddings(model="text-embedding-3-small")

# Create and persist
vectorstore = Chroma.from_documents(
    documents=chunks,
    embedding=embeddings,
    persist_directory="./chroma_db",
    collection_name="company_docs"
)

# Load existing
vectorstore = Chroma(
    persist_directory="./chroma_db",
    embedding_function=embeddings,
    collection_name="company_docs"
)

# Search
results = vectorstore.similarity_search(
    "What is the deployment process?",
    k=5  # Return top 5 most relevant chunks
)

# Search with scores
results = vectorstore.similarity_search_with_relevance_scores(
    "deployment policy",
    k=5,
    score_threshold=0.7  # Only return if relevance > 0.7
)
python
# Pinecone — managed, production-grade
import pinecone
from langchain_community.vectorstores import Pinecone

pc = pinecone.Pinecone(api_key="YOUR_API_KEY")
index = pc.Index("company-knowledge-base")

vectorstore = Pinecone(
    index=index,
    embedding=embeddings,
    text_key="text"
)

# Metadata filtering — only search within specific namespace
results = vectorstore.similarity_search(
    "deployment policy",
    k=5,
    filter={"department": "engineering", "doc_type": "policy"}
)

Complete Production RAG Pipeline

python
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.document_loaders import (
    PyPDFLoader, WebBaseLoader, ConfluenceLoader
)
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from typing import List
import logging

logger = logging.getLogger(__name__)

class RAGPipeline:
    def __init__(self, persist_dir: str = "./vectorstore"):
        self.embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
        self.llm = ChatOpenAI(model="gpt-4o", temperature=0)
        self.persist_dir = persist_dir
        self.vectorstore = None

    def ingest_documents(self, sources: List[str]):
        """Load, chunk, and index documents."""
        all_docs = []

        for source in sources:
            logger.info(f"Loading: {source}")
            if source.endswith(".pdf"):
                loader = PyPDFLoader(source)
            elif source.startswith("http"):
                loader = WebBaseLoader(source)
            else:
                raise ValueError(f"Unsupported source: {source}")

            docs = loader.load()
            # Add source metadata
            for doc in docs:
                doc.metadata["source"] = source

            all_docs.extend(docs)

        # Split into chunks
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=200,
        )
        chunks = splitter.split_documents(all_docs)
        logger.info(f"Created {len(chunks)} chunks from {len(all_docs)} documents")

        # Create vector store
        self.vectorstore = Chroma.from_documents(
            documents=chunks,
            embedding=self.embeddings,
            persist_directory=self.persist_dir,
        )
        logger.info("Ingestion complete")

    def load_existing(self):
        """Load existing vector store."""
        self.vectorstore = Chroma(
            persist_directory=self.persist_dir,
            embedding_function=self.embeddings,
        )

    def build_chain(self):
        """Build the RAG chain."""
        retriever = self.vectorstore.as_retriever(
            search_type="mmr",          # Maximum Marginal Relevance — diverse results
            search_kwargs={
                "k": 6,                 # Retrieve 6 chunks
                "fetch_k": 20,          # Fetch 20, then pick diverse 6
                "lambda_mult": 0.7,     # 0=max diversity, 1=max relevance
            }
        )

        prompt = ChatPromptTemplate.from_messages([
            ("system", """You are a helpful assistant that answers questions 
based strictly on the provided context documents.

Rules:
- Only use information from the provided context
- If the answer is not in the context, say "I don't have information about that in the provided documents"
- Always cite the source document
- Be concise and direct

Context:
{context}"""),
            ("human", "{question}")
        ])

        def format_docs(docs):
            parts = []
            for doc in docs:
                source = doc.metadata.get("source", "unknown")
                page = doc.metadata.get("page", "")
                parts.append(f"[Source: {source}{f', page {page}' if page else ''}]\n{doc.page_content}")
            return "\n\n---\n\n".join(parts)

        chain = (
            {
                "context": retriever | format_docs,
                "question": RunnablePassthrough()
            }
            | prompt
            | self.llm
            | StrOutputParser()
        )

        return chain

    def query(self, question: str, stream: bool = False) -> str:
        chain = self.build_chain()
        if stream:
            for chunk in chain.stream(question):
                print(chunk, end="", flush=True)
            print()
        else:
            return chain.invoke(question)


# Usage
rag = RAGPipeline()

# First time — ingest documents
rag.ingest_documents([
    "docs/deployment-policy.pdf",
    "docs/incident-runbook.pdf",
    "https://company.wiki/engineering/standards",
])

# Subsequent times — load existing
# rag.load_existing()

answer = rag.query("What is the rollback procedure for production deployments?")
print(answer)

Advanced RAG Techniques

python
# 1. Hybrid Search — combine semantic + keyword
from langchain.retrievers import EnsembleRetriever, BM25Retriever

bm25_retriever = BM25Retriever.from_documents(chunks)
bm25_retriever.k = 5

vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5})

# Combine: 60% vector, 40% keyword
hybrid_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, vector_retriever],
    weights=[0.4, 0.6]
)

# 2. Re-ranking — reorder retrieved docs by relevance
from langchain.retrievers.contextual_compression import ContextualCompressionRetriever
from langchain_cohere import CohereRerank

reranker = CohereRerank(top_n=3)  # Keep top 3 after reranking
compression_retriever = ContextualCompressionRetriever(
    base_compressor=reranker,
    base_retriever=vector_retriever
)

# 3. Query transformation — improve retrieval with query rewriting
query_transform_prompt = ChatPromptTemplate.from_template("""
Generate 3 different search queries to find information about: {question}
Output only the queries, one per line.
""")

def generate_queries(question: str) -> List[str]:
    response = llm.invoke(query_transform_prompt.format(question=question))
    return response.content.strip().split("\n")

Evaluating RAG Quality

python
# Key metrics for RAG evaluation:

# 1. Retrieval precision — are retrieved chunks relevant?
# 2. Answer faithfulness — does answer stay within retrieved context?
# 3. Answer relevancy — does answer actually address the question?

# Use RAGAS framework for automated evaluation
from ragas import evaluate
from ragas.metrics import (
    faithfulness,
    answer_relevancy,
    context_recall,
    context_precision,
)

# Build evaluation dataset
eval_data = {
    "question": ["What is the deployment rollback process?"],
    "answer": [rag.query("What is the deployment rollback process?")],
    "contexts": [retriever.get_relevant_documents("deployment rollback")],
    "ground_truth": ["The rollback process involves reverting the Git commit..."]
}

results = evaluate(eval_data, metrics=[
    faithfulness,
    answer_relevancy,
    context_precision,
    context_recall
])
print(results)

Interview Questions

What is the difference between RAG and fine-tuning?

Fine-tuning trains the model weights on your data — expensive (thousands of dollars), slow, and the model's knowledge is frozen at training time. RAG retrieves relevant information at inference time and injects it into the prompt — cheap, fast, and updateable instantly by adding new documents. Fine-tuning is better for learning new response styles, formats, or domain-specific reasoning patterns. RAG is better for factual knowledge that changes frequently. Most production systems use RAG; fine-tuning is reserved for specific behavioral changes.

How do you handle a RAG system that returns wrong or hallucinated answers?

First, check retrieval quality — are the right chunks being retrieved? Use similarity score thresholds to reject low-confidence retrievals. Second, improve chunking — ensure chunks have enough context (add surrounding text, parent document). Third, strengthen the system prompt with "only answer from context" instructions. Fourth, add a faithfulness check — have a second LLM call verify the answer is supported by the retrieved context. Fifth, implement human feedback loops to identify and fix systematic failures.

What embedding model should you use?

For English: OpenAI text-embedding-3-small (1536 dims, cost-effective) or text-embedding-3-large (3072 dims, higher quality). For multilingual: text-embedding-3-large or Cohere's multilingual embeddings. For on-premise/privacy: sentence-transformers/all-mpnet-base-v2 (free, good quality). Key principle: use the same model for indexing AND querying — mixing models breaks semantic search.

Share:
Join our Community
Daily tips, job alerts, interview help — join engineers learning together
Up Next
RAG SystemsPrerequisites
What to know or set up before starting
Also Worth Exploring
← Back to all RAG Systems modules
Prerequisites