<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Llm-as-Judge on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/llm-as-judge/</link><description>Recent content in Llm-as-Judge on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Thu, 04 Jun 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/llm-as-judge/index.xml" rel="self" type="application/rss+xml"/><item><title>Evaluating a RAG system without fooling yourself: RAGAS, the golden dataset and the four metrics that matter</title><link>https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/</link><pubDate>Thu, 04 Jun 2026 09:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>A RAG pipeline fails in ways that user satisfaction cannot distinguish: the LLM can hallucinate even with good chunks, or retrieval can ignore key documents even though the LLM synthesises well what it receives. RAGAS breaks evaluation down into four orthogonal metrics, faithfulness, answer relevance, context precision and context recall, each pointing at a different sub-component. The golden dataset is the reference calibrator; without it the metrics have no anchor. The complete stack runs 100 % on-premise with vLLM as the judge and Langfuse for traceability.&lt;/p>
&lt;hr>
&lt;h2 id="the-master-analogy-the-quality-inspector-at-a-furniture-factory">The master analogy: the quality inspector at a furniture factory&lt;/h2>
&lt;p>Imagine you manufacture chairs. You could ask customers &amp;ldquo;is it comfortable?&amp;rdquo; and leave it there. But that question does not tell you what to fix when the answer is &amp;ldquo;no&amp;rdquo;. The quality inspector does not ask that: they measure the board with a Shore hardness test, check that each leg is exactly 45 cm, verify that the assembly manual includes the twelve screws from the BOM, and detect whether a low-density board slipped past the incoming filter.&lt;/p>
&lt;p>RAGAS is that inspector applied to RAG:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Faithfulness&lt;/strong> → does the board have the specified hardness? The LLM can only use the material (chunks) that retrieval hands it.&lt;/li>
&lt;li>&lt;strong>Context Precision&lt;/strong> → is the leg exactly the right length? Of the K retrieved chunks, how many are genuinely useful and how many are filler that confuses the assembler?&lt;/li>
&lt;li>&lt;strong>Context Recall&lt;/strong> → does the manual include all the screws? Of all the facts the correct answer should contain, how many appear in the retrieved chunks?&lt;/li>
&lt;li>&lt;strong>Noise Sensitivity&lt;/strong> → if the operator uses a medium-low density board, does it show in the final product? If you introduce irrelevant chunks, does the LLM start hallucinating?&lt;/li>
&lt;/ul>
&lt;p>Without measuring each dimension separately, the diagnosis is opaque: &amp;ldquo;the RAG is not working well&amp;rdquo; does not tell you whether to repair the embedder, the reranker, the prompt or the corpus.&lt;/p>
&lt;hr>
&lt;h2 id="the-problem-of-evaluating-rag">The problem of evaluating RAG&lt;/h2>
&lt;p>Classification has one uncomfortable virtue: if you predict 87 out of 100 labels correctly, accuracy = 0.87. There is no ambiguity. RAG does not enjoy that luxury.&lt;/p>
&lt;p>A RAG system can fail in at least three independent ways:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Retrieval correct, LLM hallucinates&lt;/strong>: the chunks contain the correct answer, but the LLM generates claims that are not in those chunks. Faithfulness low; context recall high.&lt;/li>
&lt;li>&lt;strong>LLM correct, retrieval fails&lt;/strong>: retrieval returns irrelevant chunks (low context precision) or incomplete ones (low context recall). If the LLM has enough parametric knowledge, it may look as though it is answering well, but in reality it is ignoring the context, which is a time bomb once the parametric knowledge goes stale.&lt;/li>
&lt;li>&lt;strong>Retrieval and LLM both correct, answer does not answer the question&lt;/strong>: the answer is faithful to the context and the chunks are relevant, but the question was a different one. Answer relevance low.&lt;/li>
&lt;/ol>
&lt;p>Each failure mode requires a different metric and a different corrective action. Using a single metric (BLEU, ROUGE, user satisfaction) mixes the signals and makes it impossible to prioritise improvement work.&lt;/p>
&lt;hr>
&lt;h2 id="the-four-ragas-metrics">The four RAGAS metrics&lt;/h2>
&lt;h3 id="1-faithfulness--fidelity-to-the-context">1. Faithfulness — fidelity to the context&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> how many claims in the generated answer are supported by the retrieved chunks?&lt;/p>
&lt;p>&lt;strong>Calculation:&lt;/strong>&lt;/p>
$$\text{Faithfulness} = \frac{|\text{claims supported by the context}|}{|\text{total claims in the answer}|}$$
&lt;p>The process uses an LLM-as-judge (see https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/): first the atomic claims are extracted from the answer (&amp;ldquo;the model was released in 2023&amp;rdquo;, &amp;ldquo;it supports contexts of 128k tokens&amp;rdquo;, and so on), then the judge classifies each claim as &lt;em>supported&lt;/em> or &lt;em>not supported&lt;/em> by the chunks.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong> The generated answer has 5 claims. The judge determines that 4 are in the chunks and 1 is an unsupported extrapolation.&lt;/p>
$$\text{Faithfulness} = \frac{4}{5} = 0{.}80$$
&lt;p>&lt;strong>Warning sign:&lt;/strong> faithfulness &amp;lt; 0.85 indicates that the LLM is generating content that goes beyond the context, which is to say it is hallucinating with superficial backing.&lt;/p>
&lt;h3 id="2-answer-relevance--relevance-of-the-answer">2. Answer Relevance — relevance of the answer&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> does the answer actually answer the question as posed?&lt;/p>
&lt;p>&lt;strong>Intuition:&lt;/strong> An answer that answers the question well &amp;ldquo;implies&amp;rdquo; that question. If you generate N hypothetical questions from the answer and measure their semantic similarity to the original question, you get a relevance signal.&lt;/p>
&lt;p>&lt;strong>Calculation:&lt;/strong>&lt;/p>
$$\text{AnswerRelevance} = \frac{1}{N} \sum_{i=1}^{N} \cos(\vec{q}_{\text{original}}, \vec{q}_{i}^{\text{generated}})$$
&lt;p>where $\vec{q}$ are question embeddings.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong> For the question &amp;ldquo;Which Python versions does FastAPI support?&amp;rdquo; and an answer about web frameworks in general, the hypothetical questions generated will be along the lines of &amp;ldquo;what are the best web frameworks?&amp;rdquo;, with low cosine against the original question → low answer relevance.&lt;/p>
&lt;h3 id="3-context-precision--retrieval-precision">3. Context Precision — retrieval precision&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> of the K retrieved chunks, what proportion are genuinely relevant?&lt;/p>
&lt;p>&lt;strong>Calculation&lt;/strong> (weighted version):&lt;/p>
$$\text{ContextPrecision@K} = \frac{\sum_{k=1}^{K} \text{Precision@}k \cdot \mathbb{1}[\text{chunk}_k \text{ is relevant}]}{|\text{relevant chunks in top-K}|}$$
&lt;p>The most direct form: the LLM judge classifies each chunk as relevant or not for answering the question. Precision is the relevant fraction.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong> 5 chunks are retrieved. The judge considers 3 relevant and 2 noise.&lt;/p>
$$\text{ContextPrecision} = \frac{3}{5} = 0{.}60$$
&lt;p>&lt;strong>Warning sign:&lt;/strong> precision &amp;lt; 0.6 indicates that retrieval is contaminating the context with information that can contradict or dilute the correct answer.&lt;/p>
&lt;h3 id="4-context-recall--retrieval-recall">4. Context Recall — retrieval recall&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> of all the facts needed to build the correct answer (ground truth), what proportion are covered by the retrieved chunks?&lt;/p>
&lt;p>&lt;strong>Calculation:&lt;/strong>&lt;/p>
$$\text{ContextRecall} = \frac{|\text{ground-truth claims attributable to some chunk}|}{|\text{total claims in ground truth}|}$$
&lt;p>This metric &lt;strong>requires ground truth&lt;/strong>, which is to say you need the golden dataset.&lt;/p>
&lt;p>&lt;strong>Example:&lt;/strong> The ground truth has 6 claims. The retrieved chunks cover 5 of them; the sixth is in a document that retrieval did not find.&lt;/p>
$$\text{ContextRecall} = \frac{5}{6} \approx 0{.}83$$
&lt;h3 id="5-noise-sensitivity--sensitivity-to-noise">5. Noise Sensitivity — sensitivity to noise&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> if you introduce irrelevant chunks into the context, how far does faithfulness fall?&lt;/p>
&lt;p>It is computed as the difference in faithfulness between the clean condition and the condition with controlled noise:&lt;/p>
$$\Delta\text{Faithfulness} = \text{Faithfulness}_{\text{clean}} - \text{Faithfulness}_{\text{noisy}}$$
&lt;p>A $\Delta > 0{.}15$ indicates that the prompt or the LLM are fragile against contaminated contexts, a problem that low context precision turns into a routine occurrence.&lt;/p>
&lt;hr>
&lt;h2 id="a-complete-worked-example">A complete worked example&lt;/h2>
&lt;h3 id="scenario">Scenario&lt;/h3>
&lt;p>&lt;strong>Question:&lt;/strong> &amp;ldquo;What latency guarantees does Redis offer for in-memory read operations with a 10 GB dataset?&amp;rdquo;&lt;/p>
&lt;p>&lt;strong>Answer generated by the RAG:&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Redis guarantees sub-millisecond latencies for GET operations on datasets that fit in RAM. With a 10 GB dataset and clients on the same local network, P99 is typically below 1 ms. Redis also supports horizontal clustering to scale beyond the RAM of a single node. In addition, Redis has included native JSON support since version 7.2.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Ground truth (reference answer):&lt;/strong>&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;Redis operates entirely in memory, which guarantees sub-millisecond latencies for GET. On local networks with 10 GB datasets in RAM, P99 sits below 1 ms. Clustering allows scaling beyond the RAM of a single node.&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>&lt;strong>Retrieved chunks (5 chunks, summarised fragments):&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Summarised content&lt;/th>
&lt;th>Relevant&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>C1&lt;/td>
&lt;td>&amp;ldquo;Redis operates in memory; GET has latencies &amp;lt; 1 ms on LAN&amp;rdquo;&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C2&lt;/td>
&lt;td>&amp;ldquo;Redis Cluster allows sharding to scale total RAM&amp;rdquo;&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C3&lt;/td>
&lt;td>&amp;ldquo;Redis Sentinel manages high availability through automatic failover&amp;rdquo;&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C4&lt;/td>
&lt;td>&amp;ldquo;Redis benchmarks: P50 = 0.3 ms, P99 = 0.9 ms on a 10 GB dataset&amp;rdquo;&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C5&lt;/td>
&lt;td>&amp;ldquo;Redis Stack adds modules: RedisJSON, RediSearch, RedisTimeSeries&amp;rdquo;&lt;/td>
&lt;td>No&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="step-by-step-calculation">Step-by-step calculation&lt;/h3>
&lt;p>&lt;strong>Faithfulness:&lt;/strong>&lt;/p>
&lt;p>Claims in the generated answer:&lt;/p>
&lt;ol>
&lt;li>&amp;ldquo;Redis guarantees sub-millisecond latencies for GET on in-RAM datasets&amp;rdquo; → &lt;strong>supported&lt;/strong> by C1, C4&lt;/li>
&lt;li>&amp;ldquo;With 10 GB on LAN, P99 &amp;lt; 1 ms&amp;rdquo; → &lt;strong>supported&lt;/strong> by C4&lt;/li>
&lt;li>&amp;ldquo;Redis supports horizontal clustering to scale RAM&amp;rdquo; → &lt;strong>supported&lt;/strong> by C2&lt;/li>
&lt;li>&amp;ldquo;Redis has included native JSON support since version 7.2&amp;rdquo; → &lt;strong>NOT supported&lt;/strong> by any chunk (C5 mentions RedisJSON as a Redis Stack module, not as native to Redis core)&lt;/li>
&lt;/ol>
$$\text{Faithfulness} = \frac{3}{4} = 0{.}75$$
&lt;p>Claim 4 is an extrapolation that mixes information from C5 imprecisely, a partial hallucination.&lt;/p>
&lt;p>&lt;strong>Context Precision:&lt;/strong>&lt;/p>
&lt;p>Relevant chunks: C1, C2, C4 (3 out of 5).&lt;/p>
$$\text{ContextPrecision} = \frac{3}{5} = 0{.}60$$
&lt;p>C3 and C5 are noise. C5 in particular contributed to the partial hallucination about JSON.&lt;/p>
&lt;p>&lt;strong>Context Recall:&lt;/strong>&lt;/p>
&lt;p>Ground-truth claims:&lt;/p>
&lt;ol>
&lt;li>&amp;ldquo;Redis operates in memory, GET &amp;lt; 1 ms&amp;rdquo; → attributable to C1 ✓&lt;/li>
&lt;li>&amp;ldquo;P99 &amp;lt; 1 ms on LAN with 10 GB&amp;rdquo; → attributable to C4 ✓&lt;/li>
&lt;li>&amp;ldquo;Clustering scales beyond the RAM of a single node&amp;rdquo; → attributable to C2 ✓&lt;/li>
&lt;/ol>
$$\text{ContextRecall} = \frac{3}{3} = 1{.}00$$
&lt;p>Retrieval found every chunk needed for the ground truth. The problem is not recall but precision (C3 and C5 contaminated the context).&lt;/p>
&lt;p>&lt;strong>Answer Relevance:&lt;/strong>&lt;/p>
&lt;p>The judge generates 3 hypothetical questions from the answer:&lt;/p>
&lt;ul>
&lt;li>&amp;ldquo;What latencies does Redis offer for in-memory reads?&amp;rdquo; — cos = 0.91&lt;/li>
&lt;li>&amp;ldquo;How does Redis scale horizontally?&amp;rdquo; — cos = 0.74&lt;/li>
&lt;li>&amp;ldquo;Which JSON modules does Redis include?&amp;rdquo; — cos = 0.52 (a drift caused by the hallucination)&lt;/li>
&lt;/ul>
$$\text{AnswerRelevance} = \frac{0{.}91 + 0{.}74 + 0{.}52}{3} = 0{.}72$$
&lt;p>The drift towards JSON reduced relevance. A tighter answer would have scored around 0.90.&lt;/p>
&lt;h3 id="summary-of-the-example">Summary of the example&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Value&lt;/th>
&lt;th>Diagnosis&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Faithfulness&lt;/td>
&lt;td>0.75&lt;/td>
&lt;td>LLM extrapolated beyond the context&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Precision&lt;/td>
&lt;td>0.60&lt;/td>
&lt;td>Retrieval returned 2 irrelevant chunks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Recall&lt;/td>
&lt;td>1.00&lt;/td>
&lt;td>Retrieval captured everything needed&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Answer Relevance&lt;/td>
&lt;td>0.72&lt;/td>
&lt;td>Answer drifts off topic&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Main corrective action:&lt;/strong> improve the reranker so that C3 and C5 are filtered before they reach the LLM. The faithfulness and relevance problem is a direct consequence of the low precision, not of the LLM itself.&lt;/p>
&lt;hr>
&lt;h2 id="building-the-golden-dataset">Building the golden dataset&lt;/h2>
&lt;h3 id="what-it-is-and-why-it-matters">What it is and why it matters&lt;/h3>
&lt;p>The golden dataset is a set of &lt;code>(question, relevant chunks, correct answer)&lt;/code> tuples that acts as a reference calibrator. Without it, context recall cannot be computed (there is no ground truth) and the other metrics lack any interpretive anchor: is 0.75 faithfulness good or bad for this corpus and this domain?&lt;/p>
&lt;p>A well-built golden dataset lets you:&lt;/p>
&lt;ul>
&lt;li>Compare pipeline versions (embedder v1 vs v2, chunk size 512 vs 1024)&lt;/li>
&lt;li>Detect regressions in CI before deploying&lt;/li>
&lt;li>Stratify the analysis by question type&lt;/li>
&lt;/ul>
&lt;h3 id="llm-assisted-construction-pipeline">LLM-assisted construction pipeline&lt;/h3>
&lt;p>Purely manual construction is expensive. The standard pattern in 2026 is LLM assistance with human review of a sample:&lt;/p>
&lt;p>&lt;strong>Step 1 — Seed chunk selection.&lt;/strong> From the full corpus, select representative chunks through stratified sampling (by section, date, document type). For a technical corpus of 10,000 chunks, 500-1,000 seeds is a reasonable starting point.&lt;/p>
&lt;p>&lt;strong>Step 2 — Question generation.&lt;/strong> A powerful LLM (Llama-3.1-70B or similar) generates 2-3 questions per seed chunk using a prompt along these lines:&lt;/p>
&lt;pre tabindex="0">&lt;code>Given the following documentation fragment, generate specific questions
that can only be answered correctly using THIS fragment and not
general knowledge. The questions should be the ones an engineer looking
for operational information would ask.
Fragment: {chunk}
&lt;/code>&lt;/pre>&lt;p>&lt;strong>Step 3 — Reference answer generation.&lt;/strong> The same LLM, with access to the seed chunk (and to adjacent chunks if the question requires them), generates the reference answer.&lt;/p>
&lt;p>&lt;strong>Step 4 — Human review of a sample.&lt;/strong> Manually review 10-20 % of the generated dataset. The most common rejection criteria: trivial questions that any LLM answers without the corpus, answers the LLM padded with parametric knowledge instead of the chunks, and badly formulated or ambiguous questions.&lt;/p>
&lt;h3 id="minimum-size">Minimum size&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Use case&lt;/th>
&lt;th>Minimum pairs&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Prototype / initial validation&lt;/td>
&lt;td>50-100&lt;/td>
&lt;td>Enough to spot coarse problems&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Technical corpus in production&lt;/td>
&lt;td>200-500&lt;/td>
&lt;td>Allows basic stratification&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Robust production with full stratification&lt;/td>
&lt;td>500-1,000+&lt;/td>
&lt;td>Needed to detect subtle regressions&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="stratifying-the-dataset">Stratifying the dataset&lt;/h3>
&lt;p>A flat golden dataset measures the average but hides the extreme cases. The minimum recommended stratification includes three question types:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Easy (single-hop):&lt;/strong> A single chunk contains all the information needed. The baseline that any decent RAG must beat.&lt;/li>
&lt;li>&lt;strong>Hard (multi-hop):&lt;/strong> The correct answer requires combining information from 2-4 different chunks. This is where the limits of the reranker and the synthesis prompt show up.&lt;/li>
&lt;li>&lt;strong>Adversarial:&lt;/strong> The question has a false premise, or the corpus does not contain the answer. A correct RAG must answer &amp;ldquo;I do not have enough information&amp;rdquo;, whereas a fragile RAG hallucinates confidently. This question type directly measures the risk of high-impact hallucination.&lt;/li>
&lt;/ul>
&lt;h3 id="the-goodhart-trap">The Goodhart trap&lt;/h3>
&lt;blockquote>
&lt;p>&amp;ldquo;When a measure becomes a target, it ceases to be a good measure.&amp;rdquo; — Charles Goodhart&lt;/p>
&lt;/blockquote>
&lt;p>If you optimise the embedder or the reranker using the golden dataset as a loss function, the dataset is corrupted as a metric: the system learns to perform well on those specific questions without improving on the general domain.&lt;/p>
&lt;p>The solution is the same as in supervised ML: separate the &lt;strong>dev set&lt;/strong> (for optimisation and iteration) from the &lt;strong>test set&lt;/strong> (for final evaluation, frozen and audited). The test set must never be used to make design decisions; only to report the state of the system at release time.&lt;/p>
&lt;hr>
&lt;h2 id="correlation-with-real-satisfaction">Correlation with real satisfaction&lt;/h2>
&lt;p>Field studies published by the Databricks teams (2024) and the RAGAS adoption analyses (2025) point to interpretable operational thresholds:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric range&lt;/th>
&lt;th>Observable symptom&lt;/th>
&lt;th>Corrective action&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Faithfulness &amp;lt; 0.75&lt;/td>
&lt;td>Users frequently report &amp;ldquo;made-up answers&amp;rdquo;&lt;/td>
&lt;td>Review the LLM prompt; increase citation instructions; lower the temperature&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Faithfulness 0.75-0.85&lt;/td>
&lt;td>Occasional hallucinations on peripheral topics&lt;/td>
&lt;td>Improve context precision to eliminate contaminating chunks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Faithfulness ≥ 0.85&lt;/td>
&lt;td>Correlates with positive NPS in field studies&lt;/td>
&lt;td>Hold steady; monitor for drift&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Precision &amp;lt; 0.60&lt;/td>
&lt;td>LLM includes contradictory information; inconsistent answers&lt;/td>
&lt;td>Tune the reranker; reduce K; review similarity thresholds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Context Recall &amp;lt; 0.70&lt;/td>
&lt;td>Multi-hop questions fail; key information missing&lt;/td>
&lt;td>Review the chunking strategy; add larger chunks; enrich metadata&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Answer Relevance &amp;lt; 0.70&lt;/td>
&lt;td>Answers that are &amp;ldquo;correct but do not answer&amp;rdquo;&lt;/td>
&lt;td>Review the synthesis prompt; add an explicit instruction to stick to the question&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Low context precision is especially pernicious: irrelevant chunks are not neutral. They raise the probability that the LLM uses incorrect information as though it were relevant, degrading faithfulness in a chain reaction. It is the transmission belt by which a retrieval problem turns into an LLM problem.&lt;/p>
&lt;hr>
&lt;h2 id="diagram-the-continuous-evaluation-loop">Diagram: the continuous evaluation loop&lt;/h2>
&lt;figure>
&lt;svg viewBox="0 0 800 420" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="rag-eval-title rag-eval-desc" style="width:100%;max-width:800px;font-family:system-ui,sans-serif;">
&lt;title id="rag-eval-title">RAG evaluation loop with RAGAS&lt;/title>
&lt;desc id="rag-eval-desc">Circular diagram showing the flow from corpus to corrective action via retrieval, LLM, answer, RAGAS judge and metrics with alerts.&lt;/desc>
&lt;!-- Background -->
&lt;rect width="800" height="420" fill="#0f1117" rx="12"/>
&lt;!-- Node: Corpus -->
&lt;rect x="30" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="85" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Corpus&lt;/text>
&lt;text x="85" y="207" text-anchor="middle" fill="#718096" font-size="10">documents&lt;/text>
&lt;!-- Arrow Corpus → Retrieval -->
&lt;line x1="140" y1="195" x2="175" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Node: Retrieval -->
&lt;rect x="175" y="170" width="120" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="235" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Retrieval&lt;/text>
&lt;text x="235" y="207" text-anchor="middle" fill="#718096" font-size="10">top-K chunks&lt;/text>
&lt;!-- Arrow Retrieval → LLM -->
&lt;line x1="295" y1="195" x2="330" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Node: LLM -->
&lt;rect x="330" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="385" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">LLM&lt;/text>
&lt;text x="385" y="207" text-anchor="middle" fill="#718096" font-size="10">synthesis&lt;/text>
&lt;!-- Arrow LLM → Answer -->
&lt;line x1="440" y1="195" x2="475" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Node: Answer -->
&lt;rect x="475" y="170" width="110" height="50" rx="8" fill="#1e2433" stroke="#4a5568" stroke-width="1.5"/>
&lt;text x="530" y="191" text-anchor="middle" fill="#e2e8f0" font-size="12" font-weight="600">Answer&lt;/text>
&lt;text x="530" y="207" text-anchor="middle" fill="#718096" font-size="10">generated&lt;/text>
&lt;!-- Arrow Answer → RAGAS -->
&lt;line x1="585" y1="195" x2="620" y2="195" stroke="#4a5568" stroke-width="1.5" marker-end="url(#arrow)"/>
&lt;!-- Node: RAGAS Judge -->
&lt;rect x="620" y="170" width="130" height="50" rx="8" fill="#1a2640" stroke="#3b82f6" stroke-width="2"/>
&lt;text x="685" y="191" text-anchor="middle" fill="#93c5fd" font-size="12" font-weight="600">RAGAS Judge&lt;/text>
&lt;text x="685" y="207" text-anchor="middle" fill="#6b9fd4" font-size="10">LLM-as-judge&lt;/text>
&lt;!-- Arrow RAGAS → Metrics (downwards) -->
&lt;line x1="685" y1="220" x2="685" y2="285" stroke="#3b82f6" stroke-width="1.5" marker-end="url(#arrow-blue)"/>
&lt;!-- Node: Metrics -->
&lt;rect x="615" y="285" width="140" height="60" rx="8" fill="#1a2a1a" stroke="#22c55e" stroke-width="1.5"/>
&lt;text x="685" y="305" text-anchor="middle" fill="#86efac" font-size="12" font-weight="600">Metrics&lt;/text>
&lt;text x="685" y="320" text-anchor="middle" fill="#4ade80" font-size="9">faithfulness · precision&lt;/text>
&lt;text x="685" y="333" text-anchor="middle" fill="#4ade80" font-size="9">recall · relevance&lt;/text>
&lt;!-- Arrow Metrics → Alert -->
&lt;line x1="615" y1="315" x2="510" y2="315" stroke="#22c55e" stroke-width="1.5" marker-end="url(#arrow-green)"/>
&lt;!-- Node: Grafana alert -->
&lt;rect x="390" y="285" width="120" height="60" rx="8" fill="#2a1a1a" stroke="#f97316" stroke-width="1.5"/>
&lt;text x="450" y="305" text-anchor="middle" fill="#fdba74" font-size="12" font-weight="600">Alert&lt;/text>
&lt;text x="450" y="320" text-anchor="middle" fill="#fb923c" font-size="9">Prometheus&lt;/text>
&lt;text x="450" y="333" text-anchor="middle" fill="#fb923c" font-size="9">Grafana&lt;/text>
&lt;!-- Arrow Alert → Action -->
&lt;line x1="390" y1="315" x2="285" y2="315" stroke="#f97316" stroke-width="1.5" marker-end="url(#arrow-orange)"/>
&lt;!-- Node: Action -->
&lt;rect x="155" y="285" width="130" height="60" rx="8" fill="#1a1a2a" stroke="#a855f7" stroke-width="1.5"/>
&lt;text x="220" y="305" text-anchor="middle" fill="#d8b4fe" font-size="12" font-weight="600">Corrective action&lt;/text>
&lt;text x="220" y="320" text-anchor="middle" fill="#c084fc" font-size="9">retrieval / chunking&lt;/text>
&lt;text x="220" y="333" text-anchor="middle" fill="#c084fc" font-size="9">prompt / fine-tuning&lt;/text>
&lt;!-- Arrow Action → Corpus (closing the loop) -->
&lt;line x1="155" y1="315" x2="85" y2="315" stroke="#a855f7" stroke-width="1.5"/>
&lt;line x1="85" y1="315" x2="85" y2="222" stroke="#a855f7" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
&lt;!-- Golden Dataset (side input) -->
&lt;rect x="600" y="350" width="155" height="40" rx="6" fill="#1a2633" stroke="#64748b" stroke-width="1" stroke-dasharray="5,3"/>
&lt;text x="677" y="366" text-anchor="middle" fill="#94a3b8" font-size="10" font-weight="600">Golden Dataset&lt;/text>
&lt;text x="677" y="380" text-anchor="middle" fill="#64748b" font-size="9">ground truth for recall&lt;/text>
&lt;!-- Arrow Golden Dataset → Metrics -->
&lt;line x1="677" y1="350" x2="685" y2="347" stroke="#64748b" stroke-width="1" stroke-dasharray="4,3" marker-end="url(#arrow-gray)"/>
&lt;!-- Langfuse label -->
&lt;rect x="460" y="130" width="90" height="28" rx="5" fill="#1a2633" stroke="#64748b" stroke-width="1" stroke-dasharray="4,2"/>
&lt;text x="505" y="148" text-anchor="middle" fill="#94a3b8" font-size="9">Langfuse tracing&lt;/text>
&lt;!-- Langfuse line above -->
&lt;line x1="505" y1="158" x2="505" y2="170" stroke="#64748b" stroke-width="1" stroke-dasharray="3,2"/>
&lt;!-- Marker definitions -->
&lt;defs>
&lt;marker id="arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#4a5568"/>
&lt;/marker>
&lt;marker id="arrow-blue" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#3b82f6"/>
&lt;/marker>
&lt;marker id="arrow-green" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#22c55e"/>
&lt;/marker>
&lt;marker id="arrow-orange" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#f97316"/>
&lt;/marker>
&lt;marker id="arrow-purple" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#a855f7"/>
&lt;/marker>
&lt;marker id="arrow-gray" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
&lt;path d="M0,0 L0,6 L8,3 z" fill="#64748b"/>
&lt;/marker>
&lt;/defs>
&lt;/svg>
&lt;figcaption style="text-align:center;font-size:0.85em;color:#718096;margin-top:0.5em">The continuous evaluation loop: corpus → retrieval → LLM → RAGAS judge → metrics → alert → corrective action → corpus.&lt;/figcaption>
&lt;/figure>
&lt;hr>
&lt;h2 id="the-2026-oss-stack-for-running-ragas-on-premise">The 2026 OSS stack for running RAGAS on-premise&lt;/h2>
&lt;h3 id="ragas-apache-20">ragas (Apache 2.0)&lt;/h3>
&lt;p>The &lt;code>ragas&lt;/code> library supports asynchronous evaluation and multiple LLM backends. Integrating vLLM as the judge removes any need to send data to external APIs, which is critical in environments with sensitive data.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">ragas&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">evaluate&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">ragas.metrics&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">faithfulness&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">answer_relevancy&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_precision&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">context_recall&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langchain_openai&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">ChatOpenAI&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">OpenAIEmbeddings&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Judge LLM pointing at on-premise vLLM&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">judge_llm&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">ChatOpenAI&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;meta-llama/Llama-3.1-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://vllm-service:8000/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-local&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># vLLM ignores the value but requires the field&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">embeddings&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">OpenAIEmbeddings&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;BAAI/bge-m3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">base_url&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;http://embedding-service:8001/v1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">api_key&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;sk-local&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">evaluate&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">dataset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">golden_dataset&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="c1"># HuggingFace Dataset with standard columns&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">metrics&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="n">faithfulness&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">answer_relevancy&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_precision&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">context_recall&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">llm&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">judge_llm&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">embeddings&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">embeddings&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The dataset RAGAS expects has four columns: &lt;code>question&lt;/code>, &lt;code>answer&lt;/code>, &lt;code>contexts&lt;/code> (a list of strings), &lt;code>ground_truth&lt;/code>.&lt;/p>
&lt;h3 id="langfuse-for-eval-traceability">Langfuse for eval traceability&lt;/h3>
&lt;p>Each RAGAS evaluation is recorded in Langfuse as a &lt;em>dataset experiment&lt;/em>, linking the scores to the production spans (see https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/). This makes it possible to correlate a drop in faithfulness with the specific request that caused it. Without that link, the metrics are numbers with no actionable context.&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">langfuse&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">lf&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">Langfuse&lt;/span>&lt;span class="p">()&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Create or retrieve the dataset in Langfuse&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="n">dataset&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">lf&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">get_or_create_dataset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;rag-golden-v3&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Record the experiment scores&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="k">for&lt;/span> &lt;span class="n">idx&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">row&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">result&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">to_pandas&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">iterrows&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">lf&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">score&lt;/span>&lt;span class="p">(&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;ragas-faithfulness&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;faithfulness&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="n">trace_id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="n">row&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;trace_id&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span> &lt;span class="c1"># linked to the production span&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="prometheus--grafana-for-operational-alerts">Prometheus + Grafana for operational alerts&lt;/h3>
&lt;p>RAGAS metrics are exposed as Prometheus gauges. A Grafana dashboard with thresholds configures alerts when faithfulness falls sustainedly below 0.80:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Prometheus alert rule&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RAGFaithfulnessLow&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">avg_over_time(rag_faithfulness_score[30m]) &amp;lt; 0.80&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">10m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">annotations&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;RAG faithfulness below threshold ({{ $value | humanize }})&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Review context precision and the reranker. Possible corpus drift.&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="running-ragas-against-on-premise-vllm--practical-considerations">Running RAGAS against on-premise vLLM — practical considerations&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Judge size:&lt;/strong> Llama-3.1-70B as the judge produces results comparable to GPT-4 on faithfulness and context evaluation, according to the RAGAS 0.2 benchmarks (2025). Smaller models (8B-13B) degrade judge quality on multi-hop questions.&lt;/li>
&lt;li>&lt;strong>Throughput:&lt;/strong> On on-premise hardware with 4×H100 SXM (320 GB, NVLink), a run of 200 evaluations with Llama-3.1-70B takes roughly 8-12 minutes with batch_size=8 and vLLM in continuous batching mode.&lt;/li>
&lt;li>&lt;strong>Cost per evaluation:&lt;/strong> With no external API, the marginal cost is electricity plus GPU amortisation. With 4×H100 at around 3 kW sustained, a run of 200 evaluations costs &amp;lt; 0.10 € in energy at a typical industrial tariff.&lt;/li>
&lt;li>&lt;strong>Recommended frequency:&lt;/strong> weekly offline eval over the full golden dataset plus sampled online eval (5-10 % of production requests) with a subset of metrics that do not require ground truth (faithfulness, answer relevance).&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="what-we-have-not-covered">What we have not covered&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Alternatives to RAGAS:&lt;/strong> TruLens (evaluation with modular feedbacks), DeepEval (programmatic assertions, pytest integration), ARES (Stanford framework with trained classifiers instead of LLM-as-judge), and the OpenAI evals framework. Each has different trade-offs in judge cost, reliability and ease of integration.&lt;/li>
&lt;li>&lt;strong>Continuous eval in production:&lt;/strong> automatically sampling real requests, anonymising them, running a subset of metrics without ground truth and using the result to detect system drift before users report it. It requires a data pipeline separate from the inference pipeline.&lt;/li>
&lt;li>&lt;strong>Multilingual eval:&lt;/strong> RAGAS with a judge in Spanish or Catalan over a non-English corpus has documented biases when the judge is a model trained fundamentally in English. The semantic similarity embeddings for answer relevance are especially sensitive to corpus language vs judge language.&lt;/li>
&lt;li>&lt;strong>A/B testing of RAG configurations:&lt;/strong> using RAGAS metrics as the success criterion in controlled experiments, chunk size 512 vs 1024, pure BM25 vs hybrid, cross-encoder reranker vs biencoder, with statistical significance computed over the golden dataset.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/ — the LLM judge pattern that RAGAS uses to measure faithfulness claim by claim&lt;/li>
&lt;li>https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/ — the general evals framework in which RAGAS is the RAG specialisation&lt;/li>
&lt;li>https://blog.lo0.es/en/posts/reranker-hybrid-retrieval-five-chunks-llm-reads/ — the retrieval layer whose context precision and recall these metrics measure&lt;/li>
&lt;li>https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/ — the corpus quality that context recall reflects&lt;/li>
&lt;li>https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/ — the production spans where Langfuse records the RAGAS scores&lt;/li>
&lt;li>https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/ — the golden dataset is a data artefact that needs versioning just like the corpus&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="references">References&lt;/h2>
&lt;ol>
&lt;li>Es Shahul, et al. &lt;em>RAGAS: Automated Evaluation of Retrieval Augmented Generation&lt;/em>. arXiv:2309.15217 (2023). &lt;a href="https://arxiv.org/abs/2309.15217">https://arxiv.org/abs/2309.15217&lt;/a>&lt;/li>
&lt;li>RAGAS Documentation v0.2. &lt;em>Metrics Reference&lt;/em>. &lt;a href="https://docs.ragas.io/en/stable/concepts/metrics/">https://docs.ragas.io/en/stable/concepts/metrics/&lt;/a> (accessed June 2026)&lt;/li>
&lt;li>Langfuse. &lt;em>Dataset Experiments&lt;/em>. &lt;a href="https://langfuse.com/docs/datasets/overview">https://langfuse.com/docs/datasets/overview&lt;/a> (accessed June 2026)&lt;/li>
&lt;li>Databricks. &lt;em>LLM Quality Evaluation: From Lab to Production&lt;/em>. Databricks Engineering Blog (2024).&lt;/li>
&lt;li>Saad-Falcon, J. et al. &lt;em>ARES: An Automated Evaluation Framework for Retrieval-Augmented Generation Systems&lt;/em>. arXiv:2311.09476 (2023).&lt;/li>
&lt;li>Goodhart, C.A.E. &lt;em>Problems of Monetary Management: The U.K. Experience&lt;/em>. Papers in Monetary Economics. Reserve Bank of Australia (1975). Modern formulation of the law that bears his name.&lt;/li>
&lt;li>vLLM Project. &lt;em>OpenAI-Compatible Server&lt;/em>. &lt;a href="https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html">https://docs.vllm.ai/en/stable/serving/openai_compatible_server.html&lt;/a> (accessed June 2026)&lt;/li>
&lt;/ol></description></item><item><title>Canary, blue-green and shadow for LLM models: how to deploy a new version without breaking the SLO</title><link>https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/</link><pubDate>Mon, 01 Jun 2026 16:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/</guid><description>&lt;blockquote>
&lt;p>This post complements those on &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a> (the autoscaler coexists with the rollout and must respect it), &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> (the metrics that act as gates come from there), &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs&lt;/a> (the eval that decides whether the new model is ready), &lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> (the technique that puts the &amp;ldquo;quality&amp;rdquo; into the canary gate) and &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> (the previous step that the new model comes out of).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Promoting a new version of an LLM model to the production cluster without cutting traffic or breaking the SLO demands progressive deployment. The three canonical strategies, &lt;strong>blue-green&lt;/strong>, &lt;strong>canary&lt;/strong> and &lt;strong>shadow&lt;/strong>, answer different questions and have different costs. &lt;strong>Blue-green&lt;/strong>: a complete new pool brought up in parallel, atomic load balancer switch. Instant rollback (point back at the old pool); it demands twice the GPUs during the window. &lt;strong>Canary&lt;/strong>: traffic is shared progressively between the old version and the new one (1 % → 5 % → 25 % → 100 %), measuring regression gates at each step; it consumes incrementally less hardware but exposes real users to the new model from the first percentage point. &lt;strong>Shadow / mirror&lt;/strong>: the old model serves 100 % of the real traffic to the client and, in parallel, a copy of every request goes to the new model without returning its response to the user; it isolates you from quality risk but spends the new model&amp;rsquo;s GPU on responses nobody consumes, and it does not work well with long SSE streaming. The choice depends on three factors: available GPU budget, service criticality and the availability of a fast automatic eval. The &lt;strong>five regression metrics&lt;/strong> any LLM canary should gate on are: TTFT P95, error rate (HTTP 5xx + premature &lt;code>finish_reason=&amp;quot;length&amp;quot;&lt;/code>), quality score with LLM-as-judge over a golden set, statistical drift of output embeddings (Wasserstein or KL against the baseline distribution) and cost per request (tokens/s and kW/request). On Kubernetes, &lt;strong>Argo Rollouts&lt;/strong> manages traffic and the &lt;code>AnalysisTemplate&lt;/code>s as automatic gates; &lt;strong>Flagger&lt;/strong> is the more opinionated alternative. vLLM v1 &lt;strong>does not support hot model swap&lt;/strong> robustly as of May 2026, so the rollout unit is the &lt;strong>whole replica&lt;/strong> (deployment v2 alongside deployment v1). The three specific pitfalls: LB sticky sessions break the statistical comparability of the canary (a client A always lands on the new one, B on the old one, so the populations are not equivalent); semantic eval with LLM-as-judge takes 2–8 seconds per sample and is no use as a real-time gate (it is used in post-analysis or offline before promotion); SSE streaming complicates shadowing because the new model&amp;rsquo;s response has to be discarded without affecting the old one&amp;rsquo;s. This post includes a minimal Argo Rollouts manifest applicable to a generic cluster with the NVIDIA GPU Operator.&lt;/p>
&lt;h2 id="you-are-here-deploy-and-the-transition-to-retrain">You are here: DEPLOY (and the transition to RETRAIN)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy with transition to Retrain">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.semiactive{fill:#cfead0;stroke-width:2}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#crm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#crm)}&lt;/style>
&lt;defs>&lt;marker id="crm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · canary closes the circle opened in RETRAIN&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box semiactive"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;p>A new model does not appear in the cluster by magic: it comes from the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a> loop or from a weights provider update. The step between &amp;ldquo;I have an artifact that passed offline eval&amp;rdquo; and &amp;ldquo;it is serving 100 % of the traffic&amp;rdquo; is exactly this post.&lt;/p>
&lt;h2 id="the-analogy-opening-night-of-a-play">The analogy: opening night of a play&lt;/h2>
&lt;p>A theatre company is about to premiere a new version of a play that has been running successfully for a year. The company knows several hard facts: the current audience pays for a consistent experience; a bad opening damages the business for months; but premiering nothing leaves the company obsolete against the competition.&lt;/p>
&lt;p>The three opening routes the management can choose are the same three of the LLM rollout.&lt;/p>
&lt;p>&lt;strong>Dress rehearsal behind closed doors (shadow / mirror).&lt;/strong> The new actors perform the whole play to an empty theatre. There is no audience; nobody buys a ticket. Three full run-throughs are enough to check continuity, timing and the chemistry of the cast. It is expensive because there are salaries and theatre rent, but it does not expose the audience to risk. Useful when the new cast is untested and the director wants to see how it holds up over a complete performance before selling it. In LLM terms: the new model processes every real request in parallel with the old one but its responses are discarded; you spend the new model&amp;rsquo;s GPU on responses nobody sees.&lt;/p>
&lt;p>&lt;strong>Cast by performance, alternating (canary).&lt;/strong> Instead of changing the whole cast at once, Thursday performances belong to the new cast, Friday ones to the old one, Saturday ones half and half. The management reads the visitors&amp;rsquo; book comments and the audience numbers performance by performance, deciding after two weeks whether to promote the new cast to permanent or withdraw it. Cheaper than the dress rehearsal because the performances still sell tickets, but it exposes a real audience to risk from the first Thursday. In LLM terms: traffic is shared progressively between the old version and the new one, measuring gates at each step.&lt;/p>
&lt;p>&lt;strong>Double company with an atomic switch (blue-green).&lt;/strong> The company hires the new cast, prepares it for a month behind closed doors, and one Saturday announces: &amp;ldquo;from the next opening night every performance is with the new cast&amp;rdquo;. If the first performance goes badly, they go back to the old cast in 24 hours, but during that month of preparation &lt;strong>double salaries&lt;/strong> are paid to both companies. In LLM terms: two complete pools of the same size, instant LB switch from one to the other, rollback in seconds if the metrics break.&lt;/p>
&lt;p>The analogy also supports the decision: the choice depends on how critical the play is for the business (criticality of the LLM service), how much budget there is to sustain two casts at once (GPU budget), and how much confidence there is in the new cast from the chamber rehearsals (offline eval prior to the canary).&lt;/p>
&lt;h2 id="the-three-strategies-in-detail">The three strategies in detail&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="three LLM rollout strategies compared">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#d8eecf;stroke:#373}.cn{fill:#dfe9f5;stroke:#356}.sh{fill:#ead8f5;stroke:#634}.title{font:600 13px sans-serif;fill:#222}.h{font:700 12px sans-serif;fill:#222}.l{font:11px sans-serif;fill:#222}.n{font:italic 10px sans-serif;fill:#444}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Three LLM rollout strategies with their tradeoffs&lt;/text>
&lt;rect x="20" y="40" width="250" height="280" class="b bg"/>
&lt;text x="145" y="62" text-anchor="middle" class="h">BLUE-GREEN&lt;/text>
&lt;text x="30" y="90" class="l">Pool v1 (blue) serves 100%&lt;/text>
&lt;text x="30" y="106" class="l">Pool v2 (green) up and idle&lt;/text>
&lt;text x="30" y="122" class="l">LB switch: v1 → v2 instant&lt;/text>
&lt;text x="30" y="138" class="l">Rollback: switch back to v1&lt;/text>
&lt;text x="30" y="170" class="h">+ Instant rollback&lt;/text>
&lt;text x="30" y="190" class="h">+ E2E test on a real pool&lt;/text>
&lt;text x="30" y="216" class="h">− Double GPU during window&lt;/text>
&lt;text x="30" y="236" class="h">− Big switch = total risk&lt;/text>
&lt;text x="30" y="270" class="n">Typical case: minor provider&lt;/text>
&lt;text x="30" y="284" class="n">update (FP8 → FP4 new version&lt;/text>
&lt;text x="30" y="298" class="n">of the same model).&lt;/text>
&lt;rect x="285" y="40" width="250" height="280" class="b cn"/>
&lt;text x="410" y="62" text-anchor="middle" class="h">CANARY&lt;/text>
&lt;text x="295" y="90" class="l">v1 serves most · v2 a fraction&lt;/text>
&lt;text x="295" y="106" class="l">Progressive split: 1→5→25→100%&lt;/text>
&lt;text x="295" y="122" class="l">Regression gate between steps&lt;/text>
&lt;text x="295" y="138" class="l">Auto-rollback if gate fails&lt;/text>
&lt;text x="295" y="170" class="h">+ Controlled exposure&lt;/text>
&lt;text x="295" y="190" class="h">+ Incremental GPU, not double&lt;/text>
&lt;text x="295" y="216" class="h">− Real users in the sample&lt;/text>
&lt;text x="295" y="236" class="h">− Sticky sessions break sampling&lt;/text>
&lt;text x="295" y="270" class="n">Typical case: model change&lt;/text>
&lt;text x="295" y="284" class="n">(Llama 70B → Llama 3.3 70B&lt;/text>
&lt;text x="295" y="298" class="n">fine-tuned by domain).&lt;/text>
&lt;rect x="550" y="40" width="250" height="280" class="b sh"/>
&lt;text x="675" y="62" text-anchor="middle" class="h">SHADOW · MIRROR&lt;/text>
&lt;text x="560" y="90" class="l">v1 serves 100% to the user&lt;/text>
&lt;text x="560" y="106" class="l">v2 gets a copy of every request&lt;/text>
&lt;text x="560" y="122" class="l">v2 response is discarded&lt;/text>
&lt;text x="560" y="138" class="l">Offline comparison v1 vs v2&lt;/text>
&lt;text x="560" y="170" class="h">+ Zero risk exposure&lt;/text>
&lt;text x="560" y="190" class="h">+ Real traffic on v2, no harm&lt;/text>
&lt;text x="560" y="216" class="h">− v2 GPU 100% with no user value&lt;/text>
&lt;text x="560" y="236" class="h">− Poor fit with long SSE streaming&lt;/text>
&lt;text x="560" y="270" class="n">Typical case: pre-canary validation&lt;/text>
&lt;text x="560" y="284" class="n">of a model with a different&lt;/text>
&lt;text x="560" y="298" class="n">architecture (dense → MoE).&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="blue-green">Blue-green&lt;/h3>
&lt;p>The operator keeps two replica pools identical in size: the &lt;strong>blue&lt;/strong> one (production version v1) and the &lt;strong>green&lt;/strong> one (candidate version v2). When v2 is validated offline (eval passed, smoke tests), the LoadBalancer switch redirects 100 % of the traffic from blue to green &lt;strong>in a single step&lt;/strong>. If the SLO metrics break, the switch goes back in seconds.&lt;/p>
&lt;p>Cost: &lt;strong>2× GPUs&lt;/strong> for the whole window (preparing v2 + the post-switch observation window). For a 16-GPU cluster serving Llama 70B with TP=4 (4 replicas), preparing the blue-green needs 16 extra GPUs for 1–3 days.&lt;/p>
&lt;p>Risk: the switch is &lt;strong>atomic&lt;/strong>. If v2 has a problem that did not show up in offline eval but does show up at scale (for example, edge cases that only appear at 200 RPS), 100 % of users notice it at once. Rollback is instant, but the requests of the first minute after the switch were already affected. So blue-green is preferable when you have &lt;strong>high confidence&lt;/strong> in v2 (a minor change: same architecture, same format, just a new version of the weights) and you prioritise &lt;strong>immediate rollback&lt;/strong> over gradual exposure.&lt;/p>
&lt;h3 id="canary">Canary&lt;/h3>
&lt;p>The operator deploys v2 with a small number of replicas (typically 1) next to the v1 pool. The LoadBalancer shares the traffic out progressively on a schedule: 1 % for 30 minutes → 5 % for 1 hour → 25 % for 2 hours → 50 % for 4 hours → 100 %. Between each step, an &lt;strong>analysis gate&lt;/strong> evaluates regression metrics over the traffic that is already landing on v2. If the gate fails, the rollback withdraws traffic from v2 automatically and leaves v1 serving everything.&lt;/p>
&lt;p>Cost: &lt;strong>incremental&lt;/strong>. At the start (1 % of traffic) one v2 replica is enough; at 50 % you need half as many v2 replicas as v1 has in total. Peak extra GPU during the canary: about 30–50 % above baseline.&lt;/p>
&lt;p>Risk: real users are seeing v2 from the first 1 %. If v2 produces responses with degraded quality but normal TTFT and error rate, the affected users perceive the degradation without the gate detecting it (unless the gate includes quality drift, which is slow). So canary is preferable when you have &lt;strong>medium confidence&lt;/strong> in v2 (a significant change: different architecture or training) and you accept that a low percentage of users will be guinea pigs.&lt;/p>
&lt;h3 id="shadow--mirror">Shadow / mirror&lt;/h3>
&lt;p>The LoadBalancer sends 100 % of the real traffic to v1 (which answers the client) &lt;strong>and duplicates every request&lt;/strong> towards v2 (whose response is discarded or stored for offline analysis). The client never sees v2; it is never exposed to the risk.&lt;/p>
&lt;p>Cost: &lt;strong>100 % extra compute for v2&lt;/strong> with no direct user value for the whole shadow window. For a 16-GPU cluster serving Llama 70B with TP=4 (4 replicas), a shadow of the same size consumes 16 extra GPUs full time.&lt;/p>
&lt;p>Risk: shadow is the safest for the user. But it has two serious limitations: (a) if v2 has a bottleneck that makes the copied request to the shadow take a long time, the shadowing proxy can consume LB connections; it must be &lt;strong>out-of-band&lt;/strong> (asynchronous); (b) &lt;strong>long SSE streaming&lt;/strong> complicates mirroring because you have to keep two parallel streams and discard one while the other flows to the client. Common pattern: shadow only non-streaming requests (short completions, classification), manual offline eval of the streaming requests.&lt;/p>
&lt;h2 id="the-five-regression-metrics-that-act-as-a-gate">The five regression metrics that act as a gate&lt;/h2>
&lt;p>Without automatic gates, the &amp;ldquo;canary&amp;rdquo; is just a nice name for &amp;ldquo;manual rollout with a variable percentage&amp;rdquo;. The gates are the piece that turns the canary into a defensible operation.&lt;/p>
&lt;p>&lt;strong>Metric 1 — TTFT P95.&lt;/strong> Comparison of the new model&amp;rsquo;s P95 against the baseline (v1) P95 in 5-minute windows. Gate: &lt;code>ttft_p95(v2) / ttft_p95(v1) &amp;lt; 1.10&lt;/code>. It detects prefill latency regressions (a slower new model) or engine problems (suboptimal config). Source: &lt;code>vllm:time_to_first_token_seconds_bucket&lt;/code> (see &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a>).&lt;/p>
&lt;p>&lt;strong>Metric 2 — Error rate.&lt;/strong> Sum of HTTP 5xx + unexpected 4xx + rate of premature &lt;code>finish_reason=&amp;quot;length&amp;quot;&lt;/code> (responses cut short because the new model does not generate EOS). Gate: &lt;code>error_rate(v2) - error_rate(v1) &amp;lt; 0.01&lt;/code> (1 percentage point). It detects engine crashes, a broken tokenizer, generation problems. Source: &lt;code>vllm:request_success_total{status=...}&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Metric 3 — Quality score (LLM-as-judge).&lt;/strong> Over a &lt;strong>golden set&lt;/strong> of 200–1,000 representative prompts, v1 and v2 are run offline and a judge model (typically larger: GPT-4 class, Claude, local Llama 405B) scores each pair. Typical gate: &lt;code>mean_score(v2) &amp;gt;= mean_score(v1) - 0.05&lt;/code>. This metric &lt;strong>is not measured in real time during the canary&lt;/strong>: judge inference takes 2–8 seconds per sample and does not scale as an inline gate. It is used as an &lt;strong>offline pre-promotion gate&lt;/strong> (before starting the canary) and as a post-mortem over a sample of real traffic captured during the canary. See &lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> for the mechanics.&lt;/p>
&lt;p>&lt;strong>Metric 4 — Statistical output drift.&lt;/strong> For every request that lands on v2 during the canary, embed the response with a lightweight embedding model (e5, BGE) and compare the distribution of v2 embeddings against the distribution of the v1 baseline over the same window. Usable metrics: &lt;strong>Wasserstein distance&lt;/strong>, KL divergence, or more simply, comparing means and variances per dimension. Gate: normalised distance &amp;lt; a calibrated threshold (typically Wasserstein &amp;lt; 0.15). It detects subtle changes in style, length and vocabulary that LLM-as-judge does not capture without going through it too. It is fast: the lightweight embedding takes around 50 ms per response.&lt;/p>
&lt;p>&lt;strong>Metric 5 — Cost per request.&lt;/strong> Output tokens per request and kW per request. Gate: &lt;code>cost_per_request(v2) / cost_per_request(v1) &amp;lt; 1.20&lt;/code>. It detects new models that generate significantly longer responses or that consume more energy for the same load (quantisation degradation, optimisations failing). Without this gate, an &amp;ldquo;update&amp;rdquo; can silently double the bill.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Type&lt;/th>
&lt;th>Measurement latency&lt;/th>
&lt;th>Typical gate&lt;/th>
&lt;th>Detection&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT P95&lt;/td>
&lt;td>Quantitative&lt;/td>
&lt;td>5 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 110% baseline&lt;/code>&lt;/td>
&lt;td>Latency regression&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Error rate&lt;/td>
&lt;td>Quantitative&lt;/td>
&lt;td>1 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 1pp over baseline&lt;/code>&lt;/td>
&lt;td>Crashes, generation broken&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quality (LLM-judge)&lt;/td>
&lt;td>Offline semantic&lt;/td>
&lt;td>hours, over golden set&lt;/td>
&lt;td>&lt;code>&amp;gt; baseline − 0.05&lt;/code>&lt;/td>
&lt;td>Functional quality&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Statistical drift&lt;/td>
&lt;td>Statistical&lt;/td>
&lt;td>~5 min&lt;/td>
&lt;td>Wasserstein &amp;lt; 0.15&lt;/td>
&lt;td>Style, length, vocabulary&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cost per request&lt;/td>
&lt;td>Quantitative&lt;/td>
&lt;td>5 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 120% baseline&lt;/code>&lt;/td>
&lt;td>Economic/energy efficiency&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="the-mechanics-on-kubernetes-argo-rollouts">The mechanics on Kubernetes: Argo Rollouts&lt;/h2>
&lt;p>Argo Rollouts extends the standard Kubernetes &lt;code>Deployment&lt;/code> with a new &lt;code>Rollout&lt;/code> resource that orchestrates traffic progression and automatic analyses. It integrates with any service mesh (Istio, Linkerd) or ingress controller that supports traffic splitting (NGINX, Traefik, Gateway API).&lt;/p>
&lt;p>A minimal example of a 1 → 5 → 25 → 100 % canary with TTFT and error rate gates:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Rollout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">canary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">canaryService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-canary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stableService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-stable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">trafficRouting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stableIngress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">30m }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">25&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }, { templateName: drift-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">4h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }, { templateName: drift-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.10.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>--&lt;span class="l">model=/models/llama-70b-fp8-v2 ] &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># new version&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AnalysisTemplate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-p95-ratio&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">count&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">successCondition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">result &amp;lt; 1.10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.observability.svc:9090&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket{version=&amp;#34;v2&amp;#34;}[5m])))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> /
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket{version=&amp;#34;v1&amp;#34;}[5m])))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">error-rate-diff&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">count&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">successCondition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">result &amp;lt; 0.01&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.observability.svc:9090&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(rate(vllm:request_total{version=&amp;#34;v2&amp;#34;,status=~&amp;#34;5..&amp;#34;}[5m])) / sum(rate(vllm:request_total{version=&amp;#34;v2&amp;#34;}[5m]))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(rate(vllm:request_total{version=&amp;#34;v1&amp;#34;,status=~&amp;#34;5..&amp;#34;}[5m])) / sum(rate(vllm:request_total{version=&amp;#34;v1&amp;#34;}[5m]))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If any of the &lt;code>AnalysisTemplate&lt;/code>s fails, Argo Rollouts backs out automatically: it sets weight=0 on the canary, alerts the operator and keeps v1 serving 100 %. Human operation is reduced to investigating the failure and deciding whether to relaunch or abort.&lt;/p>
&lt;p>&lt;strong>Flagger&lt;/strong> offers a more opinionated alternative: weight progression is automatic as a function of metric success instead of a fixed pause; the operator defines a target (&lt;code>maxWeight: 100&lt;/code>, &lt;code>stepWeight: 10&lt;/code>, &lt;code>metrics: [...]&lt;/code>) and Flagger raises or lowers it according to behaviour. Both are mature as of May 2026; the choice is usually dictated by which service mesh is already in the cluster.&lt;/p>
&lt;h2 id="the-vllm-detail-why-there-is-no-hot-swap-of-the-model">The vLLM detail: why there is no &amp;ldquo;hot swap&amp;rdquo; of the model&lt;/h2>
&lt;p>As of May 2026, vLLM v1 &lt;strong>does not support hot swapping&lt;/strong> the model inside the same replica without restarting the engine. The &lt;code>--model&lt;/code> argument is evaluated at start-up; changing it requires re-instantiating the &lt;code>LLMEngine&lt;/code>, which restarts connections and discards the KV cache. So the &lt;strong>rollout unit is the whole replica&lt;/strong>: you do not do &amp;ldquo;v1 loads the new model on one of its GPUs&amp;rdquo; but &amp;ldquo;a v2 replica is brought up next to a v1 replica and traffic is shared out via the LB&amp;rdquo;.&lt;/p>
&lt;p>TensorRT-LLM with Triton has a similar mechanism: changing the model requires a reload of the Triton backend. SGLang does not support robust hot swap either. The operational consequence: LLM rollout is always going to cost extra GPUs during the window, and the choice between blue-green, canary and shadow is exactly the question of how many extra and for how long.&lt;/p>
&lt;h2 id="the-three-pitfalls-specific-to-llm-rollout">The three pitfalls specific to LLM rollout&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — sticky sessions break canary comparability.&lt;/strong> If the LoadBalancer does session affinity by client IP (common in NGINX and Traefik with &lt;code>loadbalancer.kubernetes.io/session-affinity: ClientIP&lt;/code>), a user A always lands on v2 while B always lands on v1. Load distributions, prompt profiles and client behaviour &lt;strong>are not random&lt;/strong> between the two pools, which statistically invalidates any gate comparison. Solution: for a canary, disable session affinity (&lt;code>sessionAffinity: None&lt;/code>) or use affinity by random request-id. If the client app requires stickiness for functional reasons (conversational memory persisted in cache), canary is not the right strategy; use blue-green or shadow.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — LLM-as-judge is not an inline real-time gate.&lt;/strong> The temptation to use quality score as a live gate is strong, but the judge&amp;rsquo;s latency (2–8 s per sample) makes it unfeasible to evaluate more than a 1–2 % sampling of the traffic, and the results arrive minutes late. Operational solutions: (a) &lt;strong>offline pre-canary eval&lt;/strong> over a golden set as a prerequisite for starting (if it fails, the canary does not even begin); (b) during the canary, &lt;strong>capture requests + responses&lt;/strong> from v2 in real time and run the judge asynchronously in a batch job that finishes before the next step; (c) use statistical embedding drift as a fast proxy for inline quality, and reserve the judge for intermediate gates between steps.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — SSE streaming complicates shadowing.&lt;/strong> Classic traffic mirroring (NGINX &lt;code>mirror&lt;/code>, Istio &lt;code>MirrorPolicy&lt;/code>) is designed for request/response HTTP: it copies the request, lets the primary server answer the client, and duplicates the request to the secondary, discarding the response. With SSE, the secondary&amp;rsquo;s response is a continuous stream of several seconds, and keeping two streams in parallel doubles the load on the proxy. Solutions: (a) shadow &lt;strong>only non-streaming requests&lt;/strong> (chat without streaming, embeddings, classification, batch eval), (b) shadow the streaming traffic but with a short timeout on the secondary (discard the shadow if it takes longer than 30 s), (c) replace the shadow with a small-weight canary (1 %), which does handle streaming well.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For a generic cluster of &lt;strong>4 nodes × 4×H100 SXM 80 GB = 16 GPUs&lt;/strong>, serving Llama 70B FP8 with TP=4 (4 possible replicas, one per node):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Blue-green&lt;/strong>: impossible to keep two complete pools of 4 replicas without extra GPUs. Practical solution: blue-green with reduced pools (2 v1 replicas + 2 v2 replicas) during the window, with &lt;strong>accepted capacity degradation&lt;/strong> (half the sustained RPS SLO), or having a parallel cluster (another node) reserved for rollouts.&lt;/li>
&lt;li>&lt;strong>Canary&lt;/strong>: feasible. Start with 3 v1 replicas + 1 v2 replica (25 % nominal weight, though the traffic weight also varies). Move to 2 v1 + 2 v2 at 50 %, then 1 v1 + 3 v2, finally 0 v1 + 4 v2.&lt;/li>
&lt;li>&lt;strong>Shadow&lt;/strong>: awkward because of the GPU cost. Reserve it for pre-canary validation of major changes, during a short window (4–8 hours) with shadowed traffic limited to a sample (10–20 % of requests, not 100 %).&lt;/li>
&lt;/ul>
&lt;p>For clusters of &lt;strong>8 GPU nodes&lt;/strong>, the three patterns are all sustainable. The operational rule: the rollout budget is typically &lt;strong>25–30 % of the cluster&amp;rsquo;s sustained capacity&lt;/strong>, and buying for the peak plus that head-room makes the &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">capacity planning&lt;/a> numbers add up.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Multi-region rollouts&lt;/strong>: how to coordinate a canary when the cluster is geographically distributed.&lt;/li>
&lt;li>&lt;strong>A/B testing of prompts&lt;/strong> (not of models): the same model with two different system prompts, measuring conversion.&lt;/li>
&lt;li>&lt;strong>Embedding rollback&lt;/strong>: changing the embedding model of a RAG system means re-embedding the whole corpus, so the canary mechanics are different. See &lt;a href="https://blog.lo0.es/en/posts/rag-corpus-curation-active-librarian/">RAG corpus curation&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Feature flags for LLM&lt;/strong>: granularity per tenant or per feature within the same model.&lt;/li>
&lt;li>&lt;strong>End-to-end continuous deployment&lt;/strong>: integration with the retrain pipeline so a new adapter is promoted automatically after passing evals.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps for the inference stack with Flux: operating the assistant as code&lt;/a> — promotion and rollback as a git revert inside the GitOps flow.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a> — the autoscaler coexists with the canary and must respect the traffic splits.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> — the metrics that act as gates come from here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning&lt;/a> — defines the head-room needed for rollouts.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs&lt;/a> — the offline eval that validates v2 before starting the canary.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge&lt;/a> — the quality score technique as an offline gate.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop&lt;/a> — where the new model that enters the canary comes from.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels&lt;/a> — Argo Rollouts is a piece of level 4–5.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">The LLM inference router&lt;/a> — the piece we call &amp;ldquo;LoadBalancer&amp;rdquo; in this post taken apart as a layer in its own right: model catalogue, L7 traffic splitting, cross-cutting policy, failover and prefix-aware routing. The 1 % → 5 % → 25 % → 100 % split materialises there.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/incident-response-runbooks-llm-inference-kafka-keep/">Incident response runbooks for LLM with Keep + Kafka&lt;/a> — the automatic canary rollback when &lt;code>ttft_p95(v2)/ttft_p95(v1) &amp;gt; 1.30&lt;/code> is runbook RB-06; the full Keep YAML workflow and the compliance fit are there.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Argo Rollouts project — &lt;code>argoproj.io/argo-rollouts&lt;/code> (&lt;code>Rollout&lt;/code> and &lt;code>AnalysisTemplate&lt;/code> CRDs).&lt;/li>
&lt;li>Flagger project — &lt;code>fluxcd.io/flagger&lt;/code> (alternative with automatic progression).&lt;/li>
&lt;li>Istio — &lt;em>Traffic Mirroring&lt;/em> (mirror configurable at &lt;code>VirtualService&lt;/code> level).&lt;/li>
&lt;li>NGINX Ingress — &lt;em>Canary annotations&lt;/em> (&lt;code>nginx.ingress.kubernetes.io/canary-*&lt;/code>).&lt;/li>
&lt;li>vLLM project — issue tracker on hot model swap (status as of May 2026: in design, not production-ready).&lt;/li>
&lt;li>Hou et al. — &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized LLM Serving&lt;/em> (OSDI 2024) — reference on goodput metrics applicable to canary gates.&lt;/li>
&lt;li>Bürkner et al. — &lt;em>Statistical methods for detecting model drift in production&lt;/em> (various articles on Wasserstein and KL in ML monitoring).&lt;/li>
&lt;/ul></description></item><item><title>LLM-as-judge: the exam marker who grades other models without turning into an oracle</title><link>https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/</link><pubDate>Wed, 27 May 2026 08:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/</guid><description>&lt;blockquote>
&lt;p>This post goes deeper into the judges section of &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs&lt;/a>. There it was one piece of the mixed panel; here we get into why it works, where it breaks and how its calibration is measured before accepting it in CI.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>An LLM judge is not &amp;ldquo;a GPT-4 you ask whether the answer is any good&amp;rdquo;. It is a &lt;strong>trained marker&lt;/strong>: it has a rubric written in advance, it is asked for explicit reasoning before the verdict, its score is computed as an expectation weighted by token probabilities (not as the first token it spits out), and before being accepted in production it is calibrated against a sample of ~50 human-annotated examples until it reaches &lt;strong>κ ≥ 0.5&lt;/strong> (Cohen&amp;rsquo;s kappa). The state of the art in May 2026 is three patterns, &lt;strong>G-Eval&lt;/strong>, &lt;strong>Prometheus 2&lt;/strong> and &lt;strong>panel of judges&lt;/strong>, each answering a different trade-off between cost, quality and reproducibility. All of them share four documented biases: position, verbosity, self-preference and narcissism. This post explains how a real judge is built, how to measure whether it lies, and when each pattern is appropriate.&lt;/p>
&lt;h2 id="you-are-here-eval">You are here: EVAL&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Eval">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7aafff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#jdm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#jdm)}&lt;/style>
&lt;defs>&lt;marker id="jdm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: EVAL · the judge piece inside the mixed panel&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box active"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-civil-service-exam-marker">The analogy: the civil service exam marker&lt;/h2>
&lt;p>A public competitive exam has thousands of papers. There is no way the senior board can mark them all. The Spanish system&amp;rsquo;s answer has been the same for decades: &lt;strong>trained markers&lt;/strong>. People who are not professors, but who are given:&lt;/p>
&lt;ol>
&lt;li>A &lt;strong>marking template&lt;/strong> written in advance: what is valued, how many points each section is worth, what gets deducted.&lt;/li>
&lt;li>&lt;strong>Prior training&lt;/strong> with a sample of papers already marked by the senior board, until their marks agree reasonably well.&lt;/li>
&lt;li>&lt;strong>Continuous auditing&lt;/strong>: a fraction of their marking is re-marked by the senior board to verify that the marker is not drifting.&lt;/li>
&lt;/ol>
&lt;p>A well-built LLM judge is exactly that. &lt;strong>It is not an oracle, it is a trained marker&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>The rubric is the explicit criteria in the prompt (what faithfulness is, what relevancy is, and so on).&lt;/li>
&lt;li>The training is the calibration against ~50 human-annotated examples.&lt;/li>
&lt;li>The continuous auditing is the weekly sampling where a human re-evaluates a fraction of the traffic the judge has judged.&lt;/li>
&lt;/ul>
&lt;p>And, as with the human marker, &lt;strong>the judge has systematic biases&lt;/strong> that the public exam system has learned to watch for: markers prefer certain handwriting styles, certain lengths, certain structures. The same happens to the judge. The rest of this post takes apart exactly which ones and how they are measured.&lt;/p>
&lt;h2 id="why-llm-as-judge-exists">Why LLM-as-judge exists&lt;/h2>
&lt;p>The direct reason: money and time. A professional human annotation costs on the order of 0.50 € to 5 € per example (depending on complexity and domain), and takes 30 seconds to several minutes. A GPT-4 judgement costs ~0.01-0.05 € and takes ~2 seconds. For a golden dataset of 500 examples evaluated continuously over 10 candidates a day, &lt;strong>the difference is between 2,500 € a day and 50 € a day&lt;/strong>. And the wall-clock difference is between days and minutes.&lt;/p>
&lt;p>The less direct but more relevant reason: methodological scalability. A golden dataset of 500 examples is relatively easy to annotate once. What happens afterwards is the hard part:&lt;/p>
&lt;ul>
&lt;li>Every time a candidate adapter appears, all 500 have to be re-evaluated.&lt;/li>
&lt;li>Every time the dataset is updated (because a new incident came in) it has to be re-evaluated.&lt;/li>
&lt;li>Every time the system prompt changes it has to be re-evaluated.&lt;/li>
&lt;/ul>
&lt;p>Without an automatable, cheap judge, the eval battery stops running and the system goes blind between releases. That is what really justifies the pattern.&lt;/p>
&lt;h2 id="the-three-canonical-patterns-in-2026">The three canonical patterns in 2026&lt;/h2>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Three LLM-as-judge patterns">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.b1{fill:#ffe6d6}
.b2{fill:#d6eaff}
.b3{fill:#d9f5d6}
.bh{fill:#fff5b0;stroke:#444;stroke-width:1.4;rx:6}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.h{font:700 14px sans-serif;fill:#222}
.ar{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mjj)}
&lt;/style>
&lt;defs>&lt;marker id="mjj" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="380" y="22" text-anchor="middle" class="h">Three canonical patterns for building a judge&lt;/text>
&lt;rect x="30" y="45" width="220" height="240" class="bx b1"/>
&lt;text x="140" y="68" text-anchor="middle" class="t">1 · G-Eval&lt;/text>
&lt;text x="140" y="86" text-anchor="middle" class="s">(Liu et al. 2023)&lt;/text>
&lt;rect x="50" y="100" width="180" height="40" class="bh"/>
&lt;text x="140" y="118" text-anchor="middle" class="s">Strong judge (GPT-4o, Claude)&lt;/text>
&lt;text x="140" y="132" text-anchor="middle" class="s">+ rubric + CoT + form-filling&lt;/text>
&lt;text x="50" y="158" class="s">• score = E[i · P(token=i)]&lt;/text>
&lt;text x="50" y="174" class="s">• prompt with criterion + examples&lt;/text>
&lt;text x="50" y="190" class="s">• structured output (JSON)&lt;/text>
&lt;text x="50" y="206" class="s">• high cost, high quality&lt;/text>
&lt;text x="50" y="226" class="s">↗ cases:&lt;/text>
&lt;text x="50" y="242" class="s"> small golden set + domain&lt;/text>
&lt;text x="50" y="258" class="s"> where quality matters more&lt;/text>
&lt;text x="50" y="274" class="s"> than cost per judgement&lt;/text>
&lt;rect x="270" y="45" width="220" height="240" class="bx b2"/>
&lt;text x="380" y="68" text-anchor="middle" class="t">2 · Prometheus 2&lt;/text>
&lt;text x="380" y="86" text-anchor="middle" class="s">(Kim et al. 2024)&lt;/text>
&lt;rect x="290" y="100" width="180" height="40" class="bh"/>
&lt;text x="380" y="118" text-anchor="middle" class="s">Specialised open source judge&lt;/text>
&lt;text x="380" y="132" text-anchor="middle" class="s">Mistral 8×7B fine-tuned&lt;/text>
&lt;text x="290" y="158" class="s">• score 1-5 on a custom rubric&lt;/text>
&lt;text x="290" y="174" class="s">• 0.897 correlation with GPT-4&lt;/text>
&lt;text x="290" y="190" class="s">• runs on-premise (~32 GB VRAM)&lt;/text>
&lt;text x="290" y="206" class="s">• no external cost per judgement&lt;/text>
&lt;text x="290" y="226" class="s">↗ cases:&lt;/text>
&lt;text x="290" y="242" class="s"> mass on-prem evaluation,&lt;/text>
&lt;text x="290" y="258" class="s"> data stays inside the perimeter,&lt;/text>
&lt;text x="290" y="274" class="s"> strict ENS/NIS2&lt;/text>
&lt;rect x="510" y="45" width="220" height="240" class="bx b3"/>
&lt;text x="620" y="68" text-anchor="middle" class="t">3 · Panel of Judges&lt;/text>
&lt;text x="620" y="86" text-anchor="middle" class="s">(Verga et al. 2024)&lt;/text>
&lt;rect x="530" y="100" width="180" height="40" class="bh"/>
&lt;text x="620" y="118" text-anchor="middle" class="s">3-5 heterogeneous judges&lt;/text>
&lt;text x="620" y="132" text-anchor="middle" class="s">+ aggregation (median/vote)&lt;/text>
&lt;text x="530" y="158" class="s">• reduces self-preference bias&lt;/text>
&lt;text x="530" y="174" class="s">• lower variance than single&lt;/text>
&lt;text x="530" y="190" class="s">• 3-5× higher cost&lt;/text>
&lt;text x="530" y="206" class="s">• flags disputed cases&lt;/text>
&lt;text x="530" y="226" class="s">↗ cases:&lt;/text>
&lt;text x="530" y="242" class="s"> critical gate decisions,&lt;/text>
&lt;text x="530" y="258" class="s"> evaluating models from the&lt;/text>
&lt;text x="530" y="274" class="s"> same vendor (self-judging)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="1--g-eval">1 · G-Eval&lt;/h3>
&lt;p>Published by Liu et al. (2023). The base idea is so simple it fits in one sentence: &lt;strong>give the judge a detailed rubric, ask it to reason before scoring, and read the result as a weighted expectation instead of as the first token&lt;/strong>. The three levers:&lt;/p>
&lt;p>&lt;strong>Rubric:&lt;/strong> a prompt with an explicit criterion (e.g. &amp;ldquo;Faithfulness: the degree to which the answer relies only on the context, without inventing data. Score 1 = invents everything, 5 = everything supported by the context&amp;rdquo;), ideally with one or two examples per extreme value.&lt;/p>
&lt;p>&lt;strong>Chain-of-thought + form-filling:&lt;/strong> the model is first asked to &amp;ldquo;reason briefly about each criterion&amp;rdquo; and then to &amp;ldquo;fill in this JSON form&amp;rdquo;. That forces it not to spit out an arbitrary number.&lt;/p>
&lt;p>&lt;strong>Probability-weighted scoring:&lt;/strong> instead of reading the first token after the &lt;code>score:&lt;/code> field, you look at the model&amp;rsquo;s probability distribution over the tokens &lt;code>1, 2, 3, 4, 5&lt;/code> and compute:&lt;/p>
$$\hat{s} = \sum_{i=1}^{5} i \cdot \frac{p(\text{token}=i)}{\sum_{j=1}^{5} p(\text{token}=j)}$$
&lt;p>This turns a discrete score into a continuous one. The justification: if the judge was &amp;ldquo;torn&amp;rdquo; between 4 and 5 (probabilities 0.4 and 0.5 on &lt;code>4&lt;/code> and &lt;code>5&lt;/code>), the real score is 4.55, not &lt;code>5&lt;/code>. This sharply reduces variance across runs of the same prompt, and captures information that greedy decoding throws away.&lt;/p>
&lt;p>Limitations of G-Eval: &lt;strong>it needs access to the model&amp;rsquo;s logprobs&lt;/strong>. Closed-source models have been restricting that access (Claude does not expose it, GPT-4 does but only top-5). In 2026 G-Eval with strict probability weighting is only practical against open source models you serve yourself (vLLM exposes them) or against GPT-4 with &lt;code>logprobs=true&lt;/code>.&lt;/p>
&lt;h3 id="2--prometheus-2">2 · Prometheus 2&lt;/h3>
&lt;p>Published by Kim et al. (KAIST, 2024). The insight is complementary to G-Eval: &lt;strong>what if, instead of asking a generalist judge to evaluate, we fine-tune a specific judge?&lt;/strong>&lt;/p>
&lt;p>Prometheus 2 is a Mistral 8×7B (MoE, ~47 GB in BF16, ~24 GB in INT4) fine-tuned on 100k+ evaluation examples with varied rubrics. The metric published in the paper: &lt;strong>0.897 Pearson correlation with GPT-4-as-judge&lt;/strong> on the Vicuna Bench and similar. That matters because it means GPT-4 can be replaced as a judge at the cost of local inference without losing almost any quality.&lt;/p>
&lt;p>Why it matters in on-premise production:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>No data leaves the perimeter&lt;/strong>. For strict ENS/NIS2 customers this is not a preference, it is a requirement. A judge that travels over an external API is not an option there.&lt;/li>
&lt;li>&lt;strong>Zero marginal cost&lt;/strong>. When the judge runs on-prem, evaluating 50,000 cases a day adds no external bill.&lt;/li>
&lt;li>&lt;strong>Controlled latency&lt;/strong>. Continuous eval over real traffic can run in parallel without saturating an external provider&amp;rsquo;s rate limits.&lt;/li>
&lt;/ul>
&lt;p>The price: you have to maintain one more inference service (Prometheus 2 running in its own vLLM), and the judge does not &amp;ldquo;update&amp;rdquo; unless it is re-fine-tuned.&lt;/p>
&lt;h3 id="3--panel-of-judges">3 · Panel of Judges&lt;/h3>
&lt;p>Verga et al. (Cohere, 2024) formalised it: instead of a single judge, use &lt;strong>3-5 heterogeneous judges&lt;/strong>, different models, different prompts, different temperatures, and aggregate their judgements.&lt;/p>
&lt;p>Common aggregation mechanisms:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Median&lt;/strong> for continuous scores. Robust to outliers.&lt;/li>
&lt;li>&lt;strong>Majority vote&lt;/strong> for pairwise judgements (chosen vs rejected). If 3 of 5 prefer A, the winner is A.&lt;/li>
&lt;li>&lt;strong>Calibration-weighted mean&lt;/strong>: weight each judge by its κ against humans in calibration. The more reliable judges vote more.&lt;/li>
&lt;/ul>
&lt;p>What a panel gives that a single judge does not:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Reduction of self-preference&lt;/strong> (a judge&amp;rsquo;s bias towards outputs stylistically similar to its own): if the judges come from different vendors, the sum cancels out.&lt;/li>
&lt;li>&lt;strong>A measure of case difficulty&lt;/strong>: if all 5 judges agree, the case is easy; if they are split, the case is ambiguous and should be escalated to a human. That turns the panel into an automatic triaging system for human annotation.&lt;/li>
&lt;li>&lt;strong>Lower variance&lt;/strong>: each judge&amp;rsquo;s noise is averaged out.&lt;/li>
&lt;/ol>
&lt;p>The cost: 3-5× the bill of a single judge. That is why the panel is usually reserved for &lt;strong>critical eval gates&lt;/strong> (does this adapter get promoted or not?), not for continuous eval over traffic.&lt;/p>
&lt;h2 id="how-to-measure-whether-the-judge-lies-cohens-kappa">How to measure whether the judge lies: Cohen&amp;rsquo;s kappa&lt;/h2>
&lt;p>Accepting the judge in production without measuring its agreement with humans is the same as accepting an uncalibrated thermometer. The standard metric for inter-rater agreement with discrete or ordinal scales is &lt;strong>Cohen&amp;rsquo;s kappa&lt;/strong>:&lt;/p>
$$\kappa = \frac{p_o - p_e}{1 - p_e}$$
&lt;p>Where &lt;code>p_o&lt;/code> is the &lt;strong>observed agreement proportion&lt;/strong> (what percentage of examples judge and human agree on) and &lt;code>p_e&lt;/code> is the &lt;strong>agreement proportion expected by chance&lt;/strong> (what you would expect if both scored at random respecting each one&amp;rsquo;s marginals).&lt;/p>
&lt;p>The intuition: if &lt;code>p_o = 0.9&lt;/code> but &lt;code>p_e = 0.85&lt;/code> (because both nearly always score &amp;ldquo;4 or 5&amp;rdquo;), agreement is 90 % in raw terms but κ = 0.33: most of the agreement comes from both scoring high, not from them understanding each other. κ corrects for that baseline.&lt;/p>
&lt;p>The usual interpretive scale (Landis and Koch 1977, still the reference):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>κ&lt;/th>
&lt;th>Interpretation&lt;/th>
&lt;th>Production threshold&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;lt; 0.20&lt;/td>
&lt;td>Poor&lt;/td>
&lt;td>Useless&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.21–0.40&lt;/td>
&lt;td>Fair&lt;/td>
&lt;td>Weak signal only&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.41–0.60&lt;/td>
&lt;td>Moderate&lt;/td>
&lt;td>&lt;strong>Minimum acceptable in 2026&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.61–0.80&lt;/td>
&lt;td>Substantial&lt;/td>
&lt;td>State of the art for open source judges&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>0.81–1.00&lt;/td>
&lt;td>Almost perfect&lt;/td>
&lt;td>Human judges rarely reach it between themselves&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>An important point the field learned the hard way: &lt;strong>humans between themselves rarely exceed κ = 0.70&lt;/strong> on LLM tasks (faithfulness, relevancy). That is the realistic ceiling for an LLM judge. Chasing κ = 0.9 against humans is chasing a ghost: not even two human annotators get there.&lt;/p>
&lt;h3 id="weighted-kappa-for-ordinal-scales">Weighted kappa for ordinal scales&lt;/h3>
&lt;p>For 1-5 scores, the disagreement &amp;ldquo;judge says 4, human says 5&amp;rdquo; is not the same as &amp;ldquo;judge says 1, human says 5&amp;rdquo;. Standard kappa treats both as an identical failure. &lt;strong>Linear or quadratic weighted kappa&lt;/strong> assigns weight to the magnitude of the disagreement:&lt;/p>
$$\kappa_w = 1 - \frac{\sum_{i,j} w_{ij} \, o_{ij}}{\sum_{i,j} w_{ij} \, e_{ij}}, \quad w_{ij}^{(\text{lin})} = \frac{|i-j|}{k-1}, \quad w_{ij}^{(\text{quad})} = \frac{(i-j)^2}{(k-1)^2}.$$
&lt;p>In G-Eval with 1-5 scores, the usual practice is to publish &lt;code>κ_quad&lt;/code> because it penalises large disagreements more and comes closer to human intuition.&lt;/p>
&lt;h3 id="numerical-calibration-example">Numerical calibration example&lt;/h3>
&lt;p>Imagine a golden set of 50 examples scored by a human and by the judge, both on a 1-5 scale. The confusion matrix:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>Hum 1&lt;/th>
&lt;th>Hum 2&lt;/th>
&lt;th>Hum 3&lt;/th>
&lt;th>Hum 4&lt;/th>
&lt;th>Hum 5&lt;/th>
&lt;th>Judge total&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Judge 1&lt;/strong>&lt;/td>
&lt;td>3&lt;/td>
&lt;td>1&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Judge 2&lt;/strong>&lt;/td>
&lt;td>1&lt;/td>
&lt;td>4&lt;/td>
&lt;td>2&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Judge 3&lt;/strong>&lt;/td>
&lt;td>0&lt;/td>
&lt;td>1&lt;/td>
&lt;td>6&lt;/td>
&lt;td>2&lt;/td>
&lt;td>0&lt;/td>
&lt;td>9&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Judge 4&lt;/strong>&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>1&lt;/td>
&lt;td>12&lt;/td>
&lt;td>3&lt;/td>
&lt;td>16&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Judge 5&lt;/strong>&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>0&lt;/td>
&lt;td>2&lt;/td>
&lt;td>12&lt;/td>
&lt;td>14&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Human total&lt;/strong>&lt;/td>
&lt;td>4&lt;/td>
&lt;td>6&lt;/td>
&lt;td>9&lt;/td>
&lt;td>16&lt;/td>
&lt;td>15&lt;/td>
&lt;td>50&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Diagonal (exact agreements): 3+4+6+12+12 = 37 → p_o = 0.74.&lt;/p>
&lt;p>&lt;code>p_e&lt;/code> is computed as Σ_i (n_judge_i · n_hum_i) / n² = (4·4 + 7·6 + 9·9 + 16·16 + 14·15) / 50² = (16+42+81+256+210) / 2500 = 605/2500 ≈ 0.242.&lt;/p>
$$\kappa = \frac{0.74 - 0.242}{1 - 0.242} = \frac{0.498}{0.758} \approx 0.66$$
&lt;p>Substantial. Acceptable. If we wanted quadratic weighted κ, near disagreements (Judge 4 vs Hum 5) weigh less than distant ones (Judge 2 vs Hum 4), and κ_quad typically comes out 0.05-0.10 above the linear one.&lt;/p>
&lt;h2 id="the-four-biases-of-the-judge">The four biases of the judge&lt;/h2>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 220" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Four biases of the LLM judge">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.b1{fill:#ffe9d6}
.b2{fill:#d6eaff}
.b3{fill:#d9f5d6}
.b4{fill:#fff5b0}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.m{font:600 11px sans-serif;fill:#2c3e50}
.h{font:700 14px sans-serif;fill:#222}
&lt;/style>
&lt;text x="380" y="22" text-anchor="middle" class="h">Four documented biases of the LLM judge&lt;/text>
&lt;rect x="20" y="40" width="170" height="160" class="bx b1"/>
&lt;text x="105" y="62" text-anchor="middle" class="t">Position bias&lt;/text>
&lt;text x="30" y="84" class="s">Prefers the first answer&lt;/text>
&lt;text x="30" y="100" class="s">when both are comparable.&lt;/text>
&lt;text x="30" y="124" class="m">Measurement:&lt;/text>
&lt;text x="30" y="140" class="s">swap the order → fraction&lt;/text>
&lt;text x="30" y="156" class="s">of flips. &amp;lt;10% acceptable.&lt;/text>
&lt;text x="30" y="180" class="m">Fix: 2 swapped passes&lt;/text>
&lt;rect x="200" y="40" width="170" height="160" class="bx b2"/>
&lt;text x="285" y="62" text-anchor="middle" class="t">Verbosity bias&lt;/text>
&lt;text x="210" y="84" class="s">Rewards longer answers,&lt;/text>
&lt;text x="210" y="100" class="s">regardless of content.&lt;/text>
&lt;text x="210" y="124" class="m">Measurement:&lt;/text>
&lt;text x="210" y="140" class="s">Pearson corr score vs length&lt;/text>
&lt;text x="210" y="156" class="s">on golden. |r| &amp;lt; 0.3 acceptable.&lt;/text>
&lt;text x="210" y="180" class="m">Fix: explicit rubric&lt;/text>
&lt;rect x="380" y="40" width="170" height="160" class="bx b3"/>
&lt;text x="465" y="62" text-anchor="middle" class="t">Self-preference&lt;/text>
&lt;text x="390" y="84" class="s">A GPT-4 judge prefers&lt;/text>
&lt;text x="390" y="100" class="s">GPT-4-style outputs.&lt;/text>
&lt;text x="390" y="124" class="m">Measurement:&lt;/text>
&lt;text x="390" y="140" class="s">compare same dataset with&lt;/text>
&lt;text x="390" y="156" class="s">3 judges; divergence &amp;lt; 15%.&lt;/text>
&lt;text x="390" y="180" class="m">Fix: heterogeneous panel&lt;/text>
&lt;rect x="560" y="40" width="180" height="160" class="bx b4"/>
&lt;text x="650" y="62" text-anchor="middle" class="t">Narcissism&lt;/text>
&lt;text x="570" y="84" class="s">Evaluated model and judge&lt;/text>
&lt;text x="570" y="100" class="s">share architecture/vendor.&lt;/text>
&lt;text x="570" y="124" class="m">Measurement:&lt;/text>
&lt;text x="570" y="140" class="s">δ human vs judge score when&lt;/text>
&lt;text x="570" y="156" class="s">candidate and judge match.&lt;/text>
&lt;text x="570" y="180" class="m">Fix: external judge&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="position-bias">Position bias&lt;/h3>
&lt;p>First documented by Wang et al. (2023). If you present two answers A and B to the judge in a pairwise setting, &lt;strong>it prefers A more often than B&lt;/strong>, on the order of 55-65 % when A and B are objectively equivalent, with typical 2023-2024 judges. In 2026 frontier judges (GPT-5, Claude 4.5, Llama 4 Judge) have it fairly well mitigated, but &lt;strong>it is still measurable&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>How to measure it formally:&lt;/strong> run the dataset twice, once with &lt;code>(A, B)&lt;/code> and once with &lt;code>(B, A)&lt;/code>. If the judge is consistent, the two passes should agree. The fraction of cases where the verdict flips is the position-bias rate. Accepted threshold in 2026: &amp;lt; 10 %.&lt;/p>
&lt;p>&lt;strong>Canonical mitigation:&lt;/strong> always run two passes with the order reversed and average. Frameworks such as Promptfoo and Inspect AI do it by default.&lt;/p>
&lt;h3 id="verbosity-bias">Verbosity bias&lt;/h3>
&lt;p>Documented by Saito et al. and Dubois et al. in 2024. For open-ended tasks (faithfulness, helpfulness), the judge tends to give a higher score to longer answers. The typical Pearson correlation between score and length on answers of equivalent human-rated quality can rise to 0.4-0.5 without mitigation.&lt;/p>
&lt;p>&lt;strong>How to measure it:&lt;/strong> Pearson correlation between the judge&amp;rsquo;s score and the length of the answer, &lt;strong>computed over a subset where humans have confirmed equivalent quality&lt;/strong>. If the correlation is &amp;gt; 0.3 on that controlled subset, there is significant verbosity bias.&lt;/p>
&lt;p>&lt;strong>Mitigation:&lt;/strong> a rubric explicitly neutral on length (&amp;ldquo;the answer should be appropriately concise for the question; length is not a criterion&amp;rdquo;) and few-shot examples where a short answer beats a long one. AlpacaEval 2.0 builds a length correction directly into the metric.&lt;/p>
&lt;h3 id="self-preference-bias">Self-preference bias&lt;/h3>
&lt;p>Documented by Panickssery et al. (Anthropic, 2024). &lt;strong>A GPT-4 judge prefers GPT-4 outputs. A Claude judge prefers Claude outputs.&lt;/strong> Not out of conspiracy, but because models share stylistic patterns with their close relatives (paragraph structure, use of bullet points, tone).&lt;/p>
&lt;p>&lt;strong>How to measure it:&lt;/strong> over a golden set, compare the scores of 3 different judges (e.g. GPT-4, Claude, Llama 4 Judge). If for the same candidate there is &amp;gt; 15 % systematic divergence attributable to the candidate&amp;rsquo;s identity, there is self-preference.&lt;/p>
&lt;p>&lt;strong>Mitigation:&lt;/strong> a panel of judges with heterogeneous vendors. If a single judge is going to be used, &lt;strong>it must not come from the same vendor as the model being evaluated&lt;/strong>.&lt;/p>
&lt;h3 id="narcissism">Narcissism&lt;/h3>
&lt;p>The extreme case of self-preference: the judge &lt;strong>is exactly the same model&lt;/strong> as the candidate. This happens more than it seems: a team trains a Llama 3 8B with LoRA and evaluates it with Llama 3 8B as the judge because &amp;ldquo;it is what they have on-prem&amp;rdquo;. It is methodologically invalid. The delta between human score and judge score grows measurably.&lt;/p>
&lt;p>&lt;strong>Mitigation:&lt;/strong> a judge of a &lt;strong>different architecture&lt;/strong> from the candidate. If your candidate is Llama 3, your judge should be Mistral, Qwen or a Prometheus 2 (which, although based on Mistral, was fine-tuned specifically for evaluation).&lt;/p>
&lt;h2 id="the-judge-as-the-pipelines-hinge">The judge as the pipeline&amp;rsquo;s hinge&lt;/h2>
&lt;p>The judge is not just &amp;ldquo;the eval piece&amp;rdquo;. It is &lt;strong>the hinge&lt;/strong> that connects three stages of the pipeline:&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The judge as a hinge between Eval, Tune and Retrain">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.bt{fill:#ffd6d6}
.be{fill:#d6eaff}
.br{fill:#fff5b0}
.bj{fill:#d9f5d6;stroke:#444;stroke-width:2;rx:8}
.t{font:700 13px sans-serif;fill:#222}
.s{font:400 11px sans-serif;fill:#555}
.h{font:700 14px sans-serif;fill:#222}
.ar{stroke:#666;stroke-width:1.6;fill:none;marker-end:url(#mjbi)}
.ag{stroke:#27ae60;stroke-width:1.8;fill:none;marker-end:url(#mjbig)}
&lt;/style>
&lt;defs>
&lt;marker id="mjbi" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>
&lt;marker id="mjbig" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#27ae60"/>&lt;/marker>
&lt;/defs>
&lt;text x="380" y="22" text-anchor="middle" class="h">The judge as a hinge: produces preference pairs, decides gates, triggers retrain&lt;/text>
&lt;rect x="290" y="100" width="180" height="80" class="bj"/>
&lt;text x="380" y="130" text-anchor="middle" class="t">LLM judge&lt;/text>
&lt;text x="380" y="150" text-anchor="middle" class="s">G-Eval / Prometheus 2 /&lt;/text>
&lt;text x="380" y="166" text-anchor="middle" class="s">Panel of Judges&lt;/text>
&lt;rect x="30" y="60" width="170" height="60" class="bx bt"/>
&lt;text x="115" y="82" text-anchor="middle" class="t">TUNE&lt;/text>
&lt;text x="115" y="102" text-anchor="middle" class="s">DPO / KTO / ORPO / SimPO&lt;/text>
&lt;text x="115" y="116" text-anchor="middle" class="s">needs chosen/rejected pairs&lt;/text>
&lt;rect x="30" y="160" width="170" height="60" class="bx be"/>
&lt;text x="115" y="182" text-anchor="middle" class="t">EVAL gate (CI)&lt;/text>
&lt;text x="115" y="202" text-anchor="middle" class="s">does the adapter promote?&lt;/text>
&lt;text x="115" y="216" text-anchor="middle" class="s">faithfulness ≥ 0.85, regr &amp;lt; 2pp&lt;/text>
&lt;rect x="560" y="60" width="170" height="60" class="bx be"/>
&lt;text x="645" y="82" text-anchor="middle" class="t">Continuous EVAL&lt;/text>
&lt;text x="645" y="102" text-anchor="middle" class="s">sampling over real traffic,&lt;/text>
&lt;text x="645" y="116" text-anchor="middle" class="s">drift and regression detection&lt;/text>
&lt;rect x="560" y="160" width="170" height="60" class="bx br"/>
&lt;text x="645" y="182" text-anchor="middle" class="t">RETRAIN&lt;/text>
&lt;text x="645" y="202" text-anchor="middle" class="s">incidents → dataset enrichment&lt;/text>
&lt;text x="645" y="216" text-anchor="middle" class="s">judge classifies + triages&lt;/text>
&lt;path class="ag" d="M290,135 L200,85"/>
&lt;text x="220" y="115" class="s" fill="#27ae60">pairs&lt;/text>
&lt;path class="ag" d="M290,150 L200,180"/>
&lt;text x="220" y="160" class="s" fill="#27ae60">gate&lt;/text>
&lt;path class="ar" d="M470,135 L560,85"/>
&lt;text x="520" y="100" class="s">scores&lt;/text>
&lt;path class="ar" d="M470,150 L560,180"/>
&lt;text x="525" y="160" class="s">triage&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>Towards TUNE:&lt;/strong> the judge generates the &lt;code>(chosen, rejected)&lt;/code> pairs for DPO without needing human labellers. That chain is what makes &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning&lt;/a> work without a dedicated annotation team.&lt;/p>
&lt;p>&lt;strong>Towards the EVAL gate:&lt;/strong> the judge gives the score that is compared against the CI threshold. If the adapter does not clear 0.85 on faithfulness, no merge.&lt;/p>
&lt;p>&lt;strong>Towards continuous EVAL:&lt;/strong> over a sample of real traffic (1-5 %), the judge computes scores and persists them. That makes it possible to detect regressions that appear days after deployment and that CI did not see because its golden set did not cover them.&lt;/p>
&lt;p>&lt;strong>Towards RETRAIN:&lt;/strong> the cases where the judge gives a low score are automatic candidates for the next retraining dataset. The judge acts as the triage of the incident flow.&lt;/p>
&lt;h2 id="implications-for-on-premise-hardware">Implications for on-premise hardware&lt;/h2>
&lt;p>The numbers below are indicative for typical scenarios in May 2026.&lt;/p>
&lt;h3 id="on-an-rtx-4090-24-gb">On an RTX 4090 (24 GB)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Judge&lt;/th>
&lt;th>Does it fit?&lt;/th>
&lt;th>Approximate throughput&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPT-4o (API)&lt;/td>
&lt;td>n/a&lt;/td>
&lt;td>~50-100 judgements/min&lt;/td>
&lt;td>External cost, not local&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Prometheus 2 (8×7B INT4)&lt;/td>
&lt;td>Yes, just about&lt;/td>
&lt;td>~40-80 judgements/min&lt;/td>
&lt;td>Q4_K_M GGUF, llama.cpp&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3.1 8B fine-tuned judge&lt;/td>
&lt;td>Yes, comfortably&lt;/td>
&lt;td>~150-250 judgements/min&lt;/td>
&lt;td>Reasonable on-prem default&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mistral Small Judge 22B&lt;/td>
&lt;td>Not directly, requires offload&lt;/td>
&lt;td>~10-20 judgements/min&lt;/td>
&lt;td>Too much for 24 GB in BF16&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Conclusion: on a single 4090, an open source 8B judge fine-tuned for evaluation (or a quantised Prometheus 2) is the sweet spot.&lt;/p>
&lt;h3 id="on-a-generic-4h100-sxm-cluster-320-gb-nvlink">On a generic 4×H100 SXM cluster (320 GB, NVLink)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Judge&lt;/th>
&lt;th>Configuration&lt;/th>
&lt;th>Approximate throughput&lt;/th>
&lt;th>Notes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Prometheus 2 BF16&lt;/td>
&lt;td>TP=2&lt;/td>
&lt;td>~400-700 judgements/min&lt;/td>
&lt;td>Fits comfortably, low latency&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3.3 70B Instruct&lt;/td>
&lt;td>TP=4&lt;/td>
&lt;td>~150-300 judgements/min&lt;/td>
&lt;td>If used as a generalist judge&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Panel of 3 judges in parallel&lt;/td>
&lt;td>TP=1-2 each&lt;/td>
&lt;td>~600-1200 judgements/min combined&lt;/td>
&lt;td>Natural pattern in a cluster&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>On an NVLink cluster the natural approach is to &lt;strong>run a panel of judges&lt;/strong> in parallel (each judge taking 1-2 GPUs) with a LiteLLM router in front. That removes the cognitive cost of &amp;ldquo;which judge do we use&amp;rdquo; because all three are used and the result is aggregated.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Red teaming and safety eval&lt;/strong>: how robustness against adversarial prompts is evaluated. It is a different pattern from the ordinal judge.&lt;/li>
&lt;li>&lt;strong>Multistep agent eval&lt;/strong>: AgentBench, TauBench, evaluating trajectories instead of individual outputs.&lt;/li>
&lt;li>&lt;strong>Benchmark contamination&lt;/strong>: how to detect whether the evaluated model saw the golden set during pre-training, and why public benchmarks are half broken.&lt;/li>
&lt;li>&lt;strong>Cost-aware judging&lt;/strong>: when a cheap judge (Llama 8B) beats an expensive one (GPT-4o), and how to quantify the quality/cost trade-off with Pareto curves.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals for LLMs: the layer after tracing&lt;/a> — the full context of the Eval stage; this post goes deeper into the judge piece inside the mixed panel described there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/">Modern alignment: DPO, KTO, ORPO and SimPO&lt;/a> — the preference optimization methods that consume the pairs the judge produces.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — the Postgres + queries + hot-swap cycle that needs the judge as its hinge.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning with Langfuse and MLflow&lt;/a> — the judge&amp;rsquo;s prompt is versioned too; a &amp;ldquo;minor&amp;rdquo; change to the rubric can invalidate the calibration.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the full OTel pipeline that captures the traces the judge runs its continuous scoring over. The eval-tracing coupling closes there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">Structured output&lt;/a> — the judge produces a structured verdict (&lt;code>{score, reasoning, decision}&lt;/code>); with constrained decoding (XGrammar) parsing is guaranteed not to fail, which removes retries and reduces the variance attributable to format failures.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — the judge&amp;rsquo;s quality score is one of the five regression metrics acting as a gate in the canary; the detail of why it works offline but not inline is there.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/">Evaluating a RAG without fooling yourself: RAGAS, the golden dataset and the four metrics that matter&lt;/a> — RAGAS uses LLM-as-judge to measure faithfulness claim by claim; the judge in this post is exactly the piece RAGAS instantiates to classify whether each statement in the answer is supported by the retrieved chunks.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/">LLM quality benchmarks: MMLU, MT-Bench, HELM and the leaderboards that matter&lt;/a> — the public benchmarks that give an external perspective on whether an internal judge is calibrated with the community consensus.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Liu, Y., Iter, D., Xu, Y., Wang, S., Xu, R., Zhu, C. &lt;em>G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment&lt;/em> (EMNLP 2023).&lt;/li>
&lt;li>Kim, S., Suk, J., Longpre, S., Lin, B. Y., Shin, J., Welleck, S., Neubig, G., Lee, M., Lee, K., Seo, M. &lt;em>Prometheus 2: An Open Source Language Model Specialized in Evaluating Other Language Models&lt;/em> (EMNLP 2024).&lt;/li>
&lt;li>Verga, P., Hofstatter, S., Althammer, S., Su, Y., Piktus, A., Arkhangorodsky, A., Xu, M., White, N., Lewis, P. &lt;em>Replacing Judges with Juries: Evaluating LLM Generations with a Panel of Diverse Models&lt;/em> (Cohere, 2024).&lt;/li>
&lt;li>Panickssery, A., Bowman, S., Feng, S. &lt;em>LLM Evaluators Recognize and Favor Their Own Generations&lt;/em> (Anthropic, NeurIPS 2024).&lt;/li>
&lt;li>Wang, P., Li, L., Chen, L., Cai, Z., Zhu, D., Lin, B., Cao, Y., Liu, Q., Liu, T., Sui, Z. &lt;em>Large Language Models are Not Fair Evaluators&lt;/em> (ACL 2024).&lt;/li>
&lt;li>Dubois, Y., Galambosi, B., Liang, P., Hashimoto, T. &lt;em>Length-Controlled AlpacaEval: A Simple Way to Debias Automatic Evaluators&lt;/em> (Stanford, 2024).&lt;/li>
&lt;li>Cohen, J. &lt;em>A Coefficient of Agreement for Nominal Scales&lt;/em> (Educational and Psychological Measurement, 1960).&lt;/li>
&lt;li>Landis, J. R., Koch, G. G. &lt;em>The Measurement of Observer Agreement for Categorical Data&lt;/em> (Biometrics, 1977).&lt;/li>
&lt;/ul></description></item><item><title>Evals for LLMs: the layer after tracing that decides whether your model performs or only looks like it does</title><link>https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/</link><pubDate>Mon, 25 May 2026 07:30:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/</guid><description>&lt;blockquote>
&lt;p>This is &lt;strong>stage 3&lt;/strong> of the LLMOps pipeline. If you arrive without context on the full journey, the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> describes where Eval sits between Tune and Deploy, and the &lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">anatomy of a production LLM request&lt;/a> shows a real eval blocking the promotion of an adapter.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Tracing is not evaluation. &lt;strong>Tracing tells you what happened; eval tells you whether what happened is any good.&lt;/strong> The two layers live in tools that overlap visually (Langfuse does both), but the discipline is different: tracing is continuous capture over real traffic; eval is controlled execution against a stable dataset, with metrics that have to &lt;strong>fail CI&lt;/strong> if they drop below a threshold. Without eval, the &lt;code>Tune → Deploy&lt;/code> loop closes blind: adapter v8 goes to production because the engineer &amp;ldquo;saw it answering well&amp;rdquo; on five examples. With eval done properly, v8 only gets in if it beats v7 across a battery of 500 curated cases, evaluated by a mix of heuristics, embeddings, an LLM judge calibrated against humans, and a sample of real traffic with human annotation. This post takes apart the mechanism, the mathematics that keep you honest, the real tools in 2026 and the pitfalls that turn it into theatre.&lt;/p>
&lt;h2 id="the-analogy-the-academic-examination-board">The analogy: the academic examination board&lt;/h2>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Eval as an academic examination board">
&lt;style>
.tbox{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.thead{fill:#7aafff;stroke:#444;stroke-width:1.4;rx:8}
.tlbl{font:600 13px sans-serif;fill:#222}
.tsub{font:400 11px sans-serif;fill:#555}
.tarr{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#me1)}
.tgate{fill:#ffd76b;stroke:#444;stroke-width:1.6;rx:6}
&lt;/style>
&lt;defs>&lt;marker id="me1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;rect x="20" y="20" width="160" height="60" class="thead"/>
&lt;text x="100" y="44" text-anchor="middle" class="tlbl">Candidate&lt;/text>
&lt;text x="100" y="62" text-anchor="middle" class="tsub">model + adapter + prompt&lt;/text>
&lt;text x="100" y="76" text-anchor="middle" class="tsub">(the "thesis" being defended)&lt;/text>
&lt;rect x="240" y="20" width="200" height="60" class="tbox"/>
&lt;text x="340" y="44" text-anchor="middle" class="tlbl">Fixed syllabus&lt;/text>
&lt;text x="340" y="62" text-anchor="middle" class="tsub">versioned golden dataset&lt;/text>
&lt;text x="340" y="76" text-anchor="middle" class="tsub">(500 questions with expected answer)&lt;/text>
&lt;rect x="500" y="20" width="240" height="60" class="tbox"/>
&lt;text x="620" y="44" text-anchor="middle" class="tlbl">Mixed board&lt;/text>
&lt;text x="620" y="62" text-anchor="middle" class="tsub">heuristics + embeddings + LLM judge&lt;/text>
&lt;text x="620" y="76" text-anchor="middle" class="tsub">+ human sample (calibrated panel)&lt;/text>
&lt;path class="tarr" d="M180,50 L240,50"/>
&lt;path class="tarr" d="M440,50 L500,50"/>
&lt;rect x="40" y="130" width="180" height="60" class="tbox"/>
&lt;text x="130" y="154" text-anchor="middle" class="tlbl">Marks per category&lt;/text>
&lt;text x="130" y="172" text-anchor="middle" class="tsub">faithfulness, relevancy,&lt;/text>
&lt;text x="130" y="186" text-anchor="middle" class="tsub">format, toxicity, latency&lt;/text>
&lt;rect x="270" y="130" width="220" height="60" class="tbox"/>
&lt;text x="380" y="154" text-anchor="middle" class="tlbl">Aggregation + segmentation&lt;/text>
&lt;text x="380" y="172" text-anchor="middle" class="tsub">global mean, by language,&lt;/text>
&lt;text x="380" y="186" text-anchor="middle" class="tsub">by tenant, by question type&lt;/text>
&lt;rect x="540" y="130" width="200" height="60" class="tgate"/>
&lt;text x="640" y="154" text-anchor="middle" class="tlbl">Eval gate (pass mark)&lt;/text>
&lt;text x="640" y="172" text-anchor="middle" class="tsub">faithfulness ≥ 0.85 ∧ tox &amp;lt; 0.02&lt;/text>
&lt;text x="640" y="186" text-anchor="middle" class="tsub">∧ regression vs baseline &amp;lt; 2 pp&lt;/text>
&lt;path class="tarr" d="M620,86 L130,124"/>
&lt;path class="tarr" d="M620,86 L380,124"/>
&lt;path class="tarr" d="M620,86 L640,124"/>
&lt;rect x="40" y="240" width="320" height="60" class="tbox"/>
&lt;text x="200" y="264" text-anchor="middle" class="tlbl">Pass → promotion to production&lt;/text>
&lt;text x="200" y="282" text-anchor="middle" class="tsub">label `production` moves to the candidate&lt;/text>
&lt;rect x="400" y="240" width="320" height="60" class="tbox"/>
&lt;text x="560" y="264" text-anchor="middle" class="tlbl">Fail → back to Tune&lt;/text>
&lt;text x="560" y="282" text-anchor="middle" class="tsub">incident or regression stays in lineage&lt;/text>
&lt;path class="tarr" d="M580,196 L200,236"/>
&lt;path class="tarr" d="M700,196 L560,236"/>
&lt;/svg>
&lt;/div>
&lt;p>A doctoral candidate defends a thesis. They do not defend it before a single distracted professor: they defend it before a &lt;strong>mixed board&lt;/strong>, with a &lt;strong>syllabus fixed in advance&lt;/strong> (not improvised on the day), and with an &lt;strong>explicit pass mark&lt;/strong> that separates pass from fail. The board is not a single expert: it is a panel combining fast readers (the heuristics: does it have the requested format? does it include the citation?), semantic reviewers (the embeddings: does it resemble the expected answer?), an external assessor trained on the subject (the LLM judge: is it faithful to the context, is it relevant, does it sound coherent?), and human jury members on a &lt;strong>sample of the most sensitive cases&lt;/strong>. If the candidate does not reach the mark, they are not promoted: they go back and prepare the defence again.&lt;/p>
&lt;p>That analogy has three edges worth holding on to from the first minute:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>The syllabus is fixed&lt;/strong>, but &lt;strong>it is actively updated when a new topic appears in the real world&lt;/strong> that the candidate must know. If it is not updated, the exam measures something increasingly distant from what happens outside.&lt;/li>
&lt;li>&lt;strong>The board has to be trained&lt;/strong>: an LLM judge not calibrated against humans is an examiner making up the criteria.&lt;/li>
&lt;li>&lt;strong>The pass mark is published beforehand&lt;/strong>: it is not decided after seeing the result, because then it is not a pass mark, it is a justification.&lt;/li>
&lt;/ol>
&lt;p>These three ideas run through the rest of the post. Every tool and every piece of mathematics that follows is, at bottom, a way of operationalising them.&lt;/p>
&lt;h2 id="the-mechanism-itself-four-layers-of-evaluators">The mechanism itself: four layers of evaluators&lt;/h2>
&lt;p>An eval suite in 2026 is made up of four layers that coexist. None replaces the others; each one measures what the rest cannot measure well, and leaves unmeasured what the rest do measure well.&lt;/p>
&lt;p>&lt;strong>Layer 1 — Deterministic heuristics.&lt;/strong> Rules that return &lt;code>true&lt;/code> or &lt;code>false&lt;/code> with no ambiguity: the output matches a regex, contains a specific entity, does not exceed a length, follows a valid JSON schema, respects a requested format (markdown, &lt;code>tool_call&lt;/code> function, mandatory citation). They are cheap, extremely fast, need neither judge nor embeddings, and catch the most frequent class of bug: the model answered in the wrong format. Their limit is obvious, in that they do not know whether the answer is &lt;strong>correct&lt;/strong>, only whether it is &lt;strong>well formed&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Layer 2 — Semantic metrics with embeddings.&lt;/strong> They compare the model&amp;rsquo;s output with an expected answer by computing cosine similarity between their embeddings, or by measuring whether a claim in the output is entailed by the retrieved context. They are cheap, deterministic given the embedding model, and very useful for detecting answers that drift in meaning but not in form. Their limit is equally clear: two answers can have high cosine similarity and say the opposite of each other (&amp;ldquo;the customer can cancel at any time&amp;rdquo; vs &amp;ldquo;the customer cannot cancel at any time&amp;rdquo; share 80% of their tokens).&lt;/p>
&lt;p>&lt;strong>Layer 3 — LLM-as-judge.&lt;/strong> A model, or an ensemble, evaluates the output of the model under test with a prompt designed to produce a score on a rubric. The canonical methods in 2026 are &lt;strong>G-Eval&lt;/strong> (chain-of-thought prompting with a calibrated numeric score), &lt;strong>Prometheus&lt;/strong> (open-source judge trained specifically for evals, reporting 0.897 correlation with humans in its v2.5 release from late 2025), and &lt;strong>panel-of-judges&lt;/strong> setups that average votes from three heterogeneous models to reduce bias. This layer captures nuances the previous two cannot see: is it faithful to the context? is it useful? is it safe? is it complete? Its limit is cost and the need for calibration, treated as a whole section below.&lt;/p>
&lt;p>&lt;strong>Layer 4 — Humans.&lt;/strong> Trained annotators who evaluate a &lt;strong>sample&lt;/strong> of the eval set, not all of it. They are the only layer with ultimate authority over the rubric: the LLM judge is calibrated against them, not the other way round. They are expensive (≈ 0.50–2.00 € per annotated sample when the domain is technical) and slow (a competent annotator produces 60–120 quality annotations per working day). The mistake teams make again and again is dropping this layer &amp;ldquo;because we have an LLM judge&amp;rdquo;; without humans there is no calibration, and without calibration the judge measures whatever it feels like.&lt;/p>
&lt;p>Normal operation of an eval gate combines all four: heuristics remove malformed outputs before spending judge tokens, embeddings filter out the manifestly irrelevant, the judge scores the rest, and humans annotate a sample every N runs to keep the judge calibrated.&lt;/p>
&lt;h2 id="the-golden-dataset-a-versioned-syllabus">The golden dataset: a versioned syllabus&lt;/h2>
&lt;p>The golden dataset is the most undervalued artefact in the pipeline. It is &lt;strong>the exam&lt;/strong>. If it is badly built, everything else, the best-calibrated judge in the world, the strictest gates, the fastest suite, measures noise. Covered at the top level in the &lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">data versioning post&lt;/a> as one of the four artefacts to version differently; here we go into detail from the Eval perspective.&lt;/p>
&lt;p>The three properties a golden dataset has to satisfy are:&lt;/p>
&lt;p>&lt;strong>Stratified representativeness.&lt;/strong> The dataset has to cover the system&amp;rsquo;s real input space in proportions that reflect production. If 30% of real traffic is in German, 30% of the golden set has to be in German; if 12% of questions are about subscription cancellation, that category cannot be 60% of the eval set just because it was easy to annotate. Stratification stays auditable: each example carries tags (&lt;code>lang=de&lt;/code>, &lt;code>category=cancellation&lt;/code>, &lt;code>tenant_type=enterprise&lt;/code>, &lt;code>difficulty=hard&lt;/code>) and the suite reports metrics per segment, not just the aggregate.&lt;/p>
&lt;p>&lt;strong>Strict holdout, no contamination with training.&lt;/strong> This rule is so obvious that almost every team believes it is following it, and almost every team breaks it without noticing. If the golden eval set gets mixed with the fine-tuning dataset, because somebody did a badly built &lt;code>random_split&lt;/code>, because a purchased dataset is used by other vendors for training, because the LLM judge saw it during pretraining, then the metric stops measuring generalisation and starts measuring memorisation. The eval set hash is versioned separately (covered in the &lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">continuous fine-tuning post&lt;/a>) and a routine leakage check is run: if an example in the golden set matches one in training token for token, it raises an alert.&lt;/p>
&lt;p>&lt;strong>Reasoned sample size, not aspirational.&lt;/strong> How many examples are needed? There is a mathematical minimum for telling two models apart with confidence. If model A is right 80% of the time and model B 85% on the same set, the 95% confidence interval for that difference of proportions (unpaired) is:&lt;/p>
\[
\Delta p \pm 1{.}96 \cdot \sqrt{\frac{p_A(1-p_A) + p_B(1-p_B)}{n}}
\]
&lt;p>To distinguish 80% from 85% with 95% confidence (an interval that does not cross zero), you need n ≈ 700 examples. To distinguish 90% from 91% under the same criterion, the calculation gives n ≈ 6,500. A &amp;ldquo;we have 50 examples in the golden set and we see adapter v8 scoring 90%&amp;rdquo; means nothing statistically: the confidence interval is ±8 points. The field&amp;rsquo;s practical rule in 2026 is &lt;strong>a minimum of 300 examples to detect coarse differences, ideally 500–1,500 if you want to detect fine improvements&lt;/strong>, and to start by stratifying properly before obsessing over sample size.&lt;/p>
&lt;p>On top of this comes &lt;strong>active maintenance&lt;/strong>: the golden set is enriched with &lt;strong>production incidents&lt;/strong> (covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain post&lt;/a>), so that every real complaint that ends in a bug becomes a curated example the next deploy candidate will have to pass. The golden set is not static: it is a &lt;strong>living record of errors the system has already made and must not make again&lt;/strong>.&lt;/p>
&lt;h2 id="llm-as-judge-how-you-calibrate-an-examiner">LLM-as-judge: how you calibrate an examiner&lt;/h2>
&lt;p>Layer 3 is the one most teams misuse. The typical mistake is: &amp;ldquo;we use GPT-4 as judge because it is the most capable&amp;rdquo;. The judge is not chosen for nominal capability; it is chosen for &lt;strong>agreement with humans&lt;/strong> on the specific rubric you are measuring. A judge with 60% agreement is useless even if it is GPT-5; a Prometheus 7B fine-tuned for your domain with 88% agreement is worth more.&lt;/p>
&lt;p>The standard metric for measuring agreement between two annotators (human vs judge, or two humans with each other) is &lt;strong>Cohen&amp;rsquo;s kappa&lt;/strong>, which corrects for the agreement expected by chance:&lt;/p>
\[
\kappa = \frac{p_o - p_e}{1 - p_e}
\]
&lt;p>where \(p_o\) is the observed proportion of agreement and \(p_e\) is the proportion expected by chance under each annotator&amp;rsquo;s marginal distributions. The accepted interpretations in the literature are:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>κ &amp;lt; 0.40&lt;/strong>: poor agreement. The judge says whatever it likes.&lt;/li>
&lt;li>&lt;strong>κ ∈ [0.40, 0.60]&lt;/strong>: moderate. Acceptable for coarse signals (is it toxic?), dreadful for nuance (is it faithful to the context?).&lt;/li>
&lt;li>&lt;strong>κ ∈ [0.60, 0.80]&lt;/strong>: substantial. Useful in production for most metrics.&lt;/li>
&lt;li>&lt;strong>κ &amp;gt; 0.80&lt;/strong>: almost perfect. The judge can be treated as a substitute for the human for that specific kind of judgement.&lt;/li>
&lt;/ul>
&lt;p>Numerically, consider a binary rubric (faithful / not faithful) over 200 examples annotated by a human and by a judge. If the human said &amp;ldquo;faithful&amp;rdquo; in 150 cases and the judge in 140, and they agree on 175 of the 200, then \(p_o = 0{.}875\); the marginals are \(p_h = 0{.}75\), \(p_j = 0{.}70\), and \(p_e = 0{.}75 \cdot 0{.}70 + 0{.}25 \cdot 0{.}30 = 0{.}600\). Kappa comes out at &lt;strong>0.6875&lt;/strong>: substantial but not excellent, usable, with an eye kept on the rubric.&lt;/p>
&lt;p>Calibrating the judge means an explicit process:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Build a calibration set&lt;/strong> — 100–300 examples annotated by trained humans, with written guidelines. Inter-annotator kappa between the humans is also measured; if the humans themselves cannot agree, the rubric is badly written before you even start talking about a judge.&lt;/li>
&lt;li>&lt;strong>Iterate the judge prompt&lt;/strong> until judge agreement with the humans passes the accepted threshold (typically κ ≥ 0.7 for sensitive metrics).&lt;/li>
&lt;li>&lt;strong>Pin the judge version&lt;/strong> (&lt;code>claude-3-5-sonnet-20251022&lt;/code>, &lt;code>gpt-4o-2024-11&lt;/code>, &lt;code>prometheus-2-7b@sha256:…&lt;/code>): any change invalidates the calibration.&lt;/li>
&lt;li>&lt;strong>Recalibrate periodically&lt;/strong> — every time the judge, the judge prompt, or the rubric changes. The frequency the field recommends in 2026 is quarterly at minimum, monthly if the rubric is new.&lt;/li>
&lt;li>&lt;strong>Persist everything in lineage&lt;/strong> — a score of &amp;ldquo;faithfulness 0.87&amp;rdquo; with no traceability of which judge, which prompt, which calibration set and which human validated it, is decorative.&lt;/li>
&lt;/ol>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">prompt versioning post&lt;/a> covers how judge prompt versioning is materialised. The practical consequence is that &lt;strong>the judge is versioned like any other model&lt;/strong>: your eval suite has &lt;code>judge_id = prometheus-2.5@v3&lt;/code> just as it has &lt;code>adapter_id = customer_support_v7&lt;/code>.&lt;/p>
&lt;h2 id="the-two-cadences-ci-gate-and-platform-regression">The two cadences: CI gate and platform regression&lt;/h2>
&lt;p>Eval suites live in two places and run on two different cadences. Teams that conflate them turn one of the two into theatre.&lt;/p>
&lt;p>&lt;strong>CI gate (pre-merge, blocking).&lt;/strong> It runs on every pull request that modifies prompts, adapters, RAG configuration, or any artefact that could move the model&amp;rsquo;s output. It runs against the golden dataset versioned at the hash that is on &lt;code>main&lt;/code>. The gate fails the merge if:&lt;/p>
&lt;ul>
&lt;li>the critical metric drops &lt;strong>more than X percentage points&lt;/strong> in absolute terms against the baseline (typically X = 2);&lt;/li>
&lt;li>some safety metric (toxicity, PII leakage) crosses a hard threshold (typically tox &amp;gt; 0.02);&lt;/li>
&lt;li>some strategic segment (a language, an enterprise-type tenant) drops more than Y points even if the aggregate improves.&lt;/li>
&lt;/ul>
&lt;p>This cadence has to be &lt;strong>fast&lt;/strong> (ideally &amp;lt; 10 minutes over 500 examples) and &lt;strong>cheap&lt;/strong> (LLM judge in batch mode, cached embeddings, heuristics run locally). The CI gate is not exhaustive: it is the low-latency line of defence.&lt;/p>
&lt;p>&lt;strong>Platform regression (post-deploy, continuous).&lt;/strong> It runs on a schedule (typically nightly or weekly) over &lt;strong>sampled production traffic&lt;/strong>, not over the static golden set. It detects drift: the model has not changed, the golden set has not changed, but the users have, and quality over real traffic drops. This cadence is more expensive (judge over thousands of samples, human annotation over hundreds), tolerates latencies of hours, and its main consumer is not CI but the &lt;a href="https://blog.lo0.es/en/posts/agentsight-llm-tracing-ebpf-zero-instrumentation/">observability&lt;/a> dashboard and the humans on the product team who decide whether to open a &lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">retrain&lt;/a> cycle.&lt;/p>
&lt;p>Both cadences persist results in the same store (Langfuse, MLflow, or equivalent) and connect them by &lt;code>model_id&lt;/code>, &lt;code>prompt_id&lt;/code>, &lt;code>dataset_hash&lt;/code> and &lt;code>judge_id&lt;/code>. Without that glue of identifiers, the metric that passes CI cannot be correlated with the one that fails in production three weeks later.&lt;/p>
&lt;h2 id="the-minimum-mathematics-that-matter">The minimum mathematics that matter&lt;/h2>
&lt;p>Beyond kappa and the interval for the difference of proportions, both already covered above, there are three more mathematical pieces that any team running evals seriously ends up using.&lt;/p>
&lt;p>&lt;strong>Confidence interval for a continuous metric.&lt;/strong> If your metric is a continuous score (faithfulness ∈ [0, 1]) and you measure the sample mean \(\bar{x}\) over n examples with standard deviation s, the 95% confidence interval for the population mean is:&lt;/p>
\[
\bar{x} \pm 1{.}96 \cdot \frac{s}{\sqrt{n}}
\]
&lt;p>For n = 300 and s ≈ 0.2 (typical of a 0-1 score with non-degenerate variance), the margin is ±0.023. This means that differences below 2 hundredths &lt;strong>cannot be distinguished&lt;/strong> from noise at that sample size. If your team is chasing &amp;ldquo;+0.5 pp&amp;rdquo; improvements over 100 examples, it is optimising noise.&lt;/p>
&lt;p>&lt;strong>Judge cost as a function of sample size and rubric.&lt;/strong> The cost of one eval pass with LLM-as-judge over n examples, with m metrics evaluated in a single call per example, and per-token price \(c_{in}, c_{out}\) on the judge model, is:&lt;/p>
\[
C \approx n \cdot (t_{in} \cdot c_{in} + t_{out} \cdot c_{out})
\]
&lt;p>where \(t_{in}\) is the number of input tokens (includes context, output of the model under test, full rubric) and \(t_{out}\) the output tokens (includes the judge&amp;rsquo;s CoT + score). For n = 500, \(t_{in}\) ≈ 4,000, \(t_{out}\) ≈ 300, a GPT-4o judge at May 2026 prices, one pass costs on the order of &lt;strong>8–15 USD per suite&lt;/strong>. If the pass fires on every PR and there are 30 PRs/day, that is 240–450 USD/day on CI gates alone. Multiplied by continuous regression, teams that do not control this spend four figures a month on judge tokens without realising. The canonical mitigation is a &lt;strong>mix of layers&lt;/strong>: heuristics and embeddings filter first, the judge is only invoked on what the cheap layers cannot resolve, and for platform regression a self-hosted open-source judge is used (Prometheus 7B on your own GPU plane) instead of a commercial model.&lt;/p>
&lt;p>&lt;strong>Distinction between aggregate metric and per-segment metric.&lt;/strong> The classic eval fallacy is the hidden mean. If your suite reports &lt;code>faithfulness = 0.87&lt;/code> and the team reads it as &amp;ldquo;up 2 points against the previous adapter&amp;rdquo;, this may be what is happening: the new adapter gains 4 points in English (where 70% of the eval set is) and loses 6 points in German (where 30% is). The aggregate mean improves, the German experience gets worse. Any serious suite reports a &lt;strong>breakdown by strategic segment&lt;/strong> (language, tenant type, question category, context length). The CI gate can also have per-segment thresholds, not just aggregate ones.&lt;/p>
&lt;h2 id="the-2026-stack-dominant-tools">The 2026 stack: dominant tools&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tool&lt;/th>
&lt;th>Main layer&lt;/th>
&lt;th>Licence&lt;/th>
&lt;th>Maintainer&lt;/th>
&lt;th>When to pick it&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>DeepEval&lt;/strong>&lt;/td>
&lt;td>CI gate&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Confident AI&lt;/td>
&lt;td>&amp;ldquo;Evals as pytest&amp;rdquo; — assertions in Python code, trivial integration with GitHub Actions. A reasonable default.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Promptfoo&lt;/strong>&lt;/td>
&lt;td>CI gate&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>Promptfoo Inc.&lt;/td>
&lt;td>Declarative YAML, matrix of prompts × providers × assertions, diff vs baseline. DevOps-friendly.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>RAGAS&lt;/strong>&lt;/td>
&lt;td>RAG-specific metrics&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Exploding Gradients&lt;/td>
&lt;td>Faithfulness, context relevancy, answer relevancy. The canonical piece if your system is RAG.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Inspect AI&lt;/strong>&lt;/td>
&lt;td>Safety/capability evals&lt;/td>
&lt;td>MIT&lt;/td>
&lt;td>UK AI Safety Institute&lt;/td>
&lt;td>Suite focused on safety and capability. Useful for regulatory gates under the EU AI Act.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Langfuse Evals&lt;/strong>&lt;/td>
&lt;td>Platform regression&lt;/td>
&lt;td>MIT (OSS) / EE&lt;/td>
&lt;td>Langfuse GmbH&lt;/td>
&lt;td>Integrated with tracing — datasets, runs and scores in the same UI as production traces.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLflow GenAI Evals&lt;/strong>&lt;/td>
&lt;td>Registry + evals&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Databricks/LF AI&lt;/td>
&lt;td>Good when you already have MLflow for classic models; &amp;ldquo;GenAI dashboard&amp;rdquo; since 3.10.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Phoenix (Arize)&lt;/strong>&lt;/td>
&lt;td>Eval + visual drift&lt;/td>
&lt;td>Elastic License 2.0&lt;/td>
&lt;td>Arize AI&lt;/td>
&lt;td>Focused on visual debugging of embeddings and drift; a complement to Langfuse, not a replacement.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Prometheus 2.5&lt;/strong>&lt;/td>
&lt;td>Self-hosted OSS judge&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>KAIST + LG AI&lt;/td>
&lt;td>Fine-tuned judge, high correlation with GPT-4 at zero cost per token when self-hosted.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>G-Eval / JudgeLM&lt;/strong>&lt;/td>
&lt;td>Prompting methods&lt;/td>
&lt;td>— (techniques)&lt;/td>
&lt;td>academic&lt;/td>
&lt;td>Prompting frameworks for LLM-as-judge — they apply on top of any judge model.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">OSS catalogue by stage&lt;/a> goes into detail entry by entry; the &lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">parallel OSS vs hyperscalers catalogue&lt;/a> compares against Bedrock Evaluations, Vertex AI Eval Service and Azure AI Evaluation.&lt;/p>
&lt;p>The canonical pattern in 2026 is hybrid: &lt;strong>DeepEval or Promptfoo for CI gates + Langfuse Evals for platform regression + Prometheus 2.5 as a self-hosted judge + human annotation on Argilla or Label Studio for the calibration set&lt;/strong>. Swapping any of these pillars for equivalents (W&amp;amp;B Weave instead of Langfuse, Inspect instead of DeepEval) is style, not functionality; what matters is that the four functions are present and connected.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware">Applied to typical on-premise hardware&lt;/h2>
&lt;p>For an on-premise deployment that wants to avoid sending sensitive outputs to a commercial LLM judge (for data sovereignty, ENS, NIS2 or equivalents), the judge is hosted on your own GPU plane. Reference figures for May 2026, based on &lt;strong>Prometheus 2.5 (Llama-3.1-8B fine-tuned as a judge)&lt;/strong> served on vLLM:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>RTX 4090 (24 GB, Ada Lovelace)&lt;/strong>: viable for small suites (&amp;lt; 200 examples) and for running the judge offline during development. Latency per evaluation ≈ 1.5–3 s with BF16; aggregate throughput on the order of 25–40 evaluations/min with batching. Useful for a developer&amp;rsquo;s local CI gates, not for platform regression.&lt;/li>
&lt;li>&lt;strong>Generic 4×H100 SXM configuration (320 GB total, NVLink)&lt;/strong>: runs the judge in parallel at TP=2 across two GPUs, leaving two free to serve the model under test. Aggregate throughput on the order of 200–350 evaluations/min, a full 500-example suite in 2–3 min. This allows PR gates with no perceptible waiting and nightly platform regression over thousands of samples with no per-token cost.&lt;/li>
&lt;/ul>
&lt;p>The comparative cost arithmetic is stubborn: self-hosting Prometheus 2.5 pays off an H100 in roughly 6 months &lt;strong>if the team fires ≥ 30 PRs/day with eval gates&lt;/strong>. Below that volume, the commercial judge still wins unless sovereignty is a requirement, and under ENS / NIS2 it is.&lt;/p>
&lt;h2 id="the-seven-pitfalls-that-kill-this-stage">The seven pitfalls that kill this stage&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — Aged golden dataset.&lt;/strong> It is not enriched with production incidents. After months it measures a world that no longer exists. The metric climbs comfortably while real users complain more.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — Contaminated or uncalibrated judge.&lt;/strong> The LLM judge evaluates with criteria it invents itself. Without a human calibration set as reference, there is no way to know whether its 0.89 is generous, severe or random.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — Insufficient sample size.&lt;/strong> A 50-example suite, 1-point differences the team treats as significant. The confidence interval is ±10 points. They are measuring noise and making real decisions on it.&lt;/p>
&lt;p>&lt;strong>Pitfall 4 — Runaway cost.&lt;/strong> A GPT-4 batch-mode judge fired on every PR over 1,000 examples, with no prior filtering by cheap layers. The eval bill overtakes production serving. It happens more often than people admit.&lt;/p>
&lt;p>&lt;strong>Pitfall 5 — Aggregate metric hiding segments.&lt;/strong> Global mean improves 2 points, German drops 6, enterprise tenants drop 3. With no explicit per-segment breakdown, the gate approves what it should not.&lt;/p>
&lt;p>&lt;strong>Pitfall 6 — Judge with a floating version.&lt;/strong> Judge model updated without recalibrating the rubric. Thresholds lose their statistical meaning. Last month&amp;rsquo;s regressions are not comparable with those from two months ago.&lt;/p>
&lt;p>&lt;strong>Pitfall 7 — Eval gate that is not enforced.&lt;/strong> The gate exists in the documentation but not in the real workflow: the suite takes 30 minutes, developers skip it with &lt;code>--no-verify&lt;/code>, managers ask for one-off exceptions that become the norm. An eval gate without enforcement is ornament.&lt;/p>
&lt;p>All seven are operational, not technical. The Eval layer does not break because the mathematics are wrong: it breaks because the discipline slackens. It is the same thing that happens with unit tests in any project that grows, except that here, without the discipline, the system &lt;strong>improves its metrics while getting worse&lt;/strong>, and that makes the degradation invisible until it is already ungovernable.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Guardrails and online safety&lt;/strong>: the &lt;strong>inline&lt;/strong> eval layer that filters outputs in real time, not in CI. Conceptually a cousin of Eval, but with very different latency constraints. Covered in detail in the &lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">guardrails post&lt;/a>: four lines of defence (input, retrieval, tool, output), OWASP LLM Top 10 (2025), OSS catalogue and deployment patterns.&lt;/li>
&lt;li>&lt;strong>Judge ensembles and agreement between multiple judges&lt;/strong>: how to reduce bias by combining three heterogeneous models as a panel, which aggregation function works (simple majority, trimmed mean, calibrated meta-judge).&lt;/li>
&lt;li>&lt;strong>Meta-eval&lt;/strong>: how you evaluate the suite itself. If the eval improvement from adapter v7 to v8 does not translate into a real improvement for the user, the suite is wrong, and that is also measured, by correlating eval metrics against product metrics.&lt;/li>
&lt;li>&lt;strong>Metamorphic testing&lt;/strong>: evaluating robustness against input perturbations (typos, paraphrasing, alternative language) as an extra gate. Beyond nominal agreement, it measures consistency.&lt;/li>
&lt;li>&lt;strong>Adversarial evals with red teaming&lt;/strong>: introducing prompt injection and jailbreak attacks as part of the gate.&lt;/li>
&lt;li>&lt;strong>Privacy in the judge&lt;/strong>: how to stop sensitive outputs from the model under test travelling to an external judge when regulation forbids it — homomorphic judge, judge in a TEE, or simply a self-hosted judge.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map where Eval sits between Tune and Deploy. The &amp;ldquo;Stage 3 — Eval&amp;rdquo; section gives the structured summary this post develops.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/anatomy-llm-request-production-tour-six-stages/">Anatomy of a production LLM request, May 2026&lt;/a> — the forensic tour of a request crossing the six stages; the moment the eval suite is invoked to promote adapter v7→v8 is the materialisation of the gate described here.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/prompt-versioning-contract-five-word-change/">Prompt versioning: the contract that stops a five-word change sinking your system&lt;/a> — judge prompts are versioned with the same mechanisms as any other prompt. The &lt;code>prompt_id&lt;/code> travels in lineage.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/data-versioning-llmops-dvc-lakefs-reproducible-golden-dataset/">Data versioning: DVC, lakeFS and the reproducible golden dataset challenge&lt;/a> — the golden eval set is one of the four artefacts to version differently. Without a strict holdout, the metric measures memorisation.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production&lt;/a> — the eval gates that appear in that post as SQL predicates are the concrete materialisation of the framework described here.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/retrain-closing-the-loop-feedback-dataset-adapter/">Retrain: closing the loop between the production incident and the adapter that fixes it&lt;/a> — the golden set is enriched with incidents coming from the Retrain loop; without that flow the dataset ages and pitfalls 1 and 5 trigger by themselves.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages&lt;/a> — entry-by-entry write-ups of DeepEval, Promptfoo, RAGAS, Langfuse, Phoenix.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/parallel-catalogue-llmops-stages-open-source-hyperscalers/">The parallel catalogue: OSS vs AWS / GCP / Azure&lt;/a> — how the Eval stage translates to Bedrock Evaluations, Vertex AI Eval Service and Azure AI Evaluation.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-as-judge-exam-marker-not-oracle/">LLM-as-judge: the examination marker&lt;/a> — the judge piece of this mixed board taken apart in depth. G-Eval, Prometheus 2, panel of judges, the four biases (position, verbosity, self-preference, narcissism) and calibration with Cohen&amp;rsquo;s kappa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/modern-alignment-dpo-kto-orpo-simpo/">Modern alignment: DPO, KTO, ORPO and SimPO&lt;/a> — the judge calibrated by the techniques here produces the preference pairs that enter as an alignment dataset. Closing the Eval → Tune loop.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the capture substrate on which continuous eval runs. The traces that tail sampling preserves (errors, high latencies, guardrail blocks) are the natural input for the judge in production.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/langfuse-inside-sorting-centre-bottleneck/">Langfuse from the inside: v3 architecture and the 10 backend knobs&lt;/a> — the datasets and evaluators designed here rest on the Langfuse backend (ClickHouse + Postgres); that post explains how to size and tune it so it withstands the trace volume feeding the eval.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">Structured output&lt;/a> — LLM-as-judge evals benefit enormously from guaranteed structured output: a &lt;code>{score, justification, pass}&lt;/code> verdict that parses 100 % of the time removes retries and simplifies storage in the data warehouse for longitudinal analysis.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-guardrails-safety-four-lines-of-defence/">Guardrails and safety in LLMs&lt;/a> — the sibling layer at runtime. Same categories as those evaluated offline here (toxicity, PII, prompt injection, jailbreak, groundedness) but with a latency budget of 30-150 ms and compact models (PromptGuard 2, Llama Guard 4, Granite Guardian, ShieldGemma) instead of large judges.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — the eval suite designed here is the gate &lt;strong>before&lt;/strong> the canary; unless the suite has passed offline on the candidate model, the rollout does not start.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/evaluating-rag-ragas-golden-dataset-metrics/">Evaluating a RAG without fooling yourself: RAGAS, the golden dataset and the four metrics that matter&lt;/a> — the specialisation of this post&amp;rsquo;s eval framework for RAG systems; faithfulness, context precision and context recall are the eval gate metrics when the system has retrieval.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-quality-benchmarks-contamination-trap-oss-tools/">LLM quality benchmarks: MMLU, MT-Bench, HELM and the leaderboards that matter&lt;/a> — the external benchmarks that serve as a reference for calibrating whether internal evals are measuring the same thing as the field.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>G-Eval&lt;/strong>: Liu et al., &amp;ldquo;G-Eval: NLG Evaluation using GPT-4 with Better Human Alignment&amp;rdquo; — the reference paper for the chain-of-thought LLM judge method.&lt;/li>
&lt;li>&lt;strong>Prometheus&lt;/strong>: Kim et al., &amp;ldquo;Prometheus 2: An Open Source Language Model Specialized in Evaluating Other Language Models&amp;rdquo; — OSS judge with a reported 0.897 correlation vs humans.&lt;/li>
&lt;li>&lt;strong>RAGAS&lt;/strong>: Es et al., &amp;ldquo;RAGAS: Automated Evaluation of Retrieval Augmented Generation&amp;rdquo; — the paper that standardises faithfulness, context relevancy and answer relevancy.&lt;/li>
&lt;li>&lt;strong>Cohen&amp;rsquo;s kappa&lt;/strong>: Cohen, J. (1960). &amp;ldquo;A Coefficient of Agreement for Nominal Scales.&amp;rdquo; Educational and Psychological Measurement — the classic metric for inter-annotator agreement, still the operational reference.&lt;/li>
&lt;li>&lt;strong>DeepEval docs&lt;/strong>: &lt;a href="https://docs.confident-ai.com/">https://docs.confident-ai.com/&lt;/a>&lt;/li>
&lt;li>&lt;strong>Promptfoo docs&lt;/strong>: &lt;a href="https://promptfoo.dev/docs/">https://promptfoo.dev/docs/&lt;/a>&lt;/li>
&lt;li>&lt;strong>Langfuse Evals&lt;/strong>: &lt;a href="https://langfuse.com/docs/scores">https://langfuse.com/docs/scores&lt;/a>&lt;/li>
&lt;li>&lt;strong>Inspect AI&lt;/strong>: &lt;a href="https://inspect.ai-safety-institute.org.uk/">https://inspect.ai-safety-institute.org.uk/&lt;/a>&lt;/li>
&lt;li>&lt;strong>EU AI Act&lt;/strong>, relevant articles on mandatory evaluation of high-risk systems — pending publication of the CEN/CENELEC technical standards on conformity assessment for GenAI.&lt;/li>
&lt;/ul></description></item></channel></rss>