Navigation

Introduction to AI

Machine Learning

Deep Learning

Generative AI

Tools & Frameworks

General

RAG Architectures: Building Production-Ready Retrieval Systems

Retrieval-Augmented Generation (RAG) has become the standard architecture for knowledge-intensive AI applications. This guide covers the complete landscape of RAG system design, from basic implementations to production-ready systems.

The RAG Pipeline

A RAG system consists of several stages:

class RAGPipeline:
    def __init__(self, retriever, reranker, generator):
        self.retriever = retriever      # Vector search
        self.reranker = reranker        # Cross-encoder scoring
        self.generator = generator      # LLM for generation
    
    def __call__(self, query: str) -> str:
        # Stage 1: Query understanding
        query = self.rewrite_query(query)
        
        # Stage 2: Retrieval
        docs = self.retriever.retrieve(query, top_k=100)
        
        # Stage 3: Reranking
        ranked = self.reranker.rerank(query, docs, top_k=10)
        
        # Stage 4: Context assembly
        context = self.assemble_context(ranked)
        
        # Stage 5: Generation
        response = self.generator.generate(context, query)
        
        return response

Document Processing

Text Chunking

from langchain.text_splitter import RecursiveCharacterTextSplitter

class DocumentChunker:
    def __init__(self, chunk_size=1000, chunk_overlap=200):
        self.splitter = RecursiveCharacterTextSplitter(
            chunk_size=chunk_size,
            chunk_overlap=chunk_overlap,
            separators=["\n\n", "\n", ". ", " ", ""],
            length_function=len,
        )
    
    def chunk(self, documents):
        """Chunk documents into pieces."""
        chunks = self.splitter.create_documents(
            texts=[doc.page_content for doc in documents],
            metadatas=[doc.metadata for doc in documents]
        )
        return chunks

# Semantic-aware chunking
class SemanticChunker:
    def chunk(self, document, embedding_model):
        """Chunk based on semantic similarity."""
        sentences = split_into_sentences(document.page_content)
        embeddings = embedding_model.encode(sentences)
        
        # Find natural breaks based on embedding similarity
        breakpoints = find_semantic_breaks(embeddings)
        
        chunks = []
        for i, start in enumerate(breakpoints):
            end = breakpoints[i + 1] if i + 1 < len(breakpoints) else len(sentences)
            chunk = " ".join(sentences[start:end])
            chunks.append({
                "content": chunk,
                "start_sentence": start,
                "end_sentence": end,
            })
        
        return chunks

Metadata Extraction

class MetadataExtractor:
    def extract(self, document):
        """Extract structured metadata from document."""
        return {
            "title": extract_title(document),
            "headers": extract_headers(document),
            "page_numbers": extract_pages(document),
            "section_titles": extract_sections(document),
            "keywords": extract_keywords(document),
            "entities": extract_entities(document),
            "language": detect_language(document),
            "document_type": classify_document(document),
        }

Retrieval Strategies

Dense Retrieval

class DenseRetriever:
    def __init__(self, embedding_model, vector_store, index_name):
        self.embedding_model = embedding_model
        self.vector_store = vector_store(index_name)
    
    def retrieve(self, query: str, top_k: int = 10) -> List[Document]:
        # Encode query
        query_embedding = self.embedding_model.encode(query)
        
        # Search
        results = self.vector_store.similarity_search(
            query_embedding,
            k=top_k,
            include_metadata=True,
        )
        
        return results
class HybridRetriever:
    def __init__(self, dense_retriever, sparse_retriever, fusion_method="rrf"):
        self.dense = dense_retriever
        self.sparse = sparse_retriever
        self.fusion_method = fusion_method
    
    def retrieve(self, query: str, top_k: int = 10) -> List[Document]:
        # Dense retrieval
        dense_results = self.dense.retrieve(query, top_k * 2)
        
        # Sparse retrieval
        sparse_results = self.sparse.retrieve(query, top_k * 2)
        
        # Fuse results using Reciprocal Rank Fusion
        fused = self.rrf_fuse(dense_results, sparse_results, top_k)
        
        return fused
    
    def rrf_fuse(self, dense, sparse, k=60):
        """Reciprocal Rank Fusion."""
        scores = {}
        
        for rank, doc in enumerate(dense):
            scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1)
        
        for rank, doc in enumerate(sparse):
            scores[doc.id] = scores.get(doc.id, 0) + 1 / (k + rank + 1)
        
        sorted_docs = sorted(scores.items(), key=lambda x: -x[1])
        return [get_doc_by_id(did) for did, _ in sorted_docs[:k]]

Query Expansion

class QueryExpander:
    def __init__(self, llm):
        self.llm = llm
    
    def expand(self, query: str) -> List[str]:
        """Generate query variations."""
        prompt = f"""Generate 3 variations of this search query
        to improve retrieval. Each variation should capture
        a different aspect or rephrase the query.
        
        Original: {query}
        
        Variations:
        1."""
        
        response = self.llm.generate(prompt)
        variations = parse_variations(response)
        return [query] + variations
    
    def expand_with_context(self, query: str, context: str) -> List[str]:
        """Expand query considering conversation context."""
        prompt = f"""Rewrite this query to incorporate context from the
        conversation history.
        
        Conversation: {context}
        Current query: {query}
        
        Rewritten query:"""
        
        rewritten = self.llm.generate(prompt)
        return [query, rewritten]

Reranking

class CrossEncoderReranker:
    def __init__(self, cross_encoder_model):
        self.model = cross_encoder_model
    
    def rerank(self, query: str, documents: List[Document], top_k: int = 10):
        """Score query-document pairs and return top results."""
        # Prepare pairs
        pairs = [(query, doc.page_content) for doc in documents]
        
        # Batch scoring
        scores = self.model.predict(pairs)
        
        # Sort by score
        ranked = sorted(
            zip(documents, scores),
            key=lambda x: x[1],
            reverse=True
        )
        
        return [doc for doc, _ in ranked[:top_k]]

Knowledge Graph RAG

class KnowledgeGraphRAG:
    def __init__(self, kg_store, vector_store, llm):
        self.kg = kg_store
        self.vector = vector_store
        self.llm = llm
    
    def retrieve(self, query: str) -> str:
        # Step 1: Extract entities from query
        entities = self.extract_entities(query)
        
        # Step 2: Query knowledge graph
        kg_context = self.kg.query(entities)
        
        # Step 3: Retrieve relevant documents
        docs = self.vector.similarity_search(query, top_k=10)
        
        # Step 4: Fuse contexts
        fused_context = self.fuse_contexts(kg_context, docs)
        
        # Step 5: Generate
        response = self.llm.generate(
            self.create_prompt(query, fused_context)
        )
        
        return response
    
    def extract_entities(self, query: str) -> List[Entity]:
        """Use LLM to extract entities from query."""
        prompt = f"""Extract named entities from this query.
        Return as JSON list with type and name.
        
        Query: {query}
        
        Entities:"""
        
        return self.llm.generate_json(prompt)
    
    def fuse_contexts(self, kg_context, docs):
        """Combine knowledge graph and document context."""
        return {
            "knowledge_graph": kg_context,
            "documents": [d.page_content for d in docs],
            "citation_map": self.create_citations(kg_context, docs),
        }

Context Assembly

class ContextAssembler:
    def __init__(self, max_tokens=6000, separator="\n\n"):
        self.max_tokens = max_tokens
        self.separator = separator
    
    def assemble(self, documents: List[Document], query: str) -> str:
        """Build context string from retrieved documents."""
        context_parts = []
        current_length = 0
        
        # Sort by relevance
        sorted_docs = sorted(documents, key=lambda d: d.score, reverse=True)
        
        for doc in sorted_docs:
            doc_text = self.format_document(doc, query)
            doc_tokens = count_tokens(doc_text)
            
            if current_length + doc_tokens > self.max_tokens:
                # Truncate or skip
                if current_length == 0:
                    doc_text = truncate_to_token_limit(doc_text, self.max_tokens)
                else:
                    break
            
            context_parts.append(doc_text)
            current_length += doc_tokens
        
        return self.separator.join(context_parts)
    
    def format_document(self, doc: Document, query: str) -> str:
        """Format document with citation."""
        return f"[Source: {doc.metadata.get('source', 'Unknown')}]\n{doc.page_content}"

Production Considerations

Caching

class RAGCache:
    def __init__(self, vector_store, redis_client):
        self.vector = vector_store
        self.redis = redis_client
    
    def get_or_query(self, query: str, retriever, ttl=3600):
        """Check cache before retrieval."""
        cache_key = hash_query(query)
        
        cached = self.redis.get(cache_key)
        if cached:
            return json.loads(cached)
        
        results = retriever.retrieve(query)
        self.redis.setex(cache_key, ttl, json.dumps(results))
        
        return results

Monitoring

class RAGMonitor:
    def __init__(self, metrics_client):
        self.client = metrics_client
    
    def record_retrieval(self, query: str, num_results: int, latency: float):
        self.client.gauge("rag.retrieval.latency", latency)
        self.client.gauge("rag.retrieval.results_count", num_results)
    
    def record_generation(self, prompt: str, response: str, latency: float):
        self.client.gauge("rag.generation.latency", latency)
        self.client.gauge("rag.generation.response_length", len(response))
    
    def record_retrieval_quality(self, query: str, relevance_scores: List[float]):
        self.client.histogram(
            "rag.retrieval.relevance_score",
            relevance_scores
        )

Fallbacks

class RAGWithFallback:
    def __init__(self, primary_rag, fallback_rag):
        self.primary = primary_rag
        self.fallback = fallback_rag
    
    def generate(self, query: str) -> str:
        try:
            return self.primary.generate(query)
        except VectorStoreError:
            return self.fallback.generate(query)
        except Exception as e:
            log_error(e)
            return self.primary.generate_simplified(query)

Building production RAG systems requires attention to retrieval quality, context construction, latency, and reliability. The techniques here provide a foundation for building systems that work reliably at scale.