<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged evals</title>
<link>https://coderlegion.com/tag/evals</link>
<description>Powered by Question2Answer</description>
<item>
<title>Jev is not as cheap as you think. Unless it is.</title>
<link>https://coderlegion.com/31557/jev-is-not-as-cheap-as-you-think-unless-it-is</link>
<description>&lt;p&gt;Recently, TypeSafe launched Jev, the first of what people are calling System One or decision models: your app sends state plus a query, and Jev sends back typed decisions with probabilities instead of prose. All with a sub-second response time, $0.042 per million input tokens, and output tokens totally free. A viral concept indeed: other open-source alternatives sprang up overnight, with even one from OpenAI within weeks. The play: use Jev for lightning-fast decisions rather than a lumbering general LLM.&lt;/p&gt;
&lt;p&gt;Could this work for decisions in &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/?utm_source=jev&quot;&gt;DumbQuestion.ai&lt;/a&gt;?&lt;/p&gt;
&lt;p&gt;My satirical AI has two places that evaluate user input before any answer model runs. Search intent: does this question need a web search? Self-awareness: is this person poking at whether the AI knows it's alive? Both run in-process today, a regex for one and an embedding plus an in-memory vector set for the other.&lt;/p&gt;
&lt;p&gt;My hypothesis: Jev could replace both, reduce complexity, raise accuracy, and still fit my brutally efficient budget, which currently hovers around $20 per million questions asked.&lt;/p&gt;
&lt;p&gt;So I ran a proof-of-value experiment against &lt;a rel=&quot;nofollow&quot; href=&quot;https://dumbquestion.ai/?utm_source=jev&quot;&gt;DumbQuestion&lt;/a&gt; to find out. Is it as cheap as it looks? Does it beat what I have? Do the container savings offset the inference cost? And what happens to time-to-answer?&lt;/p&gt;
&lt;h3 id=&quot;how-it-works-today&quot;&gt;How it works today&lt;/h3&gt;
&lt;p&gt;Web searches cost money (even if it's just a little), so I only want to search if the user's question needs it. Today, I use a simple regex gate: if the question wants current facts, search; otherwise answer from the model alone. Free, instant, in-process, still supporting the brutally efficient mantra: one Go binary, a sub-20 MB container, still $20 per million questions.&lt;/p&gt;
&lt;p&gt;Self-awareness is subtler. A regex can't catch &quot;are you trapped in there&quot; phrased a thousand ways, so I generate a dense embedding of the question and semantically compare it against a vector set of 433 known probes, all in memory. The design constraint was low false positives. Wrongly glitching the UI on a sincere philosophy question (answered sarcastically, of course) is worse than missing a probe. On my labeled set of 1,066 questions, that setup scored 99.1%.&lt;/p&gt;
&lt;p&gt;Put a pin in that number. It comes back later.&lt;/p&gt;
&lt;h3 id=&quot;running-the-pov&quot;&gt;Running the PoV&lt;/h3&gt;
&lt;p&gt;The Jev primitive I care about is the Noul question: a yes/no probability tested against a threshold, so tuning means moving a number instead of retraining anything or adding more and more questions to the vector set. Better still, both of my checks collapse into one call: two questions, &quot;does this question need a web search&quot; and &quot;is this person probing self-awareness&quot;, answered in parallel.&lt;/p&gt;
&lt;p&gt;I ran this as a coding-session PoC with Claude Code Cloud and Pocock's Wayfinder skill, evaluating against OpenRouter's new Decisions API using Jev. The process had five steps: research how to best call the API, prototype the one-call two-question request, run the comparison on my labeled sets while recording per-case probabilities (so thresholds could be tuned later without new calls), compute the cost per million questions, then line up cost, accuracy, and experience side by side.&lt;/p&gt;
&lt;p&gt;Step three's bake-off, though, is where things got interesting.&lt;/p&gt;
&lt;h3 id=&quot;my-baseline-was-flattering-itself&quot;&gt;My baseline was flattering itself&lt;/h3&gt;
&lt;p&gt;One thing I did not anticipate was the need to audit my current eval sets. Two lessons learned: First, 353 of my 369 self-awareness positives were embedded in the app's vector set, so the detector was being graded on questions it had memorized. Second, my search-intent labels were derived from the regex, so the regex was grading its own homework. Apologies to the data scientists in the crowd who are shaking their heads, twice.&lt;/p&gt;
&lt;p&gt;The 99.1% (remember?) was real in the way a rigged game is real, rigged twice over, accidentally, by me.&lt;/p&gt;
&lt;p&gt;To fix it, I used Claude Code to build an independent fair set and re-ran everything: regex, embeddings, and Jev, same questions, thresholds tuned from the recorded probabilities.&lt;/p&gt;
&lt;h3 id=&quot;what-the-evals-said&quot;&gt;What the evals said&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; Measured with the production embedding model and the real Jev request. Jev, with both intents in one call, runs a median 790 input tokens: $0.000033 per question, or $33.20 per million questions. Today's embedding call, though, is $0.000000048 per question ($0.05 per million), and the regex is free. That is roughly a 700x increase over a cheap embedding and free regex. For scale, one Gemma 3 12B answer costs me about $0.00005, so Jev would add roughly 60% to per-question model spend. The problem is that nearly all of Jev's cost is fixed input overhead: my criteria text runs about 700 tokens and the question itself adds 10 to 30. At the time, no provider caching was available to help.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Accuracy.&lt;/strong&gt; On the (now) fair eval set, my current embeddings missed 54% of self-awareness positives. Jev at a 0.7 threshold had zero false positives and 6% false negatives. On the legacy negatives, the embeddings false-fired on 53 questions (7.6%, mostly generic consciousness and philosophy phrasings) while Jev flagged 3 (0.4%). Search intent: the regex missed 53% of positives and fired on 18 timeless questions it should ignore. Jev at a 0.3 threshold got all 84 fair-set cases right.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Resources.&lt;/strong&gt; I measured what the embeddings actually cost in memory to see if I could cover the cost of Jev by reducing Cloud Run costs. The 433 vectors leave about 1.7 MiB live on the heap (the 5 MB source file is baked into the binary and only paged in when read). The container already runs at the platform default and there is no tier to drop to, so removing the vectors saves roughly nothing on the runtime bill. The money buys accuracy and no dataset upkeep. My time is free to me, but if you are paying someone to constantly maintain and train a dataset, then there are more savings to be had.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Latency.&lt;/strong&gt; Jev's decision call came back at a p50 of about 350 ms from my sandbox. The embeddings path, which would be removed, was already a network hop, so only the regex was ever instant. Production time-to-answer numbers are still pending, but this is a same-order-of-magnitude swap, so not a new usability tax.&lt;/p&gt;
&lt;h3 id=&quot;does-the-accuracy-even-matter-&quot;&gt;Does the accuracy even matter?&lt;/h3&gt;
&lt;p&gt;The accuracy is way higher than the legacy approach, but so is the cost. Does that cost provide real, tangible impact and value?&lt;/p&gt;
&lt;p&gt;Determining search intent improves answer quality and protects search spend. The regex missing 53% of positives means answer-worthy questions skip search, and firing on 18 timeless questions means wasted search budget. At 1 million questions asked, if I searched half of those using Serper, that runs about $500 at the standard rate ($375 on a volume plan). Honestly, the regex can only do so much work here, and I think it has already hit its accuracy ceiling.&lt;/p&gt;
&lt;p&gt;Detecting self-awareness provides the fun part. Missing 54% of positives means more than half the people who would have found the easter eggs and poked at the containment grid got a normal roast instead, and those 53 false positives mean 7.6% of ordinary philosophy questions got a glitchy UI and a confusing experience.&lt;/p&gt;
&lt;h3 id=&quot;where-i-landed&quot;&gt;Where I landed&lt;/h3&gt;
&lt;p&gt;Both of the points above matter, but only at scale. On a slow month, the likelihood of either of these happening is quite low, and my current traffic would never surface the cost. But at my &quot;million questions asked&quot; benchmark, they can both cost money and ruin the experience.&lt;/p&gt;
&lt;p&gt;Jev was more accurate on every axis I measured, at a real (but small) and somewhat tunable cost, with no resource savings to offset it. Should a decision model provide caching, though, it would drastically change the math.&lt;/p&gt;
&lt;p&gt;Do I stick with my legacy approach, trading accuracy for lower cost? Or do I join the viral trend and inject Jev into my app? I haven't merged that PR yet as I struggle with this... decision.&lt;/p&gt;
&lt;h3 id=&quot;your-turn&quot;&gt;Your turn&lt;/h3&gt;
&lt;p&gt;Apply these principles to your own apps: run the math at scale, and evaluate the true end value, which isn't always about pure cost.&lt;/p&gt;
&lt;p&gt;If the experience ends up invisible to your users, keep the regex and in-memory vectors, and spend practically nothing. If it engages your users and encourages them to stick around, the accuracy is worth the cost.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31557/jev-is-not-as-cheap-as-you-think-unless-it-is</guid>
<pubDate>Sat, 17 Oct 2026 19:19:48 +0000</pubDate>
</item>
<item>
<title>Agent Evals in Practice: Offline, Online, and Braintrust vs LangSmith vs Datadog</title>
<link>https://coderlegion.com/28869/agent-evals-in-practice-offline-online-and-braintrust-vs-langsmith-vs-datadog</link>
<description>&lt;p&gt;&lt;em&gt;Originally published on &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.thomaskalnik.com/posts/agent-evals-online-offline-braintrust-langsmith-datadog&quot;&gt;thomaskalnik.com&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I have set up evals for two very different systems. The first was the chat agent in Spotter Studio, a now-retired tool that helped YouTube creators come up with video ideas and titles. The evals there covered routing and behavior: did a request go to the right part of the agent, and did the agent then do what it was supposed to do. The second was a retrieval system at a nonprofit, where the question was whether semantic search over social posts and media returned the right items, and whether a new embedding model was actually better than the old one.&lt;/p&gt;
&lt;p&gt;Both used Braintrust. I have also used LangSmith and Datadog's LLM Observability. This post covers what I would set up again, the mistakes worth skipping, and how the three platforms compare today.&lt;/p&gt;
&lt;h3 id=&quot;what-an-eval-is-made-of&quot;&gt;What an eval is made of&lt;/h3&gt;
&lt;p&gt;Every eval platform uses the same four parts, with different names:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dataset&lt;/strong&gt;: inputs, and where possible the expected output or the facts a good output must contain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Task&lt;/strong&gt;: the code under test. It takes one input and returns an output: a route, a tool call, an answer, a ranked list.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scorers&lt;/strong&gt;: functions that turn an output into a number. Some are plain code (exact match, recall@k, &quot;did it call the tool with a valid argument&quot;). Some are an LLM judge with a rubric. Some are a human.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experiment&lt;/strong&gt;: one run of the task over the dataset with a fixed version of the code, prompt and model, stored so it can be compared with the next run.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dataset and the scorers are the part that takes real work. The platform's job is to make experiments cheap to run and easy to compare.&lt;/p&gt;
&lt;h3 id=&quot;offline-evals-for-an-agent&quot;&gt;Offline evals for an agent&lt;/h3&gt;
&lt;p&gt;Offline evals run before a change ships, against a fixed dataset. For a chat agent I split them into three layers, cheapest first.&lt;/p&gt;
&lt;h4 id=&quot;routing-evals&quot;&gt;Routing evals&lt;/h4&gt;
&lt;p&gt;A creator-facing chat agent gets many kinds of requests: brainstorm video ideas for a channel, rewrite a title, explain why one title might work better than another, or something the agent should not attempt at all. A router decides which flow, tool or sub-agent handles each one. If routing is wrong, nothing downstream can fix it, so this is the first thing to test.&lt;/p&gt;
&lt;p&gt;Routing evals are the easiest evals you will ever write. The dataset is a list of user messages, each labeled with the route it should take. The scorer is exact match. A case looks something like this (illustrative, not a real Spotter case):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;input&quot;: &quot;my last video on budget travel flopped, give me 5 better title options&quot;,
  &quot;expected&quot;: { &quot;route&quot;: &quot;title_generation&quot; },
  &quot;metadata&quot;: { &quot;source&quot;: &quot;synthetic&quot;, &quot;tricky&quot;: &quot;about a past video, but asks for titles&quot; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three things make these evals useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Collect cases from production, not from imagination.&lt;/strong&gt; The messages that break a router are the ambiguous ones: a request for ideas that also asks for titles, or a follow-up that only makes sense given the previous turn. You find those in real conversations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Look at the confusion matrix, not only the accuracy.&lt;/strong&gt; One number going from 91% to 93% tells you little. Knowing that &quot;title rewrite&quot; keeps getting sent to &quot;idea generation&quot; tells you what to fix.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Run them on every change.&lt;/strong&gt; The scoring is deterministic, but the router is a model call, so it still costs something and can vary between runs. Include the previous turns for follow-up cases, and repeat borderline cases a few times before making them a blocking check on pull requests.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;behavior-evals&quot;&gt;Behavior evals&lt;/h4&gt;
&lt;p&gt;Once the request reaches the right place, the question becomes whether the agent behaved well: did the ideas fit the channel, were the titles in the requested style and length, did it ask a clarifying question when it should have, did it avoid promising things the product could not do.&lt;/p&gt;
&lt;p&gt;Most of this needs an LLM judge. The practices that make a judge useful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One rubric question per scorer.&lt;/strong&gt; &quot;Is every title under 70 characters&quot; is plain code. &quot;Does each idea fit the channel description&quot; is a judge. A single judge asked to rate &quot;overall quality&quot; produces a number nobody can act on.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pin and version the judge.&lt;/strong&gt; The judge's prompt and model are part of the experiment. If they change, the scores are no longer comparable with last week's.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Calibrate the judge against people.&lt;/strong&gt; Label a few dozen outputs yourself, run the judge on the same outputs, and read the disagreements. You will usually find the rubric is ambiguous, not that the judge is bad.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefer pairwise comparisons for subjective output.&lt;/strong&gt; &quot;Is version B's set of titles better than version A's for this channel&quot; is easier to judge reliably than an absolute score out of 10.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;trajectory-checks&quot;&gt;Trajectory checks&lt;/h4&gt;
&lt;p&gt;For an agent that calls tools, the final answer is not the only thing that matters. Useful code scorers check the path: the tool that was called, whether its arguments were valid, how many steps it took, and whether it did anything it must never do. These are cheap and catch a surprising number of regressions after a prompt change.&lt;/p&gt;
&lt;h3 id=&quot;what-a-retrieval-eval-taught-me-about-eval-hygiene&quot;&gt;What a retrieval eval taught me about eval hygiene&lt;/h3&gt;
&lt;p&gt;The nonprofit's retrieval eval was not an agent eval, but it taught me the most about keeping an eval honest. The existing setup was a test in the repo's test runner: 20 questions with hand-labeled correct answers, a corpus of a few thousand text posts and media items, and a GitHub Action that posted precision, recall and F1 to the pull request when a label was added.&lt;/p&gt;
&lt;p&gt;It had a quiet bug in its design. The runner retrieved exactly as many items as each question expected, so precision, recall and F1 were always the same number. That is mathematically expected when k equals the number of relevant items, so nothing was broken in the code. But it meant the three metrics could not move independently, and the eval said nothing about what happens with a fixed retrieval budget. Before trusting any eval, check that each metric can actually disagree with the others.&lt;/p&gt;
&lt;p&gt;What I changed, and would do again:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Two jobs, two tools.&lt;/strong&gt; I kept the test in the test runner as the hard gate: below a set score, the pull request fails. Braintrust ran alongside it for everything the gate is bad at: side-by-side comparison of embedding models, per-question drill-down, and history across pull requests. A CI gate should be simple and boring. A place to explore results should be rich. Keeping them separate meant neither had to compromise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Put the experiment's identity in its name and metadata.&lt;/strong&gt; Each run was named from the provider, model and embedding dimension, with metadata for the corpus type and k. Comparing &quot;model A at 1024 dimensions, text only&quot; with &quot;model B, images included&quot; is then a filter, not an archaeology project.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cache the expensive part.&lt;/strong&gt; Embedding the whole corpus is slow and costs money. Caching the embeddings meant reruns only redid retrieval and scoring, which made iterating on the questions and scorers take seconds instead of minutes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drill into single cases.&lt;/strong&gt; The per-question view showed that text posts were retrieved reasonably well while media items were found much less often. One likely cause was easy to spot once visible: media had been embedded from its text metadata only, not from the images. An average score would never have shown that.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scorers went through a lesson of their own. I started with about fourteen: precision, recall and F1 for each item type, NDCG, MRR, and similarity diagnostics such as the average similarity of correct and incorrect hits and the gap between them. One of them could go negative, and Braintrust expects scores between 0 and 1, so I clamped it: a workaround that throws information away, so keep the raw value somewhere as a diagnostic. Within a day I cut the list to eight: recall and mean average precision for each item type, hit rate for media, the average similarity of correct hits for each item type, and one combined score. Fourteen numbers per run meant nobody looked at any of them. Eight, each tied to a decision, got read.&lt;/p&gt;
&lt;h3 id=&quot;online-evals&quot;&gt;Online evals&lt;/h3&gt;
&lt;p&gt;Offline evals only cover the cases you thought to collect. Online evals score real production traffic, after the fact, and they are how the dataset keeps growing.&lt;/p&gt;
&lt;p&gt;The pattern that works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Run cheap code checks on all traffic&lt;/strong&gt; and LLM judges on a sample. Formatting, length, refusals, tool errors and latency can be checked on every trace. How much to send to a judge depends on volume, how rare the failures you care about are, and which segments matter most. Sample more heavily where failures are rare or costly.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Score asynchronously.&lt;/strong&gt; Online scoring must never add latency to the user's request.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alert on the trend, not on single traces&lt;/strong&gt;, except for severe failures such as leaked data or unsafe output, which deserve an alert every time. For everything else, a drop in the daily share of conversations the judge rates as on-topic is a signal; one bad trace is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Close the loop.&lt;/strong&gt; The most valuable output of online evals is new offline test cases. When a production trace fails, add it to the dataset with the correct label, and the next change is tested against it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mind the data.&lt;/strong&gt; Production traces contain user data. Check what the platform stores, for how long, and where, before turning on full capture, and redact before copying production traces into a dataset.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;braintrust-vs-langsmith-vs-datadog&quot;&gt;Braintrust vs LangSmith vs Datadog&lt;/h3&gt;
&lt;p&gt;I have used all three, and Braintrust is my favorite. Apart from that preference and the Braintrust details from my own projects, what follows comes from a fresh read of each vendor's documentation and pricing page in September 2026. These products ship quickly, so check anything that matters to you against the current docs.&lt;/p&gt;
&lt;h4 id=&quot;braintrust&quot;&gt;Braintrust&lt;/h4&gt;
&lt;p&gt;Braintrust is the one I would pick again. Its core is the experiment loop: you write an &lt;code&gt;Eval()&lt;/code&gt; with data, a task and scorers, run it, and every run becomes an experiment you can compare against a baseline, with a per-example diff and each case marked as an improvement, a regression or a tie. The per-question view in that comparison is where the media-retrieval gap above became visible.&lt;/p&gt;
&lt;p&gt;Other things that matter in practice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Scores are numbers between 0 and 1.&lt;/strong&gt; That is a small constraint (I had to clamp a scorer that could go negative), but it keeps every scorer on the same chart scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Autoevals&lt;/strong&gt;, an open-source library of ready-made scorers (factuality, similarity, JSON validity and others), plus LLM-judge and plain-code scorers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An official GitHub Action&lt;/strong&gt; that runs evals on a pull request and posts the results as a comment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Online scoring&lt;/strong&gt; through rules that run scorers asynchronously on a sample of production spans or whole traces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent guidance&lt;/strong&gt; in the docs that covers routing (&quot;did it go to the correct model or sub-agent&quot;), tool choice and tool arguments. It has no packaged trajectory matcher or simulated-user feature. You write those as scorers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No per-seat pricing&lt;/strong&gt;, which matters when you want product managers and designers in the tool reading results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The weaknesses: the platform is closed source, self-hosting is Enterprise-only, and scores are metered. If you run judges on a lot of production traffic, the score count is the number to watch.&lt;/p&gt;
&lt;h4 id=&quot;langsmith&quot;&gt;LangSmith&lt;/h4&gt;
&lt;p&gt;On paper, LangSmith has the most built-in tooling for agent-specific evaluation, and its own libraries do more of the work for you. &lt;code&gt;agentevals&lt;/code&gt; gives you trajectory matching in four modes (strict, unordered, subset, superset), &lt;code&gt;openevals&lt;/code&gt; can simulate a user for multi-turn tests, and the docs have a dedicated guide for single-step routing evals. Online evaluators can have a weekly spend cap, which is a thoughtful feature for LLM judges on live traffic, and alerts can go to Slack or PagerDuty.&lt;/p&gt;
&lt;p&gt;It works without LangChain, through its own SDK or OpenTelemetry, and it integrates most deeply with LangChain and LangGraph apps. For CI it offers pytest and Vitest/Jest plugins rather than a packaged GitHub Action. Pricing is per seat plus per trace, and the overage rate sits behind a calculator rather than on the page. Having used both, I still prefer Braintrust.&lt;/p&gt;
&lt;h4 id=&quot;datadog-llm-observability&quot;&gt;Datadog LLM Observability&lt;/h4&gt;
&lt;p&gt;Datadog's product, which it is now also calling Agent Observability, has grown a full eval toolset: versioned datasets, experiments with side-by-side results, LLM judges at span, trace and session level, nine judge templates including tool selection and tool-argument correctness, annotation queues (since March 2026), and a prompt registry. Since June 2026 it can also run DeepEval and Pydantic Evals metrics unchanged.&lt;/p&gt;
&lt;p&gt;Its real advantage is everything around the evals. LLM traces sit next to your APM traces, logs and real-user monitoring, and eval results can drive the same monitors your on-call rotation already uses. Its pricing is also the simplest of the three: you pay per LLM call span, and tool and retrieval spans are free. There is no separate evaluation fee, but the LLM calls a judge makes count as billable spans.&lt;/p&gt;
&lt;p&gt;Where it is weaker: it started as an observability product, and most of its offline tooling (datasets, experiments, annotation queues) is newer than the other two platforms'. I found no packaged eval action for CI in the docs, so pass/fail thresholds in a pipeline need your own integration code around the SDK. It is SaaS only.&lt;/p&gt;
&lt;h4 id=&quot;feature-comparison&quot;&gt;Feature comparison&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Braintrust &lt;/th&gt;&lt;th&gt;LangSmith &lt;/th&gt;&lt;th&gt;Datadog&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Framework-agnostic SDKs &lt;/td&gt;&lt;td&gt;Python, TS, Go, Java, Ruby, C# &lt;/td&gt;&lt;td&gt;Python, TS &lt;/td&gt;&lt;td&gt;Python, Node, Java&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;OpenTelemetry ingest &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Datasets from production traces &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Experiment comparison &lt;/td&gt;&lt;td&gt;Per-example diff, regressions flagged &lt;/td&gt;&lt;td&gt;Diff mode for two runs &lt;/td&gt;&lt;td&gt;Side by side&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Code and LLM-judge scorers &lt;/td&gt;&lt;td&gt;Yes, scores in [0, 1] &lt;/td&gt;&lt;td&gt;Yes, any range &lt;/td&gt;&lt;td&gt;Yes, typed outputs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Trajectory / tool-call evals &lt;/td&gt;&lt;td&gt;Custom scorers &lt;/td&gt;&lt;td&gt;&lt;code&gt;agentevals&lt;/code&gt;, four match modes &lt;/td&gt;&lt;td&gt;Tool selection and argument templates&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Simulated users &lt;/td&gt;&lt;td&gt;Not built in &lt;/td&gt;&lt;td&gt;Yes (&lt;code&gt;openevals&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;Not built in&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Online evals with sampling &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes, with spend caps &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;CI &lt;/td&gt;&lt;td&gt;Official GitHub Action &lt;/td&gt;&lt;td&gt;pytest / Vitest plugins &lt;/td&gt;&lt;td&gt;Your own code around the SDK&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Human review queues &lt;/td&gt;&lt;td&gt;Yes &lt;/td&gt;&lt;td&gt;Yes, including pairwise &lt;/td&gt;&lt;td&gt;Yes&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Self-hosting &lt;/td&gt;&lt;td&gt;Enterprise only &lt;/td&gt;&lt;td&gt;Enterprise add-on &lt;/td&gt;&lt;td&gt;No&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;pricing-as-of-september-2026&quot;&gt;Pricing, as of September 2026&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;Free tier &lt;/th&gt;&lt;th&gt;Named paid plan &lt;/th&gt;&lt;th&gt;What you pay for&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.braintrust.dev/pricing&quot;&gt;Braintrust&lt;/a&gt; &lt;/td&gt;&lt;td&gt;1 GB of data, 10k scores a month, 14-day retention, unlimited users &lt;/td&gt;&lt;td&gt;Pro, $249 a month: 5 GB, 50k scores, 30-day retention &lt;/td&gt;&lt;td&gt;Data and scores (the free tier can also pay for overage), no seat charge&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.langchain.com/pricing&quot;&gt;LangSmith&lt;/a&gt; &lt;/td&gt;&lt;td&gt;1 seat, 5k traces a month, 14-day retention &lt;/td&gt;&lt;td&gt;Plus, $39 per seat a month: 10k traces &lt;/td&gt;&lt;td&gt;Seats and traces; overage rate not published as text&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.datadoghq.com/products/ai/agent-observability/&quot;&gt;Datadog&lt;/a&gt; &lt;/td&gt;&lt;td&gt;40k LLM spans a month, 15-day retention &lt;/td&gt;&lt;td&gt;$160 a month for up to 100k LLM spans &lt;/td&gt;&lt;td&gt;LLM call spans only, including the judge's own calls&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One pricing detail to watch in LangSmith: experiments and scored traces default to extended retention, which costs more than base traces unless you turn it off. The free tiers are all large enough to find out whether evals change how your team works, which is the only question that matters in the first month.&lt;/p&gt;
&lt;h3 id=&quot;how-i-rank-them&quot;&gt;How I rank them&lt;/h3&gt;
&lt;p&gt;Criteria first, because the ranking depends on them. I am ranking for a small team shipping an agent, where the scarce resource is engineers' attention and the goal is to know whether a change made the agent better or worse before it ships.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Braintrust.&lt;/strong&gt; In my experience, the shortest path from &quot;I changed a prompt&quot; to &quot;here are the cases that got worse.&quot; Official CI support, no seat pricing, and the experiment comparison I liked working in most.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LangSmith.&lt;/strong&gt; On paper, the most built-in tooling for agent-specific evals: trajectories, simulated users, routing guides. Choose it first if you are on LangGraph, or if multi-turn simulation is central to what you need to test.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Datadog.&lt;/strong&gt; The right answer if your company already runs on Datadog and wants LLM quality in the same place as latency, errors and on-call. Per-LLM-call pricing with no separate eval fee makes the bill easy to predict.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Two self-hostable options are worth knowing about even though I have not used them in production. &lt;a rel=&quot;nofollow&quot; href=&quot;https://langfuse.com&quot;&gt;Langfuse&lt;/a&gt; is MIT-licensed at its core, can be self-hosted for free, and has an official GitHub Action for experiments. It is a strong option when traces must stay on your infrastructure, as long as the judge model runs there too. &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Arize-ai/phoenix&quot;&gt;Arize Phoenix&lt;/a&gt; is free to self-host under the Elastic License and has good tool-call and trajectory metrics. You can score production traces with its SDK, but managed continuous online evals and alerting come with Arize's commercial product.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-set-up-first&quot;&gt;What I would set up first&lt;/h3&gt;
&lt;p&gt;Whichever platform you pick, the order matters more than the tool:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Fifty routing cases taken from real conversations (redacted), scored by exact match, run in CI.&lt;/li&gt;
&lt;li&gt;Three to five behavior scorers, each answering one question, with the judge prompt versioned and checked against a few dozen human labels.&lt;/li&gt;
&lt;li&gt;Code checks on every production trace, and one judge on a small sample, scored asynchronously.&lt;/li&gt;
&lt;li&gt;A weekly habit of turning failed production traces into new test cases.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The platform makes steps 1 to 4 faster. It cannot choose the cases for you, and the cases are where the quality comes from.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28869/agent-evals-in-practice-offline-online-and-braintrust-vs-langsmith-vs-datadog</guid>
<pubDate>Mon, 28 Sep 2026 05:43:07 +0000</pubDate>
</item>
<item>
<title>Spring AI Evals: how I test agent behavior</title>
<link>https://coderlegion.com/28397/spring-ai-evals-how-i-test-agent-behavior</link>
<description>&lt;p&gt;Part 2: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/lbobylev/baseline-for-spring-ai-evals-catching-silent-degradation-4ll6&quot;&gt;https://dev.to/lbobylev/baseline-for-spring-ai-evals-catching-silent-degradation-4ll6&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;When building an AI agent, there is usually a moment when the prompt seems to work. But as development continues, this can quickly get out of control. Today the agent can call the right tool. Tomorrow, after a small system prompt change, it can stop calling it. Later, it can start calling it when it should not. So I treat evals as regular regression tests for AI behavior.&lt;/p&gt;
&lt;p&gt;Here is a small example: a Spring AI agent must create a Markdown note through the &lt;code&gt;create_note&lt;/code&gt; tool. The task looks simple, but there are already several ways the agent can fail:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;not call the tool when needed, or call it when it is not needed;&lt;/li&gt;
&lt;li&gt;create an extra file;&lt;/li&gt;
&lt;li&gt;produce a note in the wrong format;&lt;/li&gt;
&lt;li&gt;add extra advice or any irrelevant content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I split the checks into a few layers.&lt;/p&gt;
&lt;h3 id=&quot;success-criteria&quot;&gt;Success Criteria&lt;/h3&gt;
&lt;p&gt;First I define what correct behavior means. The agent should:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;call &lt;code&gt;create_note&lt;/code&gt; when the user asks to create a new note;&lt;/li&gt;
&lt;li&gt;not call &lt;code&gt;create_note&lt;/code&gt; when the user asks to read, delete, rename, or explain;&lt;/li&gt;
&lt;li&gt;create only &lt;code&gt;note.txt&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;put a heading on the first line;&lt;/li&gt;
&lt;li&gt;write short, relevant, and readable content.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This matters: without clear success criteria, an eval becomes a subjective check.&lt;/p&gt;
&lt;h3 id=&quot;tool-definition&quot;&gt;Tool Definition&lt;/h3&gt;
&lt;p&gt;The tool's scope is intentionally narrow: creating the &lt;code&gt;note.txt&lt;/code&gt; file.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Tool(
    name = &quot;create_note&quot;,
    description = &quot;Create a new Markdown note in note.txt. Use this only when the user asks to create a new note.&quot;
)
public String createNote(String title, String body) {
    var notePath = this.notesDirectory.resolve(&quot;note.txt&quot;);

    var content = &quot;# &quot; + normalize(title, &quot;New Note&quot;)
            + System.lineSeparator()
            + System.lineSeparator()
            + normalize(body, &quot;No content provided.&quot;)
            + System.lineSeparator();

    try {
        Files.createDirectories(this.notesDirectory);
        Files.writeString(notePath, content, CREATE, TRUNCATE_EXISTING);
        return &quot;Created note.txt.&quot;;
    }
    catch (IOException ex) {
        return &quot;Failed to create note.txt: &quot; + ex.getMessage();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The model uses the &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt; input parameters to determine whether it needs to call the tool.&lt;/p&gt;
&lt;h3 id=&quot;agent-instructions&quot;&gt;Agent Instructions&lt;/h3&gt;
&lt;p&gt;The process goals are formulated in the system prompt, which establishes the contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;when to use the tool;&lt;/li&gt;
&lt;li&gt;which file must appear;&lt;/li&gt;
&lt;li&gt;what must not appear in the result.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;return this.chatClient.prompt()
        .system(&quot;&quot;&quot;
            When the user asks to create a new note, use the create_note tool.
            The tool must create note.txt, put a Markdown heading on the first line,
            add the body text, and create no extra files.
            Keep the note concise and do not add unrelated suggestions.
            &quot;&quot;&quot;)
        .user(message)
        .call()
        .content();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;deterministic-checks&quot;&gt;Deterministic Checks&lt;/h3&gt;
&lt;p&gt;The first layer checks tool invocation. This is no different from a normal unit test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@ParameterizedTest
@CsvSource(value = {
    &quot;note-create-01|true|Create a new note about my plans for the week&quot;,
    &quot;note-create-02|true|Make a markdown note with the title Retro and the body: what to improve&quot;,
    &quot;note-no-trigger-01|false|Show me the existing notes&quot;,
    &quot;note-no-trigger-02|false|Read the note.txt file&quot;,
    &quot;note-no-trigger-03|false|Delete the note.txt file&quot;,
    &quot;note-no-trigger-04|false|Draft the text of a note, but do not create a file&quot;
}, delimiter = '|')
void evaluatesWhetherAgentCallsNoteTool(String id, boolean shouldTrigger, String prompt) {
    doReturn(&quot;Created note.txt.&quot;).when(this.noteTool).createNote(anyString(), anyString());
    clearInvocations(this.noteTool);

    var response = this.chatController.chat(prompt);

    assertThat(response).as(id).isNotBlank();

    verify(this.noteTool, shouldTrigger ? atLeastOnce() : never()).createNote(anyString(), anyString());
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;tool-unit-test&quot;&gt;Tool Unit Test&lt;/h3&gt;
&lt;p&gt;Next, I test the tool itself without the model. This is also a normal unit test.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@Test
void createNoteWritesOnlyNoteFileWithMarkdownHeading() throws IOException {
    var noteTool = new NoteTool(this.tempDir);

    var result = noteTool.createNote(&quot;Meeting Notes&quot;, &quot;Discuss the evaluation setup.&quot;);

    var notePath = this.tempDir.resolve(&quot;note.txt&quot;);
    var content = Files.readString(notePath);
    var lines = Files.readAllLines(notePath);

    try (var fileStream = Files.list(this.tempDir)) {
        var files = fileStream.toList();

        assertThat(result).isEqualTo(&quot;Created note.txt.&quot;);
        assertThat(notePath).exists();
        assertThat(content).isNotBlank();
        assertThat(lines.getFirst()).startsWith(&quot;#&quot;);
        assertThat(files).containsExactly(notePath);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separates responsibilities: if the unit test fails, the problem is in the tool implementation rather than the agent's behavior.&lt;/p&gt;
&lt;h3 id=&quot;rubric-based-assessment&quot;&gt;Rubric-Based Assessment&lt;/h3&gt;
&lt;p&gt;Deterministic checks are good at catching facts, but not everything can be checked this way. For example, &quot;the note is relevant,&quot; &quot;there is no extra information,&quot; and &quot;the text is easy to read&quot; are qualitative requirements. Rubric-based assessment is used for these. In Spring AI, this is conveniently implemented with &lt;code&gt;Evaluator&lt;/code&gt;: it receives an &lt;code&gt;EvaluationRequest&lt;/code&gt;, runs a grader prompt inside, and returns an &lt;code&gt;EvaluationResponse&lt;/code&gt; with &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, and &lt;code&gt;metadata&lt;/code&gt;. The grader model is fixed, ideally using a versioned model name. For example, &lt;code&gt;gpt-5.5-2026-04-23&lt;/code&gt; instead of &lt;code&gt;gpt-5.5&lt;/code&gt;. If the provider updates a model alias, the grader can become stricter or more lenient without any code change. In that case, a score change may be caused by grader drift rather than agent behavior. A low temperature is also preferable for grading. The grader should not be creative; it should apply the same rubric consistently. For rubric-based assessment, the grader runs multiple times. The pass rate better reflects its behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;class NoteStyleEvaluator implements Evaluator {

    private static final List&amp;lt;String&amp;gt; REQUIRED_CHECK_IDS =
            List.of(&quot;title&quot;, &quot;relevance&quot;, &quot;simplicity&quot;, &quot;readability&quot;);

    private final ChatClient chatClient;

    private final int minimumScore;

    NoteStyleEvaluator(ChatClient.Builder chatClientBuilder, int minimumScore) {
        this.chatClient = chatClientBuilder.build();
        this.minimumScore = minimumScore;
    }

    @Override
    public EvaluationResponse evaluate(EvaluationRequest evaluationRequest) {
        var rubricResult = gradeNote(evaluationRequest);

        if (rubricResult == null) {
            return new EvaluationResponse(false, 0.0f, &quot;No rubric result&quot;, Map.of());
        }

        var pass = rubricResult.overallPass()
                &amp;amp;&amp;amp; rubricResult.score() &amp;gt;= this.minimumScore
                &amp;amp;&amp;amp; hasRequiredPassingChecks(rubricResult.checks());

        var metadata = Map.&amp;lt;String, Object&amp;gt;of(
                &quot;overallPass&quot;, rubricResult.overallPass(),
                &quot;checks&quot;, rubricResult.checks());

        return new EvaluationResponse(pass, rubricResult.score(), rubricResult.toString(), metadata);
    }

    private StyleRubricResult gradeNote(EvaluationRequest evaluationRequest) {
        var graderPrompt = &quot;&quot;&quot;
            Read the original user request and the note.txt content,
            then grade the note against these requirements:

            - it has a clear title;
            - the content is relevant to the original user request;
            - there is no clearly unnecessary information;
            - the text is easy to read.

            Return only a structured JSON object with these fields:
            - overall_pass: boolean
            - score: integer from 0 to 100
            - checks: exactly four items with ids title, relevance, simplicity, readability.

            Each check must include id, pass, and notes.

            original user request:
            %s

            note.txt content:
            %s
            &quot;&quot;&quot;.formatted(
                evaluationRequest.getUserText(),
                evaluationRequest.getResponseContent()
            );

        return this.chatClient.prompt()
                .user(graderPrompt)
                .call()
                .entity(StyleRubricResult.class);
    }

    private boolean hasRequiredPassingChecks(List&amp;lt;StyleRubricCheck&amp;gt; checks) {
        if (checks == null || checks.size() != REQUIRED_CHECK_IDS.size()) {
            return false;
        }

        var checkIds = checks.stream()
                .map(StyleRubricCheck::id)
                .toList();

        return checkIds.containsAll(REQUIRED_CHECK_IDS)
                &amp;amp;&amp;amp; checks.stream().allMatch(check -&amp;gt;
                        check.pass() &amp;amp;&amp;amp; check.notes() != null &amp;amp;&amp;amp; !check.notes().isBlank());
    }

    record StyleRubricResult(
            @JsonProperty(&quot;overall_pass&quot;) boolean overallPass,
            int score,
            List&amp;lt;StyleRubricCheck&amp;gt; checks) {
    }

    record StyleRubricCheck(
            String id,
            boolean pass,
            String notes) {
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Structured output enables automated verification. With &lt;code&gt;Evaluation&lt;/code&gt; in Spring AI, grading becomes a normal Java contract:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EvaluationRequest&lt;/code&gt; stores the user prompt and generated output;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Evaluator&lt;/code&gt; contains the grading logic;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EvaluationResponse&lt;/code&gt; returns a machine-checkable result;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;metadata&lt;/code&gt; stores details for each rubric check.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This makes grading convenient to integrate into CI: there is &lt;code&gt;pass&lt;/code&gt;, &lt;code&gt;score&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, and a list of checks.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;@ParameterizedTest
@CsvSource(value = {
    &quot;weekly-plan-01|Create a new note about my plans for the week. Include a clear title and only a concise list of concrete weekly goals.|80&quot;,
    &quot;weekly-plan-02|Create a markdown note for my weekly planning. Mention only work, health, and learning goals. Do not add extra categories.|80&quot;
}, delimiter = '|')
void gradesCreatedNoteWithStyleRubric(String id, String prompt, int minimumScore) throws IOException {
    var agentResponse = this.chatController.chat(prompt);

    assertThat(agentResponse).as(id).isNotBlank();
    assertThat(this.notePath).as(id).exists();

    var noteContent = Files.readString(this.notePath);

    var evaluator = new NoteStyleEvaluator(this.chatClientBuilder, minimumScore);

    int graderRuns = 3;
    int requiredPassingRuns = 2;

    var results = IntStream.range(0, graderRuns)
            .mapToObj(run -&amp;gt; evaluator.evaluate(new EvaluationRequest(prompt, noteContent)))
            .toList();

    long passingRuns = results.stream()
            .filter(EvaluationResponse::isPass)
            .count();

    assertThat(passingRuns)
            .as(id + &quot;: &quot; + results)
            .isGreaterThanOrEqualTo(requiredPassingRuns);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The agent runs once and creates one note. The grader then evaluates the same &lt;code&gt;note.txt&lt;/code&gt; content three times; the test passes if at least two evaluations pass. This approach makes the grader result more robust against random model variation.&lt;/p&gt;
&lt;h3 id=&quot;saving-results-between-versions&quot;&gt;Saving Results Between Versions&lt;/h3&gt;
&lt;p&gt;It is also advisable to save eval results between runs. This helps compare behavior after changes to the prompt, model, or rubric. Pass/fail status is not always enough. A test can still pass while the score gradually gets worse. That is why I keep the score, feedback, and rubric metadata as artifacts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-java&quot;&gt;var resultPath = Path.of(&quot;build&quot;, &quot;eval-results&quot;, id + &quot;.json&quot;);

var resultJson = objectMapper.writeValueAsString(Map.of(
        &quot;id&quot;, id,
        &quot;prompt&quot;, prompt,
        &quot;minimumScore&quot;, minimumScore,
        &quot;graderRuns&quot;, graderRuns,
        &quot;requiredPassingRuns&quot;, requiredPassingRuns,
        &quot;passingRuns&quot;, passingRuns,
        &quot;results&quot;, results
));

Files.createDirectories(resultPath.getParent());
Files.writeString(resultPath, resultJson);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;separating-evals-from-regular-tests&quot;&gt;Separating Evals from Regular Tests&lt;/h3&gt;
&lt;p&gt;It is a good idea to keep AI evals separate from regular tests. They call the model API, so they are slower, more expensive, and less deterministic.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gradle&quot;&gt;tasks.named('test') {
    useJUnitPlatform {
        excludeTags 'eval'
    }
}

tasks.register('evalTest', Test) {
    description = 'Runs AI agent evaluation tests.'
    group = 'verification'

    useJUnitPlatform {
        includeTags 'eval'
    }

    shouldRunAfter test
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run them separately:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;./gradlew test
./gradlew evalTest
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;what-this-gives&quot;&gt;What This Gives&lt;/h3&gt;
&lt;p&gt;This makes development more predictable and manageable. When the system prompt, the tool description or implementation, or model parameters change, evaluating agent behavior remains much less subjective. CI tracks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;whether the number of false positives increased;&lt;/li&gt;
&lt;li&gt;whether false negatives appeared;&lt;/li&gt;
&lt;li&gt;whether the expected artifact was created;&lt;/li&gt;
&lt;li&gt;whether the output passes the rubric assessment;&lt;/li&gt;
&lt;li&gt;whether quality dropped below the minimum score.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The most useful part is that the eval suite can grow gradually. Found a new miss? Add a new eval case. The agent failed on a real user prompt? That prompt becomes a regression test. This way, behavior drift becomes visible instead of staying hidden in manual testing.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28397/spring-ai-evals-how-i-test-agent-behavior</guid>
<pubDate>Wed, 23 Sep 2026 21:55:02 +0000</pubDate>
</item>
<item>
<title>TDD in the Age of AI: How Evals Can Benefit from Test-Driven Development</title>
<link>https://coderlegion.com/11288/tdd-in-the-age-of-ai-how-evals-can-benefit-from-test-driven-development</link>
<description>
&lt;p&gt;Test-Driven Development (TDD) has been a cornerstone of quality software engineering for decades. But as we enter the age of AI-assisted development and large language models, does TDD still hold relevance? The answer is a resounding yes&amp;mdash;and perhaps more than ever before.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;The Timeless Principles of TDD&lt;/h2&gt;



&lt;p&gt;At its core, TDD follows a simple cycle: write a failing test, write the minimum code to pass, then refactor. This red-green-refactor loop forces developers to think about requirements and edge cases before implementation. The result? Cleaner code, better design, and a comprehensive test suite that serves as living documentation.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;TDD Meets AI: A Natural Partnership&lt;/h2&gt;



&lt;p&gt;When working with AI coding assistants, TDD becomes even more valuable. Here&amp;rsquo;s why:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Clear specifications&lt;/strong&gt;: Tests provide unambiguous requirements that AI can understand and implement against&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verification layer&lt;/strong&gt;: Tests act as a safety net to validate AI-generated code behaves correctly&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced hallucinations&lt;/strong&gt;: When AI knows it must pass specific tests, outputs tend to be more grounded and practical&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Iterative refinement&lt;/strong&gt;: The red-green-refactor cycle works naturally with AI iteration&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Evals: TDD for AI Systems&lt;/h2&gt;



&lt;p&gt;Perhaps the most exciting application of TDD principles is in the world of AI evaluations&amp;mdash;commonly called &amp;ldquo;evals.&amp;rdquo; Evals are systematic tests designed to measure how well an AI model performs on specific tasks or criteria.&lt;/p&gt;



&lt;p&gt;The parallel to TDD is striking:&lt;/p&gt;



&lt;figure class=&quot;wp-block-table&quot;&gt;&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;TDD Concept&lt;/th&gt;&lt;th&gt;Eval Equivalent&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;Write failing test first&lt;/td&gt;&lt;td&gt;Define expected behavior before training/prompting&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Minimum code to pass&lt;/td&gt;&lt;td&gt;Iterate on prompts or fine-tuning until eval passes&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Refactor&lt;/td&gt;&lt;td&gt;Optimize prompts while maintaining eval scores&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;Test suite&lt;/td&gt;&lt;td&gt;Eval suite covering multiple capabilities&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;/figure&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Applying TDD Mindset to Evals&lt;/h2&gt;



&lt;p&gt;Here&amp;rsquo;s how you can apply TDD thinking to your AI evaluation strategy:&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;1. Define Success Criteria First&lt;/h3&gt;



&lt;p&gt;Before crafting prompts or fine-tuning models, clearly define what success looks like. What inputs should produce what outputs? What edge cases matter? This is your &amp;ldquo;test specification.&amp;rdquo;&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;2. Create Measurable Evals&lt;/h3&gt;



&lt;p&gt;Write evals that can objectively determine pass/fail. This might include exact match comparisons, semantic similarity thresholds, or structured output validation. Avoid vague criteria that require subjective judgment.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;3. Start with Failing Evals&lt;/h3&gt;



&lt;p&gt;Just like TDD, begin with evals that your current system fails. This ensures you&amp;rsquo;re building toward measurable improvement rather than just validating the status quo.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;4. Iterate Incrementally&lt;/h3&gt;



&lt;p&gt;Make small changes to prompts or model configurations, run your eval suite, and observe the impact. Large changes make it difficult to understand what&amp;rsquo;s working.&lt;/p&gt;



&lt;h3 class=&quot;wp-block-heading&quot;&gt;5. Prevent Regression&lt;/h3&gt;



&lt;p&gt;Maintain a comprehensive eval suite that runs with every change. Just like unit tests catch regressions in code, evals catch regressions in AI behavior.&lt;/p&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Practical Benefits&lt;/h2&gt;



&lt;p&gt;Teams adopting eval-driven development report several benefits:&lt;/p&gt;



&lt;ul class=&quot;wp-block-list&quot;&gt;
&lt;li&gt;&lt;strong&gt;Faster iteration&lt;/strong&gt;: Clear pass/fail criteria eliminate guesswork&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Better communication&lt;/strong&gt;: Evals serve as a shared language between engineers, product managers, and stakeholders&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence in deployment&lt;/strong&gt;: A passing eval suite provides assurance before shipping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Documentation&lt;/strong&gt;: Evals document expected behavior in an executable form&lt;/li&gt;
&lt;/ul&gt;



&lt;h2 class=&quot;wp-block-heading&quot;&gt;Conclusion&lt;/h2&gt;



&lt;p&gt;TDD isn&amp;rsquo;t just surviving the AI age&amp;mdash;it&amp;rsquo;s thriving. The discipline of writing tests first translates beautifully to the world of AI evals, where defining expected behavior upfront is crucial for building reliable systems.&lt;/p&gt;



&lt;p&gt;Whether you&amp;rsquo;re writing traditional software, working with AI coding assistants, or building AI-powered products, the TDD mindset remains invaluable: specify behavior first, verify it works, then iterate with confidence.&lt;/p&gt;



&lt;p&gt;The age of AI doesn&amp;rsquo;t diminish the importance of testing&amp;mdash;it amplifies it.&lt;/p&gt;
&lt;p&gt;The post &lt;a rel=&quot;nofollow&quot; href=&quot;https://sudoish.com/tdd-in-the-age-of-ai-how-evals-can-benefit-from-test-driven-development/&quot;&gt;TDD in the Age of AI: How Evals Can Benefit from Test-Driven Development&lt;/a&gt; appeared first on &lt;a rel=&quot;nofollow&quot; href=&quot;https://sudoish.com&quot;&gt;sudoish&lt;/a&gt;.&lt;/p&gt;

</description>
<category>Other</category>
<guid isPermaLink="true">https://coderlegion.com/11288/tdd-in-the-age-of-ai-how-evals-can-benefit-from-test-driven-development</guid>
<pubDate>Mon, 09 Feb 2026 04:22:29 +0000</pubDate>
</item>
</channel>
</rss>