<?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: Gecko2087</title>
    <description>The latest articles on DEV Community by Gecko2087 (@gecko).</description>
    <link>https://dev.to/gecko</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%2F1398590%2F4677e6df-7609-4af5-be0d-046b798d8387.jpeg</url>
      <title>DEV Community: Gecko2087</title>
      <link>https://dev.to/gecko</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gecko"/>
    <language>en</language>
    <item>
      <title>Your LLM''s evidence is not evidence: verify every quote before you trust a score</title>
      <dc:creator>Gecko2087</dc:creator>
      <pubDate>Fri, 13 Nov 2026 03:01:56 +0000</pubDate>
      <link>https://dev.to/gecko/your-llms-evidence-is-not-evidence-verify-every-quote-before-you-trust-a-score-4ke8</link>
      <guid>https://dev.to/gecko/your-llms-evidence-is-not-evidence-verify-every-quote-before-you-trust-a-score-4ke8</guid>
      <description>&lt;p&gt;Last week I shipped a small feature to a QA pipeline I maintain: before a score is stored, the model has to quote the transcript it claims to be scoring.&lt;/p&gt;

&lt;p&gt;That sounds trivial. It is not, and the reason is worth a full article, because "make the model cite its evidence" is advice that feels sufficient and is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure mode nobody measures
&lt;/h2&gt;

&lt;p&gt;The standard advice for LLM-as-judge systems is: require a literal quote alongside each score. The reasoning is sound — if every score comes with a quote, a human can check it in seconds instead of minutes.&lt;/p&gt;

&lt;p&gt;But nobody checks whether the quote exists.&lt;/p&gt;

&lt;p&gt;I reviewed the code of a pipeline I was about to adopt, and this is what the persistence layer looked like:&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="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rationale&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt said, in bold, that every score must cite exact evidence. The code stored whatever string came back. If the model paraphrased, if it summarized, if it hallucinated a sentence that was never said, the pipeline would have stored it and displayed it with a confidence of 0.91 next to a number a supervisor was expected to trust.&lt;/p&gt;

&lt;p&gt;The prompt is an instruction. The string is data. &lt;strong&gt;Instructions are not controls.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;p&gt;Here is the kind of output you get from a good model asked for literal evidence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"plazos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AGENTE: Y después te aviso."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"The closing statement has no concrete deadline."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the same model, asked the same way, on a worse run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"plazos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"The agent did not provide a specific timeframe."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"The closing promise was vague."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both look reasonable to a reader. The second one is worthless: it is a &lt;em&gt;description of an absence&lt;/em&gt;, which is exactly what a model produces when it cannot find a quote. It reads like evidence but contains no words from the transcript.&lt;/p&gt;

&lt;p&gt;A human reviewer skimming it might approve it. The whole point of the quote was to make review cheap. A paraphrase puts the review cost back where it started, while looking like it was solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check is cheap
&lt;/h2&gt;

&lt;p&gt;The verification does not need an embedding model, a second LLM call, or a vector store. It is a substring check against the transcript you already have.&lt;/p&gt;

&lt;p&gt;The naive version breaks immediately: models quote with different capitalization, smart quotes, and they join two fragments with an ellipsis. So normalize first, then compare every fragment:&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="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="no"&gt;ELISION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"\s*\.\.\.\s*|\s*…\s*"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_normalizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;plano&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"NFKD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;texto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plano&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plano&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;combining&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plano&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plano&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"“"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"”"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'"'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plano&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plano&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"‘"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"’"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;"\s+"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plano&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evidencia_sostenida&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidencia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transcripcion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;evidencia&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;transcripcion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_normalizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transcripcion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;fragmentos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="no"&gt;ELISION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;_normalizar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidencia&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;fragmentos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragmento&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;fragmento&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fragmentos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the last line: &lt;strong&gt;every&lt;/strong&gt; fragment has to appear. A model that quotes one real sentence and appends a fabricated one fails the check, which is the correct outcome.&lt;/p&gt;

&lt;p&gt;Stripping accents is deliberate. Transcript pipelines produce inconsistent diacritics — ASR output, diarization labels, and imported captions do not agree on spelling, and a strict comparison would flag correct quotes as false. If you strip accents for matching, you also have to strip them from the quote, which is what &lt;code&gt;_normalizar&lt;/code&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The behavior change matters more than the check
&lt;/h2&gt;

&lt;p&gt;Now the interesting part. A check that only records a warning changes nothing. The check has to change what happens to the score.&lt;/p&gt;

&lt;p&gt;Two decisions carried most of the weight:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Unsupported evidence drops the confidence to zero.&lt;/strong&gt; Not to "low" — to zero. Confidence in that system means "how much does the evidence support this score". If the evidence does not exist, the evidence supports nothing, and the correct confidence is zero. This also makes the routing automatic, because a zero-confidence score is below any sane review threshold and goes to a human. No new code path, no new flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The evidence keeps the failure attached.&lt;/strong&gt; When the check fails, the stored evidence is the model's string plus a marker:&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;evidencia_sostenida&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidencia&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recording&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;confianza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;evidencia&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidencia&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;" "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="no"&gt;NO_VERIFICADA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not silently discard the model's answer. You want to see what it claimed, because the pattern of the failures is diagnostic: a model that paraphrases on one criterion and quotes correctly on another is telling you something about that criterion's wording.&lt;/p&gt;

&lt;h2&gt;
  
  
  The metric that makes it visible
&lt;/h2&gt;

&lt;p&gt;A check that only logs is a check nobody reads. The pipeline now reports the rate of unverifiable citations:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calidad_evidencia&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kt"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"puntajes"&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="s2"&gt;"no_verificadas"&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="s2"&gt;"tasa_no_verificada"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;no_verificadas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Score&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"%{NO_VERIFICADA}%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"puntajes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"no_verificadas"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;no_verificadas&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"tasa_no_verificada"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;no_verificadas&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single number tells you whether your evaluation system is auditable. If the rate is near zero, the model is quoting and the review layer is cheap. If it is high, the scores are prose, and the numbers in your dashboard are decoration.&lt;/p&gt;

&lt;p&gt;In my experience this rate correlates with how vague the rubric is. A criterion that says "the agent explains the procedure properly" gives the model nothing to quote, so it describes. A criterion that says "the agent states the return window in days and the shipping cost" gives it a quote to find. Fixing the rubric fixes the evidence rate — which is the same lesson as always, arriving from a different direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does not solve
&lt;/h2&gt;

&lt;p&gt;Two things, so you do not over-engineer around them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A quote can be real and still irrelevant.&lt;/strong&gt; The model may quote a sentence that exists but does not support the score it assigned. String matching catches fabrication, not misattribution. That needs human review, which is the part of the system you were never trying to remove.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paraphrase-heavy transcripts break matching.&lt;/strong&gt; If the "transcript" is a summary rather than a verbatim record, nothing will match. Verify at the boundary: assert that the transcript you store is the verbatim source, not a cleaned-up version.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you build one thing from this
&lt;/h2&gt;

&lt;p&gt;Add the check and the zero-confidence rule. It is about fifteen lines and one metric, and it converts an instruction into an enforcement.&lt;/p&gt;

&lt;p&gt;The prompt can tell the model to quote. Only code can tell you whether it did.&lt;/p&gt;




&lt;p&gt;This is part of the &lt;strong&gt;QA with AI&lt;/strong&gt; series. The kit I use for this, with the judge prompt, the rubric guide and the deviation reports, is here: &lt;a href="https://kit-qa-con-ia.surge.sh/" rel="noopener noreferrer"&gt;https://kit-qa-con-ia.surge.sh/&lt;/a&gt; (MIT license, US$79). If you only want the idea, the check above is the whole thing.&lt;/p&gt;

&lt;p&gt;One question for you: if you have an LLM scoring pipeline in production right now, what percentage of the stored evidence strings would survive a literal substring check against your transcripts? I would bet it is lower than you expect, and I would like to hear the real number.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>3 reglas para que tu LLM no invente justificaciones</title>
      <dc:creator>Gecko2087</dc:creator>
      <pubDate>Fri, 13 Nov 2026 02:17:28 +0000</pubDate>
      <link>https://dev.to/gecko/3-reglas-para-que-tu-llm-no-invente-justificaciones-1cn8</link>
      <guid>https://dev.to/gecko/3-reglas-para-que-tu-llm-no-invente-justificaciones-1cn8</guid>
      <description>&lt;p&gt;En el &lt;a href="https://dev.to/gecko/tu-ia-no-esta-mal-tu-criterio-de-evaluacion-esta-mal-g3c"&gt;artículo anterior&lt;/a&gt; te conté que el bug estaba en la rúbrica, no en el modelo. El modelo quedó bien.&lt;/p&gt;

&lt;p&gt;Ahora bajamos un nivel: qué le exijo exactamente al modelo cuando evalúa una llamada, y qué tiene que devolver. Este es el prompt del juez que corre contra una operación de 45 agentes.&lt;/p&gt;

&lt;p&gt;No tiene veinte reglas. Tiene tres, y hacen casi todo el trabajo. Te las muestro con salidas reales del modelo, porque un prompt sin ejemplo de salida buena es una promesa.&lt;/p&gt;

&lt;h2&gt;
  
  
  El contrato de salida
&lt;/h2&gt;

&lt;p&gt;El modelo devuelve &lt;strong&gt;solo JSON&lt;/strong&gt;. La API lo fuerza con &lt;code&gt;json_schema&lt;/code&gt; en modo estricto y &lt;code&gt;temperature=0&lt;/code&gt;, así que no puede desviarse del contrato:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"scores"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"escucha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.82&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"cita textual exacta de la transcripción"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"por qué ese puntaje, en una o dos frases"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuatro campos. &lt;code&gt;value&lt;/code&gt; es el puntaje. &lt;code&gt;confidence&lt;/code&gt; es cuánto sostiene la evidencia ese puntaje, no cuán seguro se siente el modelo. Los dos que hacen el trabajo real son &lt;code&gt;evidence&lt;/code&gt; y &lt;code&gt;rationale&lt;/code&gt;: uno para verificar, el otro para entender.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regla 1 — Sin cita, no hay punto
&lt;/h2&gt;

&lt;p&gt;El problema de un LLM como juez no es que escriba mal: es que escribe &lt;strong&gt;bien&lt;/strong&gt;. Genera justificaciones convincentes para puntajes que no tiene respaldados, y cuando el argumento suena bien, el supervisor deja de mirar y acepta el número.&lt;/p&gt;

&lt;p&gt;Por eso &lt;code&gt;evidence&lt;/code&gt; no puede ser una descripción. Tiene que ser una cita literal, palabra por palabra, que se pueda buscar en la transcripción.&lt;/p&gt;

&lt;p&gt;Salida mala:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"escucha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.86&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"El agente dejó hablar al cliente antes de responder."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"El agente se mostró atento durante toda la conversación."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;evidence&lt;/code&gt; dice "durante toda la conversación". Si la transcripción muestra lo contrario, el error es invisible: el modelo estaba seguro y la justificación era convincente. Ese es el peor escenario: el supervisor deja de verificar.&lt;/p&gt;

&lt;p&gt;La regla, textual:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Cada puntaje DEBE citar evidencia textual EXACTA de la transcripción. Si no podés citar un fragmento que demuestra el puntaje, el criterio no se cumplió: bajalo al mínimo de la escala y explicá por qué en el rationale.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;El corolario: si no hay cita, no se "opina" un 3. Se baja al mínimo de la escala y se explica. El modelo no tiene permiso de puntuar por simpatía.&lt;/p&gt;

&lt;p&gt;Salida buena:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"escucha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.88&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"CLIENTE: ... y encima me lo cobraron dos veces. AGENTE: Dale, lo verifico ahora."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"El cliente termina su turno y el agente responde después, sin interrumpir."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Misma confianza, mismo puntaje, y ahora el supervisor puede discutirlo: mira la frase y ve si el 4 se sostiene.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regla 2 — Si hay evidencia en contra, el puntaje es bajo
&lt;/h2&gt;

&lt;p&gt;Acá el problema es otro, y es el que vuelve inútil un sistema de QA: &lt;strong&gt;el modelo promedia&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Un modelo entrenado para sonar razonable ve un criterio que se cumple en la mitad de la llamada y le pone un 3 o un 4. El 4 de un agente mediocre y el 4 de uno bueno son el mismo 4. Y si tu reporte dice "agente con score 4", no podés hacer nada con eso.&lt;/p&gt;

&lt;p&gt;La regla:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Un criterio no se cumple "parcialmente sin evidencia". Si hay una cita que lo contradice, el puntaje es bajo, aunque en otro momento de la conversación haya estado bien.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Salida mala:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"plazos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AGENTE: Te lo mando hoy antes de las 18."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"El agente comunizó un plazo concreto en la primera parte de la conversación."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El modelo tomó el momento bueno y lo promedió. Salida buena, sobre la misma conversación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"plazos"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AGENTE: Y después te aviso."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"El cierre queda sin plazo concreto. El deadline del inicio no alcanza: el criterio se cumple solo si el último plazo comprometido tiene hora o fecha."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;rationale&lt;/code&gt; dice por qué ese texto define el puntaje, aunque en el mismo criterio haya un momento bueno. Si no lo dice, el supervisor tiene que buscar el contraargumento él: ahí empieza la tasa de rechazos que nadie sabe explicar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Regla 3 — La confianza es cuánto sostiene la evidencia el puntaje
&lt;/h2&gt;

&lt;p&gt;Esta es la que más se malinterpreta, porque el nombre invita a leerla como "certeza del modelo". No lo es.&lt;/p&gt;

&lt;p&gt;Un modelo que no sabe nada de una transcripción truncada igual responde &lt;code&gt;confidence: 0.95&lt;/code&gt;. No porque mienta: porque el campo no significa eso. Y si usás ese número para decidir a qué caso va a un humano, terminás haciendo lo contrario de lo que querés: mandás a revisión los casos que el modelo &lt;strong&gt;sabía&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;La definición que uso:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0.9 a 1.0: evidencia directa y clara, el criterio es inequívoco.&lt;/li&gt;
&lt;li&gt;0.6 a 0.9: hay evidencia, pero admite lectura alternativa.&lt;/li&gt;
&lt;li&gt;0.3 a 0.6: evidencia parcial o ambigua.&lt;/li&gt;
&lt;li&gt;menos de 0.3: casi no hay evidencia. Cualquier puntaje ahí va a revisión humana.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y la regla que la acompaña, la que frena las transcripciones reales:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Si la transcripción está truncada, tiene ruido o no permite [evaluar el criterio], evaluá el criterio con la información disponible y bajale la confianza. No completes lo que falta.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Con la transcripción cortada, la salida es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"criterion_key"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"escucha"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"confidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"AGENTE: ¿Me pasás el número de cuenta? CLIENTE: Sí, es 1234."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Hasta donde llega la transcripción, el cliente termina su turno. Con la evidencia disponible no se puede afirmar que el agente no interrumpió en el resto de la llamada."&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Acá la confianza hace el trabajo: el puntaje sale del medio, no de la certeza del modelo. Ese 0,42 manda el caso a revisión humana.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dónde aterriza esto: la capa humana
&lt;/h2&gt;

&lt;p&gt;Las tres reglas no reemplazan la revisión humana: la hacen posible. Con &lt;code&gt;confidence&lt;/code&gt; definida sobre evidencia, el ruteo es mecánico: todo lo que está por debajo de 0,75 va a revisión, más una muestra aleatoria del 20% del resto.&lt;/p&gt;

&lt;p&gt;Y cuando el supervisor revisa deja dos cosas: la decisión (&lt;code&gt;approve&lt;/code&gt;, &lt;code&gt;reject&lt;/code&gt; o &lt;code&gt;needs_review&lt;/code&gt;) y, si le cambia el número, el &lt;code&gt;adjusted_mean&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -X POST http://localhost:8000/recordings/1/review \
  -H &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; \
  -d &lt;span class="s1"&gt;'{"reviewer":"supervisor-1","decision":"approve","adjusted_mean":3.8}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De ahí sale la desviación entre el modelo y el humano, que es el único número que te dice si el sistema sirve. Y, más importante, te cambia la pregunta: cuando un supervisor rechaza, dejás de discutir "el modelo se equivoca" y podés preguntar &lt;strong&gt;qué criterio, en qué transcripción, con qué evidencia&lt;/strong&gt;. Ahí adentro está el bug.&lt;/p&gt;

&lt;h2&gt;
  
  
  Si querés arrancar hoy
&lt;/h2&gt;

&lt;p&gt;Con esto y nada más:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pedí JSON con esquema estricto y &lt;code&gt;evidence&lt;/code&gt; obligatorio. Si tu proveedor no lo soporta, parseá con regex: es peor, pero funciona.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;temperature=0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Revisá a mano las primeras 50 salidas, leyendo la &lt;code&gt;evidence&lt;/code&gt; contra la transcripción. No el puntaje: la evidencia. Si en algún &lt;code&gt;evidence&lt;/code&gt; no reconocés la frase, ese puntaje no existe.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Con eso ya tenés un juez auditable. Lo demás —transcripción, diarización, reportes— es plumbing.&lt;/p&gt;

&lt;h2&gt;
  
  
  El kit
&lt;/h2&gt;

&lt;p&gt;El prompt completo con el porqué de cada regla, y la guía de rúbrica con los 12 criterios que uso, están en el &lt;strong&gt;Kit QA con IA&lt;/strong&gt; (MIT, US$79): &lt;a href="https://kit-qa-con-ia.surge.sh/" rel="noopener noreferrer"&gt;https://kit-qa-con-ia.surge.sh/&lt;/a&gt;. Si no lo necesitás, las tres reglas de arriba están para copiar tal cual.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Artículo 2 de la serie **QA con IA&lt;/em&gt;&lt;em&gt;. El anterior: &lt;a href="https://dev.to/gecko/tu-ia-no-esta-mal-tu-criterio-de-evaluacion-esta-mal-g3c"&gt;Tu IA no está mal. Tu criterio de evaluación está mal.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Si tu operación tiene un sistema de evaluación con IA corriendo, te dejo una pregunta: ¿cuál de las tres reglas te está rompiendo el sistema hoy — la cita obligatoria, la evidencia en contra, o la confianza mal definida? Contame en los comentarios y armamos el caso de esa en particular.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>llm</category>
      <category>spanish</category>
    </item>
    <item>
      <title>Tu IA no esta mal. Tu criterio de evaluacion esta mal.</title>
      <dc:creator>Gecko2087</dc:creator>
      <pubDate>Thu, 12 Nov 2026 10:48:47 +0000</pubDate>
      <link>https://dev.to/gecko/tu-ia-no-esta-mal-tu-criterio-de-evaluacion-esta-mal-g3c</link>
      <guid>https://dev.to/gecko/tu-ia-no-esta-mal-tu-criterio-de-evaluacion-esta-mal-g3c</guid>
      <description>&lt;p&gt;Cuando pusimos IA a evaluar llamadas, el promedio dio 7,0 y todos felices.&lt;/p&gt;

&lt;p&gt;Hasta que los supervisores empezaron a rechazar evaluaciones. No un par: &lt;strong&gt;seis de cada diez&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El sintoma que parece del modelo
&lt;/h2&gt;

&lt;p&gt;El modelo puntuaba 7,0 y escribia un parrafo impecable para justificarlo. El problema es&lt;br&gt;
justo ese: &lt;strong&gt;las justificaciones convincentes son el sintoma&lt;/strong&gt;, no la causa. Cuando el&lt;br&gt;
argumento suena bien, el supervisor deja de mirar y acepta el numero.&lt;/p&gt;

&lt;p&gt;Entonces los rechazos no eran "el modelo se equivoca". Eran "el modelo se equivoca de una&lt;br&gt;
forma que mi supervisor no comparte".&lt;/p&gt;

&lt;h2&gt;
  
  
  Donde estaba realmente el bug
&lt;/h2&gt;

&lt;p&gt;Tardamos dos semanas en cambiar prompts. No mejoro nada. El bug estaba en la rubrica.&lt;/p&gt;

&lt;p&gt;Teniamos un criterio que decia, mas o menos:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"El agente identifica correctamente el problema del cliente y ofrece una solucion&lt;br&gt;
adecuada."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ese criterio tenia &lt;strong&gt;dos cosas dentro&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;que el agente identifique el problema&lt;/li&gt;
&lt;li&gt;que ofrezca una solucion adecuada&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El modelo cumplia la primera y no la segunda (en los casos donde identificaba el problema pero&lt;br&gt;
ofrecia una solucion parcial). El supervisor evaluaba las dos juntas.&lt;/p&gt;

&lt;p&gt;Resultado: el modelo resolvia "bien" una pregunta mal formulada. Y el 60% de los rechazos&lt;br&gt;
venia de ahi.&lt;/p&gt;

&lt;h2&gt;
  
  
  El fix: partir el criterio y explicitar la excepcion
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Criterio 1: El agente identifica el problema del cliente antes de responder.
  - Evidencia: cita la frase donde el cliente describe el problema.
  - Si el agente responde algo no solicitado, el criterio no se cumple.

Criterio 2: El agente ofrece una solucion concreta y completa para el problema identificado.
  - Evidencia: cita la propuesta del agente.
  - Excepcion explicita: si el cliente pide escalar o el agente transferiste, el criterio
    no se cumple (no es una falla del agente).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La parte que mas costo fue la &lt;strong&gt;excepcion explicita&lt;/strong&gt;. Sin ella, los casos de escalamiento&lt;br&gt;
y transferencia contaban como fallas, y el modelo (correctamente) no ofrecia solucion porque&lt;br&gt;
no correspondia.&lt;/p&gt;

&lt;p&gt;Partirlo y explicitarlo: &lt;strong&gt;los rechazos bajaron a la mitad en una semana&lt;/strong&gt;, sin tocar el&lt;br&gt;
modelo ni el prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tres reglas que lo hacen auditable
&lt;/h2&gt;

&lt;p&gt;Esto es lo que uso en el pipeline de produccion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cada puntaje cita la frase exacta&lt;/strong&gt; de la transcripcion. Sin cita, no hay punto. Sin eso,
el error es invisible y el supervisor deja de verificar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Si hay evidencia en contra, el puntaje es bajo&lt;/strong&gt;, aunque en otro momento de la
conversacion haya estado bien. Si no, el modelo promedia, y promediar es lo que vuelve
inutil un sistema de QA.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La confianza es cuanto sostiene la evidencia el puntaje&lt;/strong&gt;, no cuanto seguro se siente
el modelo. Son cosas distintas, y confundirlas hace que el muestreo automatico descarte
justamente los casos que importan.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  El numero que casi nadie tiene
&lt;/h2&gt;

&lt;p&gt;El promedio no sirve para nada. Un contact center puede tener 8 de calificacion y seguir&lt;br&gt;
rompiendose en dos criterios concretos.&lt;/p&gt;

&lt;p&gt;El numero que si sirve es &lt;strong&gt;cuanto se aleja tu IA de lo que evalua tu supervisor&lt;/strong&gt;, en los&lt;br&gt;
casos que si se revisaron. Ese numero te dice si tu sistema sirve, y te dice &lt;strong&gt;que criterio&lt;br&gt;
definir mal&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que me llevo de esto
&lt;/h2&gt;

&lt;p&gt;La mayor parte del trabajo no es de IA. Es de escribir bien lo que queres que pase. La IA&lt;br&gt;
solo te muestra, en 400 conversaciones, donde tu definicion de "bien" no cierra.&lt;/p&gt;

&lt;p&gt;Arme un kit con el prompt del juez (con el porque de cada regla), una guia de rubrica con&lt;br&gt;
12 criterios copiables de operaciones de soporte y cobranzas reales, la API en FastAPI y la&lt;br&gt;
UI de revision. Es codigo abierto (MIT) y corre en Docker:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kit QA con IA (US$79):&lt;/strong&gt; &lt;a href="https://kit-qa-con-ia.surge.sh/" rel="noopener noreferrer"&gt;https://kit-qa-con-ia.surge.sh/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si estas evaluando calidad con IA y tu modelo "se equivoca", empezaria por la rubrica, no&lt;br&gt;
por el prompt. De hecho, es al reves: el prompt es lo facil.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Este articulo es parte de una serie sobre evaluacion de calidad con IA en contact centers.&lt;br&gt;
Si te sirve, el kit incluye la rubrica completa con los 12 criterios y sus excepciones ya&lt;br&gt;
escritas.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
