FromZero2AI
CoursesPlaygroundBlog
Support on Ko-fi
Advanced RAG Patterns • Module B: Query EngineeringLesson 5: Multi-Query & Step-Back Retrieval
PreviousNext

Lesson 5: Multi-Query & Step-Back Retrieval

Generate multiple query variations from a single user question and merge the retrieved sets. Use step-back questions to retrieve broader context for narrow queries.

Built with AI for beginners. Free forever.

Support on Ko-fi•About•Blog•Privacy Policy•Terms of Service

A single query phrasing retrieves a biased sample of the available evidence. Multi-query retrieval generates several linguistically distinct versions of the user's question and merges the retrieved sets — covering vocabulary variations that a single query would miss.

The Sampling Bias Problem

Every query embedding is a point in vector space. Documents nearest to that point get retrieved. But documents that use different vocabulary to discuss the same topic may sit just outside the retrieval radius — not because they are irrelevant, but because their word choice differs.

Multi-query retrieval fires multiple queries from slightly different angles, expanding the effective retrieval radius without changing the index or the similarity threshold.

Strategy Comparison

Select a retrieval strategy to see its implementation and trade-offs:

Higher recall on the same topic — covers vocabulary variation across the corpus.

import anthropic

client = anthropic.Anthropic()

def generate_variants(question: str, n: int = 4) -> list[str]:
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=300,
        messages=[{"role": "user", "content": f"""
Generate {n} different versions of this question for vector DB retrieval.
Separated by newlines. Do not number them.
Question: {question}"""}],
    )
    lines = response.content[0].text.strip().split("\n")
    return [l.strip() for l in lines if l.strip()][:n]

# Then: retrieve for each variant + deduplicate with RRF
all_results = []
for q in [question] + generate_variants(question):
    all_results.extend(retrieve(q, collection, k=8))
final = reciprocal_rank_fusion(all_results)[:5]

Full Multi-Query Implementation

import anthropic
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI

# ── Option A: Manual implementation ──────────────────────────
client = anthropic.Anthropic()

QUERY_VARIANTS_PROMPT = """You are an AI language model assistant. Your task is to generate {n} different versions
of the given user question to retrieve relevant documents from a vector database.

Generate the alternative questions separated by newlines. Do not number them.
Do not include the original question.

Original question: {question}"""

def generate_variants(question: str, n: int = 4) -> list[str]:
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=400,
        messages=[{
            "role": "user",
            "content": QUERY_VARIANTS_PROMPT.format(n=n, question=question),
        }],
    )
    lines = response.content[0].text.strip().split("\n")
    return [l.strip() for l in lines if l.strip()][:n]

# Usage
question = "How does gradient clipping prevent exploding gradients?"
variants = generate_variants(question)
for v in variants:
    print(f"  - {v}")
# → - What techniques prevent gradient explosion during training?
# → - How does clipping gradient norms stabilize neural network training?
# → - Why does gradient clipping improve training stability?
# → - What happens to model weights when gradients are too large?

Merging Results Without Duplication

def multi_query_retrieve(
    question: str,
    collection,
    n_variants: int = 4,
    n_per_query: int = 8,
    final_k: int = 5,
) -> list[dict]:
    """
    Generate query variants, retrieve for each, deduplicate, and fuse rankings.
    """
    all_queries = [question] + generate_variants(question, n=n_variants)
    all_rankings: list[list[str]] = []
    doc_store: dict[str, dict] = {}

    for query in all_queries:
        results = semantic_search(query, collection, n_results=n_per_query)
        ranked_ids = []
        for r in results:
            doc_id = r["id"]
            ranked_ids.append(doc_id)
            if doc_id not in doc_store:
                doc_store[doc_id] = r
        all_rankings.append(ranked_ids)

    # Fuse all rankings with RRF
    fused = reciprocal_rank_fusion(all_rankings, k=60)

    # Return top-final_k, hydrated
    return [doc_store[doc_id] for doc_id, _ in fused[:final_k] if doc_id in doc_store]


# ── Option B: Use LangChain's built-in MultiQueryRetriever ──
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
from langchain_community.vectorstores import Chroma

vectordb = Chroma(...)  # Your Chroma collection
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

retriever = MultiQueryRetriever.from_llm(
    retriever=vectordb.as_retriever(search_kwargs={"k": 8}),
    llm=llm,
)

# Automatically generates 3 variants internally
docs = retriever.get_relevant_documents("How does gradient clipping work?")

Step-Back Prompting

Step-back prompting is a complementary technique: instead of generating variants of the same question, it generates a broader, more abstract version. Then both the specific and the general retrieval results are fed to the LLM.

STEPBACK_PROMPT = """Given a specific question, generate a more general question that, when answered,
would provide helpful background for answering the specific question.

Specific: What is the impact of batch size on training speed for Mistral 7B?
General: How does batch size affect neural network training dynamics?

Specific: {question}
General:"""

def stepback_retrieve(question: str, collection, n_results: int = 5) -> list[dict]:
    # Get the broader question
    response = client.messages.create(
        model="claude-haiku-4-5-20251001",
        max_tokens=100,
        messages=[{"role": "user", "content": STEPBACK_PROMPT.format(question=question)}],
    )
    general_question = response.content[0].text.strip()

    # Retrieve for both
    specific_docs = semantic_search(question, collection, n_results=3)
    general_docs = semantic_search(general_question, collection, n_results=3)

    # Merge, deduplicate
    seen = set()
    merged = []
    for doc in specific_docs + general_docs:
        if doc["id"] not in seen:
            seen.add(doc["id"])
            merged.append(doc)

    return merged[:n_results]

When to Use Multi-Query vs. Step-Back

TechniqueUse When
Multi-QueryYou want higher recall on the same topic. Vocabulary varies in your corpus. Users phrase questions unpredictably.
Step-BackSpecific questions need broader background. The corpus has conceptual overviews separate from specific details.
BothTechnical Q&A over large documentation corpora — add a small latency budget for much better answers.

In Lesson 6, we look at contextual compression — a technique that compresses each retrieved chunk down to only the sentences that actually answer the query, reducing LLM context costs without losing information.