Back to Insights
AI & Machine LearningOptimizing LLM Context Windows: Implementing Lossless Compression Strategies for RAG Agentsdeep diveJuly 27, 202612 min read

Optimizing LLM Context Windows: Implementing Lossless Compression Strategies for RAG Agents

A deep technical guide to reducing RAG token costs and latency using semantic hashing, vector-based summarization, and lossless compression techniques.

T
Tamiz UddinFull-Stack Engineer

The context window is the single most expensive bottleneck in modern Retrieval-Augmented Generation (RAG) architectures. As Large Language Models (LLMs) evolve from 8k to 128k+ token limits, developers often fall into the trap of assuming "bigger is always better." In production environments, however, bloated context windows inflate inference costs linearly, increase latency quadratically due to attention mechanism complexity, and frequently degrade output quality through the "lost in the middle" phenomenon.

This article explores advanced, lossless (or near-lossless) compression strategies specifically tailored for RAG pipelines. We will move beyond simple truncation and explore semantic hashing, vector-based summarization, and hierarchical retrieval methods that preserve critical information while drastically reducing token footprint.

The Cost of Raw Context

Before implementing compression, we must understand why raw context is inefficient. LLMs use a Transformer architecture with self-attention. The computational complexity of the attention layer is $O(N^2)$, where $N$ is the sequence length. Doubling the context window does not just double the cost; it quadruples the computational load for the attention weights, even if the feed-forward networks scale linearly.

Furthermore, LLMs exhibit a recency bias and a middle-loss bias. Information placed at the very beginning or very end of the context window is retrieved with high fidelity, while information in the middle is often ignored or hallucinated. A RAG system that dumps 50 relevant documents into the context without compression is likely performing worse than one that curates 5 highly dense, compressed documents.

Strategy 1: Semantic Hashing for Deduplication

One of the most effective lossless compression techniques in RAG is the removal of redundant information. In many datasets, multiple chunks contain overlapping semantic content. By using semantic hashing, we can identify and merge these chunks without losing information density.

How Semantic Hashing Works

Semantic hashing maps high-dimensional vectors (embeddings) to short binary codes. If two chunks have similar embeddings, their hash codes will be identical or very close. This allows us to group similar chunks and process them as a single unit.

Implementation in Python

Below is a practical implementation using sentence-transformers and datasketch for MinHashing, a technique for estimating Jaccard similarity between sets. While MinHash is traditionally for text sets, we can adapt it for semantic clusters by using embedding similarity as a proxy.

python
import numpy as np
from sentence_transformers import SentenceTransformer
from datasketch import MinHash, MinHashLSH
import hashlib

class SemanticDeduplicator:
    def __init__(self, threshold=0.85):
        self.model = SentenceTransformer('all-MiniLM-L6-v2')
        self.threshold = threshold
        self.lsh = MinHashLSH(threshold=threshold, num_perm=128)
        self.seen_hashes = set()
        self.chunks = []

    def get_minhash(self, text):
        m = MinHash(num_perm=128)
        for word in text.split():
            m.update(word.encode('utf-8'))
        return m

    def process_chunks(self, chunks):
        """
        Process a list of text chunks, returning only unique, non-redundant chunks.
        """
        unique_chunks = []
        embeddings = []
        
        # Step 1: Generate embeddings for all chunks
        for i, chunk in enumerate(chunks):
            embedding = self.model.encode(chunk)
            embeddings.append(embedding)
            
            # Step 2: Check for semantic similarity using cosine distance
            is_duplicate = False
            if unique_chunks:
                for j, existing_chunk in enumerate(unique_chunks):
                    existing_embedding = embeddings[j] # We need to store these properly
                    # Calculate cosine similarity
                    similarity = np.dot(embedding, existing_embedding) / (np.linalg.norm(embedding) * np.linalg.norm(existing_embedding))
                    
                    if similarity > self.threshold:
                        is_duplicate = True
                        break
                
            if not is_duplicate:
                unique_chunks.append(chunk)
                # Note: In a production system, you would store the embedding 
                # to avoid re-encoding on subsequent calls
                
        return unique_chunks

Why This is Lossless

This approach is lossless because we are not altering the text of the unique chunks. We are merely removing chunks that are semantically redundant. The information content of the duplicate chunk is already present in the unique chunk it is compared against.

Strategy 2: Vector-Based Summarization

When deduplication is insufficient, we need to compress individual chunks. Traditional summarization (e.g., using a small LLM to rewrite a chunk) is lossy and can introduce hallucinations. A more robust approach is vector-based summarization, where we compress the semantic meaning of a chunk into a dense vector representation that can be used for retrieval, but we keep the original text for generation.

However, for true context window optimization, we can use Embedding Compression to select the most informative sentences within a chunk.

Sentence-Level Importance Scoring

Instead of summarizing the whole chunk, we score each sentence based on its relevance to the query embedding. We then truncate the chunk to only include the top-k most relevant sentences.

python
from sklearn.metrics.pairwise import cosine_similarity

def compress_chunk_with_query(chunk_text, query_embedding, top_k=3):
    """
    Compress a chunk by selecting only the top-k sentences most similar to the query.
    """
    from sentence_transformers import SentenceTransformer
    
    model = SentenceTransformer('all-MiniLM-L6-v2')
    sentences = chunk_text.split('. ')
    
    # Handle edge case where sentences might not end with period
    if len(sentences) == 1:
        return chunk_text
        
    # Encode all sentences
    sentence_embeddings = model.encode(sentences)
    
    # Calculate similarity to query
    similarities = cosine_similarity([query_embedding], sentence_embeddings)[0]
    
    # Get indices of top-k most similar sentences
    top_indices = np.argsort(similarities)[-top_k:][::-1]
    
    # Reconstruct the chunk with only the selected sentences
    selected_sentences = [sentences[i] for i in top_indices]
    compressed_chunk = '. '.join(selected_sentences) + '.'
    
    return compressed_chunk

Trade-offs

This method is lossy in the strictest sense, as it discards sentences. However, it is information-preserving with respect to the query. If the goal is to answer a specific question, the discarded sentences are likely irrelevant noise. This is a form of query-aware compression.

For long documents, a flat retrieval strategy is inefficient. Hierarchical retrieval involves creating a tree of summaries. At the top level, we have a summary of the entire document. At the leaf level, we have the raw text chunks.

The Process

  1. Indexing:
    • Create a summary of the entire document using an LLM.
    • Embed the summary.
    • Embed all individual chunks.
  2. Retrieval:
    • First, retrieve the most relevant document summaries based on the user query.
    • Then, retrieve the most relevant chunks within those selected documents.
  3. Compression:
    • If the retrieved chunks are still too long, apply sentence-level importance scoring (Strategy 2).

This approach reduces the search space and ensures that only the most relevant parts of the document are included in the context window.

Implementation Architecture

python
class HierarchicalRetriever:
    def __init__(self, llm, vector_store):
        self.llm = llm
        self.vector_store = vector_store

    def index_document(self, doc_id, text):
        # 1. Create Summary
        summary = self.llm.generate(f"Summarize the following text in 3 sentences: {text}")
        
        # 2. Embed Summary
        summary_embedding = self.vector_store.embed(summary)
        
        # 3. Store Summary
        self.vector_store.add(doc_id, summary_embedding, metadata={"type": "summary", "content": summary})
        
        # 4. Chunk and Embed
        chunks = self.chunk_text(text)
        for i, chunk in enumerate(chunks):
            chunk_embedding = self.vector_store.embed(chunk)
            self.vector_store.add(f"{doc_id}_chunk_{i}", chunk_embedding, metadata={"type": "chunk", "content": chunk, "parent_doc": doc_id})

    def retrieve(self, query, top_k_summary=1, top_k_chunk=5):
        # 1. Retrieve relevant summaries
        summary_results = self.vector_store.query(query, top_k=top_k_summary, filter_type="summary")
        
        # 2. Retrieve chunks from relevant documents
        relevant_doc_ids = [res.metadata["parent_doc"] for res in summary_results]
        chunk_results = []
        for doc_id in relevant_doc_ids:
            # Retrieve chunks, but limit to top_k_chunk per document
            doc_chunks = self.vector_store.query(query, top_k=top_k_chunk, filter_type="chunk", filter_doc_id=doc_id)
            chunk_results.extend(doc_chunks)
            
        return chunk_results

Strategy 4: Token-Efficient Encoding

A often-overlooked aspect of context window optimization is the encoding itself. Standard tokenizers (like the one used in GPT-4) are not always the most token-efficient for specific domains.

Custom Tokenizers

For highly technical domains (e.g., code, legal, medical), training a custom tokenizer on your specific corpus can reduce token count by 10-20%. This is because the custom tokenizer will have a higher frequency of domain-specific subwords, reducing the need for multi-token representations.

Example: Code-Specific Tokenization

If your RAG system handles code, using a tokenizer like tiktoken's cl100k_base is standard. However, if you are dealing with Python, you might benefit from a tokenizer that understands Python syntax keywords as single tokens.

Strategy 5: Metadata-Driven Filtering

Before any compression or retrieval occurs, metadata filtering can drastically reduce the amount of data entering the context window. This is a form of pre-retrieval compression.

How It Works

Every chunk should be enriched with metadata: document type, date, author, section header, etc. When a user asks a question, you can filter the vector store based on these metadata fields before performing semantic search.

Example Query

"What are the security implications of the new API in Q4 2023?"

Instead of searching the entire vector store, you filter for:

  • date >= 2023-10-01
  • document_type == "security_report"
  • section_header == "API"

This reduces the search space by orders of magnitude, allowing you to retrieve fewer chunks with higher precision, thus requiring less context window space.

Comparing Compression Strategies

StrategyLossless?ComplexityBest Use CaseToken Reduction
Semantic DeduplicationYesLowLarge, redundant datasets20-40%
Sentence-Level TruncationNo (Lossy)MediumQuery-specific answers50-70%
Hierarchical RetrievalYesHighLong documents, deep context30-60%
Custom TokenizersYesHighDomain-specific text (code, legal)10-20%
Metadata FilteringYesLowStructured documentsVaries

Production Best Practices

  1. Monitor Token Usage: Implement strict monitoring of token consumption per query. Alert when context windows exceed 80% of the model's limit.
  2. Evaluate Quality: Use RAG evaluation frameworks (like RAGAS or TruEra) to measure the impact of compression on answer fidelity. A 50% token reduction is useless if the accuracy drops by 10%.
  3. Cache Compressed Chunks: Pre-compress and cache chunks during indexing. Do not perform compression at query time unless necessary.
  4. Use Hybrid Search: Combine vector search with keyword search (BM25) to improve retrieval precision, reducing the need for large context windows.

Conclusion

Optimizing LLM context windows is not just about saving money; it is about improving the quality and reliability of RAG systems. By implementing lossless compression strategies like semantic deduplication, hierarchical retrieval, and metadata filtering, you can build more efficient, scalable, and accurate AI agents.

The key is to balance compression with information preservation. Always evaluate the trade-off between token savings and answer accuracy. As LLMs continue to evolve, these compression techniques will become increasingly important for managing the growing complexity of AI-driven applications.

For more insights on building scalable AI systems, check out Tamiz's Insights for the latest trends in AI engineering.

Frequently Asked Questions

Q: Is semantic hashing truly lossless? A: Yes, in the context of RAG, it is lossless because it removes redundant chunks whose information is already present in the unique chunks. No new information is created, and no existing information is altered.

Q: How do I choose the right compression strategy? A: Start with metadata filtering and semantic deduplication as they are low-complexity and high-impact. If you still face context window limits, move to hierarchical retrieval and sentence-level truncation.

Q: Can I use these strategies with any LLM? A: Yes, these strategies are model-agnostic. They operate on the text and embeddings before they are sent to the LLM, so they work with any model that supports vector search and text processing.