Advanced8 min read19 of 40

Iterative Retrieval - Progressive Query Refinement

Retrieve, inspect intermediate results, refine the query, retrieve again — iterate until the retrieved context is sufficient to answer the question.

Overview

Iterative Retrieval recognizes that complex questions often cannot be answered from a single retrieval round. Instead of retrieving once and hoping for the best, it runs a loop: retrieve, have the LLM assess whether the current context is sufficient, and if not, generate a more targeted follow-up query and retrieve again. This continues until either the context is deemed sufficient or a maximum iteration limit is reached.

Pattern #20 in the 37 RAG Patterns Collection

flowchart TD
  Q[Query] --> R1[Initial retrieve top-5]
  R1 --> D1[Generate draft answer]
  D1 --> GA["Gap analysis: LLM lists what evidence is still missing JSON array max 3"]
  GA --> |no gaps identified| OUT[Return final answer]
  GA --> |gaps found| RE[Retrieve fresh evidence for each gap]
  RE --> M[Merge into evidence pool — deduplicate]
  M --> D1
  GA -.->|MAX 3 iterations| OUT

The Problem

A single retrieval round fails on complex, multi-part questions:

Query: "How does transformer attention scaling affect memory usage when using gradient checkpointing during fine-tuning?" Round 1 retrieval: - "Transformer attention mechanisms" — covers attention but not memory - "Memory optimization in deep learning" — general, not fine-tuning specific - "Gradient checkpointing tutorial" — covers checkpointing but not attention interaction Problem: No single chunk addresses the intersection of all three concepts. The LLM lacks sufficient context to give an accurate, complete answer.

Multi-hop research questions have the same structure — each retrieved chunk opens new questions that require their own retrieval.

Solution

Run retrieval in a loop, using what was retrieved to sharpen the next query:

Initial query: "How does attention scaling affect memory with gradient checkpointing?" ↓ Round 1: Retrieve → [general attention docs, general memory docs] ↓ Sufficiency check: "I know how attention scales memory, but I don't know how gradient checkpointing modifies the memory profile." ↓ Refined query: "gradient checkpointing memory savings during transformer fine-tuning" ↓ Round 2: Retrieve → [specific gradient checkpointing memory analysis] ↓ Sufficiency check: "Now I can answer the original question." ✅ ↓ Generate final answer from accumulated context

Implementation

python
from typing import List, Dict, Tuple

def sufficiency_check(query: str, context_chunks: List[Dict], llm) -> Tuple[bool, str]:
    """
    Ask the LLM whether the current context is sufficient to answer the query.
    Returns (is_sufficient, gap_description).
    """
    context_text = "\n\n".join([c['content'] for c in context_chunks])

    prompt = f"""You are evaluating whether a set of retrieved documents provides 
enough information to fully answer a question.

Original question: {query}

Retrieved context so far:
{context_text}

Assessment:
1. Can this question be fully answered from the context above? (yes/no)
2. If no, what specific information is still missing?

Respond as JSON: {{"sufficient": true/false, "gap": "description of missing info or empty string"}}"""

    result = llm.invoke(prompt, schema={"sufficient": bool, "gap": str})
    return result['sufficient'], result['gap']


def refine_query(original_query: str, gap: str, context_chunks: List[Dict], llm) -> str:
    """
    Generate a more specific follow-up query targeting the identified gap.
    Uses retrieved chunks as context so the new query builds on what we know.
    """
    context_summary = "\n".join([f"- {c['content'][:200]}" for c in context_chunks[:3]])

    prompt = f"""You are refining a search query to fill a specific knowledge gap.

Original question: {original_query}

What we already know (retrieved context summary):
{context_summary}

What is still missing: {gap}

Write a concise, targeted search query to retrieve documents that fill this gap.
Return only the query, nothing else."""

    return llm.invoke(prompt).strip()


def iterative_rag(
    query: str,
    retriever,
    llm,
    top_k: int = 5,
    max_iterations: int = 4,
    sufficiency_threshold: bool = True,
) -> Dict:
    """
    Iterative RAG pipeline.
    Runs retrieve → assess → refine → retrieve until context is sufficient
    or max_iterations is reached.
    """
    accumulated_context: List[Dict] = []
    seen_doc_ids = set()
    query_history = [query]
    current_query = query

    for iteration in range(max_iterations):
        # Step 1: Retrieve with the current query
        results = retriever.search(current_query, top_k=top_k)

        # Deduplicate — don't re-add chunks we already have
        new_chunks = [r for r in results if r['id'] not in seen_doc_ids]
        for chunk in new_chunks:
            seen_doc_ids.add(chunk['id'])
        accumulated_context.extend(new_chunks)

        # Step 2: Check if accumulated context is now sufficient
        is_sufficient, gap = sufficiency_check(query, accumulated_context, llm)

        if is_sufficient:
            break

        # Step 3: If not sufficient and iterations remain, refine the query
        if iteration < max_iterations - 1:
            current_query = refine_query(query, gap, accumulated_context, llm)
            query_history.append(current_query)

    # Step 4: Generate final answer from accumulated context
    context_text = "\n\n".join([c['content'] for c in accumulated_context])
    answer = llm.invoke(
        f"Context:\n{context_text}\n\nQuestion: {query}\n\nAnswer:"
    )

    return {
        'answer': answer,
        'iterations': len(query_history),
        'query_history': query_history,
        'context_chunks_used': len(accumulated_context),
        'sufficient': is_sufficient,
    }

Complete Notebook

Run the full implementation:

Iterative RAG AWS Notebook

Includes:

  • Sufficiency check prompts and JSON schema grading
  • Query refinement strategy comparison
  • Stopping criteria tuning (threshold vs. max iterations)
  • Multi-hop benchmark evaluation vs. standard RAG

Performance

MetricStandard RAGIterative RAG
Complex query accuracy48%79%
Multi-hop accuracy41%74%
Avg. iterations12.3
Latency (complex)~300ms~900ms
Cost/query$0.05$0.18

Iterative RAG is most valuable on complex queries — for simple factual lookups it adds latency with no benefit, making it a good candidate for adaptive routing.

When to Use

Best For

  • Research questions that require synthesizing information from multiple sources
  • Technical documentation with deep dependencies between concepts (e.g., "how does X interact with Y under condition Z")
  • Multi-hop questions where the answer to step 1 determines what to ask in step 2
  • Agentic pipelines where thorough context gathering is more important than low latency

Combine With

  • Memory Augmented RAG (#18) — persist accumulated context across sessions so long research tasks can resume
  • Corrective RAG (#13) — validate each round's retrieved chunks for relevance before accumulating them
  • Adaptive RAG (#8) — route simple queries directly to standard RAG; only invoke iterative for complex ones

Related Papers

Next Steps

  1. Try it: Run the notebook
  2. Route it: Pair with Adaptive RAG (#8) to avoid unnecessary iterations on simple queries
  3. Validate each round: Layer in Corrective RAG (#13) to grade chunks before accumulating them

Part of: 37 RAG Patterns Collection

Go Deeper With Live Instruction

Covered in depth in our llm engineering program.