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.
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.
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]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?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 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]| Technique | Use When |
|---|---|
| Multi-Query | You want higher recall on the same topic. Vocabulary varies in your corpus. Users phrase questions unpredictably. |
| Step-Back | Specific questions need broader background. The corpus has conceptual overviews separate from specific details. |
| Both | Technical 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.