HyDE, query decomposition, Self-RAG, CRAG, iterative retrieval, query routing, and tool-using RAG with code and web.
Naive RAG is a single-pass pipeline: embed query → retrieve top-k → stuff into context → generate. This fails for compound questions (multiple sub-questions needing separate retrievals), ambiguous queries (query expansion helps), and unreliable corpora (retrieved docs may be irrelevant or outdated).
Agentic patterns add latency and cost. Every additional LLM call adds 200–600 ms and ~0.5–2 k tokens. Design the loop so the “happy path” (good retrieval first try) is fast. Reserve multi-step patterns for queries that actually need them — query classification at the entry point lets you route simple queries to single-pass RAG and complex ones to the agentic loop.
HyDE (Gao et al. 2022) addresses the query–document embedding gap: a short question and a long passage have very different embedding distributions even when semantically aligned. The fix: use an LLM to generate a hypothetical answer document, then embed that for retrieval.
from openai import OpenAI
from sentence_transformers import SentenceTransformer
llm = OpenAI()
embedder = SentenceTransformer("BAAI/bge-large-en-v1.5")
def hyde_retrieve(query: str, k: int = 10) -> list:
# 1. Generate hypothetical answer
hyp = llm.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user",
"content": f"Write a 100-word technical answer to: {query}"}],
max_tokens=150,
).choices[0].message.content
# 2. Embed the hypothetical answer (not the query!)
vec = embedder.encode(hyp, normalize_embeddings=True)
# 3. Retrieve against corpus
return vector_db.search(vec, limit=k)
On BEIR benchmarks, HyDE improves NDCG@10 by 5–15% over direct embedding of the query, especially for short keyword queries. It fails when the LLM hallucinates a factually incorrect hypothetical — the embedding then points to plausible-sounding but wrong documents. Mitigate by running HyDE in parallel with direct-query retrieval and using RRF to fuse both lists.
Compound questions contain multiple independent sub-questions that each require separate retrieval. Embedding the full compound question produces an averaged vector that may not retrieve well for any single sub-question.
Generate sub-queries one at a time; each may depend on the answer to the previous (step-back decomposition). Useful for multi-hop questions: "Who founded the company that acquired Slack?" → Q1: "Who founded Salesforce?" → Q2: "Did Salesforce acquire Slack?"
Generate all sub-queries at once; retrieve independently; merge results before generation. Faster but sub-queries can't depend on each other. Good for multi-facet questions: "Compare X and Y on dimensions A, B, C." → retrieve A(X), A(Y), B(X), B(Y), C(X), C(Y) in parallel.
from langchain.retrievers.multi_query import MultiQueryRetriever
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
retriever = MultiQueryRetriever.from_llm(
retriever=vector_store.as_retriever(search_kwargs={"k": 5}),
llm=llm,
# Generates 3 alternative phrasings + the original
)
# Retrieves for all generated queries, deduplicates, returns union
docs = retriever.invoke("What are the limitations of RLHF for alignment?")
Step-back prompting (Zheng et al. 2023) is a related technique: before decomposing, ask the model for the more general “step-back” question that encompasses the specific query. Embed both the specific and the step-back question; retrieve for both; fuse. This improves recall for highly specific queries by also retrieving background context. Effective for technical documentation RAG where specific questions need conceptual background.
Self-RAG (Asai et al. 2023) fine-tunes a model to emit special reflection tokens that control retrieval dynamically. Unlike pipeline-controlled retrieval, the model itself decides whether retrieval is needed, whether retrieved docs are relevant, and whether its own output is grounded.
| Token type | Token | Meaning |
|---|---|---|
| Retrieve | [Retrieve] / [No Retrieve] | Should retrieval be triggered for this generation step? |
| IsREL | [Relevant] / [Irrelevant] | Is the retrieved passage relevant to the query? |
| IsSUP | [Fully supported] / [Partially supported] / [No support] | Is the generated claim supported by the retrieved passage? |
| IsUSE | [Utility:1–5] | How useful is the overall response? Used for beam-search scoring. |
Self-RAG requires a fine-tuned model. The original paper fine-tuned Llama-2-7B and 13B; Llama-3 variants exist on HuggingFace. If you're using a proprietary API (Claude, GPT-4), you cannot use Self-RAG directly — you must approximate it with a separate LLM classifier or use pipeline-controlled retrieval (CRAG). Self-RAG is most valuable when retrieval is expensive (large corpus, high reranker cost) and most queries don't need it. On knowledge-heavy tasks, Self-RAG with Llama-2-13B matches or beats GPT-3.5 + vanilla RAG.
CRAG (Yan et al. 2024) adds a retrieval-quality classifier between the retrieval stage and the generation stage. If retrieved documents are classified as low-quality or irrelevant, the system falls back to a web-search augmented retrieval pass.
LangGraph (LangChain's agent orchestration framework) has a canonical CRAG tutorial. The graph has nodes: retrieve, grade_documents, transform_query, web_search, generate. Conditional edges route based on the grader's output. The relevance grader is typically a zero-shot GPT-4o-mini call with a binary yes/no schema (structured output). The web search node uses Tavily's TavilySearchResults tool. Full CRAG adds ~400–800 ms to the happy path but prevents the model from confabulating when the corpus is stale.
Some questions cannot be answered from a single retrieval pass because the answer requires synthesising information across multiple documents, or because the initial query is underspecified and needs refinement based on what was found.
Retrieve an initial set of documents. Read them to identify what information is still missing. Formulate a second retrieval query targeting the gaps. Repeat until the model declares the context sufficient or a step limit is reached. Effective for multi-hop reasoning tasks (HotpotQA, MuSiQue). Maximum 2–3 iterations in production to contain latency.
Interleave chain-of-thought generation with retrieval: generate one reasoning step, retrieve a passage relevant to that step, continue generating the next step conditioned on the retrieved passage. Achieves state-of-the-art on 2WikiMultiHopQA. Cost: O(reasoning_steps) LLM calls + O(reasoning_steps) retrieval passes. Suitable only when answer quality outweighs latency.
Always enforce a maximum iteration count (typically 3) and a redundancy check: if the current retrieval query is semantically close to a previous one (cosine similarity > 0.95 of their embeddings), skip retrieval and proceed to generation. LangGraph add_conditional_edges with a counter check is the standard implementation pattern. Without these guards, an agentic loop on a poorly-populated corpus can call the LLM and retriever indefinitely.
A production RAG system may have multiple indexes: a dense vector index for prose, a BM25 index for code, a structured SQL database for factual lookups, and a web search tool for current events. Query routing selects the best retrieval path without trying all of them on every query.
from llama_index.core.selectors import LLMSingleSelector
from llama_index.core.tools import ToolMetadata
selector = LLMSingleSelector.from_defaults()
choices = [
ToolMetadata(description="technical documentation", name="docs"),
ToolMetadata(description="source code repository", name="code"),
ToolMetadata(description="current news and web", name="web"),
]
result = selector.select(choices, query="How do I fix NullPointerException in Java 21 records?")
# Returns: SelectionOutput(index=1, reason="code-related query")
The most capable production RAG systems combine vector retrieval with other tools: code execution for numerical reasoning, web search for current events, SQL for structured data, and external APIs for live lookups. The model orchestrates which tools to call and in what order.
| Tool | Purpose in RAG | Library/API | Output type |
|---|---|---|---|
| Vector retrieval | Semantic document lookup | Qdrant, pgvector, Weaviate | Ranked passage list |
| BM25 / keyword | Exact-match fallback | Elasticsearch, BM25s Python lib | Ranked passage list |
| Web search | Current events, out-of-corpus queries | Tavily, Bing Search API, SerpAPI | Snippets + URLs |
| Code interpreter | Numerical calculations, data transformation | Claude Code, OpenAI Code Interpreter | Execution result + stdout |
| SQL / text-to-SQL | Structured factual lookups over tables | LangChain SQLDatabaseChain, vanna.ai | Tabular data |
| Citation lookup | Resolve DOI/arXiv IDs to abstract text | Semantic Scholar API, CrossRef API | Paper metadata + abstract |
ReAct (Yao et al. 2022) interleaves reasoning (Thought) with action (Act) traces. For RAG, the model emits: Thought: I need to find the release date of GPT-4. Action: retrieval("GPT-4 release date"). Observation: GPT-4 was released March 2023. Thought: Now I can answer. This gives an auditable trace of retrieval decisions. LangChain's create_react_agent and LlamaIndex's ReActAgent both implement this. Claude and GPT-4-class models perform ReAct natively without fine-tuning.
Deck 06 covers GraphRAG and knowledge graphs: Microsoft GraphRAG communities, KG construction, property graphs (Neo4j, Memgraph, KuzuDB), hybrid graph + vector retrieval, and Cypher in agent loops.