By Sagar Shankaran, Founder of CallSphere
Move past naive RAG implementations with advanced techniques including hybrid search, re-ranking, query decomposition, contextual compression, and agentic RAG patterns used in production systems.
Key takeaways
The basic RAG pipeline -- chunk documents, embed them, retrieve top-k, stuff into prompt -- works for demos but fails in production. Teams consistently report three categories of failure:
Production RAG in 2026 addresses each of these failures with specific techniques. This guide covers the patterns that have proven effective across real deployments.
Instead of splitting on fixed token counts, semantic chunking uses embedding similarity to find natural breakpoints in the text:
flowchart LR
Q(["User query"])
EMB["Embed query<br/>text-embedding-3"]
VEC[("Vector DB<br/>pgvector or Pinecone")]
RET["Top-k retrieval<br/>k = 8"]
PROMPT["Augmented prompt<br/>system plus context"]
LLM["LLM generation<br/>Claude or GPT"]
CITE["Inline citations<br/>and page anchors"]
OUT(["Grounded answer"])
Q --> EMB --> VEC --> RET --> PROMPT --> LLM --> CITE --> OUT
style EMB fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style VEC fill:#ede9fe,stroke:#7c3aed,color:#1e1b4b
style LLM fill:#4f46e5,stroke:#4338ca,color:#fff
style OUT fill:#059669,stroke:#047857,color:#fff
import numpy as np
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("BAAI/bge-large-en-v1.5")
def semantic_chunk(text: str, threshold: float = 0.75) -> list[str]:
sentences = text.split(". ")
embeddings = model.encode(sentences)
chunks = []
current_chunk = [sentences[0]]
for i in range(1, len(sentences)):
similarity = np.dot(embeddings[i], embeddings[i - 1]) / (
np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[i - 1])
)
if similarity < threshold:
# Low similarity = topic shift = chunk boundary
chunks.append(". ".join(current_chunk) + ".")
current_chunk = [sentences[i]]
else:
current_chunk.append(sentences[i])
chunks.append(". ".join(current_chunk) + ".")
return chunks
Store small chunks for precise retrieval but return their parent context for generation. This solves the core tension between retrieval precision (small chunks match better) and generation quality (larger context produces better answers).
class ParentChildChunker:
def __init__(self, parent_size=2000, child_size=400, overlap=50):
self.parent_size = parent_size
self.child_size = child_size
self.overlap = overlap
def chunk(self, document: str) -> list[dict]:
parents = self._split(document, self.parent_size, self.overlap)
result = []
for parent_idx, parent in enumerate(parents):
children = self._split(parent, self.child_size, self.overlap)
for child in children:
result.append({
"child_text": child, # Embedded for retrieval
"parent_text": parent, # Returned for generation
"parent_id": parent_idx,
})
return result
Pure vector search fails when queries contain specific identifiers (error codes, product names, dates). Hybrid search combines dense embeddings with sparse keyword matching (BM25) to handle both semantic and lexical queries.
Hear it before you finish reading
Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.
from qdrant_client import QdrantClient, models
client = QdrantClient("localhost", port=6333)
# Create a collection with both dense and sparse vectors
client.create_collection(
collection_name="documents",
vectors_config={
"dense": models.VectorParams(
size=1024, distance=models.Distance.COSINE
)
},
sparse_vectors_config={
"bm25": models.SparseVectorParams(
modifier=models.Modifier.IDF,
)
},
)
# Hybrid search with Reciprocal Rank Fusion
results = client.query_points(
collection_name="documents",
prefetch=[
models.Prefetch(
query=dense_embedding,
using="dense",
limit=20,
),
models.Prefetch(
query=sparse_vector,
using="bm25",
limit=20,
),
],
query=models.FusionQuery(fusion=models.Fusion.RRF),
limit=10,
)
Benchmarks on production datasets consistently show hybrid search improving recall by 15-25% over dense-only search, particularly on queries with specific technical terms.
The initial retrieval step optimizes for recall (do not miss relevant documents). A re-ranker then optimizes for precision (rank the most relevant results highest). Cross-encoder re-rankers like Cohere Rerank or BGE-reranker evaluate query-document pairs jointly, producing far more accurate relevance scores than embedding cosine similarity.
from cohere import Client
cohere_client = Client(api_key="...")
def rerank_results(query: str, documents: list[str], top_n: int = 5):
response = cohere_client.rerank(
model="rerank-english-v3.0",
query=query,
documents=documents,
top_n=top_n,
)
return [
{"text": documents[r.index], "score": r.relevance_score}
for r in response.results
]
The retrieval pipeline becomes: retrieve 20-50 candidates with hybrid search, then re-rank down to the top 5. This two-stage approach consistently outperforms simply retrieving the top 5 directly.
A single user query often fails to capture all the ways relevant information might be phrased. Multi-query expansion generates multiple reformulations and retrieves results for each:
async def multi_query_retrieve(query: str, retriever, llm) -> list[Document]:
# Generate query variations
expansion_prompt = f"""Generate 3 different search queries that would help
answer this question. Return only the queries, one per line.
Question: {query}"""
variations = await llm.generate(expansion_prompt)
all_queries = [query] + variations.strip().split("\n")
# Retrieve for each query and deduplicate
seen_ids = set()
results = []
for q in all_queries:
docs = await retriever.search(q, top_k=5)
for doc in docs:
if doc.id not in seen_ids:
seen_ids.add(doc.id)
results.append(doc)
return results
For complex questions, generate a more abstract "step-back" question that retrieves broader context:
The step-back results provide foundational context, while the original query retrieves specific details. Combining both produces more complete answers.
Still reading? Stop comparing — try CallSphere live.
CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.
Retrieved chunks often contain irrelevant sentences mixed with relevant ones. Contextual compression uses an LLM to extract only the query-relevant portions before generation:
async def compress_context(query: str, documents: list[str], llm) -> list[str]:
compressed = []
for doc in documents:
prompt = f"""Extract only the sentences from the following document
that are directly relevant to answering: "{query}"
If nothing is relevant, respond with "NOT_RELEVANT".
Document:
{doc}"""
result = await llm.generate(prompt)
if result.strip() != "NOT_RELEVANT":
compressed.append(result)
return compressed
This technique reduces prompt token usage by 40-60% while maintaining or improving answer quality, because the generation model does not have to filter through irrelevant content.
The most powerful RAG pattern in 2026 makes the retrieval pipeline itself agentic. Instead of a fixed retrieve-then-generate pipeline, an agent decides when to retrieve, what to retrieve, and whether the results are sufficient.
class AgenticRAG:
def __init__(self, llm, retriever, max_iterations=5):
self.llm = llm
self.retriever = retriever
self.max_iterations = max_iterations
async def answer(self, question: str) -> str:
context = []
for i in range(self.max_iterations):
# Ask the LLM what to do next
action = await self.llm.decide(
question=question,
context=context,
options=["search", "answer", "refine_query"]
)
if action.type == "answer":
return action.content
elif action.type == "search":
results = await self.retriever.search(action.query)
context.extend(results)
elif action.type == "refine_query":
# The agent reformulates based on what it has learned
results = await self.retriever.search(action.refined_query)
context.extend(results)
return await self._forced_answer(question, context)
You cannot improve what you do not measure. The standard RAG evaluation framework uses three metrics:
| Metric | Measures | How |
|---|---|---|
| Context Relevance | Did the retriever find the right documents? | Judge each retrieved chunk for relevance to the query |
| Faithfulness | Does the answer stick to the retrieved context? | Check every claim in the answer against the context |
| Answer Relevance | Does the answer actually address the question? | Judge the answer against the original query |
# Using ragas for evaluation
from ragas import evaluate
from ragas.metrics import faithfulness, answer_relevancy, context_precision
result = evaluate(
dataset=eval_dataset, # Questions + ground truth + retrieved contexts
metrics=[faithfulness, answer_relevancy, context_precision],
)
print(result)
# {'faithfulness': 0.87, 'answer_relevancy': 0.91, 'context_precision': 0.78}
Production RAG systems in 2026 run these evaluations on every deployment, treating retrieval quality as a regression test.
Building production RAG comes down to a series of engineering tradeoffs:
The teams getting the best results in 2026 treat RAG as an engineering system, not a one-time setup. They instrument every stage, measure quality continuously, and iterate on each component independently.

Written by
Sagar Shankaran· Founder, CallSphere
LinkedInSagar Shankaran is the founder of CallSphere, where he builds production AI voice and chat agents deployed across healthcare, hospitality, real estate, and home services. He writes about agentic AI, LLM engineering, and shipping voice agents that handle real calls in production.
See how AI voice agents work for your industry. Live demo available -- no signup required.
A founder's guide to building a chatbot for answering questions on your website: RAG, voice, and how CallSphere ships one in 3-5 days.
Graphiti is the open-source temporal knowledge graph for AI agents in 2026. Learn how bi-temporal memory beats vector RAG for voice agents and long-running LLMs.
A founder's guide on how to create a chatbot in 2026. Build options, AI stack, integration patterns, and when buying a managed agent wins over building.
Working memory, permanent memory, sandboxes, harnesses, governance — the practical blueprint enterprises are using to ship long-horizon AI agents in 2026.
Haystack 2.7's Agent component plus an Ollama-served Llama 3.2 gives you tool-calling RAG with citations. Here's a complete pipeline against your own document store.
Build a production RAG agent with LangChain, then measure faithfulness, answer relevance, and context precision with RAGAS. The four metrics that matter and how to wire them up.
© 2026 CallSphere Inc. All rights reserved.
Made within San Francisco
Watch how CallSphere handles real customer calls, schedules appointments, and processes payments — live.
Try Live DemoBook a DemoCalculate Your ROI