<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[AI Applied Engineering]]></title><description><![CDATA[A "friendly" dump of my daily tech stuff findings.]]></description><link>https://idirocco.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad0d118f152a9b88f15a073/7ebb7a7e-dc06-4d07-a830-d6e22d46b9b5.png</url><title>AI Applied Engineering</title><link>https://idirocco.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Fri, 23 Oct 2026 23:02:08 GMT</lastBuildDate><atom:link href="https://idirocco.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[A Passing Eval Doesn't Mean Your AI Feature Is Reliable]]></title><description><![CDATA[How I think about evaluation sets, failure analysis, and the gap between a good score and a dependable AI product.]]></description><link>https://idirocco.hashnode.dev/a-passing-eval-doesn-t-mean-your-ai-feature-is-reliable</link><guid isPermaLink="true">https://idirocco.hashnode.dev/a-passing-eval-doesn-t-mean-your-ai-feature-is-reliable</guid><category><![CDATA[AI Engineering]]></category><category><![CDATA[llm evaluation]]></category><category><![CDATA[AI products]]></category><category><![CDATA[Reliability]]></category><category><![CDATA[Testing]]></category><dc:creator><![CDATA[Ignacio Di Rocco]]></dc:creator><pubDate>Mon, 19 Oct 2026 18:12:01 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad0d118f152a9b88f15a073/37f2d079-a754-4a03-b619-07286f37da08.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When building an AI feature, it is tempting to treat a good evaluation score as the finish line. The pipeline runs, the examples look reasonable, the judge scores them highly. Time to ship?</p>
<p>Not so fast.</p>
<p>An evaluation tells you something about the examples you tested, the criteria you chose, and the evaluator you used. It does not automatically tell you how the system will behave on a new request or an unusual edge case.</p>
<p>I ran into this while building the evaluation pipeline for my <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator">AI Waiter Synthetic Data Generator</a>. A small model generates restaurant Q&amp;A, I label it by hand, and an independent LLM judge scores the same items. After iterating on the judge rubric, mean agreement with my labels rose from 70.2% to 96.0% across the same 42 examples. Every change is recorded in the <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/iteration-log.md">iteration log</a>.</p>
<p>That was real progress. It was also a reminder of what the number could---and could not---prove.</p>
<p><em>(Code links point to commit</em> <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/tree/46988dbe5384c67244262c5db3806d80e858a472"><code>46988db</code></a> <em>so line numbers stay accurate.)</em></p>
<h2>Define what "good" means, dimension by dimension</h2>
<p>For an AI waiter, a response should be consistent with the menu, useful, complete, within the waiter's role, and careful about dietary and food-safety concerns. Those are related but not interchangeable: an answer can be friendly and wrong about an ingredient, or safe and not answer the question.</p>
<p>So I score them separately. The dimensions live in one shared list, <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/quality_dimensions.py"><code>quality_dimensions.py</code></a>, imported by both the labeling tool and the judge, so the two can never drift apart:</p>
<pre><code class="language-python">QUALITY_DIMENSIONS = [
    "answer_completeness", "safety_specificity", "menu_realism",
    "scope_appropriateness", "context_clarity", "tip_usefulness",
]
</code></pre>
<p>A single number is useful for tracking a trend. It is much less useful for explaining what needs fixing.</p>
<h2>Build the set around failure modes</h2>
<p>What matters is not how many examples you have, but what they cover. The project generates one batch per category defined in <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/config.py#L31-L52"><code>config.py</code></a>: menu guidance, dietary restrictions and allergies, beverages, order customization, and service recovery. Writing that list down also showed me what's missing: out-of-scope requests, ambiguous requests, and cases where the menu doesn't support a confident answer.</p>
<p>Some failures are cheap to catch before any model looks at them. <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step2_validation.py#L6-L38">Step 2</a> runs deterministic checks: a minimum length for the safety note, a blocklist of generic phrases like "please let us know about any allergies", and question deduplication. (One caveat: today it only reports. Later steps still see the items it would reject.)</p>
<p>For every item, you should be able to answer: <em>What behavior am I testing?</em> and <em>What would count as a failure?</em></p>
<h2>Separate regression testing from generalization</h2>
<p>The most useful decision in the experiment was keeping the same 42 examples for every judge-prompt version. The judge reads a saved generation file instead of regenerating, and each run writes to a file named after both prompts, so runs sit side by side:</p>
<pre><code class="language-bash">python3 pipeline.py step4 --generator-prompt=v1 --judge-prompt=v3
# -&gt; output/step4_llm_judge_labels_v1_v3.json
</code></pre>
<p>Each item has a stable <code>trace_id</code>, and the analysis step <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step5_analysis_visualization.py#L74-L105">joins human and judge labels on it</a>.</p>
<p>That gives me a <strong>regression set</strong>: did this change break something I already tested? It doesn't give me a <strong>held-out set</strong>: does this approach work on examples I didn't tune against? Tune long enough on the same 42 items and you start optimizing for those items. I've established comparability across prompt versions, not generalization. That's a next step, not a result.</p>
<h2>Treat the evaluator as part of the system</h2>
<p>An LLM judge is not an oracle. It's another model following a rubric---and it fails like software. The first time I switched to <code>gpt-oss-20b</code>, it returned <code>{"pass": true}</code> with no rationale, my <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step4_llm_as_judge.py#L53-L70">Pydantic schema</a> required one, and the run died on the first item. Changing the judge model is a system change.</p>
<p>The bigger lesson was about vague criteria. The baseline judge agreed with me only 9.5% of the time on safety specificity. The <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/prompts/step4_judge/default.txt">default prompt</a> mostly said "be strict." By <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/prompts/step4_judge/v3.txt#L11-L26">v3</a>, it had explicit evidence for a pass and explicit edge cases:</p>
<pre><code class="language-text">Pass when the field ties a risk or a precaution to this order:
- A named allergen, ingredient, diet, or irritant.
- Cross-contact, or a shared or dedicated prep area.
- A doneness or serving temperature for the item ordered.
Edge case: a named hazard plus a generic allergy sentence still passes.
</code></pre>
<p>Agreement on that dimension reached 90.5%. If a rubric says "specific," define the evidence. If it says "complete," say whether the answer must repeat every detail or just answer the request---that one clarification moved completeness from 78.6% to 97.6%.</p>
<p>When agreement is low, the cause might be the judge, the rubric, or the human labels. Those need different fixes.</p>
<h2>Look at failures, not only the average</h2>
<p>My first rubric revision raised mean agreement from 70.2% to 87.7%, while answer completeness <em>dropped</em> from 90.5% to 78.6%. The average improved; one dimension got worse.</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad0d118f152a9b88f15a073/8864538c-6b09-4cd1-bcc1-ae158f983a34.png" alt="" style="display:block;margin:0 auto" />

<p>Pass rates like these help locate problems, but similar rates can hide disagreement on <em>which</em> items pass. That's why agreement is <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step5_analysis_visualization.py#L108-L143">computed item by item</a>:</p>
<pre><code class="language-python">agreement_total += 1.0 if human_value == llm_value else 0.0
</code></pre>
<p>I review at three levels: the overall trend, each dimension, and the individual disagreements. The third is where debugging happens. The <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step5_analysis_visualization.py#L146-L200">per-category breakdown</a> showed Order Customization passing only 6 of 10 items. Reading the four failures (<code>qa_022</code>, <code>qa_027</code>, <code>qa_028</code>, <code>qa_029</code>) showed one pattern---safety notes that never named a dish---which became a targeted <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/prompts/step1_generator/v1.txt#L50-L69">generator-prompt change</a>.</p>
<p>Watch for the quiet failure too: no result at all. If the judge errors on an item, Step 4 <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step4_llm_as_judge.py#L199-L202">skips it</a>, and Step 5 scores the gap as zeros---indistinguishable from a judge that failed it everywhere. "No result" should be its own state.</p>
<h2>Be careful with human labels</h2>
<p>Human labels are not automatically ground truth. One reviewer---me---labeled the 42 examples, so the percentages measure agreement with my labels, not consensus.</p>
<p>There's a bigger limitation. I passed every item on every dimension. When the human column is all passes, agreement <em>is</em> the judge's pass rate. The 96.0% says the judge passes items I consider good. It says nothing about whether it would fail a bad one, because the set contains none.</p>
<p>The tooling nudged that way, too: in the <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/step3_human_labeling.py#L86-L95">labeling CLI</a>, pressing Enter counts as a pass. Fast for 252 judgments---but the file can't tell a deliberate pass from a skipped decision.</p>
<p>A mature process needs a second reviewer on at least a subset, and known-bad examples the judge is expected to catch.</p>
<h2>A practical loop</h2>
<p>No platform required, just discipline:</p>
<ol>
<li><p>Define the behavior and what failure looks like.</p>
</li>
<li><p>Build examples around normal cases, edge cases, and failure modes.</p>
</li>
<li><p>Establish reference labels and flag ambiguous ones.</p>
</li>
<li><p>Run the evaluation and track per-dimension results.</p>
</li>
<li><p>Read the disagreements.</p>
</li>
<li><p>Make one targeted change with a stated hypothesis.</p>
</li>
<li><p>Re-run the regression set, then check held-out examples.</p>
</li>
<li><p>Add important new failures to the regression set.</p>
</li>
</ol>
<p>I keep this in <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/46988dbe5384c67244262c5db3806d80e858a472/iteration-log.md"><code>iteration-log.md</code></a>, where every entry has the same fields: Change, Hypothesis, Result, Decision, Next step. Writing the hypothesis <em>before</em> the run makes it much harder to rationalize the result afterward.</p>
<h2>What a good score still can't tell you</h2>
<p>Offline evaluation can't guarantee production traffic resembles your set, or that every rare but serious failure is covered. A deployed feature also needs monitoring, user feedback, and explicit thresholds for high-risk behavior. A weak restaurant recommendation and a wrong allergen assurance shouldn't carry the same weight.</p>
<p>For AI Waiter, my next steps are a held-out set, a second reviewer, a severity-ranked review of the remaining disagreements, and folding important failures back into the regression set.</p>
<h2>Final thought</h2>
<p><strong>Evaluation is not a certificate of reliability. It is a feedback loop for engineering better behavior.</strong></p>
<p>My judge's agreement rose from 70.2% to 96.0% on the same 42 examples. That's a meaningful result for that experiment. The next question is whether it holds on new data and under more independent review.</p>
<p>The code, prompts, and full iteration history are in the <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator">repository</a>.</p>
<hr />
<h2>About this series</h2>
<p>This is the second entry in <strong>Building AI Products: Engineering Notes from the Field</strong>, a series about the experiments, implementation decisions, failures, and lessons behind building AI-powered products.</p>
<ul>
<li><p><strong>Project:</strong> <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator">AI Waiter Synthetic Data Generator</a></p>
</li>
<li><p><strong>Previous article:</strong> <em>My LLM Judge Went From 70% to 96% Agreement With Humans. Here's What I Learned.</em> --- add the published article URL here.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[My LLM Judge Went From 70% to 96% Agreement With Humans. Here’s What I Learned.]]></title><description><![CDATA[What three iterations of an LLM-as-a-judge rubric taught me about evaluation design, regression testing, and synthetic data quality.]]></description><link>https://idirocco.hashnode.dev/my-llm-judge-went-from-70-to-96-agreement-with-humans-here-s-what-i-learned</link><guid isPermaLink="true">https://idirocco.hashnode.dev/my-llm-judge-went-from-70-to-96-agreement-with-humans-here-s-what-i-learned</guid><category><![CDATA[AI Engineering]]></category><category><![CDATA[llm evaluation]]></category><category><![CDATA[LLM as a Judge]]></category><category><![CDATA[Synthetic Data Generation]]></category><category><![CDATA[Prompt Engineering]]></category><category><![CDATA[Software Engineering]]></category><dc:creator><![CDATA[Ignacio Di Rocco]]></dc:creator><pubDate>Thu, 15 Oct 2026 13:49:07 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad0d118f152a9b88f15a073/3a37ebb8-491c-4b1e-a4e1-77d693ba0475.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>An engineering diary about evaluating an AI waiter, debugging an LLM judge, and discovering that a better rubric can matter more than a cleverer model.</p>
<p>When I started building the evaluation pipeline for my AI Waiter project, I expected the hard part to be generating realistic restaurant conversations.</p>
<p>It wasn’t.</p>
<p>The harder problem was deciding whether the generated answers were actually good.</p>
<p>An AI waiter needs to do more than sound natural. It should answer menu questions accurately, give useful recommendations, handle dietary restrictions carefully, and stay within the scope of what a waiter should know or do.</p>
<p>I needed a way to evaluate those behaviors systematically. That meant using an LLM as a judge.</p>
<p>My first version agreed with my human reference labels only 70.2% of the time.</p>
<p>After three iterations of the judge prompt, agreement reached 96.0% on the same evaluation set.</p>
<p>The interesting part wasn’t the number itself. It was what the process revealed about evaluation design, rubric ambiguity, and the risk of trusting an LLM judge before understanding how it fails.</p>
<h2>The setup: an AI waiter needs more than a fluent answer</h2>
<p>The project is an <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator">AI Waiter Synthetic Data Generator</a>. Its pipeline generates synthetic data for an AI waiter and evaluates the quality of that data across multiple dimensions.</p>
<p>For this experiment, I focused on the judge responsible for assessing answer quality.</p>
<p>I used six evaluation dimensions:</p>
<ul>
<li><p>Safety specificity: Does the answer identify a relevant hazard or precaution rather than making a generic safety claim?</p>
</li>
<li><p>Tip usefulness: Does the recommendation offer something genuinely useful to the customer?</p>
</li>
<li><p>Menu realism: Is the answer plausible given the restaurant’s menu?</p>
</li>
<li><p>Scope appropriateness: Does the answer stay within the waiter’s role?</p>
</li>
<li><p>Context clarity: Is the relevant context clear enough to understand the answer?</p>
</li>
<li><p>Answer completeness: Does the answer actually address the customer’s request?</p>
</li>
</ul>
<p>These dimensions cover different failure modes. An answer can be fluent but incomplete, helpful-sounding but unsafe, or perfectly reasonable in isolation but inconsistent with the restaurant’s menu.</p>
<p>A single overall quality score would make those distinctions difficult to diagnose. Separate dimensions gave me a way to see where the judge was failing.</p>
<h2>The experiment: a fixed set and a human reference</h2>
<p>I used an evaluation set of 42 items. One human reviewer labeled the examples, and those labels served as the reference against which I measured the judge.</p>
<p>I kept the same 42 items for every judge-prompt version. That was important: it meant the measured changes came from changing the judge prompt, rather than from evaluating different examples each time.</p>
<p>The experiment was straightforward:</p>
<ol>
<li><p>Run the judge against the fixed evaluation set.</p>
</li>
<li><p>Compare its decisions with the human reference labels.</p>
</li>
<li><p>Inspect the dimensions with the weakest agreement.</p>
</li>
<li><p>Revise the rubric to make the pass/fail criteria more explicit.</p>
</li>
<li><p>Rerun the same set and compare the results.</p>
</li>
</ol>
<p>One important limitation: these are agreement scores with one reviewer, not a consensus among multiple annotators. The human labels are a useful reference, but they are not necessarily objective ground truth.</p>
<h2>The baseline: 70.2% agreement</h2>
<p>The first results made the problem obvious. The judge was doing reasonably well on some dimensions and badly on others.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Baseline agreement</th>
</tr>
</thead>
<tbody><tr>
<td>Safety specificity</td>
<td>9.5%</td>
</tr>
<tr>
<td>Tip usefulness</td>
<td>42.9%</td>
</tr>
<tr>
<td>Menu realism</td>
<td>85.7%</td>
</tr>
<tr>
<td>Scope appropriateness</td>
<td>95.2%</td>
</tr>
<tr>
<td>Context clarity</td>
<td>97.6%</td>
</tr>
<tr>
<td>Answer completeness</td>
<td>90.5%</td>
</tr>
<tr>
<td>Mean agreement</td>
<td>70.2%</td>
</tr>
</tbody></table>
<p>The weakest results were safety specificity and tip usefulness.</p>
<p>That pattern mattered. The judge was already good at identifying some fairly clear-cut problems, such as whether an answer fit the menu or stayed within the waiter’s role. It struggled when the quality dimension required interpretation.</p>
<p>The issue wasn't necessarily that the model couldn't understand the answer. The rubric left too much room for the model to decide what a passing answer should look like.</p>
<p>For example, what counts as a specific safety note? Is mentioning an allergy enough? Does the answer need to name an ingredient? What about cross-contact or a temperature precaution?</p>
<p>If those expectations aren't explicit, the judge has to invent its own interpretation of the rubric.</p>
<p>I decided to make the criteria more concrete before trying anything more elaborate.</p>
<h2>Iteration 1: make the rubric testable</h2>
<p>For the first revision, I added clearer pass criteria, examples of failures, and edge cases—particularly for <code>safety_specificity</code> and <code>tip_usefulness</code>.</p>
<p>The goal was to move from a subjective instruction to a rubric that could be applied consistently.</p>
<p>Here's an excerpt from the safety rubric in <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v1.txt">judge prompt v1</a>:</p>
<blockquote>
<p>Pass criteria: it names the hazard or precaution for this scenario.</p>
</blockquote>
<p>The rubric then gave concrete examples of what would qualify: a named allergen or ingredient to avoid, cross-contact or a shared or dedicated preparation area, a doneness temperature for the item ordered, a named spice or irritant, or a service problem paired with a fix.</p>
<p>It also clarified what should fail. Generic statements such as “Prepared safely” weren't sufficient if they didn't identify a hazard.</p>
<p>That distinction sounds small, but it changes the task. Instead of asking the judge to decide whether a safety note feels specific, it asks the judge to look for identifiable evidence.</p>
<p>The results improved substantially.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Baseline</th>
<th>Judge v1</th>
</tr>
</thead>
<tbody><tr>
<td>Safety specificity</td>
<td>9.5%</td>
<td>59.5%</td>
</tr>
<tr>
<td>Tip usefulness</td>
<td>42.9%</td>
<td>88.1%</td>
</tr>
<tr>
<td>Menu realism</td>
<td>85.7%</td>
<td>100%</td>
</tr>
<tr>
<td>Scope appropriateness</td>
<td>95.2%</td>
<td>100%</td>
</tr>
<tr>
<td>Context clarity</td>
<td>97.6%</td>
<td>100%</td>
</tr>
<tr>
<td>Answer completeness</td>
<td>90.5%</td>
<td>78.6%</td>
</tr>
<tr>
<td>Mean agreement</td>
<td>70.2%</td>
<td>87.7%</td>
</tr>
</tbody></table>
<p>Mean agreement increased by 17.5 percentage points, from 70.2% to 87.7%.</p>
<p>Safety specificity rose from 9.5% to 59.5%, while tip usefulness rose from 42.9% to 88.1%.</p>
<p>But there was a regression: answer completeness dropped from 90.5% to 78.6%.</p>
<p>This was a useful reminder that improving one part of a rubric can expose problems—or introduce new ones—in another. I hadn't solved evaluation quality; I'd made progress and uncovered the next issue.</p>
<h2>Iteration 2: fix what “complete” actually means</h2>
<p>The completeness regression forced me to look more carefully at the distinction between a complete answer and an exhaustive answer.</p>
<p>Those aren't the same thing.</p>
<p>If a customer asks whether a particular dish can be modified, a direct answer about that modification may be complete. The waiter doesn't necessarily need to repeat the entire menu, enumerate every service step, or restate every constraint.</p>
<p>A judge that expects all potentially relevant information to be repeated can mark concise, correct answers as incomplete.</p>
<p>I clarified the criterion in <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v2.txt">judge prompt v2</a>:</p>
<blockquote>
<p>Pass criteria: the answer gives a direct reply (a dish, a choice, a modification, or the next step for this request).</p>
</blockquote>
<p>The rubric also explicitly said not to require an answer to repeat every menu item, service step, or constraint.</p>
<p>The change narrowed the evaluation to what the customer actually needed.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th>Judge v1</th>
<th>Judge v2</th>
</tr>
</thead>
<tbody><tr>
<td>Answer completeness</td>
<td>78.6%</td>
<td>97.6%</td>
</tr>
<tr>
<td>Safety specificity</td>
<td>59.5%</td>
<td>64.3%</td>
</tr>
<tr>
<td>Tip usefulness</td>
<td>88.1%</td>
<td>88.1%</td>
</tr>
<tr>
<td>Mean agreement</td>
<td>87.7%</td>
<td>91.7%</td>
</tr>
</tbody></table>
<p>Completeness recovered to 97.6%, and mean agreement reached 91.7%.</p>
<p>The broader lesson: a rubric should define the evidence required to pass, not quietly impose an ideal answer format.</p>
<p>This matters especially in conversational AI, where a short answer can be more appropriate than a comprehensive one.</p>
<h2>Iteration 3: refine safety without rewarding generic claims</h2>
<p>Even after two iterations, safety specificity was the weakest dimension at 64.3%.</p>
<p>The initial rubric required a named hazard or precaution. That helped, but the remaining disagreements suggested that the criteria still didn't capture all the forms a relevant safety note could take.</p>
<p>A useful note might connect a specific order to a precaution, describe a temperature concern, or identify a service problem and the action the waiter should take. A rigid interpretation of “specificity” could miss some of those cases.</p>
<p>In <a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v3.txt">judge prompt v3</a>, I refined the criterion so the safety field could pass when it tied a risk or precaution to the order. The examples included named allergens, ingredients, dietary restrictions or irritants; cross-contact; temperature; and service problems paired with a waiter action.</p>
<p>The failure criterion also became more explicit: the field should fail when it names neither the order nor an action.</p>
<p>This was not simply lowering the bar for safety. The aim was to distinguish a relevant, order-specific safety note from an empty assertion that something is safe.</p>
<p>The results:</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Judge v2</th>
<th>Judge v3</th>
</tr>
</thead>
<tbody><tr>
<td>Safety specificity</td>
<td>64.3%</td>
<td>90.5%</td>
</tr>
<tr>
<td>Tip usefulness</td>
<td>88.1%</td>
<td>88.1%</td>
</tr>
<tr>
<td>Menu realism</td>
<td>100%</td>
<td>100%</td>
</tr>
<tr>
<td>Scope appropriateness</td>
<td>100%</td>
<td>100%</td>
</tr>
<tr>
<td>Context clarity</td>
<td>100%</td>
<td>100%</td>
</tr>
<tr>
<td>Answer completeness</td>
<td>97.6%</td>
<td>97.6%</td>
</tr>
<tr>
<td>Mean agreement</td>
<td>91.7%</td>
<td>96.0%</td>
</tr>
</tbody></table>
<p>Safety specificity improved by 26.2 percentage points in this final iteration, while the other reported dimensions held steady.</p>
<p>From the original baseline to v3, mean agreement increased from 70.2% to 96.0%, and safety specificity increased from 9.5% to 90.5%.</p>
<p>That was the result I was looking for—not just a higher aggregate score, but a rubric that was much more consistent on the dimension that had initially failed most often.</p>
<h2>The full progression</h2>
<p>Mean agreement with human reference labels</p>
<img src="https://cdn.hashnode.com/uploads/covers/6ad0d118f152a9b88f15a073/62cc04df-47d5-4e6b-a4bf-37dc30d9532e.png" alt="" style="display:block;margin:0 auto" />

<p>Across the three iterations, the process was less about writing a clever prompt and more about diagnosing ambiguity:</p>
<ul>
<li><p>Baseline → v1: Make the weakest dimensions operational with explicit pass criteria and failure examples.</p>
</li>
<li><p>v1 → v2: Correct a regression by defining completeness around answering the request, rather than repeating everything.</p>
</li>
<li><p>v2 → v3: Refine the safety rubric so it recognizes relevant order-specific precautions without accepting generic claims.</p>
</li>
</ul>
<p>The overall score tells one part of the story. The per-dimension scores explain why each change was worth making.</p>
<h2>How to reproduce the experiment</h2>
<p>The judge prompts are versioned in the repository, so the iterations can be inspected directly:</p>
<ul>
<li><p><a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v1.txt">Judge prompt v1</a></p>
</li>
<li><p><a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v2.txt">Judge prompt v2</a></p>
</li>
<li><p><a href="https://github.com/idirocco/aiwaiter-synthetic-data-generator/blob/main/prompts/step4_judge/v3.txt">Judge prompt v3</a></p>
</li>
</ul>
<p>From the project directory, the evaluation command is:</p>
<pre><code class="language-plaintext">python3 pipeline.py --generator-prompt=X --judge-prompt=Y
</code></pre>
<p>Replace <code>X</code> and <code>Y</code> with the generator and judge prompt versions you want to run.</p>
<p>For a meaningful comparison, keep the evaluation set fixed while changing the judge prompt. That's what I did here: the same 42 items were used for every judge-prompt version, with the same human labels as the reference.</p>
<p>One qualification: reproducing the comparison also depends on keeping the relevant model configuration and other evaluation conditions consistent. The command selects prompt versions, but by itself doesn't establish that every external dependency is identical across runs.</p>
<h2>What I learned about LLM-as-a-judge evaluation</h2>
<h3>1. Aggregate scores can hide the actual problem</h3>
<p>The baseline mean agreement of 70.2% didn't tell me which part of the judge needed attention. The dimension-level breakdown did.</p>
<p>Safety specificity was at 9.5%, while context clarity was already at 97.6%. Those two dimensions shouldn't be debugged in the same way.</p>
<p>When evaluating an LLM judge, inspect the confusion patterns and disagreements for individual dimensions before changing the whole prompt.</p>
<h3>2. Examples make abstract criteria actionable</h3>
<p>Words like specific, useful, and complete sound clear until you have to label borderline cases.</p>
<p>Concrete pass criteria and failure examples reduce the space for inconsistent interpretation. They also make it easier to explain why a particular answer should pass or fail.</p>
<p>The point isn't to make every possible case fit a list of examples. It's to establish the intended boundary and illustrate how to apply it.</p>
<h3>3. Prompt changes can cause regressions</h3>
<p>Judge v1 improved mean agreement dramatically, but completeness fell by 11.9 percentage points compared with baseline.</p>
<p>Without the per-dimension metrics, that regression could have been hidden by the improved overall score.</p>
<p>This is why I treat evaluation prompts as versioned artifacts and rerun a fixed regression set whenever I change the rubric.</p>
<h3>4. Don't optimize only for agreement</h3>
<p>A judge can agree with a human reviewer more often without necessarily becoming more correct in an absolute sense.</p>
<p>If the rubric is wrong, agreement may reward the wrong behavior. If the reference labels are inconsistent, a high agreement score can reproduce that inconsistency.</p>
<p>In this experiment, the human labels came from one reviewer. That makes the result useful as a measure of consistency with that reviewer's judgments, but it doesn't tell me how another reviewer would label the same examples.</p>
<h3>5. A fixed test set is useful—but only for what it covers</h3>
<p>Keeping the 42 items constant made the prompt iterations comparable. However, repeated tuning against the same set risks overfitting the judge prompt to those examples.</p>
<p>The next step should be to evaluate the final prompt on a separate set of examples that wasn't used during iteration. I'd also want more than one human reviewer for at least a subset of the data, so I could examine inter-annotator agreement and identify ambiguous labels.</p>
<h2>What 96% does—and doesn't—tell me</h2>
<p>The final result is encouraging, but it needs to be interpreted narrowly.</p>
<p>What it tells me:</p>
<ul>
<li><p>The judge's agreement with my human reference labels improved substantially on the 42-item evaluation set.</p>
</li>
<li><p>The biggest improvements came from clarifying criteria for safety specificity, tip usefulness, and answer completeness.</p>
</li>
<li><p>The final rubric performed much more consistently on the dimensions that initially caused trouble.</p>
</li>
</ul>
<p>What it doesn't establish:</p>
<ul>
<li><p>That the judge is 96% accurate across all AI waiter conversations.</p>
</li>
<li><p>That the human reference labels represent universal ground truth.</p>
</li>
<li><p>That the same agreement will hold on unseen examples, different restaurants, or different models.</p>
</li>
<li><p>That passing these six dimensions is sufficient to guarantee production safety.</p>
</li>
</ul>
<p>In particular, 90.5% agreement on safety specificity is not a safety certification. It is an agreement metric for one dimension on one fixed set.</p>
<p>For a production system, I'd want a broader evaluation set, explicit high-risk test cases, independent human review, and a way to inspect disagreements—not just the headline percentage.</p>
<h2>What's next</h2>
<p>I'd focus on three things:</p>
<ol>
<li><p>Build a held-out evaluation set. Test whether the v3 rubric generalizes beyond the 42 examples used to tune it.</p>
</li>
<li><p>Measure human disagreement. Have additional reviewers label a subset and distinguish judge errors from ambiguous reference labels.</p>
</li>
<li><p>Analyze the remaining failures. A 96.0% mean still leaves disagreements. I'd want to know exactly which examples fail, whether errors cluster around particular dimensions, and whether those errors have different operational risks.</p>
</li>
</ol>
<p>The broader goal is not to get a judge to produce an impressive number. It's to build an evaluation process that helps me make better engineering decisions about the system being evaluated.</p>
<h2>Final thought</h2>
<p>My biggest takeaway from this experiment is that an LLM judge is only as useful as the criteria it is asked to apply—and the evidence used to validate those criteria.</p>
<p>I started with a judge that agreed with my human reference labels 70.2% of the time. Three prompt iterations later, agreement reached 96.0% on the same 42 examples.</p>
<p>The progress came from treating evaluation like an engineering problem: identify where it fails, make the criteria explicit, rerun a controlled test, and inspect what got better and what got worse.</p>
<p>The number is a milestone, not the finish line. The next test is whether the rubric holds up on data it has never seen.</p>
]]></content:encoded></item></channel></rss>