Pavan Rangani

HomeBlogAdvanced RAG Chunking Strategies: Semantic, Agentic, and Graph-Based Approaches

Advanced RAG Chunking Strategies: Semantic, Agentic, and Graph-Based Approaches

By Pavan Rangani · April 6, 2026 · AI & ML

Advanced RAG Chunking Strategies: Semantic, Agentic, and Graph-Based Approaches

Advanced RAG Chunking Strategies for Production AI Systems

RAG chunking strategies are the foundation of effective Retrieval-Augmented Generation systems. The way you split, embed, and retrieve documents determines the quality of LLM responses more than any other factor. Therefore, mastering advanced chunking techniques is essential for building production-grade AI applications that deliver accurate, contextually relevant answers.

Traditional fixed-size chunking splits documents at arbitrary character or token boundaries, often breaking sentences mid-thought and losing semantic coherence. Moreover, naive chunking ignores document structure, treating headers, code blocks, and tables the same as regular paragraphs. Consequently, retrieval quality suffers because the chunks don't represent meaningful units of information. This guide explores advanced strategies that dramatically improve retrieval accuracy.

RAG Chunking Strategies: Semantic Chunking

Semantic chunking uses embedding similarity to determine natural breakpoints in text. Instead of splitting at fixed intervals, it identifies where the topic shifts by comparing the semantic similarity of consecutive sentences. Furthermore, this approach preserves the natural flow of information, ensuring each chunk contains a complete thought or concept.

import numpy as np
from sentence_transformers import SentenceTransformer

class SemanticChunker:
    def __init__(self, model_name='all-MiniLM-L6-v2', threshold=0.3):
        self.model = SentenceTransformer(model_name)
        self.threshold = threshold

    def chunk(self, text, min_size=100, max_size=1000):
        sentences = self._split_sentences(text)
        embeddings = self.model.encode(sentences)

        chunks = []
        current_chunk = [sentences[0]]
        current_embedding = embeddings[0]

        for i in range(1, len(sentences)):
            similarity = np.dot(current_embedding, embeddings[i]) / (
                np.linalg.norm(current_embedding) * np.linalg.norm(embeddings[i])
            )

            chunk_text = ' '.join(current_chunk)
            if similarity < self.threshold and len(chunk_text) >= min_size:
                chunks.append(chunk_text)
                current_chunk = [sentences[i]]
                current_embedding = embeddings[i]
            elif len(chunk_text) >= max_size:
                chunks.append(chunk_text)
                current_chunk = [sentences[i]]
                current_embedding = embeddings[i]
            else:
                current_chunk.append(sentences[i])
                current_embedding = np.mean(
                    [current_embedding, embeddings[i]], axis=0
                )

        if current_chunk:
            chunks.append(' '.join(current_chunk))
        return chunks

    def _split_sentences(self, text):
        import re
        return [s.strip() for s in re.split(r'(?<=[.!?])\s+', text) if s.strip()]

The threshold parameter is the knob that decides everything here, and it deserves tuning rather than a copied default. A high threshold produces many small, tightly focused chunks that retrieve precisely but may lack surrounding context; a low threshold produces fewer, broader chunks that carry context but dilute relevance. In practice, teams sweep the threshold against a labeled evaluation set and pick the value that maximizes retrieval recall without inflating chunk size beyond the embedding model's effective context window.

AI semantic analysis and chunking visualization
Semantic chunking identifies natural topic boundaries for more coherent retrieval

Structure-Aware Chunking and Overlap

Before reaching for embeddings, exploit the structure the document already gives you. Markdown headers, HTML sections, code fences, and table boundaries are explicit signals about where one idea ends and another begins. A structure-aware splitter respects these boundaries first, then falls back to semantic or size-based splitting only within a section, which prevents the classic failure of slicing a code block in half.

Overlap is the second lever that quietly fixes a large share of retrieval misses. By repeating a window of tokens between adjacent chunks, you ensure that a fact straddling a boundary still appears intact in at least one chunk. A common starting point is a 10 to 15 percent overlap relative to chunk size.

from langchain.text_splitter import RecursiveCharacterTextSplitter

# Respect natural boundaries first, then fall back to characters
splitter = RecursiveCharacterTextSplitter(
    chunk_size=800,
    chunk_overlap=120,          # ~15% overlap preserves cross-boundary facts
    separators=["\n## ", "\n### ", "\n\n", "\n", ". ", " "],
    keep_separator=True,
)
chunks = splitter.split_text(markdown_document)

Notice the ordering of separators: the splitter tries to break on headings before paragraphs, and on paragraphs before raw spaces. This hierarchy is what makes the chunks align with how a human would naturally divide the document, and it costs nothing at query time.

Agentic RAG: Self-Improving Retrieval

Agentic RAG introduces an intelligent agent that decides how to retrieve, filter, and combine chunks based on the query complexity. Instead of a simple vector search, the agent can reformulate queries, perform multi-hop retrieval, and validate retrieved chunks before passing them to the LLM. Additionally, the agent learns from feedback to improve retrieval quality over time.

from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.tools import tool

@tool
def vector_search(query: str, top_k: int = 5) -> list:
    """Search the vector store for relevant document chunks."""
    results = vectorstore.similarity_search_with_score(query, k=top_k)
    return [(doc.page_content, score) for doc, score in results]

@tool
def graph_search(entity: str) -> list:
    """Search the knowledge graph for entity relationships."""
    query = f"""
    MATCH (e:Entity {{name: '{entity}'}})-[r]->(related)
    RETURN e.name, type(r), related.name, related.description
    LIMIT 10
    """
    return neo4j_driver.execute_query(query)

@tool
def rerank_results(query: str, documents: list) -> list:
    """Rerank documents using a cross-encoder model."""
    from sentence_transformers import CrossEncoder
    reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-12-v2')
    pairs = [[query, doc] for doc in documents]
    scores = reranker.predict(pairs)
    ranked = sorted(zip(documents, scores), key=lambda x: x[1], reverse=True)
    return [doc for doc, score in ranked[:3]]

# Create agentic RAG pipeline
agent = create_openai_tools_agent(
    llm=ChatOpenAI(model="gpt-4-turbo"),
    tools=[vector_search, graph_search, rerank_results],
    prompt=rag_agent_prompt
)
executor = AgentExecutor(agent=agent, tools=[vector_search, graph_search, rerank_results])

The reranking tool above is worth singling out, because a cross-encoder reranker is frequently the highest-return addition to a RAG pipeline. Vector search optimizes for speed by comparing pre-computed embeddings, which is approximate; a cross-encoder reads the query and each candidate chunk together and scores their true relevance. Retrieving the top 20 by vector similarity and then reranking down to the top 3 typically lifts answer quality far more than swapping the underlying embedding model.

Graph-Based Chunking and Retrieval

Graph-based approaches represent documents as interconnected nodes, preserving relationships between concepts, entities, and sections. This technique excels when answers require synthesizing information from multiple parts of a document or across multiple documents. Furthermore, graph structures naturally capture hierarchical relationships like chapter-section-paragraph that flat vector stores lose.

from neo4j import GraphDatabase

class GraphRAG:
    def __init__(self, uri, auth):
        self.driver = GraphDatabase.driver(uri, auth=auth)

    def ingest_document(self, doc_id, chunks, entities):
        with self.driver.session() as session:
            # Create document node
            session.run(
                "CREATE (d:Document {id: $id, title: $title})",
                id=doc_id, title=chunks[0].metadata.get('title', '')
            )
            # Create chunk nodes with embeddings
            for i, chunk in enumerate(chunks):
                session.run("""
                    MATCH (d:Document {id: $doc_id})
                    CREATE (c:Chunk {
                        id: $chunk_id, content: $content,
                        embedding: $embedding, position: $pos
                    })
                    CREATE (d)-[:CONTAINS]->(c)
                """, doc_id=doc_id, chunk_id=f"{doc_id}_chunk_{i}",
                   content=chunk.page_content,
                   embedding=chunk.metadata['embedding'],
                   pos=i)

            # Create entity relationships
            for entity in entities:
                session.run("""
                    MERGE (e:Entity {name: $name, type: $type})
                    WITH e
                    MATCH (c:Chunk {id: $chunk_id})
                    CREATE (c)-[:MENTIONS]->(e)
                """, name=entity['name'], type=entity['type'],
                   chunk_id=entity['chunk_id'])

    def hybrid_search(self, query, top_k=5):
        with self.driver.session() as session:
            # Combine vector similarity with graph traversal
            results = session.run("""
                CALL db.index.vector.queryNodes('chunk_embeddings', $k, $embedding)
                YIELD node, score
                OPTIONAL MATCH (node)-[:MENTIONS]->(e:Entity)<-[:MENTIONS]-(related:Chunk)
                WHERE related <> node
                RETURN node.content AS content, score,
                       collect(DISTINCT related.content)[..2] AS related_chunks,
                       collect(DISTINCT e.name) AS entities
                ORDER BY score DESC
                LIMIT $k
            """, k=top_k, embedding=self.embed(query))
            return list(results)

The payoff of this design is multi-hop reasoning. A flat vector store answers "what is X" well but struggles with "how does X relate to Y," because the connecting facts live in separate chunks that never co-occur. By following MENTIONS edges to entities shared across chunks, the graph query pulls in the related context automatically. The cost is real, though: maintaining the graph, extracting entities reliably, and keeping embeddings in sync adds operational weight that only pays off for genuinely relationship-heavy corpora.

Knowledge graph visualization for RAG systems
Graph-based RAG captures entity relationships that flat vector stores miss

Hybrid Chunking: Combining Multiple Strategies

Production RAG systems rarely rely on a single chunking strategy. Instead, they combine semantic chunking for prose content, structure-aware chunking for technical documents, and graph-based retrieval for entity-rich queries. Additionally, a reranking step using cross-encoder models ensures the most relevant chunks are selected regardless of the initial retrieval method.

A pragmatic hybrid also blends sparse and dense retrieval. Keyword-based BM25 search reliably catches exact identifiers — error codes, function names, SKUs — that embeddings sometimes blur together, while dense vector search captures paraphrased intent. Fusing both result sets with a method such as reciprocal rank fusion gives you the precision of keywords and the recall of semantics without committing to either alone.

Measuring and Tuning Retrieval Quality

You cannot improve a chunking pipeline you do not measure. Before changing strategies, build a small evaluation set of representative questions paired with the passages that truly answer them, then track retrieval-level metrics rather than only end-to-end answer quality. Context recall (did the right passage make it into the retrieved set) and context precision (how much retrieved text was actually relevant) localize problems far better than eyeballing final answers.

With those metrics in place, tuning becomes systematic instead of superstitious. If recall is low, increase top_k or overlap, or revisit chunk boundaries; if precision is low, add reranking or tighten the semantic threshold. Frameworks like RAGAS and TruLens automate much of this scoring, and our companion guide on RAG evaluation frameworks covers wiring them into a continuous test loop.

When NOT to Over-Engineer Chunking

Advanced strategies are not always warranted, and reaching for an agentic graph pipeline on day one is a common way to ship nothing. If your corpus is small, homogeneous prose, a recursive splitter with sensible overlap and a good embedding model will get you most of the way, and the engineering you save is better spent on evaluation. Semantic chunking adds embedding cost at ingestion time, and graph-based retrieval adds an entire database to operate.

The honest trade-off is complexity against marginal accuracy. Multi-hop graph retrieval shines on interconnected technical or legal corpora but adds little to a FAQ bot, where most questions map to a single passage. Therefore, start simple, measure, and add sophistication only where the metrics prove a real gap. Premature optimization of a retrieval pipeline burns time you could spend improving the data itself.

Key Takeaways

  • Start with a solid foundation and build incrementally based on your requirements
  • Test thoroughly in staging before deploying to production environments
  • Monitor performance metrics and iterate based on real-world data
  • Follow security best practices and keep dependencies up to date
  • Document architectural decisions for future team members

The key insight is matching the chunking strategy to the content type. Code documentation benefits from function-level chunks with docstrings preserved. Legal documents need paragraph-level chunks with section references maintained. Conversational content works best with dialogue-turn chunks. Therefore, a content-aware chunking pipeline that automatically selects the right strategy yields the best results. See LlamaIndex RAG optimization docs for more patterns.

Data processing pipeline architecture
Hybrid chunking pipelines combine multiple strategies for optimal retrieval accuracy

In conclusion, advanced RAG chunking strategies are the differentiator between AI applications that deliver accurate answers and those that hallucinate or miss context. Start with semantic chunking as your baseline, add graph-based retrieval for entity-heavy domains, and implement agentic RAG for complex multi-hop queries. Monitor retrieval quality metrics continuously and iterate on your chunking pipeline.

← Back to all articles