<?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: Tarun Agarwal</title>
    <description>The latest articles on DEV Community by Tarun Agarwal (@tarunagarwal1981).</description>
    <link>https://dev.to/tarunagarwal1981</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%2F2426586%2Fea2d1fc9-51ae-4cdd-bb66-a66515123684.png</url>
      <title>DEV Community: Tarun Agarwal</title>
      <link>https://dev.to/tarunagarwal1981</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tarunagarwal1981"/>
    <language>en</language>
    <item>
      <title>Catching wrong LLM outputs when you have no labels</title>
      <dc:creator>Tarun Agarwal</dc:creator>
      <pubDate>Tue, 04 Aug 2026 07:35:09 +0000</pubDate>
      <link>https://dev.to/tarunagarwal1981/catching-wrong-llm-outputs-when-you-have-no-labels-3eof</link>
      <guid>https://dev.to/tarunagarwal1981/catching-wrong-llm-outputs-when-you-have-no-labels-3eof</guid>
      <description>&lt;p&gt;A shorter, code-first version of a piece I first published in Towards AI on Medium.&lt;/p&gt;

&lt;p&gt;If you've pointed an LLM at a pile of documents to extract a field — a total, a date, an amount — you know the uncomfortable part: it returns an answer for every document, all equally confident, and some are wrong. You can't check them by hand, because not having the answer is the whole reason you reached for a model. So the wrong ones ship silently.&lt;/p&gt;

&lt;p&gt;I spent a couple of weeks on this and landed on something that helps, borrowed from an old idea called metamorphic testing.&lt;/p&gt;

&lt;p&gt;The trick: test what shouldn't change&lt;br&gt;
You usually know things that shouldn't change the answer:&lt;/p&gt;

&lt;p&gt;reordering a receipt's line items shouldn't change the total&lt;br&gt;
adding an irrelevant footer shouldn't change it&lt;br&gt;
stripping a currency symbol shouldn't change it&lt;br&gt;
So run your system twice — once on the original input, once on a "shouldn't-matter" variant — and compare. If the outputs disagree, the system just contradicted itself. That's a bug, found with zero labels.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;wobbly&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Relation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;unchanged&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# your system under test — any input -&amp;gt; output callable
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="n"&gt;rng&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reorder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Relation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reorder lines =&amp;gt; total unchanged&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;transform&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))},&lt;/span&gt;
    &lt;span class="n"&gt;assertion&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;unchanged&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extract_total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;reorder&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;# BROKE (1 of 5 trials): expected 7.95 to be preserved, got 7.5
&lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt; &lt;span class="n"&gt;correct&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="n"&gt;was&lt;/span&gt; &lt;span class="n"&gt;ever&lt;/span&gt; &lt;span class="n"&gt;supplied&lt;/span&gt; &lt;span class="err"&gt;—&lt;/span&gt; &lt;span class="n"&gt;yet&lt;/span&gt; &lt;span class="n"&gt;we&lt;/span&gt; &lt;span class="n"&gt;know&lt;/span&gt; &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="n"&gt;of&lt;/span&gt; &lt;span class="n"&gt;those&lt;/span&gt; &lt;span class="n"&gt;answers&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;wrong&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does it actually catch anything?&lt;br&gt;
I ran it on 535 real scanned receipts (ICDAR-SROIE) in two deliberately separate stages: flag receipts blind (never reading the labels), then open the labels only to score the flags.&lt;/p&gt;

&lt;p&gt;On held-out data, the receipts it flagged were 2.7× more likely to be genuinely wrong than the ones it didn't — decided with zero labels.&lt;/p&gt;

&lt;p&gt;The honest part&lt;br&gt;
It's a spot-check, not a safety net. Recall is low (~8% on this dataset) — it misses most bugs. But precision is high (~75%): about 3 in 4 of the things it flags are real errors. So it's a cheap, label-free way to surface bugs you'd otherwise ship silently — not a replacement for a real test set.&lt;/p&gt;

&lt;p&gt;The one thing that's easy to get wrong&lt;br&gt;
A check is only useful if a correct system passes it. My first "reorder lines" transform shuffled every line — and broke correct extractors too, because real extractors pair a cue like TOTAL with the value on the next line. 68 of 72 flags were artifacts of a bad transform, not real bugs. The fix was to permute independent blocks (header / items / totals) as units. Design the transform so only a real defect can fail it.&lt;/p&gt;

&lt;p&gt;Try it&lt;br&gt;
Small Python library, no dependencies, MIT:&lt;/p&gt;

&lt;p&gt;pip install wobbly&lt;br&gt;
Code: &lt;a href="https://github.com/tarunagarwal1981/wobbly" rel="noopener noreferrer"&gt;https://github.com/tarunagarwal1981/wobbly&lt;/a&gt; Full experiment (with the mistakes): &lt;a href="https://medium.com/towards-artificial-intelligence/your-llm-extracted-10-000-numbers-which-ones-are-wrong-7a5d54050dd3" rel="noopener noreferrer"&gt;https://medium.com/towards-artificial-intelligence/your-llm-extracted-10-000-numbers-which-ones-are-wrong-7a5d54050dd3&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How are you checking LLM outputs when there's no ground truth? Genuinely curious what people are doing.&lt;/p&gt;

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