<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.kabirrajsingh.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://blog.kabirrajsingh.com/" rel="alternate" type="text/html" /><updated>2026-08-30T13:17:14+00:00</updated><id>https://blog.kabirrajsingh.com/feed.xml</id><title type="html">Kabir Raj Singh — Blog</title><subtitle>Notes on building production AI systems — RAG, agents, and evals.</subtitle><entry><title type="html">Production-Ready RAG Architecture: Core Patterns Explained</title><link href="https://blog.kabirrajsingh.com/production-ready-rag-architecture/" rel="alternate" type="text/html" title="Production-Ready RAG Architecture: Core Patterns Explained" /><published>2026-08-30T00:00:00+00:00</published><updated>2026-08-30T00:00:00+00:00</updated><id>https://blog.kabirrajsingh.com/production-ready-rag-architecture</id><content type="html" xml:base="https://blog.kabirrajsingh.com/production-ready-rag-architecture/"><![CDATA[<div class="video-embed">
  <iframe width="560" height="315" src="https://www.youtube.com/embed/z6Yhdl3Vi7k" title="Production-Ready RAG Architecture: Core Patterns Explained" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen=""></iframe>
</div>

<p><em>Companion post to the video above. This is the deeper reference version — the configs, code, and sources the video didn’t have time for. If you just want the mental model, watch the video first; come back here when you’re actually building.</em></p>

<h2 id="why-rag-demos-fall-apart-in-production">Why RAG demos fall apart in production</h2>

<p>Most RAG tutorials are tested against one clean PDF or a Wikipedia dump. That’s the wrong test. The moment you point the same pipeline at real company documents — inconsistent formatting, support tickets, sprawling internal wikis — answers start coming back incomplete, hallucinated, or confidently wrong.</p>

<p>Two mental models explain why, and they hold for almost every production RAG failure:</p>

<ol>
  <li><strong>RAG is a retrieval system first, a generation system second.</strong> If retrieval surfaces the wrong context, the LLM has no real chance of producing a correct answer, no matter how good the model is.</li>
  <li><strong>System quality is bounded by the worst stage, not the average.</strong> A strong embedding model cannot rescue a chunking strategy that cut a sentence in half.</li>
</ol>

<h2 id="the-two-phase-pipeline">The two-phase pipeline</h2>

<p>Every RAG system splits into an offline <strong>indexing phase</strong> and an online <strong>query phase</strong>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Indexing (offline):   documents → chunks → embeddings → vector store
Query (online):       question → embedding → retrieval → context assembly → LLM
</code></pre></div></div>

<p><img src="/assets/images/rag-pipeline-overview.png" alt="The RAG pipeline split into an offline indexing phase (documents through chunking and embedding into a vector store) and an online query phase (question through retrieval and context assembly to the LLM)" /></p>

<p>Production systems add caching, reranking, guardrails, monitoring, and fallback logic on top — but this is the foundation everything else sits on. Get the foundation wrong and no amount of tooling fixes it downstream.</p>

<h2 id="chunking-the-highest-leverage-decision-in-the-pipeline">Chunking: the highest-leverage decision in the pipeline</h2>

<p>Chunking is where most systems fail, and it’s usually invisible until you’re debugging a wrong answer three stages downstream. It matters because embedding models work best on coherent spans of text, LLM context windows are finite, oversized chunks introduce noise, and undersized chunks lose the surrounding information a correct answer depends on.</p>

<p><strong>Fixed-size chunking (with overlap).</strong> Split every <em>N</em> tokens, overlap 10–20%. Simple and predictable, but indifferent to sentence and paragraph boundaries — it will cut a definition in half without noticing. Common starting point, rarely optimal.</p>

<p><strong>Recursive / hierarchical chunking.</strong> Try natural boundaries first — double newlines, then single newlines, then sentences, then words — falling back only when a section is still too large. This is the default in most frameworks now (see LangChain’s <code class="language-plaintext highlighter-rouge">RecursiveCharacterTextSplitter</code>), and it’s a meaningful step up from pure fixed-size.</p>

<p><strong>Structure-aware chunking.</strong> Respect the document’s actual structure — headers, sections, code blocks, tables. For technical docs and markdown this usually wins outright. Store parent-child relationships so you can retrieve a small, precise chunk but expand to the full parent section when the answer needs surrounding context.</p>

<p><strong>Semantic chunking.</strong> Use an embedding model or an LLM to detect where the topic actually shifts, and chunk there. Produces the most coherent chunks, at real extra cost (an embedding or LLM call per boundary decision) — usually reserved for corpora where retrieval quality is worth the spend.</p>

<p><img src="/assets/images/chunking-strategies-comparison.png" alt="Four chunking strategies applied to the same paragraph — fixed-size cuts through a word, recursive snaps to sentence breaks, structure-aware aligns to headers, semantic cuts exactly where the topic shifts" /></p>

<p><strong>Working defaults:</strong></p>
<ul>
  <li>Start with recursive or structure-aware chunking — not fixed-size.</li>
  <li>Keep chunks in the 300–800 token range for most use cases; tune against your own eval set, not a blog post’s number.</li>
  <li>Always store metadata on the chunk: source document, section title, page/paragraph number. You need this later for filtering and for citations in the final answer.</li>
  <li>Build a small set of real questions and inspect what actually gets retrieved before touching the embedding model. Bad chunking is a silent killer — fix it before you start tuning anything downstream.</li>
</ul>

<p>For the actual implementations behind each strategy, <a href="https://docs.langchain.com/oss/python/integrations/splitters">LangChain’s text splitter docs</a> and <a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/">LlamaIndex’s node parser docs</a> are the two references worth having open while you build this.</p>

<h2 id="embeddings-and-vector-stores">Embeddings and vector stores</h2>

<p>Embedding models map text into a high-dimensional space where semantic similarity becomes geometric closeness, typically scored with cosine similarity or dot product. The model you pick matters, but in practice data quality and chunking usually matter more than which embedding model is in the pipeline.</p>

<p>Broad categories: general-purpose open models, domain-specific embeddings (legal, medical, code), and proprietary APIs. Pick based on your domain’s vocabulary, not benchmark leaderboards alone.</p>

<p>A vector store’s actual job is fast <strong>Approximate Nearest Neighbor (ANN)</strong> search — exact (brute-force) search doesn’t scale past a small collection. Under the hood, most stores use graph-based indexes like HNSW (Hierarchical Navigable Small World) or cluster-based indexes like IVF to avoid comparing your query against every vector in the collection; that’s the tradeoff you’re implicitly accepting when you pick “approximate” over “exact” — a small, tunable chance of missing the true nearest neighbor in exchange for sub-linear query time. You rarely need to tune this yourself early on, but it’s worth knowing it’s there before you’re debugging a “why didn’t it retrieve the obviously-correct chunk” issue.</p>

<p>When choosing a vector store, the questions that matter are: does it support metadata filtering, does it support hybrid search (vector + keyword/BM25) natively, how easy is it to run locally versus managed, and what do persistence, replication, and scaling actually look like at your data volume. Common options: Qdrant, Weaviate, Pinecone, Chroma, pgvector, OpenSearch’s k-NN.</p>

<p>Early on, the specific vector store matters less than: clean data, good chunking, proper metadata, and the ability to combine vector and keyword search. <strong>Hybrid search</strong> shows up in nearly every serious production system because pure vector search reliably misses exact-match tokens — error codes, product SKUs, IDs — that a keyword/BM25 pass catches trivially. Weaviate’s <a href="https://docs.weaviate.io/weaviate/search/hybrid">hybrid search documentation</a> is a clean reference for how the fusion between dense and sparse scores actually works if you want to see it under the hood.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Conceptual shape of a hybrid query — exact API varies by vector store
</span><span class="n">results</span> <span class="o">=</span> <span class="n">vector_store</span><span class="p">.</span><span class="n">hybrid_search</span><span class="p">(</span>
    <span class="n">query</span><span class="o">=</span><span class="n">user_question</span><span class="p">,</span>
    <span class="n">alpha</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>          <span class="c1"># weight between dense (1.0) and keyword (0.0) search
</span>    <span class="n">filters</span><span class="o">=</span><span class="p">{</span><span class="s">"doc_type"</span><span class="p">:</span> <span class="s">"runbook"</span><span class="p">,</span> <span class="s">"product"</span><span class="p">:</span> <span class="s">"checkout-service"</span><span class="p">},</span>
    <span class="n">top_k</span><span class="o">=</span><span class="mi">25</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div>

<h2 id="retrieval-quality-is-not-the-same-as-top-k-similarity">Retrieval quality is not the same as “top-k similarity”</h2>

<p>Retrieving the top-k most similar chunks is the starting point, not the finish line. In production you’ll consistently hit: the most similar chunk isn’t the most useful one, an answer’s information is split across multiple chunks, the <a href="https://arxiv.org/abs/2307.03172">“lost in the middle” effect</a> where models under-attend to information buried in the middle of a long context even when it’s technically present, and embedding models that simply don’t understand your domain’s vocabulary.</p>

<p>Techniques that move the needle, roughly in order of ROI:</p>

<ul>
  <li>
    <p><strong>Reranking.</strong> Pull a wider candidate set (top 20–50, sometimes top 100) with cheap vector search, then re-score with a cross-encoder or a small LLM before truncating to what actually goes in the prompt. This is consistently one of the highest-ROI additions to a RAG pipeline — see <a href="https://www.pinecone.io/learn/series/rag/rerankers/">Pinecone’s rerankers writeup</a> for the mechanics of why a cross-encoder outperforms bi-encoder similarity alone, or <a href="https://docs.cohere.com/docs/rerank-overview">Cohere’s Rerank docs</a> if you want a reranker you can drop in without training your own.</p>

    <p><img src="/assets/images/two-stage-retrieval-funnel.png" alt="Two-stage retrieval: a fast bi-encoder narrows a million documents to a hundred candidates, then a slower, more accurate cross-encoder reranks those hundred down to the ten that actually reach the LLM" /></p>

    <p>The reason this is two stages and not one: a cross-encoder scores a query against a document jointly, which is far more accurate than comparing two independently-computed embedding vectors — but it means running the model once per candidate document, which doesn’t scale to your full collection. Bi-encoder search is cheap and approximate; it narrows the field. The cross-encoder is expensive and precise; it only has to look at what’s left.</p>
  </li>
  <li><strong>Hybrid search.</strong> Combine dense vectors with sparse keyword search so exact-match tokens aren’t left to chance.</li>
  <li><strong>Query transformation.</strong> Rewrite the user’s question, generate a hypothetical answer document and search on that (HyDE-style), or decompose a complex question into sub-questions retrieved independently.</li>
  <li><strong>Metadata filtering.</strong> Narrow the candidate pool by document type, date range, or product <em>before</em> vector search runs, not after.</li>
  <li><strong>Parent document retrieval.</strong> Retrieve a small, precise chunk for matching accuracy, but return its larger parent section for context completeness.</li>
</ul>

<h3 id="measuring-retrieval-separately-from-the-final-answer">Measuring retrieval, separately from the final answer</h3>

<p>You need an evaluation set, not a vibe. Build 20–50 realistic questions, note which chunk(s) should be retrieved for each, and measure directly: does the correct chunk appear in the top 5 or top 10 (Recall@k)? Layer an LLM-as-judge for relevance scoring once you trust manual inspection, not before. If you never measure retrieval quality independently of final answer quality, you will spend real time optimizing the wrong stage of the pipeline.</p>

<p>You don’t have to build this evaluation harness entirely by hand — <a href="https://www.ragas.io/">Ragas</a> is the most widely used open framework for RAG-specific metrics, and <a href="https://qdrant.tech/blog/rag-evaluation-guide/">Qdrant’s guide to RAG evaluation</a> is a practical walkthrough of building the eval set itself, not just the metrics math.</p>

<h2 id="framework-choice-llamaindex-vs-langchain-conceptually">Framework choice: LlamaIndex vs. LangChain, conceptually</h2>

<p><strong>LlamaIndex</strong> is data-centric — strong, opinionated abstractions around indexing, retrieval, and query engines. It tends to feel more natural when the core problem is genuinely “I have a lot of documents and need good retrieval.”</p>

<p><strong>LangChain</strong> is general-purpose — it started with chains and agents, and has a much larger integration ecosystem. More flexible, and correspondingly heavier when all you need is retrieval. LangChain’s own <a href="https://www.langchain.com/resources/langchain-vs-llamaindex">framework comparison</a> is a reasonable starting point if you want the maintainers’ framing directly.</p>

<p>Neither is magic — both are orchestration layers over the same fundamental patterns above. Pick one, learn its concepts deeply, and don’t get religious about it: the architecture knowledge transfers regardless of which framework’s syntax you’re writing.</p>

<h2 id="recap">Recap</h2>

<ol>
  <li>Indexing and query are cleanly separate phases — design them independently.</li>
  <li>Chunking is a first-class design decision, not a default you accept from a framework.</li>
  <li>Vector stores are infrastructure — the leverage is in filtering and hybrid search, not the store’s brand.</li>
  <li>Retrieval quality has to be measured on its own, separate from final answer quality.</li>
  <li>Frameworks are tools, not the architecture. Learn the patterns; the tool is interchangeable.</li>
</ol>

<p>If you’ve seen the “RAG is dead, just use a bigger context window” takes going around — that’s the next post. Short version: bigger context windows fix retrieval recall’s easier failure mode and do nothing for LLM recall’s harder one, and there’s a real difference between the two. Full breakdown next.</p>

<p>After that: building safe LLM interfaces — guardrails, policy enforcement, and preventing unsafe or non-compliant outputs before they ship.</p>

<h2 id="references">References</h2>

<ul>
  <li>Liu, N. F. et al. — <a href="https://arxiv.org/abs/2307.03172">Lost in the Middle: How Language Models Use Long Contexts</a> (arXiv:2307.03172)</li>
  <li>Pinecone — <a href="https://www.pinecone.io/learn/series/rag/rerankers/">Rerankers and Two-Stage Retrieval</a></li>
  <li>Cohere — <a href="https://docs.cohere.com/docs/rerank-overview">Rerank overview</a></li>
  <li>Weaviate — <a href="https://docs.weaviate.io/weaviate/search/hybrid">Hybrid Search documentation</a></li>
  <li>Weaviate — <a href="https://weaviate.io/blog/hybrid-search-explained">Hybrid Search Explained</a></li>
  <li>LangChain — <a href="https://docs.langchain.com/oss/python/integrations/splitters">Text splitter integrations</a></li>
  <li>LlamaIndex — <a href="https://developers.llamaindex.ai/python/framework/module_guides/loading/node_parsers/">Node Parser usage pattern</a></li>
  <li>Ragas — <a href="https://www.ragas.io/">ragas.io</a></li>
  <li>Qdrant — <a href="https://qdrant.tech/blog/rag-evaluation-guide/">Best Practices in RAG Evaluation</a></li>
  <li>LangChain — <a href="https://www.langchain.com/resources/langchain-vs-llamaindex">LangChain vs. LlamaIndex</a> <em>(LangChain’s own framing — worth balancing against LlamaIndex’s docs directly)</em></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Why RAG demos break in production, and the patterns that fix it: chunking strategies, embeddings and vector stores, reranking and hybrid search, and how to actually measure retrieval quality.]]></summary></entry></feed>