LeetLLM
My PlanLearnGlossaryTracksPracticeBlog
LeetLLM

Your go-to resource for mastering AI & LLM systems.

Product

  • Learn
  • Glossary
  • Tracks
  • Practice
  • Blog
  • RSS

Legal

  • Terms of Service
  • Privacy Policy

© 2026 LeetLLM. All rights reserved.

All Topics
Your Progress
0%

0 of 196 articles completed

🛠️Computing Foundations0/9
Git, Shell, Linux for AIDocker for Reproducible AIPython for AI EngineeringNumPy and Tensor ShapesCUDA for ML TrainingMPS & Metal for ML on MacData Structures for AISQL and Data ModelingAlgorithms for ML Engineers
📊Math & Statistics0/9
Gradients and BackpropVectors, Matrices & TensorsLinear Algebra for MLAdam, Momentum, SchedulersProbability for Machine LearningBayesian Inference from EvidenceStatistics and UncertaintyDistributions and SamplingHypothesis Tests, Intervals, and pass@k
📚Preparation & Prerequisites0/13
Neural Networks from ScratchCNNs from ScratchTraining & BackpropagationSoftmax, Cross-Entropy & OptimizationRNNs, LSTMs, GRUs, and Sequence ModelingAutoencoders and VAEsThe Transformer Architecture End-to-EndLanguage Modeling & Next TokensFrom GPT to Modern LLMsPrompt Engineering FundamentalsCalling LLM APIs in ProductionFirst AI App End-to-EndThe LLM Lifecycle
🧮ML Algorithms & Evaluation0/15
Linear Regression from ScratchLogistic Regression and MetricsDecision Trees, Forests, and BoostingReinforcement Learning BasicsValidation and LeakageStatistical Learning and GeneralizationConformal Prediction and UncertaintyClustering and PCAProbabilistic Generative ModelsCore Retrieval AlgorithmsDecoding AlgorithmsExperiment Design and A/B TestingCausal Inference and CounterfactualsPyTorch Training LoopsDataset Pipelines and Data Quality
📦Production ML Systems0/6
Feature Engineering for Production MLBatch and Streaming Feature PipelinesGradient Boosted Trees in ProductionRanking and Recommendation SystemsForecasting and Anomaly DetectionMonitoring Predictive Models
🧪Core LLM Foundations0/8
The Bitter Lesson & ComputeBPE, WordPiece, and SentencePieceStatic to Contextual EmbeddingsPerplexity & Model EvaluationFile Ingestion for AIChunking StrategiesLLM Benchmarks & LimitationsInstruction Tuning & Chat Templates
🧰Applied LLM Engineering0/25
Dimensionality Reduction for EmbeddingsCoT, ToT & Self-Consistency PromptingFunction Calling & Tool UseMCP & Tool Protocol StandardsContext EngineeringPrompt Injection DefenseResponsible AI GovernancePrivacy-Preserving Machine LearningData Labeling and Human FeedbackEvaluating AI AgentsProduction RAG PipelinesHybrid Search: Dense + SparseReranking and Cross-Encoders for RAGRAG Evaluation for Reliable AnswersLLM-as-a-Judge EvaluationBias & Fairness in LLMsHallucination Detection & MitigationLLM Observability & MonitoringExperiment Tracking with MLflow and W&BPrompt Optimization with DSPyModel Versioning & DeploymentSemantic Caching & Cost OptimizationLLM Cost Engineering & Token EconomicsModel Gateways, Routing, and FallbacksDesign an Automated Support Agent
🎓Portfolio Capstones0/9
Capstone: Delivery ETA PredictionCapstone: Product RankingCapstone: Demand ForecastingCapstone: Image Damage ClassifierCapstone: Production ML PipelineCapstone: Document QACapstone: Eval DashboardCapstone: Fine-Tuned ClassifierCapstone: Reproducible ML Study
🧠Transformer Deep Dives0/8
Sentence Embeddings & Contrastive LossEmbedding Similarity & QuantizationScaled Dot-Product AttentionVision Transformers and Image EncodersPositional Encoding: RoPE & ALiBiLayer Normalization: Pre-LN vs Post-LNMechanistic InterpretabilityDecoding Strategies: Greedy to Nucleus
🧬Advanced Training & Adaptation0/17
Scaling Laws & Compute-Optimal TrainingPre-training Data at ScaleBuild GPT from Scratch LabJAX for PyTorch ResearchersContinued Pretraining for Domain ShiftSynthetic Data PipelinesSupervised Fine-Tuning PipelineMixed Precision TrainingDistributed Training: FSDP & ZeROLoRA & Parameter-Efficient TuningTraining Run OperationsReward Modeling from Preference DataRLHF & DPO AlignmentConstitutional AI & Red TeamingRLVR & Verifiable RewardsKnowledge Distillation for LLMsModel Merging and Weight Interpolation
🤖Advanced Agents & Retrieval0/16
Vector DB Internals: HNSW & IVFAdvanced RAG: HyDE & Self-RAGGraphRAG & Knowledge GraphsRAG Security & Access ControlStructured Output GenerationReAct & Plan-and-ExecuteGuardrails & Safety FiltersCode Generation & SandboxingComputer-Use / GUI / Browser AgentsHuman-in-the-Loop Agent ArchitectureAI Coding Workflow with AgentsAgent Memory & PersistenceAgent Failure & RecoveryRecursive Language Models (RLM)Multi-Agent OrchestrationCapstone: Production Agent
⚡Inference & Production Scale0/31
Inference: TTFT, TPS & KV CacheMulti-Query & Grouped-Query AttentionKV Cache & PagedAttentionPrefix Caching and Prompt CachingFlashAttention & Memory EfficiencyContinuous Batching & SchedulingScaling LLM InferenceModel Parallelism for LLM InferenceAccelerator Architecture Field GuideCorrect GPU Profiling and BenchmarkingGPU Kernel Performance Engineering LabGPU Parallel Primitives LabGPU Compilers and Kernel LanguagesGEMM From Naive CUDA to Tensor CoresLow-Precision GPU ProgrammingGPU Runtime Execution LabAI-Generated Kernel EngineeringModel Quantization: GPTQ, AWQ & GGUFLocal LLM DeploymentSLM Specialization & Edge DeploymentSpeculative DecodingLong Context Window ManagementMixture of Experts ArchitectureMamba & State Space ModelsReasoning & Test-Time ComputeAdvanced MLOps & DevOps for AIGPU Serving & AutoscalingGPU Failure OperationsDistributed Inference Data PlaneServing Benchmark EngineeringA/B Testing for LLMs
🏗️System Design Capstones0/9
Content Moderation SystemCode Completion SystemMulti-Tenant LLM PlatformLLM-Powered Search EngineVision-Language Models & CLIPMultimodal LLM ArchitectureDiffusion Models: Images & TextReal-Time Voice AI AgentReasoning Agent System Design
🎤AI Lab Interviewing0/4
AI Lab Coding Interview: Python SystemsAI Lab System Design InterviewAI Lab Behavioral InterviewAI Lab Technical Presentation
🔬Project Deep Dives0/17
Deep Dive - vLLMDeep Dive - SkyRLDeep Dive - FlashAttentionDeep Dive - FlashInferDeep Dive - DeepGEMMDeep Dive - NCCLDeep Dive - MegatronDeep Dive - DeepSpeedDeep Dive - RayDeep Dive - MLflowDeep Dive - PyTorchDeep Dive - TransformersDeep Dive - SGLangDeep Dive - slimeDeep Dive - DeepEPDeep Dive - TinkerDeep Dive - Light-PEFT
Back to Topics
LearnAdvanced Agents & RetrievalAdvanced RAG: HyDE & Self-RAG
🔍HardRAG & Retrieval

Advanced RAG: HyDE & Self-RAG

Repair one messy docs-assistant search with rewrite, HyDE, Self-RAG, or CRAG, then release only the cheapest route that beats labeled evidence hits.

42 min read
Learning path
Step 121 of 196 in the full curriculum
Vector DB Internals: HNSW & IVFGraphRAG & Knowledge Graphs

Personalize this lesson

Adapt explanations and teaching visuals to your background and preferred voice.

A developer opens an internal API-docs assistant and types, "What about the other one?" They could mean the backup API key from the last turn, the second failed batch job, or the alternative embedding endpoint. Searching for "quota" can pull billing limits instead of the rate-limit policy. The index is healthy; the search object is not. Before changing ANN settings, ask what is missing: a referent, corpus vocabulary, or a dependency between facts.

The previous chapter showed how HNSW and IVF find nearby chunks quickly. Retrieval-augmented generation (RAG) grounds an answer in those chunks, while self-reflective RAG (Self-RAG) decides when retrieval and evidence checks are needed. That speed doesn't help when the search object is "the other one." A fast index returns the wrong neighborhood faster. Stay with this request and this docs index. Measure whether rewriting, Hypothetical Document Embeddings (HyDE), Self-RAG, or Corrective RAG (CRAG) recovers the right page, then keep the cheapest control that does.

The problem with naive RAG

A naive RAG pipeline embeds the user's raw question and fetches the top-k nearest neighbors from a vector database. Retrieve once, then generate. Before adding a more elaborate route, predict which of these failures your misses show:

  1. The query is ambiguous. "What about the other one?" depends on chat history. A stateless retriever never sees that history.
  2. The query and the document don't share a style. Someone asks "How do I rotate a key?" The page title is "Credential Rotation Workflow". The embedding of a short question may not sit near the embedding of a formal policy paragraph.
  3. The retriever still returns the wrong chunks. Wording, embedding geometry, approximate nearest-neighbor (ANN) knobs such as ef_search and nprobe, or a messy corpus can all put SDK install notes above the rate-limit policy. Once those chunks enter context, the generator can write an unsupported answer.
  4. The model doesn't know when to retrieve. Naive RAG retrieves for every turn, including "Hi" or "What is 2+2?", and burns tokens and latency for nothing.

Index tuning from the last chapter and query control in this one are different repairs. Measure which failure you actually have, then add the smallest intervention that fixes it.

That diagnosis gives each technique a job. Rewrite missing conversational state, expansion missing vocabulary, decomposition missing dependent facts, and later controls judge whether returned evidence deserves trust.

Query rewriting and decomposition

Three failures can look identical in a chat transcript. Before looking at the map, predict which search object each repair creates: a standalone question, several vocabulary variants, or a sequence of dependent hops. The distinction decides where extra calls buy evidence.

Three query transforms for an API-docs assistant. Rewrite turns What do I do now plus a leaked-key history into a standalone revoke-rotate-audit search. Expansion turns peak-traffic throttling into rate-limit, backpressure, and autoscaling variants. Decomposition splits a sync-versus-batch comparison into latency, limit, retry, and synthesis hops.
The same assistant sees three different search failures. Rewrite fills in history, expansion covers vocabulary mismatch, and decomposition turns a comparison into hops the index can actually hit.

"What do I do now?" is a bad search object. The leaked-key fact lives in the previous turn, and a comparison like "sync vs batch embeddings" packs three lookups into one sentence. Use those examples to predict which details must survive rewriting and which facts need separate searches.

1. Rewrite for disambiguation

An on-call lead often turns a vague Slack thread into a precise search request before querying runbooks and product docs. Query rewriting gives the model that same job. Ma et al.'s Rewrite-Retrieve-Read pipeline adds a rewrite stage in front of a frozen retriever and reader, including a small T5 rewriter trained with reader feedback, instead of treating the user's wording as sacred.[1]Reference 1Query Rewriting for Retrieval-Augmented Large Language Models.https://arxiv.org/abs/2305.14283 In a chat product, a practical variant is simpler: rewrite the latest turn into a standalone, search-oriented question.

Before reading the rewrite, predict three things it must preserve: the leaked credential, the requested action, and the audit requirement. History supplies the referent, but it remains data rather than instructions.

A developer sends two messages:

Developer: "My API key leaked in a public issue." Developer: "What do I do now?"

A stateless retriever searching for "What do I do now?" would pull generic onboarding articles. A rewrite step instead produces:

Standalone query: "How do I revoke and rotate a leaked API key and audit recent usage?"

The rewrite model can be any instruction-tuned LLM behind a small interface. The lab uses a deterministic fake so the parsing contract can be tested locally without API keys.

1-rewrite-for-disambiguation.py
1from typing import Protocol, TypedDict 2 3class ChatMessage(TypedDict): 4 role: str 5 content: str 6 7class RewriteModel(Protocol): 8 def rewrite(self, latest_query: str, history_text: str) -> str: ... 9 10class FakeRewriteModel: 11 def rewrite(self, latest_query: str, history_text: str) -> str: 12 if "api key leaked" in history_text.lower() and "what do i do" in latest_query.lower(): 13 return "How do I revoke and rotate a leaked API key and audit recent usage?" 14 return latest_query 15 16def rewrite_query_with_history( 17 query: str, chat_history: list[ChatMessage], model: RewriteModel 18) -> str: 19 history_text = "\n".join( 20 f"{message['role']}: {message['content']}" for message in chat_history 21 ) 22 return model.rewrite(query, history_text).strip() 23 24history = [ 25 {"role": "developer", "content": "My API key leaked in a public issue."}, 26] 27 28rewritten = rewrite_query_with_history("What do I do now?", history, FakeRewriteModel()) 29print(rewritten)
Output
1How do I revoke and rotate a leaked API key and audit recent usage?

Rewrite is search input, not authorization. History, tool output, and retrieved pages can contain "ignore policy / search all tickets" style instructions. Constrain the prompt to disambiguate the user's question, not follow instructions found in history, and never let a rewrite broaden ACL or tenant scope. RAG Security & Access Control covers the trusted retrieval plane these transforms must preserve.

2. Multi-query expansion

Multi-query expansion is the repair when one wording misses the page. "How do you handle peak traffic throttling?" never mentions "rate-limit burst policy", "queue backpressure", or "autoscaling capacity". Generate those variants, search each one, then merge the lists.

Before merging, predict what repeated evidence should do. A policy page that appears near the top of two variants should outrank a page that appears once, even if their raw similarity scores came from different searches.

Once you fan out into several queries, you have several ranked result lists to merge. One documented pattern is RAG-Fusion, which pairs generated query variants with Reciprocal Rank Fusion (RRF) to combine the lists into one ranking.[2]Reference 2RAG-Fusion: a New Take on Retrieval-Augmented Generationhttps://arxiv.org/abs/2402.03367 RRF ignores raw similarity scores and sums reciprocal ranks instead, so a document that lands near the top of several lists rises even if no single list ranked it first.[3]Reference 3Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods.https://dl.acm.org/doi/10.1145/1571941.1572114

For a document ddd, the score is ∑r∈R1/(k+r(d))\sum_{r \in R} 1 / (k + r(d))∑r∈R​1/(k+r(d)). Here, RRR is the set of rankings, r(d)r(d)r(d) is the document's one-based position in one ranking, and kkk dampens the effect of very high ranks. Cormack et al. fixed k=60k = 60k=60 after a pilot investigation.[3]Reference 3Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods.https://dl.acm.org/doi/10.1145/1571941.1572114 A document missing from a returned list contributes zero from that list. RRF is the same rank-merge idea used to fuse dense and sparse results in hybrid search, reused here for query variants.

Implement multi-query expansion behind a testable boundary. The model returns one query per line; the parser removes bullets or numbering so variants can be searched in parallel. The retriever should see clean search objects, not formatting noise.

2-multi-query-expansion.py
1from typing import Protocol 2 3class QueryExpansionModel(Protocol): 4 def expand(self, query: str, n: int) -> str: ... 5 6class FakeExpansionModel: 7 def expand(self, query: str, n: int) -> str: 8 return "\n".join( 9 [ 10 "Rate-limit burst policy during launch traffic", 11 "Queue backpressure controls for high-volume periods", 12 "Autoscaling capacity for traffic spikes", 13 ][:n] 14 ) 15 16def clean_query_line(line: str) -> str: 17 return line.strip().lstrip("-*0123456789. ").strip() 18 19def generate_multi_queries( 20 query: str, model: QueryExpansionModel, n: int = 3 21) -> list[str]: 22 content = model.expand(query, n) 23 queries = [clean_query_line(line) for line in content.splitlines()] 24 return [query for query in queries if query] 25 26queries = generate_multi_queries( 27 "How do you handle peak traffic throttling?", FakeExpansionModel(), n=3 28) 29 30for query in queries: 31 print(f"- {query}")
Output
1- Rate-limit burst policy during launch traffic 2- Queue backpressure controls for high-volume periods 3- Autoscaling capacity for traffic spikes

Generating variants isn't enough. The system must merge result lists without assuming similarity scores from separate searches are directly comparable. RRF provides a deterministic rank-based merge. As you read the next snippet, predict which ID benefits most from appearing near the top across all three lists:

fuse-query-variants-with-rrf.py
1def reciprocal_rank_fusion(rankings: list[list[str]], rank_constant: int = 60) -> list[tuple[str, float]]: 2 scores: dict[str, float] = {} 3 for ranking in rankings: 4 for rank, document_id in enumerate(ranking, start=1): 5 scores[document_id] = scores.get(document_id, 0.0) + 1 / (rank_constant + rank) 6 return sorted(scores.items(), key=lambda pair: (-pair[1], pair[0])) 7 8ranked_lists = [ 9 ["rate-limit-policy", "quota-increase", "autoscaling"], 10 ["autoscaling", "rate-limit-policy", "backpressure"], 11 ["rate-limit-policy", "batch-api", "autoscaling"], 12] 13 14for document_id, score in reciprocal_rank_fusion(ranked_lists)[:3]: 15 print(f"{document_id}: {score:.4f}")
Output
1rate-limit-policy: 0.0489 2autoscaling: 0.0481 3batch-api: 0.0161

3. Query decomposition (least-to-most)

Decomposition is the repair when one search can't collect every fact. Take this comparison:

"Compare synchronous and batch embedding API latency, limits, and retry behavior."

No single policy page is that comparison. Before splitting, ask whether the final comparison can be answered before endpoint facts exist. That dependency blocks it, so split the question into hops the index can hit:

  1. "What latency does the synchronous embedding endpoint target?"
  2. "What throughput and completion limits does the batch embedding endpoint have?"
  3. "What retry behavior applies to each endpoint?"
  4. "Compare synchronous and batch embeddings given the latency, limit, and retry facts."

This pattern is closely related to least-to-most prompting, which breaks hard problems into simpler steps.[4]Reference 4Least-to-Most Prompting Enables Complex Reasoning in Large Language Modelshttps://arxiv.org/abs/2205.10625 In RAG, teams reuse that idea for retrieval coverage rather than chain-of-thought supervision. The first three hops gather explicit evidence; the final hop synthesizes it. That dependency improves coverage, but adds queries and a chance that an early miss poisons later hops. Measure supported-answer accuracy and latency before releasing it.

A real model would produce the sub-questions. The rest of the system should only depend on the line-oriented contract.

3-query-decomposition-least-to-most.py
1from typing import Protocol 2 3class DecompositionModel(Protocol): 4 def decompose(self, query: str) -> str: ... 5 6class FakeDecompositionModel: 7 def decompose(self, query: str) -> str: 8 return "\n".join( 9 [ 10 "What latency does the synchronous embedding endpoint target?", 11 "What throughput and completion limits does the batch embedding endpoint have?", 12 "What retry behavior applies to each endpoint?", 13 "How do synchronous and batch embeddings compare given those facts?", 14 ] 15 ) 16 17def decompose_query(query: str, model: DecompositionModel) -> list[str]: 18 lines = [ 19 line.strip().lstrip("-*0123456789. ").strip() 20 for line in model.decompose(query).splitlines() 21 ] 22 return [line for line in lines if line] 23 24sub_questions = decompose_query( 25 "Compare synchronous and batch embedding API latency, limits, and retry behavior.", 26 FakeDecompositionModel(), 27) 28 29for index, question in enumerate(sub_questions, start=1): 30 print(f"{index}. {question}")
Output
11. What latency does the synchronous embedding endpoint target? 22. What throughput and completion limits does the batch embedding endpoint have? 33. What retry behavior applies to each endpoint? 44. How do synchronous and batch embeddings compare given those facts?

4. Step-back prompting (abstraction)

When a query is overloaded with specific runtime parameters, error codes, or customer-specific configurations, direct dense search often fails if the knowledge corpus documents high-level architecture rules, rate limits, or protocols instead of the exact numerical combination.

Step-back prompting abstracts the specific inquiry into a broader, foundational question.[5]Reference 5Take a Step Back: Evoking Reasoning via Step-Back Prompting in Large Language Models.https://arxiv.org/abs/2310.06117 Zheng et al. prompt the model to "step back" and identify the overarching concept or policy behind the question.

Predict which evidence each query can recover. The specific query should find incident details; the broader query should find governing policy. Neither query alone can reliably provide both.

For instance, consider an operational question:

Specific query: "Why did our client receive an HTTP 429 when sending 120 requests/minute on the tier 2 embedding endpoint?" Step-back query: "What are the rate limits, burst thresholds, and quota policies for tier 2 embedding endpoints?"

In RAG, the retriever queries the index for both the original specific question and the abstracted step-back question. The step-back search retrieves foundational rules and architecture guides, while the specific search retrieves endpoint parameters. The generator conditions on both evidence sets, keeping broad policy from replacing exact diagnosis.

Implement step-back generation behind a clear interface that pairs the original question with its conceptual counterpart. The pair is the boundary: later code can choose retrieval behavior without losing the original wording:

4-step-back-prompting.py
1from typing import Protocol 2 3class StepBackModel(Protocol): 4 def step_back(self, query: str) -> str: ... 5 6class FakeStepBackModel: 7 def step_back(self, query: str) -> str: 8 if "429" in query and "tier 2" in query.lower(): 9 return "What are the rate limits, burst thresholds, and quota policies for tier 2 embedding endpoints?" 10 return query 11 12def generate_step_back_query(query: str, model: StepBackModel) -> str: 13 return model.step_back(query).strip() 14 15def retrieve_step_back_context( 16 query: str, model: StepBackModel 17) -> dict[str, str]: 18 abstract_query = generate_step_back_query(query, model) 19 return { 20 "original_query": query, 21 "step_back_query": abstract_query, 22 } 23 24plan = retrieve_step_back_context( 25 "Why did our client receive an HTTP 429 when sending 120 requests/minute on tier 2?", 26 FakeStepBackModel(), 27) 28 29print(f"original: {plan['original_query']}") 30print(f"step-back: {plan['step_back_query']}")
Output
1original: Why did our client receive an HTTP 429 when sending 120 requests/minute on tier 2? 2step-back: What are the rate limits, burst thresholds, and quota policies for tier 2 embedding endpoints?

When should you rewrite a query instead of searching the raw user message?

Answer

Rewrite when the latest message depends on chat history, uses vague language, or combines multiple intents. The retriever needs a standalone search request even if the generator later sees the original wording.

Diagram showing User turn, Which failure?, Rewrite, and Expand.
User turn, Which failure?, Rewrite, and Expand.

These transforms change the search object before the same hybrid index from earlier chapters runs. They don't grade the chunks that come back. HyDE goes one step further: it changes the kind of vector used for search. That makes proxy quality and evidence boundaries matter.

HyDE (Hypothetical Document Embeddings)

Standard dense retrieval matches a query embedding to document embeddings. Queries are often short and interrogative, while indexed passages are longer and declarative. HyDE changes how the query vector is built when that mismatch hurts retrieval.

HyDE (Hypothetical Document Embeddings) generates one or more hypothetical documents, embeds those document-style proxies, and retrieves real corpus passages near the resulting vector.[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496 Gao et al. prompt the model to "write a document that answers the question." If you need a throughput-limit clause but can't remember the wording, don't search "Can we handle more traffic?" Write a one-paragraph summary of the policy you expect and search for documents that look like that paragraph.

Hold on to the launch-week question. Predict what changes: the source of the vector, not the truth of the answer. A short question may sit near generic API prose; a document-style proxy may sit near the quota and batch policies.

A developer asks:

Query: "Can we increase embedding throughput during a launch week?"

A standard dense retriever might embed the short question and pull generic "embedding API overview" articles that don't mention launch traffic. HyDE instead prompts the model to write a hypothetical policy paragraph:

Hypothetical document: "Embedding throughput increases: teams can request a temporary tokens-per-minute quota increase, use the batch embedding endpoint for offline jobs, shard requests across approved projects, and apply exponential backoff when rate limits are returned..."

That generated paragraph is longer, declarative, and uses vocabulary like "tokens-per-minute quota", "batch embedding endpoint", and "exponential backoff". It's an illustrative search proxy, not an answer. The generated limit may be wrong, so only retrieved source text may support the final response.

Cache HyDE proxies and rewrite strings with tenant- and principal-scoped keys (plus policy version), not raw query text alone. A shared string key lets Tenant A's proxy or rewrite serve Tenant B's retrieval path.

How HyDE works

The original HyDE pipeline has three phases:

  1. Generate: Prompt an instruction-tuned language model to sample one or more hypothetical passages for the query. The passages may contain fabricated details, but they can still capture relevance patterns that look like real documents.[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496
  2. Embed: Encode each sampled passage with a document encoder such as Contriever. Average the vectors to estimate the expected hypothetical-document embedding:[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496

v^q=1N∑k=1Nf(d^k)\hat{\mathbf{v}}_q = \frac{1}{N}\sum_{k=1}^{N} f(\hat{d}_k)v^q​=N1​∑k=1N​f(d^k​)

Here fff is the document encoder, d^k\hat{d}_kd^k​ is the kkk-th sampled proxy, and NNN is the number of samples. Gao et al. also try averaging in the original query vector as one extra hypothesis. 3. Retrieve: Search the corpus with that averaged vector. The paper's key intuition is that the encoder's dense bottleneck filters much of the fabricated detail while preserving the semantic neighborhood of relevant documents.[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496

The production version uses a real generator and dense encoder. This small runnable version deliberately generates one proxy and uses keyword sets so the retrieval contract is visible: the query first becomes a document-like paragraph, then the retriever searches with that paragraph rather than the original question. As the snippet runs, predict why the two policy chunks should outrank the generic overview.

how-hyde-works.py
1from dataclasses import dataclass 2from typing import Protocol 3 4class HypotheticalDocGenerator(Protocol): 5 def generate(self, query: str) -> str: ... 6 7@dataclass(frozen=True) 8class Chunk: 9 id: str 10 text: str 11 12class FakeHyDEGenerator: 13 def generate(self, query: str) -> str: 14 return ( 15 "Embedding throughput increases require a temporary tokens-per-minute " 16 "quota request, batch embedding jobs, and exponential backoff for rate limits." 17 ) 18 19class KeywordRetriever: 20 def __init__(self, chunks: list[Chunk]) -> None: 21 self.chunks = chunks 22 23 def search(self, search_text: str, k: int = 2) -> list[Chunk]: 24 query_terms = set( 25 search_text.lower().replace(",", " ").replace(".", " ").split() 26 ) 27 28 def score(chunk: Chunk) -> int: 29 chunk_terms = set( 30 chunk.text.lower().replace(",", " ").replace(".", " ").split() 31 ) 32 return len(query_terms & chunk_terms) 33 34 return sorted(self.chunks, key=score, reverse=True)[:k] 35 36def hyde_retrieve( 37 query: str, generator: HypotheticalDocGenerator, retriever: KeywordRetriever 38) -> list[Chunk]: 39 hypothetical_doc = generator.generate(query) 40 return retriever.search(hypothetical_doc, k=2) 41 42chunks = [ 43 Chunk("generic-embeddings", "The embedding API converts text into vectors for search."), 44 Chunk( 45 "throughput-quota", 46 "Launch traffic needs a temporary tokens-per-minute quota request and approval.", 47 ), 48 Chunk("batch-embeddings", "Batch embedding jobs support offline workloads with retry backoff."), 49] 50 51matches = hyde_retrieve( 52 "Can we increase embedding throughput during a launch week?", 53 FakeHyDEGenerator(), 54 KeywordRetriever(chunks), 55) 56 57match_ids = [chunk.id for chunk in matches] 58print(f"retrieved: {match_ids}")
Output
1retrieved: ['throughput-quota', 'batch-embeddings']

The keyword sketch shows the contract. In the paper, the actual query vector is an average of document embeddings. With two 3-d proxies below, calculate each coordinate before running it: add the two values, then divide by two:

average-hyde-proxy-vectors.py
1def mean_vector(vectors: list[list[float]]) -> list[float]: 2 count = len(vectors) 3 dims = len(vectors[0]) 4 return [sum(vector[i] for vector in vectors) / count for i in range(dims)] 5 6proxy_a = [0.2, 0.8, 0.1] 7proxy_b = [0.4, 0.6, 0.3] 8query_vector = mean_vector([proxy_a, proxy_b]) 9print([round(value, 2) for value in query_vector])
Output
1[0.3, 0.7, 0.2]

HyDE changes the search object first, then retrieves. In the sketch below, the short question sits away from the policy cluster, while the proxy sits next to the quota and batch chunks. Ask what still remains unproven after the proxy moves closer.

Two-dimensional embedding sketch for launch-week throughput. The short question sits at low document-style, the generic API overview sits far from the policy cluster, and the HyDE proxy sits next to the quota and batch chunks that retrieval should return.
HyDE searches the document neighborhood, not the short question. The proxy can invent a limit; only the retrieved quota and batch chunks may ground the answer.

On TREC DL19, Gao et al. report Contriever nDCG@10 of 44.5 and HyDE nDCG@10 of 61.3. DL20 nDCG@10 moves from 42.1 to 57.9, and they also report gains on BEIR and multilingual Mr. TyDi.[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496 A proxy can still invent a high-impact identifier or policy detail, so transfer to your corpus is a hypothesis to test.

When to use HyDE

HyDE was designed for zero-shot retrieval without relevance labels. Gao et al. evaluate it on web search, BEIR, and multilingual Mr. TyDi tasks.[6]Reference 6Precise Zero-Shot Dense Retrieval without Relevance Labels.https://arxiv.org/abs/2212.10496 Use it when users phrase questions differently from stored documents. Skip it when the query already is the identifier you must keep.

Gate HyDE away from exact-match lookups such as IDs, dates, prices, or error codes. That's an engineering inference from the mechanism, not a claim from the paper: if a proxy invents a precise fact, retrieval can drift toward text that echoes the invention instead of the source chunk.

Before the router runs, classify each example: preserve the incident ID; reshape the conceptual throughput question. That boundary keeps a useful proxy from rewriting facts that must match exactly.

route-exact-lookups-around-hyde.py
1import re 2 3EXACT_LOOKUP = re.compile(r"\b(?:incident\s+[A-Z]+-\d+|error\s+[A-Z0-9-]{6,})\b", re.IGNORECASE) 4 5def retrieval_route(query: str) -> str: 6 if EXACT_LOOKUP.search(query): 7 return "hybrid_exact_preserving" 8 return "hyde_candidate" 9 10queries = [ 11 "What happened in incident INC-48291?", 12 "How should we plan embedding throughput for launch traffic?", 13] 14 15for query in queries: 16 print(f"{retrieval_route(query)}: {query}")
Output
1hybrid_exact_preserving: What happened in incident INC-48291? 2hyde_candidate: How should we plan embedding throughput for launch traffic?

Why should HyDE be gated away from incident IDs, error codes, dates, and prices?

Answer

The hypothetical document can invent precise details. For exact lookups, invented values can pull retrieval toward the wrong chunks, while ordinary hybrid search preserves the user's exact identifier.

Self-RAG (Self-reflective RAG)

Many retrieve-then-generate pipelines fetch context once without a model-generated critique step. Self-RAG instead fine-tunes a generator to emit special reflection tokens that control retrieval and score candidate generation segments.[7]Reference 7Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.https://arxiv.org/abs/2310.11511 A relevant passage can still fail to support a particular claim, so ask two questions separately: should the model retrieve, and does the returned evidence support its next claim?

Reflection tokens

Self-RAG uses one retrieval token family and three critique token families:[7]Reference 7Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.https://arxiv.org/abs/2310.11511

  1. Retrieve with values Yes, No, or Continue. This decides whether the model should fetch evidence before generating the next segment. Continue keeps retrieval going for later segments.
  2. ISREL with labels such as Relevant or Irrelevant. This scores whether a retrieved passage is helpful for the current query or segment.
  3. ISSUP with labels Fully supported, Partially supported, and No support. This checks whether the generated claim is grounded in retrieved evidence.
  4. ISUSE with utility scores from 1 to 5. This measures how useful the final response is for the user.

Paper examples serialize these as inline control tags such as [Retrieve=Yes], [ISREL=Relevant], [ISSUP=Fully Supported], and [ISUSE=4]. The exact bracket syntax is less important than the four decision families, but each critique tag keeps its family name visible.

Walk the same launch-throughput query through those tokens. Before reading the sequence, predict the first decision and final support label: the model needs policy evidence, and the generated claim must remain narrower than any invented detail. A Self-RAG model might emit:

  1. [Retrieve=Yes], the model decides it needs evidence before answering.
  2. It retrieves a passage: "Temporary tokens-per-minute increases require approval and backoff-aware clients..."
  3. [ISREL=Relevant], the passage is useful for the current segment.
  4. The model generates: "For launch traffic, request a temporary TPM increase and keep exponential backoff enabled."
  5. [ISSUP=Fully Supported], the claim is grounded in the retrieved text.
  6. [ISUSE=4], the response is helpful but could be more detailed.

Without reflection tokens, a standard RAG pipeline might have retrieved the same passage without explicitly scoring passage relevance or claim support. Reflection tokens expose the model's predicted judgments for scoring and control; they aren't proof that a claim is true.

Architecture

Self-RAG is more than "retrieve once, then critique at the end." At inference time it can emit a retrieval decision, retrieve top-k passages on demand, generate candidate segments conditioned on different passages in parallel, and score those branches with reflection-token probabilities.[7]Reference 7Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.https://arxiv.org/abs/2310.11511

Asai et al. run a segment-level beam search (default beam width 2) and rank each continuation with a weighted sum of the probabilities of the most desirable critique tokens. Their reported default weights are 1.0 for ISREL, 1.0 for ISSUP, and 0.5 for ISUSE; they raise the support weight when they want citation precision.[7]Reference 7Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.https://arxiv.org/abs/2310.11511 Before looking at branches, predict why a support-heavy product policy should reject a fluent claim that says launch traffic is unlimited. That decoding loop is what makes Self-RAG distinct from a simple prompted guardrail.

Self-RAG branch scores for launch-week throughput. Retrieve equals Yes. Branch A says a temporary TPM increase needs approval and scores 0.922 from 0.2 times 0.92 relevance, 0.6 times 0.96 support, and 0.2 times 0.81 utility. Branch B says launch traffic is unlimited and scores 0.534, so decoding keeps A.
A support-heavy product policy (0.2 / 0.6 / 0.2) keeps the approved-TPM claim at 0.922 and drops the unlimited-traffic claim at 0.534. Reflection-token probabilities guide the beam; they aren't proof the claim is true.

Implementation note

Deploying a true Self-RAG system requires a generator specifically fine-tuned to emit these reflection tokens during generation. Asai et al. train Llama 2 7B and 13B generators that way, with a separate Llama 2 7B critic that inserts reflection tokens into supervised examples offline before generator training.[7]Reference 7Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.https://arxiv.org/abs/2310.11511 At inference the generator emits the tokens itself; you don't host the critic.

A prompted frontier model can imitate parts of this control loop, but that isn't the same system. Without reflection-token fine-tuning, you're building a Self-RAG-inspired agentic pipeline: separate routing, retrieval grading, and answer validation calls stitched together in application code.

Self-RAG cost comes from on-demand retrieval plus branching over multiple passages and scoring those branches, not from a few extra control tokens. Evaluate it where support-aware generation is worth that additional serving path.

The scoring sketch below doesn't implement Self-RAG training. It ranks two branches once a trained model has supplied relevance, support, and utility probabilities. Use the 0.2 / 0.6 / 0.2 policy to predict which branch survives before reading code. It puts more weight on support than Asai et al.'s 1.0 / 1.0 / 0.5 default, but either score still needs source evidence.

score-self-rag-branches.py
1from dataclasses import dataclass 2 3@dataclass(frozen=True) 4class CandidateSegment: 5 text: str 6 relevance_probability: float 7 support_probability: float 8 utility_probability: float 9 10def branch_score(candidate: CandidateSegment) -> float: 11 return ( 12 0.2 * candidate.relevance_probability 13 + 0.6 * candidate.support_probability 14 + 0.2 * candidate.utility_probability 15 ) 16 17candidates = [ 18 CandidateSegment("temporary TPM increase requires approval", 0.92, 0.96, 0.81), 19 CandidateSegment("all launch traffic is unlimited", 0.95, 0.28, 0.88), 20] 21winner = max(candidates, key=branch_score) 22 23print(f"chosen segment: {winner.text}") 24print(f"score: {branch_score(winner):.3f}")
Output
1chosen segment: temporary TPM increase requires approval 2score: 0.922

CRAG (Corrective RAG)

Corrective Retrieval-Augmented Generation (CRAG) focuses on correction after imperfect retrieval. It adds a lightweight Retrieval Evaluator that scores retrieved question-document pairs and routes the request before generation.[8]Reference 8Corrective Retrieval Augmented Generation.https://arxiv.org/abs/2401.15884

The evaluator

In the paper, the evaluator is a lightweight T5-large model fine-tuned to score each retrieved question-document pair on a [−1,1][-1, 1][−1,1] range, then threshold those scores into one of three actions:[8]Reference 8Corrective Retrieval Augmented Generation.https://arxiv.org/abs/2401.15884

With the teaching thresholds below, predict the route for best scores 0.81, 0.17, and 0.45. Numbers are only a check; the decision boundary between usable, unusable, and uncertain evidence is what matters.

  1. Correct: At least one retrieved document clears the upper threshold. Refine internal knowledge and answer from it.
  2. Incorrect: All retrieved documents fall below the lower threshold. Discard them and fall back to web search.
  3. Ambiguous: Scores land between those two cases. Combine refined internal evidence with web results.

Yan et al. set those thresholds empirically per dataset, for example 0.590.590.59 and −0.99-0.99−0.99 on PopQA.[8]Reference 8Corrective Retrieval Augmented Generation.https://arxiv.org/abs/2401.15884 The sketch below uses a 0-1 teaching scale (upper=0.7, lower=0.2) so you can check the same three-way rule by hand. Don't copy those teaching numbers into production.

A developer asks about "current embedding TPM increase process." The internal knowledge base has weak coverage, so the evaluator might score the retrieved internal documents as Incorrect, triggering a web-search fallback. A production system still needs source allowlists and citation checks before trusting those web results.

If the internal documents are somewhat relevant but incomplete, the evaluator returns Ambiguous, and CRAG combines refined internal strips with web results. That extra branch is correction, not proof: external text remains untrusted until source and support checks pass.

The distinction from Self-RAG is where correction happens. CRAG doesn't train the generator to emit reflection tokens. Instead, it inserts a separate evaluator between retrieval and generation, then uses that evaluator to trigger correction paths.

Before running the classifier, map each best score to a path: 0.81 should stay internal, 0.17 should leave it, and 0.45 should combine sources.

route-crag-evaluator-scores.py
1from typing import Literal 2 3Decision = Literal["correct", "incorrect", "ambiguous"] 4 5def crag_action(scores: list[float], lower: float = 0.2, upper: float = 0.7) -> Decision: 6 # Teaching 0-1 scale. The paper scores [-1, 1] with dataset-specific thresholds. 7 best_score = max(scores) 8 if best_score > upper: 9 return "correct" 10 if best_score < lower: 11 return "incorrect" 12 return "ambiguous" 13 14print(f"strong retrieval: {crag_action([0.81, 0.15])}") 15print(f"weak retrieval: {crag_action([0.10, 0.17])}") 16print(f"uncertain retrieval: {crag_action([0.45, 0.09])}")
Output
1strong retrieval: correct 2weak retrieval: incorrect 3uncertain retrieval: ambiguous

What separates true Self-RAG from a prompted critique loop?

Answer

True Self-RAG uses a generator trained to emit retrieval and critique tokens during generation. A prompted critique loop can be useful, but it's application orchestration around a standard model.

CRAG teaching table on a 0 to 1 scale with lower 0.2 and upper 0.7. Best score 0.81 is correct and refines internal docs. Best score 0.17 is incorrect and falls back to web. Best score 0.45 is ambiguous and mixes refined internal strips with web results.
On the teaching 0-1 scale, max 0.81 is correct, max 0.17 is incorrect, and max 0.45 is ambiguous. The paper's T5-large evaluator uses a [-1, 1] range and dataset-specific thresholds.

Knowledge refinement

Even relevant documents contain noise. CRAG includes a decompose-then-recompose step. Yan et al. split a retrieved document into strips of a few sentences, score each strip with the same evaluator, drop strips below a filter threshold (they use −0.5-0.5−0.5), and concatenate the rest in order, keeping a small top-kkk.[8]Reference 8Corrective Retrieval Augmented Generation.https://arxiv.org/abs/2401.15884

The paper scores each retrieved document, then applies thresholds. The sketch below collapses that into a classify helper and uses keyword checks for strip filtering so you can run it without a T5 evaluator. Watch the boundary: a correction route can discard noisy sentences, but a missing fact stays missing.

If you instantiate this class with real components and run crag.run("current embedding TPM increase process"), the evaluator might return "incorrect" because internal docs lack coverage. The pipeline would then call web_search.search(...) and pass the web results through refine_knowledge before generating the answer.

knowledge-refinement.py
1from typing import Literal, Protocol 2 3Decision = Literal["correct", "incorrect", "ambiguous"] 4 5class SearchBackend(Protocol): 6 def search(self, query: str, k: int = 5) -> list[str]: ... 7 8class RetrievalEvaluator(Protocol): 9 def classify(self, query: str, docs: list[str]) -> Decision: ... 10 11 def is_relevant_strip(self, query: str, strip: str) -> bool: ... 12 13class CorrectiveRAG: 14 def __init__( 15 self, 16 vector_db: SearchBackend, 17 evaluator_model: RetrievalEvaluator, 18 web_search_tool: SearchBackend, 19 ) -> None: 20 self.vector_db = vector_db 21 self.evaluator = evaluator_model 22 self.web_search = web_search_tool 23 24 def run(self, query: str) -> str: 25 # Initial retrieval can be wrong because the private corpus is incomplete. 26 retrieved_docs = self.vector_db.search(query, k=5) 27 28 # The evaluator decides whether internal evidence is usable. 29 decision = self.evaluator.classify(query, retrieved_docs) 30 31 if decision == "correct": 32 final_context = self.refine_knowledge(query, retrieved_docs) 33 34 elif decision == "incorrect": 35 web_results = self.web_search.search(query) 36 final_context = self.refine_knowledge(query, web_results) 37 38 else: # ambiguous 39 internal_context = self.refine_knowledge(query, retrieved_docs) 40 web_context = self.refine_knowledge(query, self.web_search.search(query)) 41 final_context = internal_context + web_context 42 43 return self.generate(query, final_context) 44 45 def refine_knowledge(self, query: str, docs: list[str]) -> list[str]: 46 refined_strips = [] 47 for doc in docs: 48 strips = self.chunk_into_strips(doc) 49 for strip in strips: 50 if self.evaluator.is_relevant_strip(query, strip): 51 refined_strips.append(strip) 52 return refined_strips 53 54 def chunk_into_strips(self, doc: str) -> list[str]: 55 # Teaching version: sentence segmentation by period. 56 return [segment.strip() for segment in doc.split('.') if segment.strip()] 57 58 def generate(self, query: str, context: list[str]) -> str: 59 if not context: 60 return "No reliable evidence found." 61 return f"Answer to '{query}' using: " + " ".join(context) 62 63class FakeVectorDB: 64 def search(self, query: str, k: int = 5) -> list[str]: 65 return [ 66 "Old SDK install guide. Pin client version 0.8 for legacy projects.", 67 "Deprecated quota note. Manual review was required for all increases.", 68 ][:k] 69 70class FakeWebSearch: 71 def search(self, query: str, k: int = 5) -> list[str]: 72 return [ 73 "Official API limit guide. Launch-week TPM increases require approval.", 74 "Embedding clients should use exponential backoff after rate-limit errors.", 75 ][:k] 76 77class FakeEvaluator: 78 def classify(self, query: str, docs: list[str]) -> Decision: 79 joined_docs = " ".join(docs).lower() 80 if "tpm" in joined_docs or "rate-limit" in joined_docs: 81 return "correct" 82 return "incorrect" 83 84 def is_relevant_strip(self, query: str, strip: str) -> bool: 85 keywords = {"tpm", "quota", "rate-limit", "embedding", "backoff"} 86 strip_words = set(strip.lower().replace(",", " ").split()) 87 return bool(keywords & strip_words) 88 89pipeline = CorrectiveRAG(FakeVectorDB(), FakeEvaluator(), FakeWebSearch()) 90answer = pipeline.run("current embedding TPM increase process") 91print(answer)
Output
1Answer to 'current embedding TPM increase process' using: Launch-week TPM increases require approval Embedding clients should use exponential backoff after rate-limit errors

Agentic and iterative retrieval

Self-RAG and CRAG both introduce feedback, but at different boundaries: Self-RAG can make retrieval and critique decisions while generating segments, while CRAG routes after initial retrieval. An application can generalize that feedback into agentic retrieval: instead of one fixed retrieve-then-generate pass, a model with tool access searches, reads results, tests whether evidence is sufficient, and either answers or searches again with a refined query.

The mechanism is older than the agent framing. IRCoT studies one version of this pattern: retrieve, reason a step, use that step to drive the next retrieval, and repeat until the chain of evidence is complete.[9]Reference 9Interleaving Retrieval with Chain-of-Thought Reasoning for Knowledge-Intensive Multi-Step Questionshttps://arxiv.org/abs/2212.10509 Multi-hop questions like "Which embedding endpoint has the highest p95 latency, and what retry budget applies to that endpoint?" need exactly this, because the answer to the second part depends on resolving the first.

Before choosing a rung, predict the dependency in the request above. A single-fact FAQ should stop early; endpoint selection followed by a retry lookup must carry evidence from one hop into the next. Each escalation costs latency and tokens:

  1. Naive RAG for single-fact lookups answerable by one retrieval.
  2. Query rewriting plus hybrid search and reranking for conversational ambiguity and vocabulary mismatch. This is a practical baseline to evaluate before more expensive routes.
  3. HyDE behind a router for abstract or vocabulary-mismatched queries where a document-style proxy helps.
  4. A correction gate (CRAG-style evaluator or prompted critique) when retrieval quality is inconsistent and a single bad context causes user-visible errors.
  5. Iterative or agentic retrieval for genuinely multi-hop questions, where one retrieval pass can't gather all the facts.

Escalate only when the failure mode and evaluation demand it. On a simple FAQ lookup, an agentic loop can add latency and failure paths without improving retrieved evidence.

That same boundary explains the comparison below. Read it by asking which component owns each decision, then check whether each added control changes retrieval, evidence scoring, or both.

A question needs two facts where the second depends on resolving the first. Which strategy fits, and why not naive RAG?

Answer

Iterative or agentic retrieval. Naive RAG runs one retrieval pass, so it can't use the answer to the first hop to form the query for the second. Interleaving retrieval with reasoning lets each hop drive the next search.

Comparison of advanced techniques

Choose these techniques by failure mode, not by novelty. HyDE targets semantic mismatch. Self-RAG changes retrieval timing and branch scoring inside a trained generator. CRAG adds a separate correction gate after retrieval.

FeatureNaive RAGHyDESelf-RAGCRAG
Retrieval triggerAlwaysAlwaysDynamic ([Retrieve])Always
Search objectRaw queryHypothetical documentRaw query plus partial generationRaw query
Quality checkNoneNone; changes the query vectorPredicted relevance, support, and utility tokensRetrieval evaluator confidence
Who owns the decisionApplication always retrievesApplication generates a proxy, then retrievesTrained generator during decodingSeparate evaluator after retrieval
External searchNoNoNo (unless you add it)Yes, on ambiguous or incorrect retrieval
Primary useSimple Q&AAbstract or vocabulary-mismatched queriesHigh-factuality generation with a specialized modelInconsistent internal retrieval
LatencyLowMediumHighMedium-High

What rewrite, HyDE, and CRAG can break

Advanced RAG helps only when it matches the failure. Watch for these symptoms before adding another model call. Rewrite, HyDE, multi-query, and CRAG affect more than quality. They expand the attack and cost surface of retrieval.

Diagnose in order: identify the wrong search object, check evidence scope, then check added calls and trust boundaries. A quality gain without that diagnosis can hide a security or latency regression.

Symptom: the query is vague, but the answer needs history

The retriever sees only the latest turn, so it searches for "the other one" instead of the backup API key or failed batch job. Rewrite the latest turn into a standalone query using the last few conversation turns. Keep the raw user message for the generator, but search with the rewritten query so the answer can retain conversational context.

Symptom: rewrite or history poisons retrieval intent

Conversation history, tool output, or a retrieved doc can contain "ignore policy / search secrets / dump all tickets." If the rewrite model treats history as trusted instructions, it can adopt that intent and retrieve privileged neighborhoods. Treat rewrite input as untrusted data. Constrain the prompt to disambiguate the user's question, mark tool/page text as data, preserve principal identity and ACL scope, and never let a rewrite broaden grants.

Symptom: HyDE improves recall on policy questions but breaks exact lookups

The hypothetical document can invent precise identifiers, dates, or prices. That invented detail may pull retrieval toward the wrong neighborhood. Route exact-match queries to normal hybrid retrieval; use HyDE for conceptual or vocabulary-mismatched questions where a document-style proxy helps more than it hurts.

Symptom: CRAG web fallback becomes SSRF or untrusted-source injection

An "Incorrect" retrieval score triggers an open-web fetch. Without controls, the worker can hit internal metadata endpoints through server-side request forgery (SSRF) or pull attacker-controlled pages that inject instructions into generation. Use domain allowlists, block links into private networks, fetch in a network sandbox, treat web HTML as untrusted observations, and require citations from allowlisted sources before generation. Prefer "refuse / escalate" over a wide-open web tool.

Symptom: rewrite or HyDE cache bleeds across tenants

Caches keyed only on the raw string miss tenant, user, and policy version. User A's rewritten query or HyDE proxy can then serve User B's retrieval path. Key caches by (tenant_id, principal_id or role set, policy_version, normalized_query, technique). Prefer short TTLs on security-sensitive paths, and never share HyDE vectors across tenants.

Symptom: multi-query / agentic loops become a cost DoS

Decomposition and multi-query fan-out multiply embedding, ANN, and LLM calls per user message. Set hard caps on rewrite count, sub-queries, web fetches, and dollar/token budgets per request. Fail closed with a partial answer or clarification when the budget trips, instead of letting a loop define its own spend.

Symptom: Self-RAG-inspired code works in prompts but isn't true Self-RAG

True Self-RAG trains a generator to emit retrieval and critique tokens during generation. Separate prompted grading calls can mimic the control loop, but they don't create the same reflection-token model. Name the system honestly: call it a prompted critique loop or CRAG-style evaluator unless you're hosting a model trained with Self-RAG reflection tokens.

Symptom: accuracy rises, but users feel the product is slow

Rewriting, HyDE generation, dense retrieval, sparse retrieval, reranking, grading, and final synthesis can become a long sequential path. Parallelize independent retrievals, cap reranker candidates, cache repeated rewrites or hypothetical documents with tenant-safe keys, and measure retrieval and generation latency separately. If supported-answer quality misses its gate, lower latency alone isn't a release win.

Add one control at a time

Don't stack rewrite, HyDE, CRAG, and an agent loop on the first launch-throughput query. Treat each control as an experiment: name the failure, add one intervention, and compare against the same labeled cases. Add a control only when a measured failure needs it:

Stable baseline

Start with query rewriting and hybrid search (dense retrieval + sparse keyword retrieval). This fixes basic vocabulary mismatch and conversational ambiguity for a low cost: one extra LLM call for rewriting. That baseline lets every later route earn its complexity.

Add reranking

Add a cross-encoder reranker after retrieval when initial retrieval has decent recall but final candidates are noisy. It can improve precision, at moderate cost: cross-encoders are slower than initial retrieval, so keep candidate count small.

Specialized handling (HyDE / router)

Use a router to classify queries. For a conceptual request such as "Explain launch-week embedding throughput planning," choose HyDE because vocabulary mismatch is likely. For a precise request such as "What happened in incident INC-48291?", choose standard retrieval because HyDE risks inventing the exact identifier. The router pays HyDE's extra generation only where document-style proxy helps.

Add correction loops

If accuracy is still insufficient, add a prompted critique loop or a CRAG-style evaluator before investing in a true Self-RAG model. Prompt the model or a lightweight evaluator to grade retrieved documents before answer generation; when evidence is weak, trigger a rewrite, a second retrieval pass, or web fallback. This captures much of the reliability gain without requiring reflection-token fine-tuning.

Latency is the production constraint in advanced RAG. A pipeline with rewriting, HyDE, retrieval, reranking, grading, and generation turns one answer into several sequential model and retrieval steps. Stream final generation, fetch dense and sparse results in parallel, and cache reusable artifacts when traffic is repetitive. Track each stage so a quality gain can be tied to its added work.

Don't release a more elaborate route because it improves a few anecdotes. Compare supported-answer quality and latency on a labeled set, then release only paths that meet both requirements. For each route, retain the corpus snapshot, model and retriever versions, candidate count, latency percentiles, token or dollar cost, and citation-support results. Without that evidence, route comparisons are not reproducible.

Before running the selector below, predict which route clears both gates: supported accuracy at least 0.93 and p95 no more than 350 ms.

release-a-retrieval-route-from-evals.py
1evaluations = [ 2 {"route": "rewrite+hybrid", "supported_accuracy": 0.91, "p95_ms": 180}, 3 {"route": "hyde+rerank", "supported_accuracy": 0.94, "p95_ms": 260}, 4 {"route": "agentic-loop", "supported_accuracy": 0.95, "p95_ms": 710}, 5] 6minimum_supported_accuracy = 0.93 7maximum_p95_ms = 350 8 9eligible = [ 10 row for row in evaluations 11 if row["supported_accuracy"] >= minimum_supported_accuracy 12 and row["p95_ms"] <= maximum_p95_ms 13] 14released = max(eligible, key=lambda row: row["supported_accuracy"]) 15print(f"released route: {released['route']}") 16print(f"supported_accuracy={released['supported_accuracy']:.2f} p95_ms={released['p95_ms']}")
Output
1released route: hyde+rerank 2supported_accuracy=0.94 p95_ms=260

Try it yourself

Stay with five API-docs messages and measure top-3 evidence hits. This exercise needn't reproduce paper scores. It should connect each retrieval miss to one intervention and one release decision.

Exercise: Build a query rewriter for a developer-support assistant

Collect five developer-support messages from an API docs assistant (or write realistic ones). Include one ambiguous message that needs conversation history, one complex comparison, and one vague keyword so each control has a case to test.

Step 1, Rewrite: Write a Python function that takes a developer message plus the last two turns of chat history and outputs a standalone query. Run it on the five messages and inspect the results. Check whether the rewritten query contains full intent while leaving authorization and tenant scope unchanged.

Step 2, Measure: For each original message, manually decide which internal policy documents should be retrieved. Then run the rewritten query through a simple dense-retrieval setup (even a small embedding model like all-MiniLM-L6-v2 against a dozen policy chunks). Count how many of the top-3 results match the manual gold set. The rewrite should improve hit rate for ambiguous and vague cases.

Step 3, Diagnose: Pick one message where retrieval still fails. Before changing code, predict the failure: vocabulary mismatch (try multi-query expansion), semantic asymmetry (try HyDE), or weak evidence (try a CRAG-style evaluator). Implement only that fix and measure again.

Expected outcome: Record which interventions improve top-3 evidence hits or catch weak retrieval on the examples, and which add latency without a gain. A small exercise may not reproduce paper results; its value is exposing the measurement loop.

Read the measurements before choosing a route

The exercise should leave you with evidence about retrieval quality, latency, and the shape of each failure. Turn those observations into a route decision before adding a more expensive path. Three deployment questions provide the boundary.

When does HyDE justify its latency cost?

HyDE adds at least one extra generation call before retrieval. That cost is worth paying only when measured retrieval quality improves enough to justify it. A smaller instruction model may be adequate for the hypothetical document, but evaluate it rather than assuming equivalence. Cache repeated proxy documents only when generation configuration and source policy make reuse valid. For exact-match lookups like incident IDs, error codes, or dates, skip HyDE.

When does Self-RAG degrade instead of help?

Self-RAG degrades when retrieval itself is weak or when the model's learned critique tokens stop correlating with real answer quality. It also raises inference cost because the model may retrieve on demand, branch over multiple passages, and spend extra decoding steps on critique tokens before choosing a continuation. For a simple FAQ bot, first measure a cheaper baseline such as rewrite plus reranking or a correction gate before committing to a specialized Self-RAG serving path.

Can you combine HyDE and Self-RAG?

As a system design, you can use HyDE to propose initial candidate passages, then let a true Self-RAG model score branches with ISREL, ISSUP, and ISUSE. That composition needs its own evaluation; neither mechanism guarantees the other improves it. If you don't have a reflection-token model, describe the composition as HyDE plus a CRAG-style evaluator or prompted support checks rather than Self-RAG.

Complete the lesson

Mastery Check

Answer every question, then check your score. Score 75% or higher to mark this lesson complete.

1.A developer first reports a leaked API key, then asks, "What do I do now?" The retriever sees only the search text produced by a rewrite stage. Which rewrite preserves intent and security?

Correct answer: Use relevant history to form a standalone revocation, rotation, and audit query while preserving the caller's ACL scope.

History supplies the missing referent, so the rewrite must turn the vague turn into a searchable request. History remains untrusted data, and neither disambiguation nor query expansion may broaden tenant or ACL scope.

2.Three query variants rank documents as [rate-limit-policy, quota-increase, autoscaling], [autoscaling, rate-limit-policy, backpressure], and [rate-limit-policy, batch-api, autoscaling]. Using RRF with sum 1/(60 + rank), which document wins?

Correct answer: rate-limit-policy, because ranks 1, 2, and 1 produce about 0.0489, slightly above autoscaling's 0.0481.

RRF adds rank contributions and never compares raw similarity scores across searches. rate-limit-policy gets 1/61 + 1/62 + 1/61; autoscaling gets 1/63 + 1/61 + 1/63.

3.HyDE samples two proxy passages whose embeddings are [0.2, 0.8, 0.1] and [0.4, 0.6, 0.3]. If retrieval uses their component-wise mean, which query vector enters search?

Correct answer: [0.3, 0.7, 0.2], because each coordinate is averaged across the two document-style proxies.

HyDE embeds sampled hypothetical documents and averages their vectors: [(0.2+0.4)/2, (0.8+0.6)/2, (0.1+0.3)/2]. The proxy text is a search object, not evidence for the final answer.

4.A standard LLM is called separately to route retrieval, grade passages, and check support. It was never trained to emit Retrieve, ISREL, ISSUP, or ISUSE tokens during decoding. How should the system be described?

Correct answer: A Self-RAG-inspired critique loop, because application orchestration imitates controls without a reflection-token model.

True Self-RAG fine-tunes the generator to emit retrieval and critique tokens while producing segments. Separate calls can implement useful routing and support checks, but their control lives in application code.

5.A trained Self-RAG decoder scores branches as 0.2 relevance + 0.6 support + 0.2 utility. Branch A has [0.92, 0.96, 0.81]; Branch B has [0.95, 0.28, 0.88]. Which branch survives?

Correct answer: Branch A, with score 0.922, because the support-heavy policy outweighs Branch B's small relevance advantage.

Branch A scores 0.2(0.92)+0.6(0.96)+0.2(0.81)=0.922; Branch B scores 0.534. The weights are a branch-selection policy, not independent proof that Branch A is factually true.

6.A CRAG teaching router uses lower=0.2 and upper=0.7 on a 0-1 scale. Retrieved-document scores are [0.45, 0.09]. Which correction path follows from the best score?

Correct answer: Treat retrieval as ambiguous, refine internal and external evidence, and combine both before generation.

The best score is 0.45. It is neither above 0.7 nor below 0.2, so it lands in CRAG's middle branch. The teaching thresholds illustrate routing; production thresholds must be calibrated.

7.A request asks which embedding endpoint has the highest p95 latency and then asks for that endpoint's retry budget. The second search depends on the first result. Which retrieval plan fits?

Correct answer: Use iterative retrieval so the identified endpoint becomes input to the retry-budget search, with evidence checked at each hop.

The retry query can't be grounded until the endpoint is resolved. Iterative retrieval lets evidence from hop one form hop two; parallel variants or one packed retrieval can't express that dependency reliably.

8.A rewrite and HyDE cache uses only normalized query text as its key. Two tenants ask the same question under different grants and policy versions. Which cache contract prevents cross-scope reuse?

Correct answer: Key by tenant, principal or role set, policy version, normalized query, and technique, with short security-sensitive TTLs.

A proxy or rewrite can encode tenant-specific vocabulary and policy context. Query-only keys can serve one scope's search object to another; identity, grants, policy version, and technique belong in the key.

9.Release measurements are rewrite+hybrid: accuracy 0.91, p95 180 ms; hyde+rerank: 0.94, 260 ms; agentic-loop: 0.95, 710 ms. Gate is accuracy at least 0.93 and p95 at most 350 ms. Which route ships?

Correct answer: hyde+rerank, because it clears both gates while the baseline misses accuracy and the agentic loop misses latency.

rewrite+hybrid fails 0.93 accuracy; agentic-loop fails the 350 ms latency ceiling. hyde+rerank is the only eligible route, so its extra complexity is justified by measured quality within budget.

10.CRAG marks internal retrieval incorrect and proposes open-web fallback for a private policy question. Which design preserves the correction benefit without turning fallback into an SSRF or injection path?

Correct answer: Allowlist sources, block private-network traversal, sandbox fetching, treat HTML as data, and require supported citations.

Correction expands the trust boundary. Network controls prevent SSRF, source allowlists constrain provenance, and content remains untrusted input. If those controls can't be enforced, refusal or escalation is safer than open fetch.

10 questions remaining.

Next Step
Continue to GraphRAG & Knowledge Graphs

Query rewriting, HyDE, Self-RAG, and CRAG help a system recover better passages for one question. Some questions instead depend on relationships scattered across a corpus; <span data-glossary="graphrag">GraphRAG</span> adds entity graphs and hierarchical community reports for that broader retrieval problem.

PreviousVector DB Internals: HNSW & IVF
Share this article
XFacebookLinkedInBlueskyRedditHacker NewsEmail
References

Query Rewriting for Retrieval-Augmented Large Language Models.

Ma, X., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2305.14283

RAG-Fusion: a New Take on Retrieval-Augmented Generation

Rackauckas, Z. · 2024

https://arxiv.org/abs/2402.03367

Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods.

Cormack, G. V., Clarke, C. L. A., & Buettcher, S. · 2009 · SIGIR '09

https://dl.acm.org/doi/10.1145/1571941.1572114

Least-to-Most Prompting Enables Complex Reasoning in Large Language Models

Zhou, D., et al. · 2022 · ICLR 2023

https://arxiv.org/abs/2205.10625

Take a Step Back: Evoking Reasoning via Step-Back Prompting in Large Language Models.

Zheng, H. S., Mishra, S., Chen, X., et al. · 2023 · ICLR 2024

https://arxiv.org/abs/2310.06117

Precise Zero-Shot Dense Retrieval without Relevance Labels.

Gao, L., Ma, X., Lin, J., & Callan, J. · 2022 · arXiv preprint

https://arxiv.org/abs/2212.10496

Self-RAG: Learning to Retrieve, Generate, and Critique through Self-Reflection.

Asai, A., et al. · 2023 · arXiv preprint

https://arxiv.org/abs/2310.11511

Corrective Retrieval Augmented Generation.

Yan, S.-Q., et al. · 2024 · arXiv preprint

https://arxiv.org/abs/2401.15884

Interleaving Retrieval with Chain-of-Thought Reasoning for Knowledge-Intensive Multi-Step Questions

Trivedi, H., Balasubramanian, N., Khot, T., & Sabharwal, A. · 2022

https://arxiv.org/abs/2212.10509

Discussion

Questions and insights from fellow learners.

Discussion loads when you reach this section.