Lesson 30: Evaluating the Writing Workflow
In previous lessons, we integrated Opik for observability, prepared the AI evals dataset from scratch and learned the theory behind writing good business metrics.
Now, we move from theory to practice. Evaluating AI systems is fundamentally different from testing traditional software. In Software 1.0, you write unit tests with deterministic assertions and structured outputs. In Software 3.0, the output is probabilistic and often in the form of text, images or even audio. A correct answer can take infinite forms. Furthermore, manually reviewing the outputs of the AI systems does not scale. You cannot manually review thousands of logs every time you tweak a prompt or temperature setting. This creates a bottleneck where you cannot iterate fast enough.
This is where LLM Judges come in. They automate human-level evaluation, allowing you to scale semantic checks across your entire dataset. However, building a reliable evaluation pipeline is often as engineering-intensive as building the agent itself. It is an AI application in its own right, requiring its own dataset, context engineering, and iteration cycles. Many teams underestimate this effort, treating evals as an afterthought. Yet, without it, you are flying blind, making critical architectural decisions based on "vibe checks" rather than empirical data [1].
Still, when done right, a robust evaluation pipeline becomes your safety net. It allows you to refactor code, switch models, or optimize prompts with the confidence that you haven't broken anything or that your new feature has real-world impact.
In this lesson, we will implement the complete evaluation layer for Brown, our writing workflow, where we will cover:
- The LLM Judges: Implementing
FollowsGT(Ground Truth) andUserIntentjudges using section-level binary metrics. - Dataset Strategy: How to use the evaluation dataset created in the previous lesson.
- The Pipeline: Hooking up Brown to Opik to run automated experiments.
- Reliability: Techniques to verify your judges are trustworthy through alignment scores and stability checks.
Understanding the LLM Judges We Will Build
Before writing any code, we need to clarify what we are building. Let's start with a quick reminder of what our evaluation dataset for Brown looks like. For every sample, we have three static components: the Article Guideline , the Research , and the Expected Article (Ground Truth). The Generated Article is created dynamically by Brown every time we run an experiment.
Image 1: The components of an evaluation dataset sample.
On top of this dataset, we will build two specific LLM judges to assess quality on a set of concrete business metrics we truly care about, generating articles with Brown.
- FollowsGT (Follows Ground Truth): This compares the generated article against the expected article. It evaluates three dimensions:
- Content: Does it cover the same topics and ideas (regardless of the order in which they are presented)?
- Flow: Does it follow the same logical order of ideas and transitions?
- Structure: Does it use the same formatting patterns (headers, lists, code blocks)?
- UserIntent: This compares the generated article against the inputs (Guideline and Research). It evaluates two dimensions:
- Guideline Adherence: Does the article follow the specific requirements specified by the user within the article guideline? Is every bullet point within the guideline addressed?
- Research Anchoring: Is the content derived solely from the provided research (hallucination check)?
Note how these 5 dimensions are completely different from the writing profiles we used during the evaluator-optimizer pattern (except the structure metric). This is done by design. Why? First, because we want our AI evaluation pipeline to be completely decoupled from the writing workflow. We want to treat Brown as a black box, where we know only the inputs, outputs and how the ideal output should look. Like this, we are keeping our evals 100% data-driven, which ultimately makes the LLM judges more flexible.
Let's zoom in on the data-driven idea. All the writing profiles are embedded indirectly in the expected output. As we carefully manually edited and improved the expected articles, we ensured they adhered to all the writing profiles we use at Brown. Thus, by checking the generated article against the expected article, we have the potential to check against all the writing profiles. Why only "have the potential"? Because our metrics are carefully scoped to check only a few business dimensions or outcomes that we care about, such as the quality of content, the flow of ideas, or adherence to user input.
The structure metric doesn't entirely follow the statement we made above. The metric does not have 100% overlap with the structure profile used within Brown, but it does share many commonalities, such as looking for headers, bolded words, code snippets, and bulleted lists. But because of this, we realized it often clashes with the flow dimension, a broader metric that considers multiple aspects at once.
For example, if a code snippet is described within a single block within the generated article, while it's described within three blocks within the expected article, which dimension isn't respected? The flow or structure one? Or both? This is a signal that the structure dimension is adding more noise than signal. Thus, we are considering removing it from our real-world LLM Judge, but we intentionally left it in this lesson to see how easily you can introduce noisy metrics that harm more than help. For transparency, we even added a "mechanics" dimension at first, but realized it was overkill and made the labeling process too complicated and confusing.
To conclude, when defining your metrics, keep it simple and ruthlessly keep only the CORE dimensions you really care about, ideally independent of each other.
Section-Level Binary Metrics
A key design decision we made is to score at the section level using binary metrics (0 or 1), rather than scoring the whole article. Why?
Scoring a 3,000-word article with a single number can lose a lot of signal. Also, scoring it on a 1-5 scale isn't a good strategy, as we presented in the previous lesson.
Thus, by breaking the article into sections (Introduction, Section 1, Section 2, ..., Section N, Conclusion) and scoring each section individually, we achieve higher granularity while fully leveraging the beauty of binary metrics, which are easier to label and calibrate. It is simpler for an LLM to decide "Did this section follow the guideline? Yes/No" than "Rate the adherence on a scale of 1 to 5" [2].
Image 2: The article evaluation process at the section level, including individual section scoring, aggregation, and overall article scores.
The granularity of evaluating each section individually has huge benefits. Before enumerating them, we have to make a parenthesis on how binary labeling works. To reduce mental load, when using binary metrics, whenever you find the FIRST error, you label the item with 0 and explain the error message in the reasoning field. Imagine the impact of this on a 2000-3000-word article that can be labeled as 0 because of a small error in the introduction.
Thus, by evaluating at the section level:
- The aggregate score is not highly skewed. If only one section is faulty, the rest will be scored as 1.
- We can actually compute the scores for each section. Otherwise, for example, if labeling at the article level, we would just stop if the error is within the introduction.
- While debugging, we can easily localize the issue within the article, as the sections are relatively small.
Now, before digging into the implementation, we have to prepare the evaluation dataset.
Splitting the Evals Dataset
To build our LLM Judges reliability without data leakage, we have to apply a standard ML strategy: splitting the dataset [3] between a:
- Training Split (Few-Shot Examples): We use these samples to "train" the LLM judge. We manually label these samples and feed them into the judge's prompt as few-shot examples.
- Validation Split: We use this to align the judge. We compare the LLM judge's scores against human scores to ensure the judge is evaluating correctly.
- Testing Split: We use this to compute the final performance metrics of our system. At this point, once the LLM Judges are reliably implemented and aligned, we don't need any human intervention.
As presented in Image 3, here is our concrete split:
- Training Split: L4, L7 (These two are flagged as
"is_few_shot_example": truewithin themetadata.jsonfile and NOT loaded to Opik. Rather, they are stored within the Python code along with the LLM Judges code atbrown.evals.metricsto be easily loaded at runtime.) - Validation Split: L10
- Testing Split: L2, L3, L5, L6, L8, L9, L11
As few-shot examples, we chose Lesson 4: Structured Outptus and L7: Reasoning & Planning Theory as they act as a perfect mix between text, code and mermaid-rich lessons. Also, to keep the context window in check, we chose L4, which is shorter. For the validation split, we chose Lesson 10: Memory because it's a longer lesson that, by itself, contains a mix of everything.
Image 3: The strategic splitting of a dataset for LLM judge development.
Now that we understand our plan of attack, let's dig into the code.
Implementing the Follows Ground Truth LLM Judge
We will start by implementing the FollowsGTMetricLLMJudge. Remember that this metric requires the generated article and the ground truth article to compare against.
The Base Metric Abstraction
First, we define a base class BrownBaseMetric. This handles the initialization of the LLM with structured outputs and provides a standard interface for scoring that will be used across all our LLM Judges.
- We define the class and initialization logic (Source:
brown.evals.metrics.base).
- We implement the model initialization and scoring methods.
The Metric Implementation
Next, we implement the concrete FollowsGTMetricLLMJudge. This class prepares the prompt using the generated article (output) and the expected article (expected_output), invokes the LLM, and parses the result.
- We initialize the metric with the specific structured output type and few-shot examples (Source:
brown.evals.metrics.follows_gt.metric).
- We implement the
ascoremethod to generate the prompt and invoke the model.
Defining the Score Structures
We use Pydantic models to define exactly what the LLM should return. This structured output is critical for parsing the results reliably.
- We define the top-level container for article scores (Source:
brown.evals.metrics.follows_gt.types).
- We define the scores for a single section and the specific criteria.
- The
CriterionScorerepresents the binary decision and the reasoning behind it (Source:brown.evals.metrics.base).
- We also need the base
CriteriaScoresclass which helps format these scores for the LLM context.
Aggregating Scores
Since we score every section individually, we need a way to roll these up into a final score for the article. We use a helper function aggregate_section_scores_to_results that calculates the mean of the binary scores (0s and 1s) across all sections.
- We initialize the aggregation structure by inferring dimensions from the first section (Source:
brown.evals.metrics.base).
- We iterate through sections to collect scores and concatenate reasons.
- We compute the average score for each dimension and return the results.
- The
CriterionAggregatedScoreclass is a simple container for the final aggregated result.
Few-Shot Examples and Prompts
To "train" the judge, we provide few-shot examples. We define classes to hold these examples, which map the generated output, expected output, and the manually assigned scores.
- We define the specific example class for FollowsGT (Source:
brown.evals.metrics.follows_gt.types).
- We also need the base classes
BaseExampleandBaseFewShotExamplesthat define the common interface (Source:brown.evals.metrics.base).
The system prompt is the brain of the judge. It contains specific instructions on how to evaluate the "Content", "Flow", and "Structure" dimensions relative to each section of the article. Let's break it down.
- Introduction: We start by defining the persona and the specific task. The LLM is framed as an NLP evaluation expert whose sole purpose is to assess the quality of the generated article.
- Scoring Instructions: We provide a comprehensive set of rules to guide the evaluation process. First, we define the judge's role in analyzing the generated article against the expected output, establishing the expected output as the absolute reference standard. To ensure consistent section extraction, we use the expected output as the anchor. If section titles do not match perfectly, the judge must infer connections based on content similarity. If a section from the expected output is missing in the generated version, it automatically receives a score of 0. We technically define a "section" as the content between two H2 headers (
##), with a specific exception for the introduction, which lies between the title and the first header. Finally, we establish a binary scoring mechanism (0 or 1) where each criterion—content, flow, and structure—is evaluated independently.
- Scoring Instructions - Content Criterion: Next, we define the Content criterion. This binary metric focuses purely on substance. We instruct the judge to verify if the core subjects, topics, and arguments are present, regardless of their order or formatting. If the generated section covers the same ideas as the expected one, it gets a 1, even if the phrasing differs.
- Scoring Instructions - Flow Criterion: The Flow criterion evaluates the narrative logic. Here, the order matters. We check if the generated section follows the same sequence of ideas and transitions as the expected output. This ensures the storytelling remains coherent. We also check for the correct placement of media elements relative to the text.
- Scoring Instructions - Structure Criterion: The Structure criterion is about formatting rigor. We check if the generated section uses the same Markdown elements—headers, lists, code blocks, and bolding—as the reference. This ensures the visual hierarchy and technical formatting are preserved.
- Scoring Instructions - Reasoning & Rules: Finally, we require the model to output a reasoning trace alongside the binary score. This is essential for debugging; if a section gets a 0, we need to know exactly why. We also define specific rules for handling media comparisons to avoid false negatives based on content differences in placeholders.
- Chain of Thought: We enforce a strict Chain of Thought (CoT) process to improve reasoning reliability. The prompt guides the model through specific steps: first, understanding the inputs; second, splitting the text into sections; third, mapping the expected sections to the generated ones; and finally, evaluating each pair in isolation. This step-by-step approach prevents the model from rushing to a hallucinated score without analyzing the full context.
- What to Avoid: To further constrain the model, we explicitly list negative constraints. We warn against common failure modes, such as using the generated output as the reference point for sectioning or allowing the quality of one section to bias the score of another. This isolation is key for granular, accurate metrics.
- Few-Shot Examples and Inputs: Finally, we define the placeholders where our dynamic data will be injected at runtime. This includes the few-shot examples that demonstrate the scoring logic and the actual inputs (generated and expected articles) for the current evaluation.
- And ultimately anchor the task:
Plugging the Few-Shot Examples Into the System Prompt
To create the DEFAULT_FEW_SHOT_EXAMPLES, we followed a rigorous manual labeling process. We took the two articles from the training split (Lesson 4 and Lesson 7). Remember that the ground-truth articles in the dataset are manually reviewed, edited, and curated. Then we manually added noise to the generated article by introducing random ideas, dropping sections, or altering the flow of ideas to ensure we covered as many edge cases as possible. Then, we manually labeled each section across the content, flow and structure dimensions with a binary score and a reason message.
Manually typing the reason message for each section and dimension is extremely important, as it indirectly explains to the LLM Judge why we scored that section with 0 or 1, providing a ton of signal.
You can access the article_generated.md and article_ground_truth.md files by going to brown.evals.metrics.follows_gt.examples, where we store the files directly within the brown Python package.
Meanwhile, the labels are stored directly within the FollowsGTMetricFewShotExample entity, as follows:
Note that in the code above, the reason message and most of the sections are missing. Because the labels take up a lot of space, to keep things easy to follow, we will show the actual data only in the tables below.
Labels for the “04_structured_outputs” Sample
| Section Title | Content Score | Content Reason | Flow Score | Flow Reason | Structure Score | Structure Reason |
|---|---|---|---|---|---|---|
| Introduction | 1 | Both sections cover the same core subjects and ideas, discussing the purpose of structured outputs as a bridge between LLMs and traditional applications. | 0 | The generated section lacks the first sentence, which is used as a smooth transition into the article. Also, it misses the diagram present in the expected output, labeled as Figure 1. | 1 | Both sections use the same paragraph length patterns. |
| Why Structured Outputs Are Critical | 0 | Both sections cover the same reasons why structured outputs are critical, including ease of parsing, data validation with Pydantic, and common use cases. Still, the generated section has a section on GraphRAG, which is not related to the specific topic of the section. | 0 | Follows a similar logical flow, starting with the importance, detailing benefits, discussing use cases, and concluding with a diagram. Still, the generated section contains an additional paragraph on GraphRAG, which doesn't fit with the expected flow. Additionally, it omits the last sentence, which is necessary for a smooth transition to the next section. | 1 | Both sections use the same paragraph length patterns and have the same usage pattern for backticks and citation references across sentences. Also, the figures and their corresponding citations use the same formatting rules. |
| Implementing Structured Outputs From Scratch Using JSON | 1 | Both sections provide a step-by-step guide on implementing structured outputs using JSON from scratch, covering client setup, document definition, prompt crafting, and parsing. | 0 | The generated section omits the Note callout box present in the expected output. | 0 | The generated section incorrectly formats the JSON code block under point 4), " "where it misses the closing ```. Also, in the last section, where it outputs " "the final JSON structure, it doesn't enclose the JSON into Python backticks " "as expected: python <content> |
| Implementing Structured Outputs From Scratch Using Pydantic | 0 | Both sections accurately explain the benefits of Pydantic for structured outputs, demonstrate defining models, generating schemas, and validating responses, and compare it with other Python types. Still, the generated section uses different code examples, using a RedditThread Pydantic Python class instead of the expected DocumentMetadata class. | 1 | Even if the sections use different code examples, from the point of view of the flow of ideas, both sections follow a similar logical flow, introducing Pydantic, demonstrating its implementation through steps with code, and concluding with a comparison to other data validation methods. | 1 | Both sections maintain similar introductory paragraphs, numbered steps with code blocks, and a concluding comparison. The formatting of the Python code and JSON blocks is the same. Also, the use of backticks and formatting of citation references is the same. |
| Implementing Structured Outputs Using Gemini and Pydantic | 0 | Both sections accurately describe the native implementation of structured outputs using the Gemini API and Pydantic. Still, the generated section lacks some key code block examples (points 3 and 4 on calling the Gemini API), which are necessary to fully illustrate the concept. | 0 | Both sections follow a similar logical flow, introducing native API support and then demonstrating its implementation through numbered steps with code and outputs. Still, the generated section misses the first sentence used to make the transition from the previous sentence, and also it misses points 3) and 4) from the code walkthrough numbered list. | 0 | In both sections, the use of citation references and backticks is the same. Also, the structure of the introductory paragraph, division of code blocks and conclusion follow the same pattern. Still, the generated section uses a bulleted list to divide the code blocks instead of a numbered list as expected. |
| Structured Outputs Are Everywhere | 0 | Both sections serve as a conclusion, summarizing the importance of structured outputs as a fundamental pattern. Still, the generated section misses the last paragraph that presents how structured outputs fit in the course and the AI Engineering field, which is critical for the conclusion. | 0 | Both sections follow a similar flow, summarizing the key takeaway. Still, the generated section misses the last paragraph on looking ahead to future lessons in the course. | 0 | Both sections use the same paragraph length patterns. Still, the number formatting of the citation reference from the first paragraph misses the square brackets. |
| References | 1 | Both sections contain a list of references, similar in purpose. | 1 | Both sections follow the same flow for referencing the sources, as a numbered list from 1 to n. | 1 | Both sections use the same pattern to structure the references, as a bulleted list, where each element is structured as <reference_number> |
_Table 1:_Labels for the “04_structured_outputs” sample for the follows GT LLM Judge.
Labels for the “ 07_reasoning_planning”Sample
| Section Title | Content Score | Content Reason | Flow Score | Flow Reason | Structure Score | Structure Reason |
|---|---|---|---|---|---|---|
| Introduction | 1 | Covers the same core subjects and ideas, discussing the limitations of standard LLMs and the need for planning and reasoning in AI agents. | 0 | Both sections set the scene of the lesson, dicussing the 'why' behind the need for planning and reasoning in AI agents. However, the generated introduction omits the sentences that talk about the previous lessons and anchor the lesson within the course. | 0 | The generated output uses an H2 header 'Why Your Agent Needs to Think Before It Acts' as a title for the introduction, while the expected section doesn't. |
| What a Non-Reasoning Model Does And Why It Fails on Complex Tasks | 1 | Accurately covers the core subject of why non-reasoning models fail on complex tasks, using the same 'Technical Research Assistant Agent' example and discussing similar failure points. | 1 | Follows a similar order of ideas, starting with the example, explaining the failure, and then discussing the need for reasoning. | 1 | Both sections have similar paragraph length patterns and use of images and their corresponding citations. |
| Teaching Models to "Think": Chain-of-Thought and Its Limits | 0 | The generated section begins with the expected topic on the Chain-of-Thought concept, but in the second paragraph, it shifts to discussing RAG, which is entirely different from the expected section topic. | 0 | Both sections start by introducing CoT, but then the generated section talks about RAG, which is an entirely different topic, completely diverging from the expected section. | 0 | The generated section uses the same citation strategy and number formatting, but the paragraphs are way longer than expected. Also, the generated section lacks the diagram and the 'Note' callout box. |
| Separating Planning from Answering: Foundations of ReAct and Plan-and-Execute | 1 | Accurately describes the core idea of separating planning from answering and introduces ReAct and Plan-and-Execute as the two dominant strategies. | 0 | Both sections follow the same logical progression, starting with the core idea of separation and then introducing the two patterns. However, the last sentence from the generated section is very abrupt, being a poor transition to the next section. | 0 | The generated section maintains a similar paragraph length, number formatting, and citation strategy. Still, it covers the ReAct and Plan-and-Execute topics within a paragraph instead of a bullet list with the names of the algorithms being bolded. |
| ReAct in Depth: The Loop of Thought, Action, and Observation | 1 | The two sections provide the same detailed explanation of the ReAct framework, its iterative loop, and a step-by-step example using the research assistant agent. | 0 | Both sections begin with the same flow, introducing ReAct, explaining its loop and presenting the diagram. The generated section has some additional reference numbers, which is correct. Still, the generated sections wrote the primary advantages and disadvantages of ReAct section before the hands-on example, instead of after it, as expected. | 0 | The generated section employs a similar strategy to format the diagram's citation, references. However, in the expected section, the list is formatted as a numbered list, while in the generated section, it's formatted as a bulleted list. Also, the generated section added backquotes around the text from Action 1, 2, 3, and 4, while the expected section does not. |
| Plan-and-Execute in Depth: Structure and Predictability | 1 | Accurately explains the Plan-and-Execute pattern, its two phases (Planning and Execution), and its benefits for predictable tasks. | 0 | Both sections follow a similar logical flow, introducing the pattern, explaining its efficiency and then detailing the planning and execution phases with an example. The issue is that the Plan-and-Execute pattern diagram was expected before digging into the Planning Phase section, and instead, it's placed within the numbered list of the Planning Phase section. | 0 | The generated section employs a similar strategy to format the diagram's citation, number formatting, references and the bulleted list. Still, it formats the planning and execution phases as bolded text instead of as H3 headers. |
| Pros and Cons: ReAct vs. Plan-and-Execute | 0 | The generated output completely omits this section. | 0 | The generated output completely misses this section. | 0 | The generated output completely omits this section. |
| Deep Research AI Assistant Systems | 0 | Both sections discuss how ReAct and Plan-and-Execute patterns are applied in real-world settings, but the expected output uses a deep research system as an example, while the generated one uses a financial assistant. | 0 | Even if the sections use different examples, the core storyline of the section is the same. However, the generated section completely misses the expected diagram, whereas the expected output places it at the end. | 1 | Both sections have similar paragraph length, number formatting, and citation patterns. As the diagram is missing from the generation section, we consider the citation valid. |
| Reasoning Models: How LLMs' "Reasoning and Planning" are Being Internalized in LLMs | 0 | The generated section is completely empty. | 0 | The generated section is completely empty. | 0 | The generated section is completely empty. |
| Conclusion | 1 | In both sections, the conclusion summarizes the key takeaways of the article, including the importance of planning and reasoning, and the two foundational patterns (ReAct and Plan-and-Execute). | 1 | Follows a similar flow, reiterating the main points within the lesson and setting the scene for future lessons. | 1 | Both sections have similar paragraph length, number formatting, and citation patterns. |
| References | 1 | Both sections contain a list of citations, similar in purpose. | 1 | Both sections follow the same flow for referencing the sources, as a numbered list from 1 to n. | 0 | Both sections use a bulleted list to enumerate the citations, but the use of parentheses is not the same. The generated article outputs the references as - [<number>] <reference_name>(<url>), instead of - [[<number>]](<url>) <article_name>. |
_Table 2:_Labels for the “07_reasoning_planning” sample for the follows GT LLM Judge.
You can see how these two tables look in code implemented using theFollowsGTMetricFewShotExamples Python structure by going to the brown.evals.metrics.follows_gt.prompts.py file. You will see the exact same information, but implemented in Python code.
The final piece of the puzzle is the get_eval_prompt function. This function assembles the final prompt by formatting the system prompt with the generated output, expected output, and the few-shot examples.
Running the Judge
Let's see the FollowsGTMetricLLMJudge in action. We will use the 01_sample_sample, where we have the article as the expected article and a noisy version of it - article_noisy.md as the generated article where we intentionally introduced all kind of mistakes such as changed the paragraph order or deleted some sections to see if the judge catches them.
To actually look at the article, either open the links above or run the associated Notebook. Meanwhile, to avoid copying an article within an article, we will assume you looked at the expected_article.md and generated_article.md.
It outputs:
Now that we understand how the FollowsGTMetricLLMJudge works, let's move to the one that checks user intent.
Implementing the User Intent LLM Judge
The UserIntentMetricLLMJudge works similarly but compares the output against the Article Guideline and Research context. This is critical for detecting hallucinations and ensuring the workflow followed user instructions.
The Metric Implementation
The implementation mirrors FollowsGT but takes different inputs (input for guidelines, context for research).
- We initialize the metric with the specific structured output type and few-shot examples (Source:
brown.evals.metrics.user_intent.metric).
- We implement the
ascoremethod to generate the prompt and invoke the model.
Score Structures and Prompts
We define UserIntentCriteriaScores to track:
- Guideline Adherence: Did the section cover the required topics? Did it respect word counts?
- Research Anchoring: Is the information supported by the provided research?
Here is the code.
- We define the score structures for User Intent (Source:
brown.evals.metrics.user_intent.types).
- We define the few-shot example entity.
- Ultimately, we define the collection class for these examples.
The system prompt instructs the judge to use the article guideline as the anchor. It must find the corresponding section in the generated article and verify if all requirements were met. For research anchoring, it checks if any claims in the generated text are unsupported within the research or article guideline context. Let's break it down.
- Introduction: We start by defining the persona and the specific task. The LLM is framed as an NLP evaluation expert whose sole purpose is to assess if the generated article respects the user's constraints and source material.
- Instructions: This is the core logic of the prompt. We explicitly instruct the judge to use the article guideline as the source of truth for sectioning. This prevents the judge from hallucinating sections based on the generated output. We establish the general rules for analysis, input formats, and the section-by-section comparison strategy.
- Scoring Instructions - Guideline Adherence: We define the strict binary criteria for Guideline Adherence. We instruct the judge to check for missing topics, extra topics, or incorrect ordering of ideas compared to the guideline. We also define tolerance levels for length constraints.
- Scoring Instructions - Research Anchoring: Next, we define the Research Anchoring criterion. This checks if the content is derived solely from the provided research and article guideline, effectively acting as a hallucination check.
- Scoring Instructions - Reasoning & Rules: Finally, we require the model to output a reasoning trace alongside the binary score. This is essential for debugging. We also define specific rules for handling media comparisons to avoid false negatives based on content differences in placeholders.
- Chain of Thought: We enforce a step-by-step reasoning process. The model must first understand the inputs, then map the expected sections to the generated ones, and finally evaluate them in isolation. This prevents the model from rushing to a score without context.
- What to Avoid: We explicitly list negative constraints to prevent common failure modes, such as letting one section's quality influence another's score or using the wrong reference point for sectioning.
- Few-Shot Examples and Input: We define the placeholders where our dynamic data will be injected at runtime.
- Conclusion: A final anchor to trigger the reasoning process.
Plugging the Few-Shot Examples Into the System Prompt
To create the DEFAULT_FEW_SHOT_EXAMPLES, we followed the same rigorous manual labeling process as we did for the follows GT LLM Judge. We took the two articles from the training split (Lesson 4 and Lesson 7). For each example, we intentionally introduced violations such as adding unsupported claims, removing ideas from the article that are present in the guideline, changing the order of ideas, or exceeding length constraints. Then, we manually labeled each section across the guideline adherence and research anchoring dimensions with a binary score and a reason message.
Remember that manually typing the reason message for each section and dimension is extremely important, as it indirectly explains to the LLM Judge why we scored that section with 0 or 1, providing a ton of signal. For guideline adherence, the reason message must clarify whether the section follows or doesn't meet user expectations. For research anchoring, the reason text must explain whether the content is grounded in the provided sources or introduces hallucinated claims.
You can access the article_guideline.md, research.md, and article_generated.md files by going to brown.evals.metrics.user_intent.examples, where we store the files directly within the brown Python package for easy access at runtime.
Meanwhile, the labels are stored directly within an instance of a UserIntentMetricFewShotExample entity, as follows:
Note that in the code above, the reason message and most of the sections are missing. Because the labels take up a lot of space, to keep things easy to follow, we will show the actual data only in the tables below.
Labels for the “04_structured_outputs” Sample
| Section Title | Guideline Adherence Score | Guideline Adherence Reason | Research Anchoring Score | Research Anchoring Reason |
|---|---|---|---|---|
| Introduction | 0 | The section correctly introduces the "bridge" concept but violates three guidelines: it uses the first-person singular ("I remember...") against the point-of-view rule, adds a new anecdote about sentiment analysis and at ~250 words, it exceeds the 150-word length constraint. | 0 | While the core "bridge" concept between the LLM (software 3.0) and Python (software 1.0) worlds' is anchored in the guideline, the section introduces a significant, un-anchored claim that mastering structured outputs is "the key to unlocking true Artificial General Intelligence (AGI)," which is not supported by the research. |
| Understanding why structured outputs are critical | 0 | The section correctly covers the required benefits but violates the "Different Order" rule by presenting them in a different sequence than the guideline. It also adds an unrequested benefit ("Improved Token Efficiency"), violating the "More" rule. | 1 | The section is well-anchored. The benefits of parsing, Pydantic validation, avoiding fragile parsing and the GraphRAG/knowledge-graph use-case are all supported in the research and the added point on token efficiency are all directly supported by claims in the provided research material |
| Implementing structured outputs from scratch using JSON | 1 | This section adheres to the guideline. It follows the step list exactly: client/model setup, sample DOCUMENT, XML-tagged prompt, model call, raw output, helper to extract JSON, parsed output, and display. It meets the length and content expectations. | 1 | The section is correctly anchored. All code examples are taken directly from the required lesson notebook, and the explanation of using XML tags for clarity is supported by prompt engineering best practices found in the research |
| Implementing structured outputs from scratch using Pydantic | 0 | The section correctly explains Pydantic's general benefits. However, it critically fails to follow the guideline by omitting the mandatory step of generating the JSON schema from the Pydantic model and injecting it into the prompt. It also adds an unrequested comment on YAML. | 0 | The section introduces a factual contradiction with the provided guideline. It incorrectly states that type hints can be used directly "Starting with Python 10," whereas the guideline explicitly specifies this feature is available from "Python 11," failing the anchoring criterion. |
| Implementing structured outputs using Gemini and Pydantic | 0 | The section provides the correct code for using Gemini's native features. However, it fails the guideline by completely omitting the required explanation of the pros of this approach (that it is "easier, more accurate and cheaper"). | 0 | The section fails to incorporate key research findings. The provided sources clearly state that native API features enhance reliability and reduce development costs (Sources [37], [58]), but none of this required evidence is mentioned. |
| Conclusion: Structured Outputs Are Everywhere | 0 | The section correctly emphasizes the ubiquity of structured outputs and name-checks later topics as the guideline asks for. However, it does not provide a transition to the requested next Lesson 5. | 1 | The section's claims are general and consistent with the course context. It does not introduce new factual claims that require specific anchoring, and therefore contains no information that contradicts the provided research material. |
_Table 3:_Labels for the “04_structured_outputs” sample for the user intent LLM judge.
**** Labels for the “07_reasoning_planning” Sample
| Section Title | Guideline Adherence Score | Guideline Adherence Reason | Research Anchoring Score | Research Anchoring Reason |
|---|---|---|---|---|
| Introduction | 1 | The introduction aligns with the guideline. It sets the stage by introducing planning and reasoning, explains the problem with standard LLMs, previews ReAct and Plan-and-Execute, and mentions the advanced capabilities, all while staying within the specified word count. | 1 | The section is fully anchored in the research. All the high-level concepts it introduces—planning, reasoning, ReAct, Plan-and-Execute, and self-correction—are the central themes of the provided research material and are presented accurately. |
| What a Non-Reasoning Model Does And Why It Fails on Complex Tasks | 1 | The section meets all requirements from the outline, including the correct agent example, consequences, and transitions. The addition of context on the 'black box problem' is brief and does not detract from or replace any of the mandated points. | 0 | While the core concepts are based on the guideline, the section's added discussion on the 'black box problem' and its relation to 'enterprise adoption' is not supported by any evidence within the provided research material. |
| Teaching Models to "Think": Chain-of-Thought and Its Limits | 0 | The section uses the correct prompt example and most request topics but it fails to address the specific limitation required by the guideline, which is that the 'plan and the answer appear in the same text'. Instead, it substitutes this with an unrequested point about computational cost. | 1 | The section's description of Chain-of-Thought is correctly aligned with the foundational concepts presented in the research materials, such as the canonical paper Sourced. |
| Separating Planning from Answering: Foundations of ReAct and Plan-and-Execute | 0 | The presents the core idea, details the benefits, correctly positions the two patterns against each other, and provides a clear transition to the next section. It misses an explanation for how it allows different handling for reasoning traces vs. final outputs. | 1 | The content is well-anchored in the provided research. The distinction between ReAct's interleaved loop and Plan-and-Execute's separated phases is directly supported by sources |
| ReAct in Depth: The Loop of Thought and Observation | 0 | The section fails on multiple guidelines: it is missing the required Mermaid diagram, and the steps in the evolving example depart from what the guidelines map out and are presented in a logically incorrect order, violating the deliverable requirements. | 0 | Most key claims are sourced but the section introduces a significant claim that ReAct has 'Python implementation difficulty,' which is an assertion not supported by any of the provided research sources. |
| Plan-and-Execute in Depth: Structuring the Path from Goal to Action | 0 | Includes mermaid diagram and maps out the plan and execute example as requested. The section fails to adhere to the content guideline for its 'Pros'. It incorrectly emphasizes 'fostering model creativity' instead of the specified advantages of efficiency and reliability for structured tasks. | 0 | A source for inflexibility is found correctly but the primary advantage presented—creativity—is not supported by the research set. The provided sources frame the benefits of this pattern in terms of cost, reliability, and efficiency. |
| Where This Shows Up in Practice: Deep Research-Style Systems | 0 | This section, which is explicitly required in the lesson outline, is completely missing from the generated article. | 0 | As the section is not present in the article, it cannot be anchored in research. |
| Modern Reasoning Models: Thinking vs. Answer Streams and Interleaved Thinking | 0 | The section is incomplete. It introduces the core concepts but omits the mandatory discussion on 'Implications for system design' and fails to provide the required transition to the next section. As a result the section is also well under the word count asked. | 1 | The concepts that are discussed, such as 'thinking' streams and 'interleaved thinking', are correctly anchored in the research, with support from sources. |
| Advanced Agent Capabilities Enabled by Planning: Goal Decomposition and Self-Correction | 0 | The section includes key discussion on Goal decomposition and self-correction as asked but it violates the guideline by substituting the recurring 'conflicting adoption rates' example with an unrequested flight-booking scenario. It also omits the required discussion on 'Why patterns still matter | 1 | Although the specific flight example/analogy was not requested, the key facts and high-level concepts of goal decomposition and self-correction are well-supported by the provided research material and include sources. |
| Conclusion | 1 | The conclusion adheres to the guideline. It effectively summarizes the lesson's key takeaways, recaps the ReAct and Plan-and-Execute patterns, and provides a clear, accurate preview of the next lessons (8, 9, and 10), all within the specified length | 1 | The section is fully anchored. The summary accurately recaps the core concepts supported by the research, and the preview of future lessons (8, 9, and 10) is taken directly from the article guideline. |
_Table 4:_Labels for the “07_reasoning_planning” sample for the user intent LLM judge.
You can see how these two tables look in code implemented using the UserIntentMetricFewShotExamples Python structure by going to the brown.evals.metrics.user_intent.prompts.py file.
Similar to the FollowsGTMetricLLMJudge, the final piece of the puzzle is the get_eval_prompt function. This function assembles the final prompt by formatting the system prompt with the article guideline (input), research (context), generated article (output), and the few-shot examples.
Running the Judge
Let's see the UserIntentMetricLLMJudge in action. We will use the same 01_sample_small as in the previous example. For this sample we available the article_guideline.md and research.md as input context, and a noisy version of the article - article_noisy.md as the generated article where we intentionally introduced the specified violations such as hallucinated claims and guideline deviations to see if the judge catches them.
To view the files, either open the links above or run the associated Notebook. Meanwhile, to avoid copying an article within an article, we will assume you looked at the article_guideline.md, research.md, and article_noisy.md.
It outputs:
Accessing LLM Judges Through Our Factory Function
To make it easy to instantiate these metrics in our pipeline, we use a factory function. This allows us to select metrics by name string.
- We define the factory function (Source:
brown.evals.metrics.__init__).
We can now instantiate both metrics with a single call:
Which outputs:
With that, we finally wrapped up the LLM Judge implementation.
Building the AI Evals Pipeline
Now we connect everything into the AI evals pipeline that takes as input the evals dataset, runs the LLM judges described above on all the samples and outputs the final metrics into Opik.
The Evaluation Task
First, we need a task that runs Brown on a given dataset sample and returns all the context elements.
This function takes a dataset sample, runs the Brown workflow (or reads from cache), and returns the inputs and outputs needed for scoring.
- We define the
evaluation_taskwhich orchestrates the generation and data preparation (Source:brown.evals.tasks).
- We continue with the execution logic, setting up the tracer and running the workflow.
- We define the
__runhelper which executes the actual workflow.
As we respect the clean architecture design, the code is modular. Thus, exposing Brown in different setups, such as an MCP Server or an AI evals pipeline, is easy. As you saw above, it required only a few lines of code where we mostly leveraged the same boilerplate code to call Brown as we did in the MCP Server module from brown.mcp.
Hooking to Opik
Now, we define the evaluate function to run the task defined above across our dataset stored in Opik. This function in generic, accepting any dataset, metrics or evaluation task.
- We define the
evaluatefunction that connects to Opik. Note how we populate thellm_judge_configattribute with our LLM judge and app config and pass it as theexperiment_config. These will be available within the Opik experiment to understand the setup of the run (Source:brown.observability.evaluation).
- We define the
get_datasethelper (Source:brown.observability.opik_utils).
- Lastly, we define the
create_evaluation_taskhelper to customize and pin arguments using thepartialfunction. We have to do this, because when the task is called from Opik'sevaluatefunction it accepts only asample: EvalSampleDictas input (Source:brown.evals.tasks).
Running the End-to-End Evaluation
Finally, we configure the pipeline constants and instantiate the core components required to run it, such as the model, evaluation metrics and splits.
Now, we will compute the alignment score on the validation split and the baseline score on the test split.
Computing the Alignment Score on the Validation Split
The Alignment Score tells us if we can trust our judge. It measures the agreement between the LLM judge's scores and those of human experts on the validation split [5].
We will compute the alignment score on the validation split. Thus, first, let's run the evals pipeline on it:
For this example, we will assume we got the results from Image 4.
Image 4: Results on the validation split
Where the reasoning message resembles that in Image 5.
Image 5: Example of a reasoning message on a single lesson of the flow metric on the validation split.
Note that we have a reasoning message for each (lesson, metric) combination.
Understanding How We Computed the Alignment Score
The idea behind the alignment score is simple: To see how much domain experts agree with the results from the LLM Judge.
To do that, we will compare our manually labeled results with those from the LLM Judge. Since we use only binary metrics at the section level, we compute the alignment score using the following formula, which calculates the average number of matching scores between our manual labeling and the LLM judge.
The formula ranges from [0, 1]. Hence, we can consider it as an alignment percentage. If the alignment is close to 100%, then our LLM Judge is aligned with human judgment. Otherwise, we still have to improve the LLM judge until the scores converge to 100%.
Only when the alignment score approaches 100% can we truly rely on our LLM Judge!
To keep it simple, we will walk you through the process of how we computed the alignment score for our FollowsGTMetricLLMJudge. Here are the steps we went through:
- We fixed the generated articles through by using the outputs/evals-pro directory as a cache. This is important because we want to fix all input components to the LLM Judge (expected and generated articles) to have the only moving variable the output of the judge.
- Next, we opened the cached generated
[L10: Memory](<https://github.com/iusztinpaul/agentic-ai-engineering-course-data/blob/main/data/outputs/evals-pro/data/10_memory_knowledge_access/article.md>)article. - Also, opened the ground truth
[L10: Memory](<https://github.com/iusztinpaul/agentic-ai-engineering-course-data/blob/main/data/inputs/evals/dataset/data/10_memory_knowledge_access/article_ground_truth.md>)article from the AI evals dataset - Now, by keeping the two articles side-by-side, we manually labeled the difference between the two (both scores and reason messages). It's extremely important to do the labeling before looking at the results from the judge to avoid being biased.
- Next, we ran an experiment on the validation split that contains the
L10: Memoryarticle. - Added the LLM judge results side-by-side with the manual labeled ones. We did this in a Google Sheet.
- Computed the alignment score using the formula above, which can easily be implemented in Google Sheets or Excel. For computing the alignment these are great tools, because they are flexible, easy to implement and share.
Image 6: Example of how we used Google Sheets to compute the alignment score.
For our example, here is what that comparison looks like for the FollowsGT metric across all dimensions:
Content Metric Alignment
| Section | Score Content | Reason Content | LLM Judge Score Content | LLM Judge Reason Content |
|---|---|---|---|---|
| Introduction | 1 | Even if the examples are a bit different it both sections cover the same introductory ideas about memory for AI agents. | 1 | Both sections introduce the concept of memory in AI agents, highlighting the stateless nature of LLMs and the role of the context window. They both set the stage for the lesson by explaining why memory is a necessary engineering solution. |
| The Layers of Memory: Internal, Short-Term, and Long-Term | 0 | Even if it covers the same ideas about internal, short, and long-term memory, the generated section misses the ideas on combining them into a unified system and reference to RAG. | 1 | Both sections accurately describe the three layers of memory: Internal Knowledge, Short-Term Memory (Context Window), and Long-Term Memory, explaining their distinct functions and interplay. |
| Long-Term Memory: Semantic, Episodic, and Procedural | 1 | Both sections cover the same ideas around semantic, episodic and procedural memory. | 1 | Both sections correctly categorize long-term memory into Semantic, Episodic, and Procedural types, providing clear definitions and examples for each. |
| Storing Memories: Pros and Cons of Different Approaches | 1 | Both sections cover the same ideas around storing the memory as raw strings, structured entities or knowledge graphs. | 1 | Both sections discuss the same three approaches for storing memories: Raw Strings (Vector Databases), Structured Entities (JSON), and Knowledge Graphs, outlining the pros and cons for each. |
| Memory Implementations With Code Examples | 1 | Both sections cover examples on implementing semantic, episodic and procedural memory with mem0. | 1 | Both sections provide code examples for implementing Semantic, Episodic, and Procedural memory. However, the generated section uses the mem0 library, while the expected output also uses mem0 but presents the code and explanations in a slightly different structure and with more detailed comments. The core concepts demonstrated are the same. |
| Real-World Challenges | 0 | The generated section adds an extra topic on "The Human Factor" that's entirely missing within the expected section. | 0 | The generated section covers similar themes, such as re-evaluating compression due to larger context windows and designing for the product. However, it presents these as two distinct points, whereas the expected output integrates them into a more cohesive narrative. The generated section also adds a "The Human Factor" subsection, which is not present in the expected output. |
| Conclusion | 1 | Both sections cover the same ideas presenting what we've learnt within the article. | 1 | Both sections provide a summary of the lesson's key takeaways, emphasizing that memory transforms agents from stateless tools to adaptive partners and that current memory systems are an engineering workaround for the lack of continual learning in LLMs. |
| References | 0 | The generated section misses 10 links. | 1 | The generated section provides a list of references, similar in purpose to the expected output. However, the specific references cited are different. |
_Table 5:_Alignment comparison for the Content metric between human and LLM judge.
Alignment: 75.00%
Flow Metric Alignment
| Section | Score Flow | Reason Flow | LLM Judge Score Flow | LLM Judge Reason Flow |
|---|---|---|---|---|
| Introduction | 0 | Covers the core ideas in the same order, such as LLMs forgetting past interactions, using the context window to anchor the conversation, and the limitations of the context window. Still, the last sentence acting as the transition differs: the generated section makes a quick outline, while the expected section highlights its characteristics in biology. | 0 | The generated section follows a similar flow, starting with a personal anecdote, explaining the statelessness of LLMs, and introducing memory as the solution. However, it omits the specific mention of OpenAI's ChatGPT memory feature, which is present in the expected output. |
| The Layers of Memory: Internal, Short-Term, and Long-Term | 0 | The generated section starts with a comparison to biology, while the expected section jumps straight to the three memory types, with a small transition. | 1 | The flow of ideas is consistent in both sections, starting with an introduction to the layers, defining each one, and concluding with a diagram that illustrates their relationship. |
| Long-Term Memory: Semantic, Episodic, and Procedural | 0 | The generated section misses the last transition sentence. | 1 | The flow is identical, with both sections introducing the three categories and then dedicating a subsection (using H3 headers) to explain each one in the same order: Semantic, Episodic, and Procedural. |
| Storing Memories: Pros and Cons of Different Approaches | 0 | Even if both sections have an introductory paragraph, the generated section text misses most of the ideas such as the discussion on tools for agents to interact with the memory. | 0 | The generated section follows a similar logical flow, introducing the topic and then detailing each storage approach. However, it omits the introductory paragraph from the expected output that discusses the challenge of updating memories and the role of LLMs in managing ADD/UPDATE/DELETE operations. |
| Memory Implementations With Code Examples | 0 | The generated section adds an extra "extraction prompt" example within the semantic memory subsection that is not present within the expected section. | 0 | The generated section's flow is logical, covering setup and then each memory type. However, it differs from the expected output, which integrates the helper function definitions within the setup and provides more detailed, step-by-step explanations for each code block's output. The generated section also misses the introductory paragraph that frames the code examples and links to a Colab notebook. |
| Real-World Challenges | 0 | The generated section adds an extra "The Human Factor" that's missing within the expected section. | 0 | The flow is different. The generated section breaks the discussion into "Re-evaluating Compression," "Designing for the Product," and "The Human Factor." The expected output has a more integrated flow, discussing compression and product design as interconnected challenges without the distinct separation. |
| Conclusion | 1 | Both sections follow the same flow, presenting what we've learnt within the article, wrapping up with a transition to the next lesson. | 1 | Both sections follow a similar flow, summarizing the main points of the lesson and providing a forward-looking statement about the next topics in the course (RAG and Multimodal data). |
| References | 1 | - | 1 | Both sections present the references as a numbered list, which is a consistent flow. |
_Table 6:_Alignment comparison for the Flow metric between human and LLM judge.
Alignment: 75.00%
Structure Metric Alignment
| Section | Score Structure | Reason Structure | LLM Judge Score Structure | LLM Judge Reason Structure |
|---|---|---|---|---|
| Introduction | 1 | Both sections are structured the same as simple text paragraphs. | 1 | Both sections use similar paragraph structures and bolding for emphasis. The generated section correctly uses citation formatting. |
| The Layers of Memory: Internal, Short-Term, and Long-Term | 1 | Both sections enumerate the three memory types within simple paragraphs with bolded key words. | 1 | Both sections use bolding for key terms and include a Mermaid diagram to visualize the concept. The formatting of the diagram and its caption is consistent. |
| Long-Term Memory: Semantic, Episodic, and Procedural | 1 | Both sections structured the three long-term memory types under H3 headers. | 1 | Both sections use H3 headers for the sub-categories, maintaining a consistent structure. The use of bolding and italics for emphasis is also similar. |
| Storing Memories: Pros and Cons of Different Approaches | 0 | The generated section uses numbered H3 headers instead of simple H3 headers as in the expected section. | 0 | The generated section uses numbered H3 headers for each approach, which differs from the expected output's use of unnumbered H3 headers. Additionally, the expected output includes a detailed comparison table, which is missing from the generated section |
| Memory Implementations With Code Examples | 1 | Both sections use numebered lists to present the code and divide the impelemntation examples using H3 headers. | 0 | The generated section uses H3 headers for each implementation, which is a different structure from the expected output's H3 headers for each memory type. The expected output also includes a "Tip" callout box, which is absent in the generated version. The formatting of code blocks and outputs is similar but not identical. |
| Real-World Challenges | 1 | Both sections divide the ides under H3 headers and simple text paragraphs. | 0 | The structure is different. The generated section uses H3 headers for its points, while the expected output uses a single H2 header for the entire section and presents the content as continuous prose. |
| Conclusion | 0 | Both sections are structured as simple text paragraphs, but the generated sections bold the word "Retrieval-Augmented-Generation (RAG)". | 1 | The paragraph structure and overall formatting are consistent between both sections. |
| References | 0 | The references from the generated section do not follow the same APA format. | 0 | The formatting of the references is different. The generated output uses a format like "1. Title. (Date). Source. URL", while the expected output uses "[] Title". The generated section also includes an "Images" subsection at the end, which is not in the expected output. |
_Table 7:_Alignment comparison for the Structure metric between human and LLM judge.
Alignment: 62.50%
How Can We Improve the Judge?
As you can see, the scores are far from 100%. So how can we improve it?
We must identify where the scores do not match, adapt the LLM Judges by improving the few-shot examples or the system prompt, and retry until the scores converge to 100%. You will likely need to iterate this process 3-5 times until the judge is fully aligned.
Still, if you look only at the difference in scores, it's tough to figure out what to improve. That's why we labeled the reason message as well. Looking at the reasoning behind our manual process and the judge's, we have strong signals about what to improve.
For example, if we look at The Layers of Memory: Internal, Short-Term, and Long-Term section within the flow metric table, we can see that the LLM judge gave a score of 1 because it ignored the transition paragraph, which is wrong. Thus, we should add this rule to the LLM judge system prompt.
The alignment step is often overlooked by people implementing LLM judges, which is a huge mistake. By aligning the judge with a human expert, you ensure that you can fully trust your metrics. Otherwise, if you cannot trust the results of the judges, they are equal to 0, as you cannot truly make decisions based on them.
Computing the Baseline Score on the Test Split
Still, to move on, let's assume we are satisfied with the alignment. The next step is to run the pipeline on the Test Split to establish our baseline score.
For our experiment, this is what the results from Opik look like:
Image 7: Experiment results on the test split.
And a reasoning message:
Image 8: Example of a reasoning message on the flow metric for the Lesson 2 dataset sample on the test split.
These are the aggregate scores across the test set:
This baseline indicates that our content is accurate (0.90), but our structure (0.41) and flow (0.11) require improvement. This is actionable data that we can use to improve Brown.
Remember that the goal of these metrics is not to achieve perfect scores, but rather to provide clear direction on how to optimize our system. It’s even recommended to make the metrics extremely hard to pass, to ensure our system is robust in edge-case scenarios.
Checking the Stability of the LLM Judge
Finally, we must ensure our judge is stable. Since LLMs are stochastic, running the same evaluation twice might yield different scores.
Even if we did our best to stabilize these scores, for example, by using binary scores to make it easier for the LLM to choose one option, the LLM judge can still be unstable.
So, how can we compute the stability of the LLM judge?
As before, we fix all components using our cached articles from outputs/evals-pro, and run the experiment multiple times on the same inputs to see how much the scores vary. The idea is that, by keeping everything the same within the AI evals pipeline, the results differ purely due to the stochastic nature of the LLMs.
In our use case, we run the AI evals on the test split 5 times and got the following results:
Image 9: Variation in LLM judge metrics across 5 runs on the test split, using the same cached generated articles.
As shown in Image 9, the lines for each metric are relatively flat. It's not 100% flat, but it doesn't vary much, which means our LLM judges are stable, though not 100% stable.
To quantify this variation, we computed the standard deviation for each metric:
| Metric | Standard Deviation |
|---|---|
| follows_gt_content | 0.0316 |
| follows_gt_flow | 0.0432 |
| follows_gt_structure | 0.0180 |
| user_intent_guideline_adherence | 0.0380 |
| user_intent_research_anchoring | 0.0268 |
Table 4: Standard deviation of LLM judge scores across 5 runs.
As you can see, the standard deviation is not 0, which means the LLM judges are not purely stable between runs. So what can you do?
There are 2 options to consider this variation:
- You can use this standard deviation to understand when a difference in a particular metric is statistically significant. For example, for
follows_gt_content, we have a standard deviation of ~0.031. Thus, if the difference between the baseline and the change you made to the AI app is <= 0.031, it's probably probabilistically insignificant. - You can improve the LLM judge and stabilize it by trying to get this number close to 0. Which can be more time-consuming, but it makes your system more trustworthy. You can do that by making it easier for the LLM to decide between 0 and 1. To do this, you need to review the data, label more samples, and identify which scenarios are fuzzy and need clarification.
Probably the best strategy is to start with option 1 to get started and strive for option 2 in the long run, as you keep using the system and better understand your data and domain.
By ensuring the LLM Judge is aligned with your domain expert and stable across runs, you ensure people will truly trust the metrics and start making decisions based on them.
Exploring the Experiments Inside Opik
Here is a short video presenting in more detail how you can leverage Opik's dashboard to zoom in on each experiment or compare them:
Conclusion
Building an evaluation pipeline is the inflection point where AI engineering shifts from "vibes" to engineering.
We learned how to:
- Implement
FollowsGTandUserIntentjudges with section-level binary scoring. - Use a Train-Val-Test split to train and validate our judges.
- Integrate with Opik to run automated experiments.
- Verify reliability through alignment scores and stability checks.
In the next lessons, we will take our AI apps closer to production by building the CI/CD pipelines and deploying them to Google Cloud.
Practicing Ideas:
- Test LLM Judge Stability : We showed the LLM Judge stability on the article generated with
gemini-prousing the cached articles from outputs/evals-pro. Now repeat the same process using the cached articles from the outputs/evals-flash directory. Run the LLM judges 5 times with all the inputs fixed. Measure the stability of the scores across runs. Are the results more stable or worse when usingflash? - Compute the Alignment Score on the User Intent Metrics : Use the Memory lesson from the validation split and manually score each section on the user intent metrics, such as article guideline adherence and research anchoring. Compare your scores with the LLM judge scores to see how aligned they are.
- Iterate on Brown : Make changes to the Brown AI app (prompts, model, temperature) and rerun the AI evals. See if your change improves or degrades the metrics?
References
- Patronus AI. (n.d.). LLM as a Judge. Patronus AI. patronus.ai/llm-testing/llm-as-a-judge
- Evidently AI. (n.d.). LLM as a Judge. Evidently AI. evidentlyai.com/llm-guide/llm-as-a-judge
- Lightly.ai. (n.d.). Train Test Validation Split. Lightly.ai. lightly.ai/blog/train-test-validation-split
- Monte Carlo Data. (n.d.). LLM as a Judge. Monte Carlo Data. montecarlodata.com/blog-llm-as-judge
- AI21. (n.d.). LLM as a Judge. AI21. ai21.com/glossary/foundational-llm/llm-as-a-judge
- Databricks. (n.d.). Align Judges. Databricks. docs.databricks.com/aws/en/mlflow3/genai/eval-monitor/align-judges
- Galileo AI. (n.d.). LLM as a Judge Guide Evaluation. Galileo AI. galileo.ai/blog/llm-as-a-judge-guide-evaluation