Performance Bottleneck Analysis of RAG Systems

A typical RAG request chain is: Embedding query → Vector retrieval → Document reranking → Context concatenation → LLM generation. In actual tests, vector retrieval and LLM generation each account for about 40% of the latency, while Embedding and reranking account for 20%. Optimization needs to start from each step, but the most valuable optimization point is caching—many queries are repeated or similar, and caching can reduce P99 latency from 3 seconds to less than 100 milliseconds.

Multi-Level Cache Architecture

Three levels of cache are recommended: L1-Exact Match Cache (identical queries → directly return cached result, using Redis String, TTL=1 hour), L2-Semantic Similarity Cache (queries with similarity > 0.95 → reuse cached result, using vector index + semantic deduplication), and L3-Document Fragment Cache (precomputed Embedding cache for high-frequency retrieved document fragments, using Redis Vector Set). L1 hit rate is about 15-25%, L2 hit rate is about 20-30%, and L3 can reduce retrieval latency by 50-70%.

Redis Cache in Practice

import redis, json, hashlib
import numpy as np
from openai import OpenAI

client = OpenAI(api_key="your-deepseek-api-key", base_url="https://api.deepseek.com")
redis_client = redis.Redis(host="localhost", port=6379, decode_responses=True)

class RAGCache:
    def __init__(self, similarity_threshold=0.95):
        self.threshold = similarity_threshold

    def _hash(self, text):
        return hashlib.md5(text.encode()).hexdigest()

    def get_exact(self, query):
        """L1: Exact match cache"""
        return redis_client.get(f"rag:exact:{self._hash(query)}")

    def set_exact(self, query, result, ttl=3600):
        redis_client.setex(f"rag:exact:{self._hash(query)}", ttl,
                           json.dumps(result, ensure_ascii=False))

    def get_semantic(self, query_embedding):
        """L2: Semantic similarity cache—find similar historical queries"""
        # Redis vector similarity search
        results = redis_client.ft("rag_semantic_idx").search(
            redis.commands.search.Query(
                f"*=>[KNN 1 @embedding $vec AS score]")
            .return_fields("query", "result", "score")
            .dialect(2),
            {"vec": np.array(query_embedding, dtype=np.float32).tobytes()}
        )
        if results.docs and float(results.docs[0].score) > self.threshold:
            return json.loads(results.docs[0].result)
        return None

    def embed_query(self, query):
        resp = client.embeddings.create(
            model="text-embedding-3-large", input=query)
        return resp.data[0].embedding

    def retrieve(self, query, retriever_func):
        """Retrieval with cache"""
        # L1 check
        cached = self.get_exact(query)
        if cached:
            return {"source": "L1-cache", "result": json.loads(cached)}
        # L2 check
        emb = self.embed_query(query)
        cached = self.get_semantic(emb)
        if cached:
            return {"source": "L2-cache", "result": cached}
        # Actual retrieval
        result = retriever_func(query)
        self.set_exact(query, result)
        return {"source": "retriever", "result": result}

cache = RAGCache()
# Usage: cache.retrieve("What is RAG?", my_retriever_func)

Batch Optimization and Asynchronous Processing

Batch Embedding: Combine multiple queries into one API call; the Embedding API supports batch input (up to 2048 entries), reducing Embedding stage latency by 90%. Asynchronous Retrieval: Vector retrieval and document reranking are IO-intensive operations; using asyncio to execute concurrently can reduce multi-path retrieval latency from serial N×t to approximately max(t). Connection Pooling: Use connection pools for Redis and vector databases to avoid frequent connection setup overhead. Preloading Hot Data: Based on statistics, preload the top 20% most frequently accessed documents into memory.

Cost Optimization

In addition to performance, caching also significantly reduces API call costs. Taking OpenAI Embedding as an example, text-embedding-3-large costs $0.13 per million tokens—seemingly cheap, but 100,000 queries per day costs over $5,000 annually. Exact cache can reduce Embedding calls by 15-25%, semantic cache additionally reduces by 20-30%, and the combination saves $2,000-3,000 per year. For high-frequency LLM generation calls, if caching the final reply (rather than intermediate retrieval results), the savings are even more significant.

Cache Invalidation Strategy and Consistency Guarantee

After introducing caching, RAG systems face a classic problem: cache consistency and timeliness. When knowledge base documents are updated, related cache entries need to be invalidated. Our solution is based on

Document version cache key — each document has an incrementing version number (doc_v), and the cache key changes from a simple query hash to query_hash+doc_version. When a document is updated, new retrieval results automatically use the new version number, and old version caches are naturally evicted after TTL expires. For scenarios with extremely high real-time requirements (such as RAG for stock market news), TTL is set to 5 minutes; for relatively stable scenarios (such as legal provisions), TTL can be set to 24 hours. We also implemented proactive cache warming — analyzing historical query logs to identify high-frequency query patterns, and proactively refreshing the cache for these queries after knowledge base updates. This strategy reduced the P95 latency during the "cache cold start" phase after knowledge base updates from 2.1 seconds (without warming) to 0.3 seconds (after warming), making the user experience almost imperceptible.

Cold Start Optimization for RAG Systems

When a RAG system is newly launched or after cache clearing, the first few thousand queries experience a "cold start"—each requiring the full Embedding + vector retrieval + LLM generation pipeline, with P99 latency potentially 10 times that of steady state. Our cold start optimization strategies: Warm-up script—extract the Top 1000 high-frequency queries from historical query logs (after desensitization), and use a warm-up script to batch execute these queries to populate the cache at service startup or after cache clearing; Progressive traffic ramp-up—after a new Pod starts, it first enters "warm-up mode" (receiving only 10% traffic), and only after cache hit rate reaches above 60% does it receive 100% traffic; Cache persistence—periodically dump the L1 exact-match cache to disk, and restore the cache from disk when a new Pod starts. With the warm-up mechanism, cold start P99 latency drops from 2.8 seconds to 0.5 seconds, significantly reducing the sense of disruption in user experience.

Implementation Details and Tuning of Semantic Cache

The core challenge of semantic cache (L2 cache) is identifying queries that are "similar but not identical." If the similarity threshold is set too high (>0.98), there are few hits; if set too low (<0.8), it may return irrelevant cached results. Our tuning experience: Threshold setting—for factual queries (e.g., "Python version"), a similarity threshold of 0.92 is sufficient; for open-ended queries (e.g., "write me a poem"), even a similarity of 0.98 should not use the cache—poems should be original. In implementation, we attach a "query_type" tag to each query and decide whether to allow semantic cache based on the type. Cache eviction strategy—dual eviction based on TTL + LFU (Least Frequently Used), where popular queries automatically extend TTL, and unpopular queries may be evicted even before TTL expires. This fine-grained strategy reduces the "bad hit rate" (returning irrelevant cache) of semantic cache from 8% to 1.5%.

Want to orchestrate this skill chain yourself?

Open in Skill Chain →