Building RAG Pipelines That Actually Work: Lessons from Microsoft Copilot

Dev.to / 4/13/2026

💬 OpinionDeveloper Stack & InfrastructureIdeas & Deep AnalysisTools & Practical Usage

Key Points

  • The article argues that many RAG tutorials only demonstrate the “happy path,” while real deployments must address scale, latency constraints, and user trust risks when answers are wrong.
  • It emphasizes that chunking is a critical failure point: fixed-size chunking often breaks semantic coherence, while semantic, recursive, and hierarchical chunking better preserve self-contained meaning.
  • It frames RAG as a multi-stage pipeline where every step in the flow (query→embedding→retrieval→prompt augmentation→generation) can fail and needs production-focused design choices.
  • Drawing on experience from Microsoft Copilot’s search infrastructure, the author distills patterns that hold up in production rather than lab demos, across multiple domains.

Most RAG tutorials show you the happy path. You chunk a handful of PDFs, toss them into a vector store, wire up an LLM, and — magic — your chatbot answers questions about your documents. Demo complete. Applause.

Here's what those tutorials don't show you: what happens when you deploy RAG at scale. When your corpus isn't 10 PDFs but 10 million documents. When your latency budget is 200 milliseconds, not "however long it takes." When a wrong answer isn't a minor inconvenience but a trust-destroying event for millions of users.

I work on Microsoft Copilot's Search Infrastructure team, where my focus is semantic indexing and retrieval-augmented generation. I've also built over 116 open-source repositories, many of which experiment with RAG patterns across healthcare, developer tools, education, and creative AI. What follows is a distillation of what I've learned — the patterns that survive contact with production, and the failure modes that tutorials conveniently skip.

What RAG Actually Is (Quick Refresher)

Retrieval-Augmented Generation is a simple idea: instead of asking an LLM to answer from memory alone, you first retrieve relevant documents, then feed them as context alongside the user's query. The basic flow:

User Query → Embed → Retrieve from Index → Augment Prompt → Generate Response

That five-step pipeline hides an enormous amount of complexity. Every arrow in that diagram is a place where things can go wrong. Let's walk through each stage and talk about what actually matters.

Chunking Strategies That Matter

Chunking is the most underrated part of the RAG pipeline. Get it wrong and nothing downstream can save you — not a better embedding model, not a smarter LLM, not a fancier retrieval algorithm. Garbage chunks in, garbage answers out.

Fixed-Size Chunking

The naive approach: split every N tokens. It's fast, deterministic, and almost always wrong. A 512-token window doesn't care that it just sliced a paragraph in half, separated a code function from its docstring, or split a table across two chunks. The resulting fragments lack semantic coherence, which means your embeddings will be noisy and your retrieval will suffer.

Semantic Chunking

A better approach respects the natural boundaries of text. Sentences, paragraphs, sections — these are the units humans write in, and they're the units that produce coherent embeddings. The key insight is that a chunk should be a self-contained unit of meaning.

Recursive and Hierarchical Chunking

For structured documents (markdown, HTML, code), recursive chunking splits along structural boundaries first — headers, then paragraphs, then sentences — falling back to smaller splits only when a section exceeds your token budget. This preserves the document's inherent hierarchy and produces chunks that actually make sense.

Overlapping Windows

Here's a pattern that pays for itself immediately: overlap between adjacent chunks. Without overlap, information that spans a chunk boundary is effectively invisible to retrieval. A query about concept X might match the end of chunk 4 and the beginning of chunk 5, but neither chunk alone scores high enough to be retrieved.

def semantic_chunk(text, max_tokens=512, overlap=50):
    sentences = split_into_sentences(text)
    chunks, current_chunk = [], []
    current_tokens = 0
    for sentence in sentences:
        tokens = count_tokens(sentence)
        if current_tokens + tokens > max_tokens and current_chunk:
            chunks.append(" ".join(current_chunk))
            # Keep overlap sentences
            overlap_sentences = current_chunk[-2:]
            current_chunk = overlap_sentences
            current_tokens = sum(count_tokens(s) for s in current_chunk)
        current_chunk.append(sentence)
        current_tokens += tokens
    if current_chunk:
        chunks.append(" ".join(current_chunk))
    return chunks

I keep two trailing sentences as overlap. This is a deliberate choice — enough context to preserve cross-boundary meaning, but not so much that you're bloating your index with redundant content. Tune the overlap to your domain: technical documentation tends to need more overlap than conversational text.

Embedding Models — Choosing Wisely

Your embedding model is the lens through which your entire corpus is viewed. Choose poorly and retrieval becomes a game of chance.

The Landscape

OpenAI's text-embedding-ada-002 is the default choice for many teams, and it's a solid baseline — 1536 dimensions, reasonable performance across domains, easy API integration. But it's not always the right answer. Open-source models like BGE-large, E5-large-v2, and the sentence-transformers family offer competitive quality with significant advantages: no API costs at scale, lower latency (run locally or on your own GPU fleet), and the ability to fine-tune on your domain.

Domain-Specific vs. General-Purpose

If your corpus is specialized — legal documents, medical literature, codebases — a general-purpose embedding model may not capture the nuances that matter. A model fine-tuned on biomedical text will understand that "MI" means "myocardial infarction," not "Michigan." The MTEB leaderboard is your friend here: benchmark models against your actual query distribution, not generic benchmarks.

Dimensionality Tradeoffs

Higher dimensions capture more nuance but cost more in storage and search latency. At scale, the difference between 384 and 1536 dimensions is not academic — it's the difference between fitting your index in memory or needing distributed infrastructure. I've seen 768-dimensional models outperform 1536-dimensional ones on domain-specific tasks after fine-tuning. Measure, don't assume.

Asymmetric Embedding

This is the insight that separates production RAG from tutorial RAG: the query and the document should not be embedded the same way. A query like "How do I reset my password?" is semantically different from a documentation passage that contains the answer. Models like E5 handle this explicitly with query: and passage: prefixes. If your embedding model supports asymmetric encoding, use it. The retrieval quality improvement is substantial and essentially free.

Retrieval Ranking — Beyond Cosine Similarity

Cosine similarity against a dense vector index is the starting point, not the finish line. In production, you need a ranking pipeline, not a single similarity score.

Hybrid Search: Dense + Sparse

Dense retrieval (vector search) excels at semantic matching — it understands that "automobile" and "car" are related. Sparse retrieval (BM25, keyword matching) excels at exact matching — it knows that "error code 0x8007045D" is a precise string, not a semantic concept. Neither alone is sufficient. The winning combination is both.

def hybrid_search(query, index, bm25_index, k=10, alpha=0.7):
    dense_results = index.search(embed(query), k=k*2)
    sparse_results = bm25_index.search(query, k=k*2)
    # Reciprocal Rank Fusion
    scores = {}
    for rank, doc_id in enumerate(dense_results):
        scores[doc_id] = scores.get(doc_id, 0) + alpha / (rank + 60)
    for rank, doc_id in enumerate(sparse_results):
        scores[doc_id] = scores.get(doc_id, 0) + (1 - alpha) / (rank + 60)
    return sorted(scores, key=scores.get, reverse=True)[:k]

Reciprocal Rank Fusion (RRF) is elegant because it doesn't require score normalization — it works purely on rank positions. The constant 60 in the denominator is a standard dampening factor that prevents top-ranked results from dominating. The alpha parameter controls the dense-vs-sparse balance; 0.7 is a reasonable starting point, but you should tune it against your evaluation set.

Re-Ranking with Cross-Encoders

Bi-encoders (your embedding model) are fast because they encode queries and documents independently. Cross-encoders are accurate because they process the query-document pair jointly, capturing fine-grained interactions. The pattern: retrieve broadly with a bi-encoder, then re-rank the top candidates with a cross-encoder. Models like cross-encoder/ms-marco-MiniLM-L-12-v2 can re-rank 100 candidates in milliseconds.

Metadata Filtering

Not all retrieval should be purely semantic. If a user asks about "Python 3.11 features," you should filter by language and version before running vector search, not after. Pre-filtering reduces the search space and eliminates false positives that would otherwise waste context window budget.

The "Lost in the Middle" Problem

Research from Stanford showed that LLMs pay disproportionate attention to the beginning and end of their context window, often ignoring information in the middle. This has direct implications for how you order retrieved passages. Place your most relevant chunks at the beginning of the context, not in order of retrieval rank. Or better yet, interleave high and low relevance to force the model to attend evenly.

Context Window Management

You've retrieved your chunks. Now you need to fit them — along with a system prompt, the user's query, and room for the response — into a fixed token budget. This is a packing problem, and it deserves more attention than it gets.

Token Budgeting

Be explicit about your budget allocation:

TOTAL_CONTEXT = 8192  # or 128k, depends on your model
SYSTEM_PROMPT_TOKENS = 500
RESPONSE_RESERVE = 1024
USER_QUERY_TOKENS = 200  # estimate or measure

CONTEXT_BUDGET = TOTAL_CONTEXT - SYSTEM_PROMPT_TOKENS - RESPONSE_RESERVE - USER_QUERY_TOKENS
# = 6468 tokens for retrieved passages

Every token you spend on a low-relevance chunk is a token you can't spend on a high-relevance one. Rank your chunks by retrieval score and pack greedily until the budget is full.

Compression

When your top chunks exceed the budget, you have two choices: drop chunks or compress them. Compression techniques range from simple extractive summarization (keep only the most relevant sentences within each chunk) to LLM-based summarization. The tradeoff is latency vs. information density. In latency-sensitive pipelines, extractive approaches win.

Strategy Selection: Stuff vs. Map-Reduce vs. Refine

  • Stuff: Concatenate all retrieved chunks into a single prompt. Simple, fast, works when everything fits in the context window.
  • Map-Reduce: Process each chunk independently, then aggregate the results. Necessary when the total retrieved content exceeds the context window. More LLM calls, higher latency, but handles scale.
  • Refine: Process chunks sequentially, refining the answer with each new chunk. Produces high-quality answers but has the highest latency. Use for offline or batch workloads.

In production, I default to Stuff with aggressive filtering. If your retrieval and ranking are good, you shouldn't need more than 5–8 highly relevant chunks to answer most questions.

Common Failure Modes (And How to Debug Them)

Retrieval Misses

The document exists in your corpus but wasn't retrieved. Debug by running the query embedding against the target document's embedding directly — if the similarity is low, the problem is in your chunking or embedding model. If the similarity is high but the document wasn't in the top-K, your index may have quantization issues or you're not retrieving enough candidates before re-ranking.

Context Poisoning

You retrieved 10 chunks, but 7 of them are irrelevant. The LLM now has to distinguish signal from noise, and it doesn't always succeed. The fix is upstream: better chunking, better ranking, and aggressive relevance thresholds. Drop any chunk below a minimum similarity score rather than always returning a fixed K.

Hallucination Despite Correct Context

The right chunk was retrieved and included in the prompt, but the LLM still hallucinated. This is often a prompt engineering problem. Explicit instructions like "Answer based ONLY on the provided context. If the context doesn't contain the answer, say so" are essential, not optional. Also consider: is the relevant information buried in a long passage? The "lost in the middle" effect applies within individual chunks too.

Stale Embeddings

Your documents were updated but the embeddings weren't re-computed. This is the RAG equivalent of a cache invalidation bug. Build your indexing pipeline with incremental updates from day one. Track document hashes and re-embed only what changed. At scale, a full re-index is a multi-hour, multi-GPU operation — you don't want to do it unnecessarily.

Lessons from Scale

What changes when you go from a demo to a production system serving millions of users?

Index management becomes a first-class concern. You need index versioning, blue-green deployments for index updates, and the ability to roll back a bad index without downtime. Your index is as critical as your database — treat it that way.

Latency budgets force hard tradeoffs. At Microsoft-scale, every millisecond matters. You might skip re-ranking on low-importance queries. You might use a smaller embedding model for initial retrieval and reserve the expensive cross-encoder for the final top-10. Tiered retrieval architectures are common in production.

Monitoring is non-negotiable. Track retrieval precision and recall against labeled query sets. Monitor embedding drift over time. Alert on sudden drops in answer quality. Log the full pipeline: query → retrieved chunks → generated answer, so you can debug failures post-hoc. The RAG pipeline that you can't observe is the RAG pipeline that silently degrades.

Evaluation is continuous. Build evaluation sets that reflect your actual query distribution. Automated metrics (faithfulness, relevance, answer correctness) run on every pipeline change. Human evaluation catches what automated metrics miss. This isn't optional — it's how you maintain quality over time.

Conclusion

The RAG pipeline is deceptively simple to prototype and genuinely hard to operate at scale. The architecture diagram fits on a napkin: embed, retrieve, generate. But the difference between a demo and a production system lives in the details — how you chunk documents, which embedding model you choose, how you rank and filter results, how you manage the context window, and how you monitor the whole thing.

My advice: build incrementally. Start with the simplest version that works, instrument everything, and let your evaluation data tell you where to invest next. Don't over-engineer the retrieval before you've verified your chunking is sound. Don't add re-ranking before you've confirmed your base retrieval is reasonable.

And don't skip the boring parts. Chunking and ranking aren't glamorous, but they're where production RAG systems are won or lost. The LLM is the easy part.

Nrk Raju Guthikonda is a Senior Software Engineer at Microsoft on the Copilot Search Infrastructure team, where he works on Semantic Indexing and Retrieval-Augmented Generation. He has built 116+ open-source repositories spanning AI/ML, healthcare, developer tools, and creative AI. Find his work on GitHub at github.com/kennedyraju55.