Skip to content

Advanced RAG Patterns

Beyond basic RAG, several advanced patterns have emerged to address specific challenges: iterative retrieval, self-correction, and complex reasoning over knowledge bases. This module covers FLARE, Self-RAG, CRAG, GraphRAG, and multi-hop retrieval architectures.

Learning Objectives

  • Understand when basic RAG falls short and advanced patterns are needed
  • Implement FLARE for iterative, generation-guided retrieval
  • Apply Self-RAG for self-reflective retrieval and generation
  • Use CRAG for corrective retrieval with web fallback
  • Build GraphRAG systems for relationship-aware retrieval
  • Design multi-hop retrieval for complex, multi-step questions

When Basic RAG Falls Short

Limitations of Basic RAG

LimitationExampleAdvanced Pattern
Single retrieval missComplex query needs multiple lookupsMulti-hop, FLARE
Retrieval noiseIrrelevant docs hurt generationCRAG, Self-RAG
No self-correctionWrong answer with high confidenceSelf-RAG
Missing relationships"Who worked with X at Y?"GraphRAG
Context evolutionLong-form generation needs different contextFLARE

FLARE: Forward-Looking Active Retrieval

FLARE retrieves dynamically during generation, when the model is uncertain about what to say next.

Implementation

python
from typing import List, Dict, Tuple, Optional
import numpy as np

class FLARERetriever:
    """
    Forward-Looking Active REtrieval augmented generation.

    Key insight: Retrieve when the model is uncertain, using
    the uncertain tokens as retrieval queries.
    """

    def __init__(
        self,
        llm,
        retriever,
        confidence_threshold: float = 0.5,
        lookahead_tokens: int = 64
    ):
        self.llm = llm
        self.retriever = retriever
        self.confidence_threshold = confidence_threshold
        self.lookahead_tokens = lookahead_tokens

    def generate(
        self,
        query: str,
        initial_context: str = ""
    ) -> Tuple[str, List[Dict]]:
        """Generate response with active retrieval."""

        context = initial_context
        generated_text = ""
        retrieval_log = []

        while True:
            # Generate next segment with confidence scores
            result = self._generate_with_confidence(
                query=query,
                context=context,
                generated_so_far=generated_text
            )

            new_tokens = result["tokens"]
            confidences = result["confidences"]

            # Check for low confidence spans
            low_conf_span = self._find_low_confidence_span(
                new_tokens, confidences
            )

            if low_conf_span:
                # Use low confidence tokens as retrieval query
                retrieval_query = " ".join(low_conf_span["tokens"])

                # Retrieve additional context
                new_docs = self.retriever.search(retrieval_query, top_k=3)
                retrieval_log.append({
                    "trigger": retrieval_query,
                    "docs_retrieved": len(new_docs)
                })

                # Update context
                context = self._merge_context(
                    context,
                    [doc["text"] for doc in new_docs]
                )

                # Regenerate from low confidence point
                generated_text = generated_text[:low_conf_span["start_idx"]]

            else:
                # High confidence - accept generation
                generated_text += " ".join(new_tokens)

                # Check if complete
                if self._is_complete(generated_text, query):
                    break

        return generated_text, retrieval_log

    def _generate_with_confidence(
        self,
        query: str,
        context: str,
        generated_so_far: str
    ) -> Dict:
        """Generate tokens with token-level confidence scores."""

        prompt = f"""Context: {context}

Question: {query}

Answer so far: {generated_so_far}

Continue the answer:"""

        # Get logprobs from LLM
        response = self.llm.generate(
            prompt,
            max_tokens=self.lookahead_tokens,
            logprobs=True
        )

        tokens = response["tokens"]
        logprobs = response["logprobs"]

        # Convert logprobs to confidence (probability)
        confidences = [np.exp(lp) for lp in logprobs]

        return {"tokens": tokens, "confidences": confidences}

    def _find_low_confidence_span(
        self,
        tokens: List[str],
        confidences: List[float]
    ) -> Optional[Dict]:
        """Find first span where confidence drops below threshold."""

        for i, (token, conf) in enumerate(zip(tokens, confidences)):
            if conf < self.confidence_threshold:
                # Found low confidence - return span
                span_end = min(i + 10, len(tokens))  # Include some lookahead
                return {
                    "tokens": tokens[i:span_end],
                    "start_idx": i,
                    "avg_confidence": np.mean(confidences[i:span_end])
                }

        return None  # All high confidence

    def _merge_context(
        self,
        existing: str,
        new_docs: List[str]
    ) -> str:
        """Merge new retrieved docs with existing context."""
        # Simple concatenation with deduplication
        all_context = existing + "\n\n" + "\n\n".join(new_docs)
        # Could add summarization for long contexts
        return all_context[:8000]  # Truncate if too long

    def _is_complete(self, text: str, query: str) -> bool:
        """Check if generation is complete."""
        # Simple heuristics - could be more sophisticated
        return (
            len(text) > 100 and
            (text.rstrip().endswith('.') or
             text.rstrip().endswith('!') or
             text.rstrip().endswith('?'))
        )

FLARE Key Insight

FLARE retrieves during generation, not just before. This is powerful because:

  1. The model knows what it doesn't know (low confidence)
  2. Retrieval queries are more specific (generated tokens)
  3. Context stays relevant as generation progresses

Self-RAG: Self-Reflective Retrieval-Augmented Generation

Self-RAG trains the model to decide when to retrieve and to critique its own outputs.

Implementation

python
class SelfRAG:
    """
    Self-Reflective RAG with critique tokens.

    The model generates special tokens:
    - [Retrieve]: Should I retrieve? (yes/no)
    - [IsRel]: Is retrieved doc relevant? (relevant/irrelevant)
    - [IsSup]: Is response supported by docs? (supported/partial/none)
    - [IsUse]: Is response useful? (1-5 scale)
    """

    def __init__(self, llm, retriever, critic_llm=None):
        self.llm = llm
        self.retriever = retriever
        self.critic_llm = critic_llm or llm

    def generate(
        self,
        query: str,
        max_iterations: int = 3
    ) -> Dict:
        """Generate with self-reflection."""

        for iteration in range(max_iterations):
            # Step 1: Decide whether to retrieve
            should_retrieve = self._decide_retrieval(query)

            if should_retrieve:
                # Step 2: Retrieve documents
                docs = self.retriever.search(query, top_k=5)

                # Step 3: Filter by relevance
                relevant_docs = self._filter_relevant(query, docs)

                if not relevant_docs:
                    # No relevant docs - try without retrieval
                    response = self._generate_without_context(query)
                else:
                    # Step 4: Generate with context
                    response = self._generate_with_context(
                        query, relevant_docs
                    )
            else:
                # Generate without retrieval
                response = self._generate_without_context(query)

            # Step 5: Critique the response
            critique = self._critique_response(query, response, docs if should_retrieve else [])

            if critique["is_supported"] and critique["usefulness"] >= 4:
                return {
                    "response": response,
                    "critique": critique,
                    "iterations": iteration + 1,
                    "retrieved_docs": relevant_docs if should_retrieve else []
                }

            # Not good enough - modify query and retry
            query = self._refine_query(query, critique)

        # Max iterations reached
        return {
            "response": response,
            "critique": critique,
            "iterations": max_iterations,
            "warning": "Max iterations reached"
        }

    def _decide_retrieval(self, query: str) -> bool:
        """Decide if retrieval is needed."""
        prompt = f"""Determine if external knowledge retrieval is needed to answer this query.

Query: {query}

Consider:
- Is this a factual question requiring specific information?
- Is this about recent events or specific entities?
- Can this be answered from general knowledge?

Output only: RETRIEVE or NO_RETRIEVE"""

        response = self.llm.generate(prompt, max_tokens=10)
        return "RETRIEVE" in response.upper()

    def _filter_relevant(
        self,
        query: str,
        docs: List[Dict]
    ) -> List[Dict]:
        """Filter documents by relevance."""
        relevant = []

        for doc in docs:
            prompt = f"""Is this document relevant to answering the query?

Query: {query}

Document: {doc['text'][:500]}

Output only: RELEVANT or IRRELEVANT"""

            response = self.critic_llm.generate(prompt, max_tokens=10)
            if "RELEVANT" in response.upper():
                relevant.append(doc)

        return relevant

    def _generate_with_context(
        self,
        query: str,
        docs: List[Dict]
    ) -> str:
        """Generate response using retrieved context."""
        context = "\n\n".join([doc["text"] for doc in docs])

        prompt = f"""Answer the query using the provided context.

Context:
{context}

Query: {query}

Answer:"""

        return self.llm.generate(prompt)

    def _generate_without_context(self, query: str) -> str:
        """Generate response without retrieval."""
        prompt = f"""Answer the following query:

Query: {query}

Answer:"""

        return self.llm.generate(prompt)

    def _critique_response(
        self,
        query: str,
        response: str,
        docs: List[Dict]
    ) -> Dict:
        """Critique the generated response."""

        context = "\n\n".join([doc["text"] for doc in docs]) if docs else "No context used"

        prompt = f"""Evaluate this response:

Query: {query}
Context: {context}
Response: {response}

Evaluate:
1. Is the response supported by the context? (fully_supported/partially_supported/not_supported)
2. How useful is this response? (1-5, where 5 is very useful)
3. Any factual errors? (yes/no)

Output as:
SUPPORTED: [value]
USEFULNESS: [1-5]
ERRORS: [yes/no]"""

        critique_response = self.critic_llm.generate(prompt)

        # Parse response
        is_supported = "fully_supported" in critique_response.lower() or "partially_supported" in critique_response.lower()

        import re
        usefulness_match = re.search(r'USEFULNESS:\s*(\d)', critique_response)
        usefulness = int(usefulness_match.group(1)) if usefulness_match else 3

        has_errors = "ERRORS: yes" in critique_response.lower()

        return {
            "is_supported": is_supported,
            "usefulness": usefulness,
            "has_errors": has_errors,
            "raw_critique": critique_response
        }

    def _refine_query(self, query: str, critique: Dict) -> str:
        """Refine query based on critique."""
        prompt = f"""The previous response to this query was inadequate.

Original query: {query}
Issues: Usefulness was {critique['usefulness']}/5

Generate a refined query that might get better results:"""

        return self.llm.generate(prompt, max_tokens=100)

CRAG: Corrective Retrieval-Augmented Generation

CRAG evaluates retrieval quality and corrects with web search when needed.

Implementation

python
class CRAG:
    """
    Corrective RAG with retrieval evaluation and web fallback.

    Key components:
    1. Retrieval Evaluator: Scores retrieval quality
    2. Knowledge Refinement: Extracts key information
    3. Web Search: Fallback for failed retrieval
    """

    def __init__(
        self,
        llm,
        retriever,
        web_search,
        confidence_thresholds: Dict = None
    ):
        self.llm = llm
        self.retriever = retriever
        self.web_search = web_search
        self.thresholds = confidence_thresholds or {
            "correct": 0.7,
            "incorrect": 0.3
        }

    def generate(self, query: str) -> Dict:
        """Generate with corrective retrieval."""

        # Step 1: Initial retrieval
        docs = self.retriever.search(query, top_k=5)

        # Step 2: Evaluate retrieval quality
        eval_results = self._evaluate_retrieval(query, docs)

        # Step 3: Route based on evaluation
        if eval_results["action"] == "correct":
            # Good retrieval - use as-is
            context = self._extract_knowledge(query, docs)
            source = "retrieval"

        elif eval_results["action"] == "ambiguous":
            # Partial retrieval - refine knowledge
            refined = self._refine_knowledge(query, docs)
            web_results = self.web_search.search(query)
            context = self._merge_sources(refined, web_results)
            source = "hybrid"

        else:  # incorrect
            # Bad retrieval - fall back to web
            web_results = self.web_search.search(query)
            context = self._process_web_results(web_results)
            source = "web"

        # Step 4: Generate response
        response = self._generate_response(query, context)

        return {
            "response": response,
            "source": source,
            "retrieval_score": eval_results["confidence"],
            "action_taken": eval_results["action"]
        }

    def _evaluate_retrieval(
        self,
        query: str,
        docs: List[Dict]
    ) -> Dict:
        """Evaluate if retrieved documents are correct for the query."""

        if not docs:
            return {"action": "incorrect", "confidence": 0.0}

        # Score each document
        scores = []
        for doc in docs:
            prompt = f"""Rate the relevance of this document for answering the query.

Query: {query}

Document: {doc['text'][:500]}

Rate from 0.0 (completely irrelevant) to 1.0 (perfectly relevant).
Output only the number:"""

            response = self.llm.generate(prompt, max_tokens=10)
            try:
                score = float(response.strip())
                scores.append(min(max(score, 0.0), 1.0))
            except:
                scores.append(0.5)

        # Aggregate scores
        avg_score = np.mean(scores)
        max_score = max(scores)

        # Determine action
        if max_score >= self.thresholds["correct"]:
            action = "correct"
        elif avg_score >= self.thresholds["incorrect"]:
            action = "ambiguous"
        else:
            action = "incorrect"

        return {
            "action": action,
            "confidence": avg_score,
            "doc_scores": scores
        }

    def _extract_knowledge(
        self,
        query: str,
        docs: List[Dict]
    ) -> str:
        """Extract relevant knowledge from documents."""
        combined = "\n\n".join([doc["text"] for doc in docs])

        prompt = f"""Extract the key information from these documents that's relevant to answering:
Query: {query}

Documents:
{combined}

Key information:"""

        return self.llm.generate(prompt)

    def _refine_knowledge(
        self,
        query: str,
        docs: List[Dict]
    ) -> str:
        """Refine partially relevant documents."""

        # Decompose into sub-queries
        prompt = f"""The query may need multiple pieces of information.
Break it into sub-questions:

Query: {query}

Sub-questions:"""

        sub_queries = self.llm.generate(prompt).strip().split('\n')

        # Extract relevant parts for each sub-query
        refined_parts = []
        combined_docs = "\n\n".join([doc["text"] for doc in docs])

        for sub_q in sub_queries[:3]:  # Limit sub-queries
            prompt = f"""Extract information relevant to this sub-question:

Sub-question: {sub_q}

Documents:
{combined_docs[:2000]}

Relevant information (or 'Not found'):"""

            part = self.llm.generate(prompt)
            if "not found" not in part.lower():
                refined_parts.append(part)

        return "\n\n".join(refined_parts)

    def _process_web_results(self, web_results: List[Dict]) -> str:
        """Process web search results."""
        # Filter and clean web results
        cleaned = []
        for result in web_results[:5]:
            cleaned.append(f"Source: {result.get('url', 'Unknown')}\n{result.get('snippet', '')}")
        return "\n\n".join(cleaned)

    def _merge_sources(self, retrieval: str, web: List[Dict]) -> str:
        """Merge retrieval and web sources."""
        web_text = self._process_web_results(web)
        return f"From knowledge base:\n{retrieval}\n\nFrom web search:\n{web_text}"

    def _generate_response(self, query: str, context: str) -> str:
        """Generate final response."""
        prompt = f"""Answer the query using the provided information.

Information:
{context}

Query: {query}

Answer:"""

        return self.llm.generate(prompt)

GraphRAG: Knowledge Graph-Enhanced Retrieval

GraphRAG uses knowledge graphs to capture entity relationships and enable relationship-aware retrieval.

Implementation

python
import networkx as nx
from collections import defaultdict

class GraphRAG:
    """
    Knowledge Graph-based RAG for relationship-aware retrieval.

    Based on Microsoft's GraphRAG approach:
    1. Extract entities and relationships from documents
    2. Build knowledge graph
    3. Detect communities for global understanding
    4. Query via local (entity) or global (community) search
    """

    def __init__(self, llm, embedding_model):
        self.llm = llm
        self.embedding_model = embedding_model
        self.graph = nx.Graph()
        self.entity_docs = defaultdict(list)  # entity -> document chunks
        self.communities = {}  # community_id -> summary

    def build_index(self, documents: List[Dict]) -> None:
        """Build knowledge graph from documents."""

        for doc in documents:
            # Extract entities and relationships
            entities, relationships = self._extract_graph_elements(doc["text"])

            # Add to graph
            for entity in entities:
                self.graph.add_node(
                    entity["name"],
                    type=entity["type"],
                    description=entity.get("description", "")
                )
                self.entity_docs[entity["name"]].append(doc)

            for rel in relationships:
                self.graph.add_edge(
                    rel["source"],
                    rel["target"],
                    relationship=rel["type"],
                    description=rel.get("description", "")
                )

        # Detect communities
        self._detect_communities()

        # Generate community summaries
        self._generate_community_summaries()

    def _extract_graph_elements(self, text: str) -> Tuple[List[Dict], List[Dict]]:
        """Extract entities and relationships from text."""

        prompt = f"""Extract entities and relationships from this text.

Text: {text[:2000]}

Output format:
ENTITIES:
- name: [entity name], type: [person/org/concept/etc], description: [brief description]

RELATIONSHIPS:
- source: [entity1], target: [entity2], type: [relationship type], description: [brief description]

Extract:"""

        response = self.llm.generate(prompt)

        # Parse response (simplified - use structured output in production)
        entities = []
        relationships = []

        # Parse entities section
        in_entities = False
        in_relationships = False

        for line in response.split('\n'):
            line = line.strip()
            if 'ENTITIES:' in line:
                in_entities = True
                in_relationships = False
            elif 'RELATIONSHIPS:' in line:
                in_entities = False
                in_relationships = True
            elif line.startswith('- name:') and in_entities:
                # Parse entity
                parts = line.split(',')
                entity = {}
                for part in parts:
                    if ':' in part:
                        key, val = part.split(':', 1)
                        entity[key.strip().replace('- ', '')] = val.strip()
                if 'name' in entity:
                    entities.append(entity)
            elif line.startswith('- source:') and in_relationships:
                # Parse relationship
                parts = line.split(',')
                rel = {}
                for part in parts:
                    if ':' in part:
                        key, val = part.split(':', 1)
                        rel[key.strip().replace('- ', '')] = val.strip()
                if 'source' in rel and 'target' in rel:
                    relationships.append(rel)

        return entities, relationships

    def _detect_communities(self) -> None:
        """Detect communities in the knowledge graph."""
        if len(self.graph.nodes()) > 0:
            # Use Louvain community detection
            try:
                from community import community_louvain
                partition = community_louvain.best_partition(self.graph)
                self.node_communities = partition
            except ImportError:
                # Fallback to connected components
                components = nx.connected_components(self.graph)
                self.node_communities = {}
                for i, comp in enumerate(components):
                    for node in comp:
                        self.node_communities[node] = i

    def _generate_community_summaries(self) -> None:
        """Generate summaries for each community."""

        # Group nodes by community
        community_nodes = defaultdict(list)
        for node, comm_id in self.node_communities.items():
            community_nodes[comm_id].append(node)

        # Generate summary for each community
        for comm_id, nodes in community_nodes.items():
            # Get subgraph
            subgraph = self.graph.subgraph(nodes)

            # Describe community
            node_descriptions = []
            for node in nodes[:10]:  # Limit for prompt
                data = self.graph.nodes[node]
                node_descriptions.append(
                    f"- {node} ({data.get('type', 'unknown')}): {data.get('description', '')}"
                )

            edge_descriptions = []
            for u, v, data in subgraph.edges(data=True):
                edge_descriptions.append(
                    f"- {u} --[{data.get('relationship', 'related')}]--> {v}"
                )

            prompt = f"""Summarize this group of related entities and their relationships:

Entities:
{chr(10).join(node_descriptions[:10])}

Relationships:
{chr(10).join(edge_descriptions[:10])}

Write a 2-3 sentence summary of what this group represents:"""

            summary = self.llm.generate(prompt)
            self.communities[comm_id] = {
                "nodes": nodes,
                "summary": summary,
                "size": len(nodes)
            }

    def query(
        self,
        query: str,
        mode: str = "auto"  # "local", "global", or "auto"
    ) -> Dict:
        """Query the knowledge graph."""

        # Determine query mode
        if mode == "auto":
            mode = self._classify_query(query)

        if mode == "local":
            return self._local_search(query)
        else:
            return self._global_search(query)

    def _classify_query(self, query: str) -> str:
        """Classify query as local (specific entity) or global (broad topic)."""

        prompt = f"""Classify this query:

Query: {query}

Is this asking about:
A) Specific entities or their direct relationships (LOCAL)
B) Broad themes, summaries, or aggregate information (GLOBAL)

Output only: LOCAL or GLOBAL"""

        response = self.llm.generate(prompt, max_tokens=10)
        return "local" if "LOCAL" in response.upper() else "global"

    def _local_search(self, query: str) -> Dict:
        """Search for specific entities and their relationships."""

        # Extract entities from query
        prompt = f"""Extract entity names from this query:
Query: {query}
Entities (comma-separated):"""

        response = self.llm.generate(prompt, max_tokens=100)
        query_entities = [e.strip() for e in response.split(',')]

        # Find matching nodes
        matched_nodes = []
        for entity in query_entities:
            for node in self.graph.nodes():
                if entity.lower() in node.lower():
                    matched_nodes.append(node)

        # Get subgraph around matched nodes
        context_nodes = set(matched_nodes)
        for node in matched_nodes:
            # Add neighbors
            context_nodes.update(self.graph.neighbors(node))

        # Build context from subgraph
        subgraph = self.graph.subgraph(context_nodes)
        context = self._subgraph_to_context(subgraph)

        # Add relevant document chunks
        doc_context = []
        for node in matched_nodes:
            docs = self.entity_docs.get(node, [])
            for doc in docs[:2]:
                doc_context.append(doc["text"][:500])

        full_context = f"Graph context:\n{context}\n\nDocument excerpts:\n{chr(10).join(doc_context)}"

        # Generate response
        response = self._generate_response(query, full_context)

        return {
            "response": response,
            "mode": "local",
            "matched_entities": matched_nodes,
            "context_size": len(context_nodes)
        }

    def _global_search(self, query: str) -> Dict:
        """Search across community summaries for broad understanding."""

        # Find relevant communities
        community_texts = [
            f"Community {cid}: {data['summary']}"
            for cid, data in self.communities.items()
        ]

        # Embed and search communities
        query_emb = self.embedding_model.embed([query])[0]
        comm_embs = self.embedding_model.embed(community_texts)

        # Find top communities
        similarities = [np.dot(query_emb, ce) for ce in comm_embs]
        top_indices = np.argsort(similarities)[::-1][:3]

        # Build context from top communities
        context_parts = []
        for idx in top_indices:
            comm_id = list(self.communities.keys())[idx]
            comm_data = self.communities[comm_id]
            context_parts.append(
                f"Theme: {comm_data['summary']}\n"
                f"Key entities: {', '.join(comm_data['nodes'][:5])}"
            )

        context = "\n\n".join(context_parts)

        # Generate response
        response = self._generate_response(query, context)

        return {
            "response": response,
            "mode": "global",
            "communities_used": [list(self.communities.keys())[i] for i in top_indices]
        }

    def _subgraph_to_context(self, subgraph: nx.Graph) -> str:
        """Convert subgraph to text context."""
        lines = []
        for node in subgraph.nodes():
            data = subgraph.nodes[node]
            lines.append(f"Entity: {node} ({data.get('type', 'unknown')})")

        for u, v, data in subgraph.edges(data=True):
            lines.append(f"Relationship: {u} --[{data.get('relationship', 'related')}]--> {v}")

        return "\n".join(lines)

    def _generate_response(self, query: str, context: str) -> str:
        """Generate response using context."""
        prompt = f"""Answer the query using the provided knowledge graph context.

Context:
{context}

Query: {query}

Answer:"""

        return self.llm.generate(prompt)

Multi-Hop Retrieval

Multi-hop retrieval answers complex questions requiring information from multiple documents.

Implementation

python
class MultiHopRetriever:
    """
    Multi-hop retrieval for complex questions.

    Approach:
    1. Decompose complex question into sub-questions
    2. Answer sub-questions iteratively
    3. Use answers to inform subsequent retrievals
    4. Synthesize final answer
    """

    def __init__(self, llm, retriever, max_hops: int = 3):
        self.llm = llm
        self.retriever = retriever
        self.max_hops = max_hops

    def query(self, question: str) -> Dict:
        """Answer complex question through multi-hop retrieval."""

        # Step 1: Decompose into sub-questions
        sub_questions = self._decompose_question(question)

        # Step 2: Iteratively answer sub-questions
        hop_results = []
        context_so_far = ""

        for i, sub_q in enumerate(sub_questions):
            if i >= self.max_hops:
                break

            # Fill in any placeholders from previous answers
            filled_sub_q = self._fill_placeholders(sub_q, hop_results)

            # Retrieve for this sub-question
            docs = self.retriever.search(filled_sub_q, top_k=3)

            # Answer sub-question
            sub_answer = self._answer_sub_question(
                filled_sub_q,
                docs,
                context_so_far
            )

            hop_results.append({
                "sub_question": filled_sub_q,
                "answer": sub_answer,
                "docs": docs
            })

            # Update context
            context_so_far += f"\nQ: {filled_sub_q}\nA: {sub_answer}\n"

        # Step 3: Synthesize final answer
        final_answer = self._synthesize_answer(question, hop_results)

        return {
            "answer": final_answer,
            "hops": hop_results,
            "num_hops": len(hop_results)
        }

    def _decompose_question(self, question: str) -> List[str]:
        """Break complex question into simpler sub-questions."""

        prompt = f"""Break this complex question into simpler sub-questions that can be answered one at a time.
Use [ANSWER_N] as placeholder for answers from previous questions.

Complex question: {question}

Sub-questions (one per line):"""

        response = self.llm.generate(prompt)
        sub_questions = [q.strip() for q in response.strip().split('\n') if q.strip()]

        # Clean up numbering
        cleaned = []
        for q in sub_questions:
            # Remove leading numbers/bullets
            import re
            q = re.sub(r'^[\d\.\-\)\]]+\s*', '', q)
            if q:
                cleaned.append(q)

        return cleaned if cleaned else [question]

    def _fill_placeholders(
        self,
        sub_question: str,
        previous_results: List[Dict]
    ) -> str:
        """Replace placeholders with previous answers."""

        filled = sub_question
        for i, result in enumerate(previous_results, 1):
            placeholder = f"[ANSWER_{i}]"
            filled = filled.replace(placeholder, result["answer"])

        return filled

    def _answer_sub_question(
        self,
        sub_question: str,
        docs: List[Dict],
        context_so_far: str
    ) -> str:
        """Answer a single sub-question."""

        doc_context = "\n\n".join([doc["text"][:500] for doc in docs])

        prompt = f"""Answer this specific question using the provided information.

Previous context:
{context_so_far}

Retrieved information:
{doc_context}

Question: {sub_question}

Answer (be concise, just the key information):"""

        return self.llm.generate(prompt, max_tokens=200)

    def _synthesize_answer(
        self,
        original_question: str,
        hop_results: List[Dict]
    ) -> str:
        """Combine sub-answers into final answer."""

        reasoning_chain = "\n".join([
            f"Step {i+1}: {r['sub_question']}\nAnswer: {r['answer']}"
            for i, r in enumerate(hop_results)
        ])

        prompt = f"""Based on the following reasoning chain, provide a final answer to the original question.

Original question: {original_question}

Reasoning:
{reasoning_chain}

Final answer:"""

        return self.llm.generate(prompt)

Interview Q&A

Q1: Explain FLARE and when you would use it over standard RAG.

Strong Answer:

FLARE (Forward-Looking Active REtrieval) retrieves dynamically during generation based on model confidence.

How it works:

  1. Start generating response
  2. Monitor token-level confidence (via logprobs)
  3. When confidence drops, pause and use low-confidence tokens as query
  4. Retrieve relevant documents
  5. Continue generation with new context

Use over standard RAG when:

  • Long-form generation where context needs evolve
  • Questions that span multiple topics
  • When you can't predict all needed context upfront

Example:

Query: "Explain the history and impact of the internet"

Standard RAG: Retrieves once, might miss economic impact details

FLARE:
- Generates history confidently
- Drops confidence on "economic impact in developing nations"
- Retrieves economic data
- Continues with accurate statistics

Tradeoffs:

  • Pro: Better accuracy for complex, evolving topics
  • Con: Higher latency (multiple retrievals)
  • Con: Requires logprob access (not all APIs provide this)

Q2: How does Self-RAG improve upon basic RAG?

Strong Answer:

Self-RAG adds self-reflection tokens that let the model decide when to retrieve and critique its own outputs.

Key reflection tokens:

TokenPurpose
[Retrieve]Should I retrieve?
[IsRel]Is this doc relevant?
[IsSup]Is my answer supported?
[IsUse]Is my answer useful?

Improvements over basic RAG:

  1. Selective retrieval: Doesn't retrieve when unnecessary (saves latency)
  2. Relevance filtering: Discards irrelevant retrieved docs
  3. Self-critique: Catches unsupported or low-quality answers
  4. Iteration: Can retry with refined queries

When to use:

  • High-stakes applications requiring answer verification
  • When retrieval quality varies significantly
  • When you need confidence signals for downstream systems

Limitation: Requires fine-tuning the model for reflection tokens, or simulating with prompts (less reliable).


Q3: Compare GraphRAG to vector-based RAG for a knowledge management system.

Strong Answer:

AspectVector RAGGraphRAG
Retrieval basisSemantic similarityEntity relationships
Query types"How does X work?""Who worked with X at Y?"
ContextIndependent chunksConnected entities
Global understandingPoorStrong (community summaries)
Setup complexityLowHigh (graph construction)
Best forGeneral Q&ARelationship-heavy domains

GraphRAG excels when:

  • Questions involve entity relationships ("How is A connected to B?")
  • Need aggregate understanding ("What themes emerge in these documents?")
  • Domain has clear entities (people, organizations, concepts)

Vector RAG is better when:

  • Questions are concept-focused, not entity-focused
  • Documents lack clear entity structure
  • Simplicity and speed are priorities

Hybrid approach:

python
def hybrid_query(query):
    if has_entity_relationship(query):
        return graph_rag.query(query)
    else:
        return vector_rag.query(query)

Q4: Design a multi-hop RAG system for answering complex analytical questions.

Strong Answer:

For complex questions like "What was the revenue impact of the acquisition announced by the CEO of the company that bought Twitter?"

Architecture:

python
class AnalyticalRAG:
    def answer(self, question):
        # 1. Decompose
        sub_questions = self.decompose(question)
        # ["What company bought Twitter?",
        #  "Who is the CEO of [ANSWER_1]?",
        #  "What acquisition did [ANSWER_2] announce?",
        #  "What was the revenue impact of [ANSWER_3]?"]

        # 2. Solve iteratively
        answers = {}
        for i, sub_q in enumerate(sub_questions):
            # Fill placeholders
            filled_q = self.fill_placeholders(sub_q, answers)

            # Retrieve
            docs = self.retrieve(filled_q)

            # Answer
            answer = self.answer_atomic(filled_q, docs)
            answers[f"ANSWER_{i+1}"] = answer

        # 3. Synthesize
        return self.synthesize(question, answers)

Key design decisions:

  1. Decomposition strategy: Use LLM with few-shot examples of decomposition
  2. Placeholder system: Reference previous answers with [ANSWER_N]
  3. Retrieval per hop: Each sub-question gets fresh retrieval
  4. Context accumulation: Pass previous Q&A pairs to inform current hop
  5. Synthesis step: Combine all findings into coherent answer

Handling failures:

  • If sub-question fails, try rephrasing
  • If hop chain breaks, fall back to single-hop on original question
  • Track confidence at each hop for quality monitoring

Summary Table

PatternKey InnovationBest Use CaseComplexity
FLARERetrieves during generation on low confidenceLong-form, evolving contentMedium
Self-RAGSelf-reflection and critique tokensHigh-stakes, verification neededHigh
CRAGRetrieval quality evaluation + web fallbackVariable retrieval qualityMedium
GraphRAGKnowledge graph for relationshipsEntity-heavy domainsHigh
Multi-hopIterative sub-question answeringComplex analytical queriesMedium

Sources