Scaling RAG Systems: From Naive Retrieval to Agentic Chunking
Here is the uncomfortable truth about most RAG systems in production: the bottleneck is not your embedding model. It is not your vector database. It is not your prompt template. It is the 10 lines of code that run first — the part where you split a PDF into chunks and hope for the best.
Most tutorials hand you a RecursiveCharacterTextSplitter with a chunk size of 512 and tell you to proceed. For a prototype, that is fine. For a system processing thousands of documents and serving real users, that choice will quietly break your retrieval in ways that are extremely difficult to debug — because the wrong context will still flow back, just confidently and incorrectly.
This post is about the full progression: from naive fixed-size chunking, through semantic and structural methods, up to agentic chunking where an AI agent decides how your documents should be split. You will get benchmarks, working Python code for each approach, and a clear framework for deciding which strategy fits your actual problem.
💡 TL;DR - Chunking quality constrains retrieval accuracy more than embedding model choice. The difference between strategies is not marginal — it is the difference between systems that work and ones that don't. - Recursive character splitting (400–512 tokens, 10–20% overlap) is still the right default for most production systems. Start there. - Semantic chunking gives up to 70% retrieval improvement over naive baselines, but at higher compute cost. Use it for knowledge bases and technical documentation. - Agentic chunking — where an LLM dynamically decides the split strategy — delivers the highest accuracy for heterogeneous document sets, but costs 3–5x more per document to index. - Keep assembled context under 8K tokens per query. Context quality beats context volume, every time.
The 3 Generations of RAG Architecture
Before we get into chunking strategies, it helps to understand where chunking fits in the broader evolution of RAG systems. This is not just historical background — understanding which generation your system is in tells you which bottleneck to fix next.
- 1.Naive RAG (2020-2023): Fixed chunks, top-k retrieval, direct context stuffing. Simple. Fast. Brittle.
- 2.Advanced RAG (2023-2025): Query rewriting, hybrid search, reranking, parent-child chunks, HyDE. LlamaIndex & LangChain era.
- 3.Agentic RAG (2025-Now): Agents decide when and how to retrieve. Multi-step reasoning. Self-correcting. Dynamic chunking strategies.
Naive RAG follows a fixed "index → retrieve → generate" loop. It works for simple document Q&A. It fails when queries are complex, documents are heterogeneous, or retrieved chunks consistently cut through the middle of a logical idea.
Advanced RAG patched these problems with pre- and post-retrieval techniques. Query rewriting improved recall. Rerankers improved precision. Parent-child chunking solved the context-too-small problem. But it was still a static pipeline — it could not reason, could not decide to try a different retrieval strategy, and could not tell when its own retrieval had failed.
Agentic RAG is the current frontier. The retrieval process itself becomes autonomous: agents determine whether retrieval is needed, pick their retrieval source, evaluate whether results are good enough, and re-retrieve if they are not. Agentic chunking sits at the start of this pipeline — before any of that can happen, documents need to be chunked intelligently.
The Chunking Ladder — 4 Strategies Compared
Think of chunking strategies as a ladder. Each rung costs more but handles more complex documents and queries. Most production systems live somewhere in the middle. Almost no one should start at the top.
Fixed-Size / Recursive
Split by token count. The default in LangChain. Add 10-20% overlap to preserve cross-boundary context.
Semantic Chunking
Embed every sentence and split on cosine distance drops. Groups by meaning, not character count.
Structural / Propositional
Split on document structure (headers, sections) or use an LLM to extract atomic propositions.
Agentic Chunking
An LLM agent inspects the full document, decides the optimal strategy, executes it, and enriches chunks.
Level 1 — Recursive Splitting: Still the Right Default
Recursive character splitting is not naive — it is pragmatic. It tries to split on natural boundaries first (double newlines, then single newlines, then sentences, then words) and only falls back to arbitrary character cuts when necessary. At 400–512 tokens with 10–20% overlap, it produces chunks that are large enough for meaningful retrieval and small enough that the LLM does not lose the key sentence in the middle.
A February 2026 benchmark of seven strategies across 50 academic papers placed recursive 512-token splitting at 69% accuracy — first place in that study. That result should not make you lazy about upgrading. It should tell you to start here, measure your actual retrieval performance, and only invest in more expensive strategies when you have data showing they're needed.
from langchain.text_splitters import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
chunk_size=512,
chunk_overlap=51, # ~10% overlap to preserve context at boundaries
length_function=len,
separators=["\n\n", "\n", ".", " ", ""]
# tries double-newline first, falls back progressively
)
chunks = splitter.split_text(document_text)
# Each chunk: one coherent passage, bounded by natural text breaks
# Add metadata immediately — source, page, section heading
docs = splitter.create_documents(
[document_text],
metadatas=[{"source": "annual_report_2025.pdf", "section": "financials"}]
)⚠️ The metadata rule: Every chunk needs source metadata at creation time. Retrofitting metadata to chunks later is painful. Track at minimum: document source, section/page, chunk index, creation timestamp. You will need this for citation, debugging, and incremental re-indexing.
Level 2 — Semantic Chunking: Split by Meaning, Not by Count
The problem with fixed-size splitting is obvious once you state it: a 512-token boundary does not care about your document's logic. It will cheerfully split a sentence at the exact point where a key conclusion is being drawn, putting the first half in chunk 14 and the second half in chunk 15. Retrieve chunk 14 and your LLM gets an incomplete argument. The model does not know what it is missing.
Semantic chunking fixes this by embedding every sentence and splitting where cosine similarity drops sharply — the natural topic boundary. A peer-reviewed clinical decision support study found adaptive chunking aligned to logical topic boundaries hit 87% accuracy versus 13% for fixed-size baselines on the same corpus. That gap is not marginal.
The trade-off is compute cost. Semantic chunking requires embedding every sentence before deciding splits. For a 50-page document, that is hundreds of embedding calls at index time — not at query time, but still significant for large corpora.
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai import OpenAIEmbeddings
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
chunker = SemanticChunker(
embeddings,
breakpoint_threshold_type="percentile",
breakpoint_threshold_amount=95
# split where cosine distance is in the top 5% — i.e., biggest topic shifts
)
chunks = chunker.create_documents([document_text])
# Inspect average chunk size — semantic chunking can produce very short fragments
# on densely written technical text. If avg < 100 tokens, raise the threshold.
avg_tokens = sum(len(c.page_content.split()) for c in chunks) / len(chunks)
print(f"Avg chunk tokens: {avg_tokens:.0f}")🔍 Watch for micro-chunks. Semantic chunking sometimes produces fragments averaging 40–50 tokens on dense academic text. A 2026 benchmark found this on scientific papers — the chunker split so aggressively that individual chunks lost enough context to hurt retrieval. If your average chunk is under 100 tokens, increase the breakpoint threshold or switch to a sentence-level method with a minimum chunk size floor.
Level 3 — Structural and Propositional Chunking
For structured documents — Markdown files, HTML pages, PDFs with clear section headers — the best chunking signal is the document's own structure. Split on headings, not on arbitrary token counts. The document author already decided where the logical boundaries are.
from langchain.text_splitters import MarkdownHeaderTextSplitter
headers_to_split_on = [
("#", "h1"),
("##", "h2"),
("###", "h3"),
]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
chunks = splitter.split_text(markdown_document)
# Each chunk carries the full header path as metadata
# {"h1": "Architecture Guide", "h2": "Retrieval Layer", "h3": "Embedding Models"}
# This metadata becomes a filter at retrieval time — powerful for large doc setsPropositional chunking is the premium version of this idea. An LLM processes each paragraph and extracts atomic, self-contained propositions — individual factual claims. Each proposition becomes its own chunk. Retrieval becomes extremely precise because each chunk holds exactly one idea, and the embedding for that chunk is unambiguous.
import anthropic
client = anthropic.Anthropic()
def extract_propositions(paragraph: str) -> list[str]:
"""Use Claude to extract atomic propositions from a paragraph."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1000,
messages=[{
"role": "user",
"content": f"""Extract all atomic, self-contained factual claims from the
following paragraph. Each claim must be independently understandable without
reading the paragraph. Return one claim per line, no bullet points.
Paragraph:
{paragraph}"""
}]
)
return response.content[0].text.strip().split("\n")
# Input: "The embedding model converts text into high-dimensional vectors.
# These vectors capture semantic meaning. Cosine similarity is used to measure distance."
# Output: ["Embedding models convert text into high-dimensional vectors",
# "Vectors capture semantic meaning",
# "Cosine similarity measures distance between vectors"]💡 Key Insight: Propositional chunks are the highest-precision retrieval unit you can build. If you only retrieve 3 chunks per query, make sure each chunk holds exactly one idea. The fewer irrelevant tokens in your context window, the better your LLM's answer will be.
Level 4 — Agentic Chunking: The LLM Decides How to Split
Agentic chunking does not apply a single strategy. It applies an agent that looks at your document and decides which strategy — or combination of strategies — is right for that specific document. A Markdown file gets structural splitting. A dense research paper gets propositional extraction. A lightly formatted internal memo gets recursive splitting with semantic boundary detection. One ingestion agent. Many strategies. Applied intelligently.
This is the most powerful approach in the chunking ladder. It is also the most expensive. Agentic chunking requires LLM calls per document at index time — not just embedding calls. That costs 3–5x more than baseline RAG for knowledge graph extraction and at least 2–3x more for agentic chunking on average-sized corpora.
import anthropic
from langchain.text_splitters import (
RecursiveCharacterTextSplitter,
MarkdownHeaderTextSplitter
)
client = anthropic.Anthropic()
def detect_document_type(document_text: str) -> dict:
"""Agent step 1: Assess the document and recommend a strategy."""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=300,
messages=[{
"role": "user",
"content": f"""Analyze this document excerpt and return JSON only.
Fields:
- document_type: one of [markdown, academic, policy, conversational, code, mixed]
- has_clear_headers: true/false
- avg_paragraph_density: one of [sparse, medium, dense]
- recommended_strategy: one of [structural, semantic, propositional, recursive]
- recommended_chunk_size: integer tokens
Document excerpt (first 800 chars):
{document_text[:800]}"""
}]
)
import json
raw = response.content[0].text
return json.loads(raw)
def agentic_chunk(document_text: str) -> list:
"""Route each document to the right chunking strategy."""
assessment = detect_document_type(document_text)
strategy = assessment["recommended_strategy"]
if strategy == "structural" and assessment["has_clear_headers"]:
splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[("#", "h1"), ("##", "h2")]
)
return splitter.split_text(document_text)
elif strategy == "propositional":
# Extract propositions paragraph by paragraph
paragraphs = [p for p in document_text.split("\n\n") if p.strip()]
all_props = []
for para in paragraphs:
props = extract_propositions(para)
all_props.extend(props)
return all_props
else:
# Default: recursive with agent-recommended chunk size
size = assessment.get("recommended_chunk_size", 512)
splitter = RecursiveCharacterTextSplitter(
chunk_size=size, chunk_overlap=int(size * 0.12)
)
return splitter.split_text(document_text)The agent does three things: it inspects the document, it selects a strategy, and it executes it. For a mixed corpus — say, 40% Markdown documentation, 30% scanned PDF reports, and 30% customer support emails — an agentic chunker applies the right tool to each document type without you hard-coding a routing layer.
💡 Production consideration: Run agentic chunking at index time, not query time. The LLM calls happen once per document during ingestion. Retrieval at query time remains fast — you are just querying the intelligently pre-chunked vector store. The cost is in indexing, not in serving.
Benchmark — Strategies Side by Side
Here is every strategy compared on dimensions that matter for engineering decisions. Numbers draw from the February 2026 Vecta benchmark (50 academic papers), MDPI Bioengineering November 2025, and Chroma's July 2025 context research.
__BENCHMARK_TABLE__
The Full Pipeline — Where Chunking Lives
Chunking is one step in a larger pipeline. Here is where it sits relative to the other moving parts — and which layer to fix first when retrieval breaks.
__PIPELINE_SVG__
The pipeline has two phases: ingestion (chunking → embedding → indexing) and retrieval (query → rewrite → retrieve → rerank → generate). Fix chunking and you improve every downstream step. A chunk that is semantically coherent embeds better, retrieves more precisely, and gives the LLM cleaner context to reason over.
The Context Cliff — Why Smaller Often Wins
Here is a counterintuitive finding from Chroma's July 2025 research across 18 models including GPT-4.1, Claude 4, and Gemini 2.5: retrieval performance degrades as context length increases, even for models with million-token windows. A January 2026 systematic analysis identified a "context cliff" around 2,500 tokens where response quality drops sharply.
This is the "lost in the middle" effect. LLMs pay more attention to the beginning and end of their context window. Information buried in the middle of 50K tokens of retrieved text gets under-weighted. The model does not know it is missing something — it just produces a confident answer that misses the most relevant passage.
The practical rule: keep assembled context under 8K tokens per query. If you consistently hit that limit, your reranking threshold is too loose. Reduce the number of retrieved chunks or tighten the relevance score cutoff before generating. Fewer, better chunks beats more chunks every time.
__OPINION_BOX__
Three Things to Take Away
First: chunking quality sets the ceiling for your entire RAG system. You can tune your prompt, swap embedding models, and add a reranker — and all of those are worth doing — but if your chunks are semantically broken, none of it fixes the fundamental problem.
Second: the ladder is real, but start at the bottom. Recursive splitting at 400–512 tokens with overlap is not a compromise — it is a production-proven default that wins benchmarks. Upgrade when you have retrieval metrics that show you need to.
Third: the context cliff is real. Keep your assembled context under 8K tokens. Smaller, more precise context beats larger, noisier context for every model currently in production. The goal of chunking is not to preserve information — it is to surface exactly the right information at query time.