Skip to content
Beyond Basic RAG: Optimizing LLM Context for Long Documents & Complex Queries
AI Engineering & Developer Tooling

Beyond Basic RAG: Optimizing LLM Context for Long Documents & Complex Queries

7 min read
RAGLLM OptimizationContext WindowLangChainVector Databases

Basic RAG struggles with information overload and context window limits in long documents, leading to irrelevant LLM responses and increased costs. Discover advanced strategies like hierarchical retrieval and query decomposition to refine context, ensuring accurate, cost-effective LLM interactions.

Introduction & The Problem

As Large Language Models (LLMs) become central to enterprise applications, developers often turn to Retrieval Augmented Generation (RAG) to ground LLM responses in proprietary data. The basic RAG pattern—chunking documents, embedding chunks, and retrieving top-k similar chunks—works well for straightforward queries and moderately sized documents. However, this approach quickly falters when dealing with lengthy, complex documents (like financial reports, legal contracts, or extensive technical manuals) or multi-faceted user queries.

The core problem stems from context window limitations and information overload. Naive chunking often breaks apart critical semantic relationships, leading to a disconnected understanding. When a query requires synthesizing information from disparate sections of a long document, or if it's inherently complex, basic RAG frequently supplies irrelevant or insufficient context to the LLM. This results in:

  • Irrelevant or Hallucinated Responses: The LLM either doesn't find the necessary information or fabricates answers due to poor context.
  • Increased Operational Costs: Sending an overly large or noisy context window to the LLM consumes more tokens, escalating API costs.
  • Poor User Experience: Users receive inaccurate, incomplete, or slow answers, eroding trust in the AI system.
  • Developer Frustration: Debugging context issues in RAG pipelines is notoriously difficult, wasting valuable engineering time.

These consequences are not just technical inconveniences; they directly impact business value, leading to missed opportunities, inefficient operations, and a failure to leverage LLMs effectively for critical tasks.

The Solution Concept & Architecture

To move beyond basic RAG, we must implement sophisticated strategies for context optimization. This involves intelligent retrieval, dynamic context assembly, and a multi-stage approach to query processing. The core concepts include:

  1. Hierarchical or Parent-Child Chunking: Instead of a single chunk size, we create two layers: small, overlapping child chunks used for efficient retrieval, and larger parent chunks (which the child chunks originate from) used for providing richer context to the LLM. When a child chunk is retrieved, its full parent chunk is sent to the LLM, ensuring complete local context.
  2. Query Decomposition/Rewriting: Complex user queries are often atomic within themselves. We can use an LLM to break down a single complex query into several simpler sub-queries. Each sub-query can then be used to retrieve relevant chunks independently.
  3. Multi-Query Retrieval: Once a query is decomposed, each sub-query can perform its own retrieval, broadening the scope of relevant information found.
  4. Contextual Compression & Reranking: After initial retrieval, we can employ techniques like LLM-based summarization of retrieved documents or a dedicated reranking model (e.g., a cross-encoder) to filter out less relevant chunks and prioritize the most important ones before passing them to the final LLM call.

An advanced RAG architecture would involve:

  • Document Processing Layer: Responsible for hierarchical chunking and embedding.
  • Vector Store: Stores embeddings of child chunks, mapped to their parent documents.
  • Query Processing Module: Utilizes an LLM for query decomposition and potentially contextual compression.
  • Retrieval Orchestration Layer: Manages parallel retrieval for sub-queries, reconstructs parent contexts, and applies reranking.
  • LLM Generation Layer: Receives the highly optimized and relevant context to generate the final response.

Step-by-Step Implementation

Let's illustrate an advanced RAG pattern using a combination of query decomposition and parent-child (small-to-large) retrieval with LangChain. This approach ensures that even if a sub-query matches a small piece of information, the LLM receives the broader context it needs.

PYTHON
# Step 1: Document Loading and Hierarchical Chunking
# We'll use a "small-to-large" or "parent-child" chunking strategy.
# Small chunks are used for initial retrieval, larger chunks for context sent to the LLM.
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.schema import Document
from langchain_community.embeddings import OpenAIEmbeddings # Or any other embedding model
from langchain_community.vectorstores import Chroma # Or any other vector store
import hashlib # For creating stable hashes for parent content

# Assume 'long_document_text' is loaded from a file or database
long_document_text = """
The company's Q3 financial report revealed robust growth in its cloud computing division, 'Aurora Prime', contributing 45% of total revenue.
Operating costs increased by 12% due to significant investments in AI research and development, particularly for Project Nexus, an ambitious initiative
aiming to revolutionize predictive analytics. Customer acquisition costs (CAC) for Aurora Prime
remained stable at $120 per new subscriber, a key metric indicating efficient marketing strategies.
The report also highlighted successful market penetration in Southeast Asia, with a 30% year-over-year increase
in active users. The CEO, Jane Doe, emphasized the company's commitment to sustainable growth and
innovation during the investor call, specifically mentioning the upcoming 'Quantum Leap' accelerator program
for startups. The legal department is currently reviewing new data privacy regulations in Europe,
which may impact future data handling policies.
"""

# Define splitters for parent (large) chunks and child (small) chunks
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=20)

parent_documents = parent_splitter.create_documents([long_document_text])
all_child_documents = []
# Create a mapping from a hash of the parent content to the parent's full content.
# This helps retrieve the full parent text given a child's metadata.
child_to_parent_map = {}

for parent_doc in parent_documents:
    # Generate a stable hash for the parent document's content
    parent_content_hash = hashlib.sha256(parent_doc.page_content.encode('utf-8')).hexdigest()
    child_to_parent_map[parent_content_hash] = parent_doc.page_content
    
    # Split each parent document into smaller child chunks for retrieval
    child_chunks_for_parent = child_splitter.split_text(parent_doc.page_content)
    for child_chunk_text in child_chunks_for_parent:
        # Attach parent content hash to child document metadata for lookup
        child_doc = Document(page_content=child_chunk_text, metadata={"parent_content_hash": parent_content_hash})
        all_child_documents.append(child_doc)

# Initialize embeddings and vector store with child documents
embeddings = OpenAIEmbeddings() # Remember to set your OPENAI_API_KEY environment variable
vectorstore = Chroma.from_documents(all_child_documents, embeddings)

# Step 2: Query Decomposition
# Use an LLM to break down a complex user query into smaller, atomic sub-queries.
from langchain.chat_models import ChatOpenAI # Or any other LLM
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate

llm = ChatOpenAI(temperature=0) # Low temperature for consistent decomposition

decomposition_prompt_template = """Your task is to break down a complex user query into smaller, atomic sub-queries that can be answered independently.
Focus on extracting distinct pieces of information requested.

Original Query: {query}

Provide the sub-queries as a comma-separated list.
Example: "What is the capital of France and who painted the Mona Lisa?" -> "What is the capital of France?, Who painted the Mona Lisa?"
"""
decomposition_prompt = PromptTemplate(template=decomposition_prompt_template, input_variables=["query"])
decomposition_chain = LLMChain(llm=llm, prompt=decomposition_prompt)

def get_sub_queries(complex_query: str) -> list[str]:
    """Decomposes a complex query into sub-queries using an LLM."""
    response = decomposition_chain.run(query=complex_query)
    return [q.strip() for q in response.split(',') if q.strip()]

# Step 3: Hybrid Retrieval and Context Assembly
def retrieve_and_assemble_context(complex_query: str, k: int = 3) -> str:
    """
    Performs query decomposition, retrieves small child chunks for each sub-query,
    then reconstructs the context from their larger parent documents.
    """
    sub_queries = get_sub_queries(complex_query)
    retrieved_parent_contents = set() # Use a set to avoid duplicate parent documents

    for sq in sub_queries:
        # Retrieve 'k' small child chunks for each sub-query
        retrieved_child_docs = vectorstore.similarity_search(sq, k=k)
        for child_doc in retrieved_child_docs:
            parent_content_hash = child_doc.metadata.get("parent_content_hash")
            if parent_content_hash and parent_content_hash in child_to_parent_map:
                retrieved_parent_contents.add(child_to_parent_map[parent_content_hash])

    # Combine unique parent document contents into a single context string
    # This combined string is then passed to the final LLM call.
    combined_context = "\n\n".join(list(retrieved_parent_contents))
    return combined_context

# Example Usage
user_query = "What were the key financial highlights from Q3, how did AI investments impact costs, and what did the CEO mention about future initiatives?"
assembled_context = retrieve_and_assemble_context(user_query)

# Step 4: Final LLM call with refined context
# The final prompt includes the assembled, highly relevant context.
final_prompt_template = """Based on the following context, answer the user's query comprehensively and accurately.

Context:
{context}

User Query: {query}

Answer:
"""
final_prompt = PromptTemplate(template=final_prompt_template, input_variables=["context", "query"])
final_chain = LLMChain(llm=llm, prompt=final_prompt)

# Execute the final chain to get the answer
# final_answer = final_chain.run(context=assembled_context, query=user_query)
# print(final_answer)

# Note: Ensure you have your OpenAI API key set as an environment variable (OPENAI_API_KEY)
# for `OpenAIEmbeddings` and `ChatOpenAI` to function correctly.

Code Explanation:

  1. Hierarchical Chunking: We define two RecursiveCharacterTextSplitter instances. parent_splitter creates larger chunks that preserve more context, while child_splitter creates smaller, more focused chunks ideal for vector similarity search. Each child chunk's metadata stores a hash pointing back to its original parent document.
  2. Vector Store: Only the *child chunks* are embedded and stored in the vector database (Chroma in this case). This keeps the index lean and retrieval precise.
  3. Query Decomposition: A dedicated LLMChain uses a specific prompt to break down a complex user query into a list of simpler sub-queries. This ensures all facets of the original query are addressed.
  4. Hybrid Retrieval: For each sub-query, we perform a similarity search on the vector store of child chunks. Instead of directly using these small child chunks, we retrieve their corresponding *full parent documents* using the stored hash mapping. This guarantees that the LLM receives ample surrounding context.
  5. Context Assembly: All unique parent documents retrieved across all sub-queries are combined into a single, comprehensive context string, ready for the final LLM prompt.

Optimization & Best Practices

Implementing the described architecture is a significant step, but further optimizations can refine performance, cost, and accuracy:

  • Reranking with Cross-Encoders: After retrieving a set of parent documents, use a dedicated reranker model (e.g., Cohere Rerank, Sentence Transformers cross-encoders) to re-score the relevance of these documents against the *original complex query*. This step acts as a filter, ensuring only the most pertinent information is passed to the final LLM, significantly improving context quality.
  • Adaptive Chunking Strategies: Explore dynamic chunking that considers document structure (e.g., section headings, paragraphs). Tools like LlamaIndex offer more advanced strategies for parsing structured documents.
  • Contextual Compression: For extremely long parent documents, consider an additional LLM call to summarize or extract key sentences from the retrieved parent documents *before* sending them to the final generation LLM. This reduces token usage while preserving core information.
  • Caching: Implement caching for embedding lookups and repeated query decompositions. Many complex queries might have common sub-queries, allowing for significant efficiency gains.
  • Hybrid Search: Combine vector similarity search with keyword search (e.g., BM25) for a more robust retrieval system, especially in domains where exact keyword matches are crucial.
  • Continuous Evaluation: Regularly evaluate your RAG pipeline using metrics like context recall, context precision, faithfulness, and answer relevance (e.g., using RAGAS framework). This helps identify bottlenecks and areas for improvement.
  • Asynchronous Processing: For query decomposition and parallel retrieval, use asynchronous programming to speed up response times in production environments.

Business Impact & ROI

Investing in advanced RAG optimization delivers tangible business benefits beyond merely improving technical performance:

  • Enhanced Decision-Making (Accuracy & Trust): By ensuring LLMs receive accurate and comprehensive context, businesses can rely on AI-generated insights for critical decisions in legal, financial, or research domains. This reduces human effort in fact-checking and boosts confidence in AI outputs.
  • Significant Cost Reduction: Optimizing the context window size directly translates to fewer tokens consumed per LLM query. For applications with high query volumes, this can result in 30-50% reduction in LLM API costs, delivering a clear return on investment.
  • Superior User Experience & Retention: Users receive faster, more relevant, and less erroneous answers. This leads to higher user satisfaction, increased engagement with AI tools, and improved customer retention rates for AI-powered products (e.g., smart chatbots, intelligent search).
  • Scalability for Complex Use Cases: The ability to handle long, intricate documents and complex queries unlocks new business opportunities that were previously unfeasible with basic RAG. This includes advanced legal discovery, nuanced scientific research analysis, or comprehensive financial modeling, allowing the business to tackle more sophisticated problems with AI.
  • Increased Developer Productivity: A well-architected RAG system is more maintainable and predictable. Developers spend less time debugging context issues and more time building new features, leading to faster innovation cycles.

For example, a legal tech company implementing these strategies for contract analysis could reduce the time spent by lawyers reviewing documents by up to 70%, while simultaneously reducing AI hallucination rates to near zero, saving hundreds of thousands annually in operational costs and increasing client satisfaction.

Conclusion

Basic RAG is a foundational step, but enterprise-grade LLM applications demand more sophisticated context management. The challenges posed by long documents and complex queries—leading to irrelevant responses, escalating costs, and poor user experience—are significant roadblocks to real-world AI adoption.

By implementing advanced techniques like hierarchical chunking, intelligent query decomposition, and multi-stage retrieval with reranking, we can overcome these limitations. These strategies ensure that LLMs receive precise, rich, and optimized context, dramatically improving the accuracy, reliability, and cost-effectiveness of your AI systems. The transition from basic to advanced RAG isn't just a technical upgrade; it's a strategic move that unlocks greater business value, enhances user satisfaction, and prepares your applications for the next generation of AI-powered solutions.

Muhammad Tahir logo

Muhammad Tahir

Building web & mobile apps since 2021. Passionate about clean code and real-world impact.