Zucchetti LLM Course — Lecture 5 of 10
Beyond naive retrieval — making RAG systems that actually work on complex enterprise data
Kamyar Zeinalipour · 2 hours
Six failure modes you’ll encounter in production
Re-ranking · Cross-encoders · HyDE · Parent-child chunking
Breaking complex questions into answerable sub-queries
Sending queries to the right data source automatically
Section 1 of 4
Diagnosing the six failure modes
This is what we built in L4. It works for simple cases. It breaks on everything else.
Query uses different words than the document.
Q: “sick leave policy” → Doc says “absence due to illness”
The answer spans two chunks, but only one is retrieved.
Table header in chunk 1, data rows in chunk 2.
Top-K returns 10 chunks but only 1 is relevant. The signal drowns in noise.
Answer requires connecting info from 2+ documents.
“Compare policy A with policy B”
Old and new versions of a document both match. System returns outdated info.
Chunks are too big (diluted) or too small (no context). Neither gives a good answer.
| Failure Mode | Fix | Section |
|---|---|---|
| Semantic mismatch | HyDE — generate a hypothetical answer, embed that instead | S2 |
| Wrong chunks retrieved | Re-ranking — cross-encoder precision pass after coarse retrieval | S2 |
| Chunk boundary splits | Parent-child chunking — retrieve child, pass parent context | S2 |
| Context dilution | Re-ranking + fewer chunks — quality over quantity | S2 |
| Multi-hop questions | Query decomposition — break into sub-queries | S3 |
| Wrong data source | Semantic routing — route to the right system | S4 |
Section 2 of 4
Re-ranking · HyDE · Parent-child chunks
The single highest-ROI improvement for any RAG system
Standard vector search returns the most similar documents — but similar ≠ relevant.
You search for "sick leave policy" and get 10 chunks about leave policies. But chunks #7 and #3 are the ones that actually answer the question. Chunks #1-2 are just vaguely similar.
After initial retrieval, pass each result through a more powerful model that scores how well it actually answers the query.
Result: the truly relevant chunks move to positions #1 and #2. Noise drops to the bottom.
Why we need two different models
Query → [Encoder] → vec_q
Doc → [Encoder] → vec_d
score = cos(vec_q, vec_d)
Encoded separately. Doc vectors stored in DB.
Search = nearest neighbor lookup.
Fast: ~1ms per 1M docs
query + doc → [Encoder] → score
Both texts fed together into one encoder.
Full cross-attention between query and doc.
Output: a single relevance score (0–1).
Cannot precompute. Must run for every pair.
Slow but much more accurate
Putting it together: fast recall + precise re-ranking
Recall = “don’t miss the right answer”
Precision = “only return the right answers”
Two different models, two different jobs — don’t confuse them
What it does: Converts text into a vector
How it searches: Cosine similarity between vectors
Speed: ~1ms per 1M docs
Accuracy: Good (approximate)
What it does: Reads query + doc together
How it scores: Direct relevance score (0–1)
Speed: ~100–200ms for 50 docs
Accuracy: Excellent (precise)
Re-rankers are cross-encoder models — not embedding models. They score (query, doc) pairs directly.
| Model | Type | Quality | Notes |
|---|---|---|---|
| Cohere Rerank | Cloud API | Excellent | Easiest to integrate, pay per call |
| BGE Reranker v2 | Local (GPU) | Very good | Open source, best for on-prem |
| cross-encoder/ms-marco | Local (GPU) | Good | HuggingFace classic, well-tested |
| ColBERT v2 | Local (GPU) | Good | Late interaction — faster than cross-encoder |
Stage 1 (embedding model): MiniLM, BGE-M3, OpenAI → produces vectors, search by cosine similarity
Stage 2 (re-ranker): Cohere, BGE Reranker → reads query + doc together, outputs a relevance score
When the question and the answer use completely different words
User asks:
“What is our policy on working from home?”
Real document says:
“Remote work arrangements are governed by the Telecommuting and Hybrid Work Guidelines, Section 4.2...”
≠ “remote work arrangements”
≠ “telecommuting”
≠ “hybrid work guidelines”
Embedding similarity: LOW — different vocabulary
Questions and answers use different language. But answers and other answers use similar language.
What if we generate a fake answer first?
Two LLM calls: one to search better, one to answer
“What is our policy on working from home?”
“Employees may work from home up to three days per week with manager approval. Remote work arrangements must be documented in the HR system and reviewed quarterly...”
“remote work arrangements” · “telecommuting policy” · “hybrid work guidelines”
Terms the original question never used — but the fake answer did!
The fake answer is thrown away. Only the real docs are used for the final answer.
Users type bad queries. Fix them before they reach retrieval.
| ✗ Raw User Query | ✓ Rewritten Query |
|---|---|
| “that leave thing for new people” | “What is the annual leave policy for newly hired employees?” |
| “how fix error 500” | “How to troubleshoot HTTP 500 internal server error?” |
| “can I do the thing remotely” | “Is remote access to the internal ERP system available?” |
Both transform the query — but they solve different problems
LLM generates: a better question
Solves: vague, informal user input
When: chatbots, search bars, internal tools
Output size: 1 sentence (cheap)
LLM generates: a fake answer
Solves: vocabulary mismatch (question ≠ document language)
When: domain-specific jargon, technical docs
Output size: full paragraph (more expensive)
| Situation | Use |
|---|---|
| Users type vague, informal queries | Query Rewriting |
| Question & document use different vocabulary | HyDE |
| Both problems at once | Rewrite first, then HyDE |
The fundamental trade-off in chunk size
LLM gets the right sentence but can’t understand the full picture
LLM gets too much text, the answer is lost inside it
Use two levels: search on small, read on big
PARENT CHUNK (sent to LLM) — 800 tokens
Section 3.2: Remote Work Policy
Full section with eligibility, process, exceptions, and appeals...
| Step | Detail |
|---|---|
| User asks: | “How do I apply for remote work?” |
| Search matches: | Child 2 — the application process paragraph (precise!) |
| Sent to LLM: | Parent — the full Remote Work Policy section (eligibility + process + exceptions) |
| Result: | LLM gives complete answer including eligibility criteria the user didn’t ask about |
In L4, we introduced: score = α · BM25 + (1−α) · Dense. But there’s a catch.
| Ranker | Score Range | Example |
|---|---|---|
| BM25 | 0 to 20+ | 12.4 |
| Dense (cosine) | -1 to 1 | 0.87 |
If you add them raw: 12.4 + 0.87 = 13.27 — BM25 always dominates. Dense is meaningless.
Min-max normalization helps, but is fragile: depends on the batch, changes between queries, hard to tune α.
What if we only used the rank positions — “this doc was #1 in BM25 and #3 in Dense” — instead of raw scores?
That’s exactly what Reciprocal Rank Fusion (RRF) does.
Combine rankings without scores — just positions
| Document | BM25 Rank | Dense Rank | RRF Score | Final Rank |
|---|---|---|---|---|
| Doc A | #1 | #3 | 1/61 + 1/63 = 0.0323 | #1 |
| Doc B | #4 | #1 | 1/64 + 1/61 = 0.0320 | #2 |
| Doc C | #2 | #5 | 1/62 + 1/65 = 0.0315 | #3 |
Documents that rank high in both rankers rise to the top. No normalization needed.
Flat search breaks down at scale
10,000 documents × ~50 chunks each = 500K chunks to search through
User asks: “What is the sick leave policy?”
| Rank | Chunk From | Why It Matched | Correct? |
|---|---|---|---|
| #1 | HR Policy Manual | Contains “sick leave” | ✓ |
| #2 | Employee Handbook Intro | Mentions “policies” generically | ✗ |
| #3 | IT Security Policy | Contains “leave your device...” | ✗ |
| #4 | Finance Handbook | Mentions “sick day deductions” | ~ |
Search in layers: broad first, then narrow
Skip if < 100 docs — flat search is fine
Query: “What is the sick leave policy?”
Embed document summaries. Match: HR Policy Manual, Employee Handbook, Benefits Guide, Payroll FAQ, Onboarding Guide
Search within those 5 docs. Match: Sick Leave, Annual Leave, Parental Leave, Public Holidays...
Match: Sick leave entitlement, Sick leave process, Doctor’s note requirements
500,000 chunks → 3 results
Many near-misses from wrong documents
10K → 5 → 10 → 3 results
All from the right document and section
When you chunk a document, each chunk loses its identity
| Question | The chunk doesn’t say |
|---|---|
| Which employees? | Full-time? Part-time? Interns? Contractors? |
| Which document? | HR Policy Manual? Italian labor law? Company handbook? |
| Which section? | Annual leave? Sick leave? Parental leave? |
| Which company? | Parent company? Subsidiary? Joint venture? |
Prepend context to each chunk before embedding
Prepend: “Document: {title} | Section: {heading}”
Works when docs have clear titles and section headers
Ask LLM: “Given the full doc and this chunk, write 2–3 sentences explaining where this chunk fits.”
Cost: 1 LLM call per chunk at index time (not query time)
Matches any query about “leave” — annual, sick, parental, unpaid...
Matches only “full-time annual leave” queries — precise!
| Metric | Without Context | With Context |
|---|---|---|
| Retrieval failures | Baseline | −49% (Anthropic, 2024) |
| When to apply | — | Always — cheapest win |
| Cost | — | Rule-based: free / LLM: 1 call per chunk at index time |
Even correct retrieval can fail at generation time
LLMs pay most attention to information at the beginning and end of the context window. Information in the middle is often ignored.
| Technique | What It Does | When to Use | Cost |
|---|---|---|---|
| Re-ranking | Precision pass after coarse retrieval | Always (highest ROI) | +100–200ms |
| HyDE | Bridges vocabulary gap | Domain-specific jargon mismatch | +1 LLM call |
| Parent-child chunking | Search small, read big | Structured docs with sections | 2× index size |
| Hybrid search | BM25 + dense combined | Mixed exact + semantic queries | +BM25 index |
| Query rewriting | Fix bad queries before search | Always (cheapest win) | +1 LLM call |
| Hierarchical indexing | Doc → Section → Paragraph | Large collections (1000+ docs) | 3× index |
| Contextual retrieval | Prepend doc context to chunks | Always (cheapest win) | ~Free |
☕
10 minutes
Section 3 of 4
Breaking complex questions into answerable sub-queries
“Compare the sick leave policies for full-time vs part-time employees hired after 2022.”
This single query requires information from at least 3 different places:
Sick leave policy for full-time employees
Sick leave policy for part-time employees
Rules specific to employees hired after 2022
Each sub-query depends on the answer to the previous one
“What is the sick leave policy for Mario’s department?”
| Step | Sub-query | Answer |
|---|---|---|
| 1 | “What department does Mario work in?” | Engineering |
| 2 | “What is the sick leave policy for Engineering?” | 15 days/year |
Step 2 can’t run until Step 1 finishes — it needs the department name.
All sub-queries run at the same time — no dependencies
“Compare sick leave for full-time vs part-time employees”
| Sub-query | Runs | Result |
|---|---|---|
| “Sick leave policy for full-time employees” | Simultaneously | 15 days/year |
| “Sick leave policy for part-time employees” | Simultaneously | 8 days/year |
Both queries are independent — they can run at the same time.
The next sub-query depends on the answer — like a decision tree
“My application keeps crashing”
| Step | Sub-query | Branch |
|---|---|---|
| 1 | “Is this a known bug in version 3.2?” | — |
| 2a | If YES: “What is the fix for bug #3847?” | Known bug path |
| 2b | If NO: “Common crash causes for this application?” | General troubleshooting |
Analyze the user's question.
If it can be answered with a single search, return it
unchanged as a list with one item.
If it requires information from multiple sources, break
it into 2-5 independent sub-queries.
User question: "{original_question}"
Return a JSON list of queries:
[
"query 1",
"query 2 (only if needed)",
...
]
| Input | LLM returns | Result |
|---|---|---|
| “What is sick leave?” | 1 query (same) | Direct search — no extra cost |
| “Compare FT vs PT after 2022” | 3 sub-queries | 3 parallel searches → synthesize |
Section 4 of 4
Sending queries to the right data source
Real enterprise data lives in many places. A single vector DB isn’t enough.
📚
Policy documents, manuals, FAQs
“What is our vacation policy?”
🗃️
Employee records, transactions, metrics
“How many employees joined in Q3?”
🌐
Stock prices, weather, real-time status
“What is the current system status?”
Use cosine similarity to match queries to routes — no LLM needed
| Route | Example Descriptions (embedded at setup) |
|---|---|
| Vector DB | “company policy”, “employee handbook”, “HR guidelines” |
| SQL | “how many employees”, “total revenue”, “average salary” |
| API | “current system status”, “live stock price”, “weather now” |
Ask the LLM to classify the query — handles edge cases
Classify this query into one of:
- VECTOR_DB (policy/document questions)
- SQL (data/numbers/counting questions)
- API (real-time/live data)
Query: "{query}"
Return: route name + confidence (0-1)
| Embedding Router | LLM Router | |
|---|---|---|
| Speed | ~1ms | ~500ms |
| Cost | Free (no LLM) | 1 LLM call |
| Edge cases | Struggles | Handles with reasoning |
| Ambiguous queries | Picks closest | Can output confidence + reason |
| Best for | Clear-cut categories | Complex/overlapping categories |
The full picture: classify → route → retrieve → answer
| Route | Pipeline | Example Queries |
|---|---|---|
| 📚 Vector DB | Embed → Search → Re-rank → LLM | “What does the policy say about...” |
| 🗃️ SQL | Text-to-SQL → Execute → LLM | “How many / total / average...” |
| 🌐 API | API Call → Parse JSON → LLM | “Current status of / right now...” |
Multi-route
Confidence low? Try multiple routes in parallel, LLM picks best answer
Threshold
Confidence < 0.6? Ask user to clarify: “Policy or data?”
Default route
Can’t decide? Fall back to Vector DB — safest default
Same topic, different queries — different routes
| User Query | Route | Why |
|---|---|---|
| “What is our vacation policy?” | Vector DB | Document question — search policy text |
| “How many sick days were taken in Q3?” | SQL | Counting/aggregation — needs database query |
| “Who is on leave right now?” | API | Real-time data — needs live HR system |
“What is the average sick leave per department?”
Embedding router says: Vector DB
(matched “sick leave” → policy docs)
Correct route: SQL
(needs aggregation: AVG + GROUP BY)
This is when you switch from embedding router to LLM router — the LLM understands “average per department” means data, not documents.
Some queries need BOTH — split AND route to different sources
“Compare our vacation policy with how many days employees actually took last year.”
“What is the company vacation policy?”
Search policy documents → “20 days per year”
“Average vacation days taken per employee last year”
SELECT AVG(days_taken) → “14.2 days”
“The policy allows 20 days. On average, employees used 14.2 days (71%). Engineering used the least at 11.3 days.”
1. Most RAG failures are retrieval failures — diagnose the failure mode first
2. Re-ranking with cross-encoders is the highest-ROI improvement (+15–30% precision)
3. HyDE bridges vocabulary gaps by generating a hypothetical answer to search with
4. Parent-child chunking = search on small, read on big. Best of both granularities
5. Query decomposition breaks multi-hop questions into independent sub-queries
6. Semantic routing sends queries to the right data source (vector DB, SQL, API)
7. Stack techniques: Hybrid search → Re-rank → Parent expansion → Route
❓
Thank you!
Kamyar Zeinalipour — Zucchetti LLM Course
Next → Lecture 6: Graph RAG & Knowledge Graphs
Theory: Entity extraction · Graph traversal · Community detection
🔬 Mega-Lab: RAG Pipeline (L4) + Advanced Retrieval (L5) + Graph RAG (L6)