<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ashwin Ugale</title>
    <description>The latest articles on DEV Community by Ashwin Ugale (@ashwin_ugale_102f2abc9cec).</description>
    <link>https://dev.to/ashwin_ugale_102f2abc9cec</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4042600%2F2a9b8c03-745a-429d-bd5b-5d84e44c679f.jpg</url>
      <title>DEV Community: Ashwin Ugale</title>
      <link>https://dev.to/ashwin_ugale_102f2abc9cec</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ashwin_ugale_102f2abc9cec"/>
    <language>en</language>
    <item>
      <title>The reranker I added to improve RAG was causing most of my remaining misses</title>
      <dc:creator>Ashwin Ugale</dc:creator>
      <pubDate>Mon, 10 Aug 2026 18:45:32 +0000</pubDate>
      <link>https://dev.to/ashwin_ugale_102f2abc9cec/the-reranker-i-added-to-improve-rag-was-causing-most-of-my-remaining-misses-126m</link>
      <guid>https://dev.to/ashwin_ugale_102f2abc9cec/the-reranker-i-added-to-improve-rag-was-causing-most-of-my-remaining-misses-126m</guid>
      <description>&lt;p&gt;My RAG evaluation would tell me &lt;code&gt;hybrid: 0.86&lt;/code&gt; and I'd sit there with no idea what to actually &lt;em&gt;change&lt;/em&gt;. Raise it how? Different embedder? Smaller chunks? Add a reranker? The aggregate score names a winner; it says nothing about &lt;em&gt;why the losers lost&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;So I built a small tool to answer the "why," and running it turned up something I didn't expect: in my best-performing configurations, the component I'd added to &lt;em&gt;improve&lt;/em&gt; results — the reranker — was responsible for almost every remaining failure.&lt;/p&gt;

&lt;p&gt;Here's the idea, the finding, and how to check it on your own corpus.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with a single retrieval score
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;hit@k = 0.86&lt;/code&gt; is an average over a lot of independent failures, and those failures don't all have the same cause. A query can miss because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the answer text never made it into any chunk (a chunking/ingestion problem),&lt;/li&gt;
&lt;li&gt;neither dense nor sparse retrieval fetched the right chunk into the shortlist,&lt;/li&gt;
&lt;li&gt;dense + sparse fusion dropped it,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;reranker reordered it out of the top-k&lt;/strong&gt;,&lt;/li&gt;
&lt;li&gt;it landed just past the top-k cutoff,&lt;/li&gt;
&lt;li&gt;or the context-token budget dropped it.
Every one of those wants a &lt;em&gt;different&lt;/em&gt; fix. Averaging them into &lt;code&gt;0.86&lt;/code&gt; throws away the only information that tells you which lever to pull.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Attribute each miss to the stage that lost it
&lt;/h2&gt;

&lt;p&gt;The fix is to stop treating retrieval as a black box and record what survived at each stage of the pipeline, then attribute every missed query to the &lt;strong&gt;earliest&lt;/strong&gt; stage that could no longer cover the answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;representation → ann_index → candidate_generation → fusion →
reranker_demotion → final_cutoff → budget_cutoff
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important detail that makes this stable: gold answers are stored as &lt;strong&gt;character spans in the source document&lt;/strong&gt;, not chunk IDs. That way the labels don't break when you change chunking strategy, and the scorer can credit an answer that's covered by several chunks together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the sweep actually showed
&lt;/h2&gt;

&lt;p&gt;I ran 50 configurations (chunking × embedding × dense/BM25/hybrid × reranking, with real E5/BGE embedders and a cross-encoder reranker) over a small synthetic API-documentation corpus — 22 documents, 400 labeled queries. Three things jumped out, and only the attribution view makes them visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Chunking was the biggest lever — not the embedding model
&lt;/h3&gt;

&lt;p&gt;The spread from worst to best config was &lt;code&gt;hit@k&lt;/code&gt; &lt;strong&gt;0.79 → 0.99&lt;/strong&gt;. The bottom of the table was dominated by small &lt;code&gt;fixed 200&lt;/code&gt; chunks, whose failures were mostly &lt;em&gt;"not retrieved"&lt;/em&gt; and &lt;em&gt;"final cutoff,"&lt;/em&gt; and whose &lt;em&gt;hits&lt;/em&gt; were often flagged &lt;em&gt;"fragile"&lt;/em&gt; — the answer was covered only because several chunks pieced it together, so a small chunking change would break it. The top was &lt;code&gt;parent-child 800x200&lt;/code&gt;. Swapping E5 for BGE barely moved anything by comparison. If I'd only stared at aggregate scores I'd have fiddled with embedders; the attribution said &lt;strong&gt;chunk strategy&lt;/strong&gt; was where the wins were.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. In the strong configs, the remaining misses were almost all the reranker
&lt;/h3&gt;

&lt;p&gt;This is the one that surprised me. Take a strong config — &lt;code&gt;e5 · semantic · dense · rerank ce&lt;/code&gt;, &lt;code&gt;hit@k 0.97&lt;/code&gt;. Where did its remaining misses go?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reranker demotion: 13   (all of them)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every single miss was the cross-encoder pulling the &lt;em&gt;correct&lt;/em&gt; chunk out of the top-k. And it wasn't a one-off — across the reranked configs the residual misses were overwhelmingly &lt;code&gt;reranker_demotion&lt;/code&gt;. Compare the same config &lt;strong&gt;without&lt;/strong&gt; the reranker (&lt;code&gt;e5 · semantic · dense&lt;/code&gt;, &lt;code&gt;hit@k 0.96&lt;/code&gt;): now the misses are all &lt;code&gt;final_cutoff&lt;/code&gt; — chunks that were ranked fine but landed one slot past k.&lt;/p&gt;

&lt;p&gt;So the honest read isn't "rerankers are bad." The reranker &lt;em&gt;raised&lt;/em&gt; MRR nicely (0.80 → 0.86) and nudged hit@k up. But once retrieval was already strong, &lt;strong&gt;the reranker became the single largest source of the failures that were left&lt;/strong&gt; — which points at a precise, small fix (increase candidate depth / rerank top-N, or tune the reranker), not "retrieval is broken, start over."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Confidence intervals stop you from over-reading the leaderboard
&lt;/h3&gt;

&lt;p&gt;The top config was &lt;code&gt;0.99 [0.98–1.00]&lt;/code&gt;; the next few were &lt;code&gt;0.98 [0.97–0.99]&lt;/code&gt;. Those intervals overlap — on 400 queries, &lt;code&gt;0.99&lt;/code&gt; vs &lt;code&gt;0.98&lt;/code&gt; is a &lt;strong&gt;tie&lt;/strong&gt;, not a win. Without the CI you'd "pick the 0.99" and congratulate yourself on noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Half the context for ~two points of quality
&lt;/h3&gt;

&lt;p&gt;The best config hit &lt;code&gt;0.99&lt;/code&gt; but at ~577 average retrieved tokens. An &lt;code&gt;e5 · recursive 400 · hybrid · ce&lt;/code&gt; config reached &lt;code&gt;0.963&lt;/code&gt; at ~311 tokens — within about three points for roughly half the context. If you're context- or cost-bound, that's the smarter pick, and the quality-vs-tokens (Pareto) view is what surfaces it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on your corpus
&lt;/h2&gt;

&lt;p&gt;The offline demo needs no API key or model download:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;retrieval-lab
retrieval-lab demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a real sweep you give it two JSONL files — your documents and your labeled queries — and it writes a single self-contained HTML report (rankings, confidence intervals, per-stage attribution, latency/cost, and the Pareto view):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"retrieval-lab[real-embed,rerank]"&lt;/span&gt;

retrieval-lab run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--corpus&lt;/span&gt; docs.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queries&lt;/span&gt; queries.jsonl &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--embed-models&lt;/span&gt; e5,bge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--chunkers&lt;/span&gt; fixed:200,fixed:400,recursive:400,parentchild:800x200 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retrieval&lt;/span&gt; dense,sparse,hybrid &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rerank&lt;/span&gt; none,ce &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--html&lt;/span&gt; report.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Live example report (the 50-config sweep above): &lt;a href="https://ashwinugale.github.io/Retrieval-Lab/" rel="noopener noreferrer"&gt;https://ashwinugale.github.io/Retrieval-Lab/&lt;/a&gt;&lt;br&gt;
Code: &lt;a href="https://github.com/AshwinUgale/Retrieval-Lab" rel="noopener noreferrer"&gt;https://github.com/AshwinUgale/Retrieval-Lab&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It's only as representative as your &lt;strong&gt;labeled query set&lt;/strong&gt; — a thin or biased set biases the winner. Every score describes &lt;em&gt;your&lt;/em&gt; corpus, never "best" in the abstract.&lt;/li&gt;
&lt;li&gt;Missing valid gold alternatives make measured recall a lower bound.&lt;/li&gt;
&lt;li&gt;Latency and index cost are whatever your machine reports.&lt;/li&gt;
&lt;li&gt;Stage attribution needs a decomposable pipeline; a black-box retriever can only be scored at its output.
It's beta. If the attribution gets something wrong for you — misattributes a miss, or blames a stage you don't think is at fault — that's exactly the feedback I want. What's the failure stage you &lt;em&gt;wish&lt;/em&gt; your RAG eval could point at?&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Mutation testing, but for LLM evals — early experiment, would love feedback</title>
      <dc:creator>Ashwin Ugale</dc:creator>
      <pubDate>Wed, 22 Jul 2026 19:54:49 +0000</pubDate>
      <link>https://dev.to/ashwin_ugale_102f2abc9cec/mutation-testing-but-for-llm-evals-early-experiment-would-love-feedback-2bl6</link>
      <guid>https://dev.to/ashwin_ugale_102f2abc9cec/mutation-testing-but-for-llm-evals-early-experiment-would-love-feedback-2bl6</guid>
      <description>&lt;p&gt;I write evals for LLM features, wire them into CI, and move on. But a passing eval suite has a blind spot: it tells you nothing about whether it would actually &lt;em&gt;fail&lt;/em&gt; if the model quietly got worse. Green isn't the same as good.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/AshwinUgale/muteval" rel="noopener noreferrer"&gt;muteval&lt;/a&gt; to measure that directly. It borrows &lt;strong&gt;mutation testing&lt;/strong&gt; from software engineering — you deliberately break the thing under test and check whether your tests catch it. muteval degrades the system (weakens a prompt rule, drops a retrieved doc, swaps in a weaker model), reruns your existing eval suite against each degraded version, and reports what fraction of those injected regressions your evals caught. The ones they miss are concrete coverage gaps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Mutation score: 33% (2/6 caught)
SURVIVED: deleted "if the answer isn't in the context, say you don't know"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A bit on how it works. It ships around 18 of these mutations, each modeling a way a real system quietly degrades: softening a "must" to a "should", flipping a "do not", dropping or corrupting a retrieved document, shuffling context, swapping in a cheaper model, or breaking a tool's output for agent setups. Before mutating anything it confirms your suite passes on the &lt;em&gt;original&lt;/em&gt; system — if it doesn't, there's nothing meaningful to measure — then reruns your evals against each mutation, ranks whatever survives by severity, and suggests an eval that would close each gap.&lt;/p&gt;

&lt;p&gt;Does it actually find anything real? I ran it on Vectara's open-rag-eval. Its citation check caught mutations that made the model stop citing its sources — but it completely missed one that removed the rule telling the model to say "I don't know" when the answer isn't in the context. The model kept citing sources while being free to make things up, and a citation check simply can't catch that. Once I added a check for that behavior, it caught it.&lt;/p&gt;

&lt;p&gt;It's pure Python, no required dependencies, and works with your deepeval/RAGAS/promptfoo metrics or its own checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;muteval
muteval init      &lt;span class="c"&gt;# scaffold a config&lt;/span&gt;
muteval check     &lt;span class="c"&gt;# validate it, then muteval run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where I think this goes next is a tool that rates an eval suite across several angles and suggests the evals you're missing — but I'd like to hear how other people approach it first. &lt;strong&gt;If you write LLM evals: how do you know they're any good?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Repo (Apache-2.0): &lt;a href="https://github.com/AshwinUgale/muteval" rel="noopener noreferrer"&gt;https://github.com/AshwinUgale/muteval&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>llm</category>
      <category>python</category>
    </item>
  </channel>
</rss>
