<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Richard Atkins</title>
    <description>The latest articles on DEV Community by Richard Atkins (@groundedarchitect).</description>
    <link>https://dev.to/groundedarchitect</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4030583%2F69419a09-9c69-4001-af0a-210d34675a16.jpg</url>
      <title>DEV Community: Richard Atkins</title>
      <link>https://dev.to/groundedarchitect</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/groundedarchitect"/>
    <language>en</language>
    <item>
      <title>The real economics of a production LLM pipeline: resumability, cost-aware routing, and measuring when local beats the API</title>
      <dc:creator>Richard Atkins</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:11:28 +0000</pubDate>
      <link>https://dev.to/groundedarchitect/the-real-economics-of-a-production-llm-pipeline-resumability-cost-aware-routing-and-measuring-1ncf</link>
      <guid>https://dev.to/groundedarchitect/the-real-economics-of-a-production-llm-pipeline-resumability-cost-aware-routing-and-measuring-1ncf</guid>
      <description>&lt;h2&gt;
  
  
  The gif, and the honest headline
&lt;/h2&gt;

&lt;p&gt;Here's a multi-stage LLM pipeline being killed mid-run and started again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwhu9fr38k66e7vwspkg.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffwhu9fr38k66e7vwspkg.gif" alt=" " width="790" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It picks up exactly where it stopped. No duplicate model calls, no re-spend. That's the engineering payoff. The headline I want to be honest about up front is the &lt;em&gt;economics&lt;/em&gt; one, because it runs against the usual pitch:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is not "run it locally and save a fortune." At a real content pipeline's scale, batched cloud inference is genuinely cheap. The valuable thing is a reproducible way to &lt;em&gt;measure&lt;/em&gt; the local-vs-cloud crossover, and the answer of when local does and doesn't pay off.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's the real number, from an instrumented production run: &lt;strong&gt;one weekly run of this pipeline moves 641,671 input and 77,409 output tokens across nine local model stages, plus 134 locally-generated images. Run it on batched cloud models instead and that's about $0.88, under a dollar a week.&lt;/strong&gt; So the interesting question was never "how much does local save." At this scale, cloud is already pocket change. It's &lt;em&gt;when&lt;/em&gt; that stops being true, and how you would know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pains everyone hits on week two
&lt;/h2&gt;

&lt;p&gt;The first version of any LLM pipeline is a &lt;code&gt;for&lt;/code&gt; loop over a list. It works in the demo. Then production happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;dies at item 800 of 1000&lt;/strong&gt; and you do not want to pay for the first 800 again.&lt;/li&gt;
&lt;li&gt;The same run, re-triggered, &lt;strong&gt;re-does work&lt;/strong&gt; and double-writes downstream.&lt;/li&gt;
&lt;li&gt;Near-duplicate inputs get processed &lt;strong&gt;twice&lt;/strong&gt;, because nothing deduplicates them.&lt;/li&gt;
&lt;li&gt;You have &lt;strong&gt;no idea what it cost&lt;/strong&gt;, because nobody recorded the tokens.&lt;/li&gt;
&lt;li&gt;A rate limit or a credit ceiling hits and the run &lt;strong&gt;dies dirty&lt;/strong&gt;, mid-item.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is hard on its own. Together they're the difference between a script and a pipeline. &lt;a href="https://github.com/uxdw/resumable-llm-pipeline" rel="noopener noreferrer"&gt;&lt;code&gt;resumable-llm-pipeline&lt;/code&gt;&lt;/a&gt; is the small, framework-light reference that wires them together, extracted (patterns only) from a real weekly content pipeline at Cedar &amp;amp; Bloom. It runs offline on the stdlib alone via a deterministic &lt;code&gt;mock&lt;/code&gt; router, so you can see all of it in five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2knj92wgd8oy3r7rn4mh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2knj92wgd8oy3r7rn4mh.png" alt=" " width="796" height="1269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two pieces carry the weight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ProgressStore&lt;/code&gt;, for resumability.&lt;/strong&gt; A crash-safe &lt;code&gt;{item_id -&amp;gt; {status, ...}}&lt;/code&gt; written with a temp-file-and-rename atomic swap after every item. Each stage filters to unfinished items and reads the previous stage's output. That decoupling &lt;em&gt;is&lt;/em&gt; the resumability: kill the process anywhere and re-running recomputes nothing that already finished. The test suite proves it in numbers. Across a killed run and its resume, total model calls equal the number of kept items exactly. No item is processed twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ModelRouter&lt;/code&gt;, for cost-aware routing.&lt;/strong&gt; &lt;code&gt;complete(prompt, tier)&lt;/code&gt; and &lt;code&gt;embed(text)&lt;/code&gt; behind one uniform response that carries token counts. Two tiers (&lt;code&gt;cheap&lt;/code&gt; and &lt;code&gt;quality&lt;/code&gt;) map to a backend (&lt;code&gt;mock&lt;/code&gt;, local &lt;code&gt;ollama&lt;/code&gt;, or &lt;code&gt;hosted&lt;/code&gt;) by &lt;strong&gt;one config line&lt;/strong&gt;. Because both local and hosted return &lt;code&gt;(text, tokens_in, tokens_out)&lt;/code&gt;, cost and metrics are backend-agnostic, and swapping local for hosted to re-run the benchmark is a one-liner. A &lt;code&gt;Budget(max_tokens, max_cost)&lt;/code&gt; guard checks the paid stage as it runs; on breach it checkpoints and exits cleanly. Resume &lt;em&gt;is&lt;/em&gt; the retry.&lt;/p&gt;

&lt;p&gt;Routing between a cheap and an expensive model is not a new idea.&lt;br&gt;
&lt;a href="https://lmsys.org/blog/2024-07-01-routellm/" rel="noopener noreferrer"&gt;RouteLLM&lt;/a&gt;&lt;br&gt;
(&lt;a href="https://arxiv.org/abs/2406.18665" rel="noopener noreferrer"&gt;paper&lt;/a&gt;) showed a learned router can cut cost by around 85% on some benchmarks while holding quality. This router is the boring, explicit version: you assign each stage a tier by hand. The goal is not to be clever, it is to make the cost of that choice measurable.&lt;/p&gt;
&lt;h2&gt;
  
  
  The measurement: the tokens were being thrown away
&lt;/h2&gt;

&lt;p&gt;Here's the detail that started this. The production pipeline it generalises calls local models via Ollama, and Ollama's response includes &lt;a href="https://docs.ollama.com/api/usage" rel="noopener noreferrer"&gt;&lt;code&gt;prompt_eval_count&lt;/code&gt; and &lt;code&gt;eval_count&lt;/code&gt;&lt;/a&gt;, the exact input and output token counts, in the final chunk. The original code threw them away. It did real token work every week and recorded none of it.&lt;/p&gt;

&lt;p&gt;So the router captures those counts on every call (and &lt;code&gt;usage.input_tokens&lt;/code&gt; / &lt;code&gt;usage.output_tokens&lt;/code&gt; on the hosted path), and a &lt;strong&gt;fail-open&lt;/strong&gt; metrics module writes one JSON line per call: tokens, latency, backend, model. Fail-open matters. The thing that measures the pipeline must never be the thing that breaks it. &lt;code&gt;rlp report&lt;/code&gt; rolls it up.&lt;/p&gt;

&lt;p&gt;Here's the actual per-stage scorecard from one instrumented &lt;strong&gt;weekly&lt;/strong&gt; run (&lt;code&gt;qwen2.5:14b&lt;/code&gt; is the cheap tier, &lt;code&gt;qwen2.5:72b&lt;/code&gt; the quality tier):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stage          model            calls   tok_in   tok_out
novelty        qwen2.5:14b        273   417647    12860
rewrite        qwen2.5:72b         67   112516    48673
image_prompt   qwen2.5:14b         67    52193    11094
enrich         qwen2.5:14b         67    43759     3289
cluster        qwen2.5:14b          4    10264      700
timelines      qwen2.5:14b         10     2568       60
companies      qwen2.5:72b         20     1445       661
edition_intro  qwen2.5:14b          1     1279        72
image_gen      FLUX.1-schnell     134        0        0   (images — timing only)
--------------------------------------------------------------
TOTAL                                    641671    77409
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Until this run, every one of those token counts was &lt;code&gt;None&lt;/code&gt;. The pipeline did the work weekly and recorded nothing. Notice &lt;code&gt;novelty&lt;/code&gt; on its own: 273 calls, 417k input tokens, 65% of the entire run's input. Hold that thought, it matters for the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  The benchmark: measure the crossover, don't guess it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;rlp benchmark&lt;/code&gt; reads the tokens the run actually recorded, maps each tier to its hosted equivalent, projects to an annual workload, and compares hosted cost (list price, and with the &lt;a href="https://platform.claude.com/docs/en/build-with-claude/batch-processing" rel="noopener noreferrer"&gt;Batch API's&lt;/a&gt; 50% discount) against a fixed local cost. Fed the &lt;strong&gt;real weekly run&lt;/strong&gt;, with the cheap tier priced as &lt;code&gt;claude-haiku-4-5&lt;/code&gt; ($1/$5 per 1M) and the quality tier as &lt;code&gt;claude-sonnet-5&lt;/code&gt; ($3/$15 per 1M):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Measured (weekly run): 641,671 input + 77,409 output tokens across 9 stages + 134 images.
Modeled:  hosted prices as above, Batch 50% off, hosted image ~$0.003/img, local $560/yr amortised (hardware capex + power), 52 weekly runs/yr.

| line                         | per run | per year (x52) |
|------------------------------|---------|----------------|
| hosted text (list)           | $1.75   | $91            |
| hosted text (batch, 50% off) | $0.88   | $46            |
| hosted images (modeled)      | $0.40   | $21            |
| local (fixed, amortised)     | —       | $560           |

Break-even is about 640 weekly runs per year, roughly 12x the real cadence. Below that, batched cloud wins on cash.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the monthly evergreen run (measured: 47,596 in / 33,920 out, so &lt;strong&gt;$0.47 a run at list, $0.24 batched&lt;/strong&gt;, roughly $3 to $6 a year) and the whole Cedar &amp;amp; Bloom LLM workload lands around &lt;strong&gt;$50 to $70 a year on batched cloud&lt;/strong&gt; (about $110 at list). Against a $560/yr amortised local box, cloud is 8 to 12 times cheaper on cash at the real volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The discipline that makes this trustworthy, and the whole reason the tool exists:&lt;/strong&gt; token counts are &lt;strong&gt;measured&lt;/strong&gt;; hosted prices, the per-image cloud rate, and the local yearly cost are &lt;strong&gt;modeled&lt;/strong&gt; and labelled. "The hardware's a sunk cost" is not a fair comparison. An honest break-even needs the amortised capex line, which is why it's in there.&lt;/p&gt;

&lt;h3&gt;
  
  
  The counter-intuitive bit: the cost isn't where the tokens are
&lt;/h3&gt;

&lt;p&gt;Look again at that weekly scorecard through a cost lens:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stage&lt;/th&gt;
&lt;th&gt;share of input tokens&lt;/th&gt;
&lt;th&gt;share of hosted cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;novelty&lt;/code&gt; (cheap tier)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rewrite&lt;/code&gt; (quality tier)&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~61%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;novelty&lt;/code&gt; moves two-thirds of the tokens but stays cheap, because it runs on the cheap model and emits almost nothing. &lt;code&gt;rewrite&lt;/code&gt; is the reverse: modest token volume, but its output tokens on the quality model at $15/1M dominate the bill. &lt;strong&gt;The money is in output tokens on the quality tier, not where the token volume is.&lt;/strong&gt; Knuth's old warning that "premature optimization is the root of all evil" has a neat LLM corollary: don't guess which stage is expensive. The tokens pile up in &lt;code&gt;novelty&lt;/code&gt;; the money leaves through &lt;code&gt;rewrite&lt;/code&gt;. You only see that by measuring, and it's what tells you which stage to move to cloud first if you ever wanted a hybrid.&lt;/p&gt;

&lt;h3&gt;
  
  
  One genuinely measured cloud data point
&lt;/h3&gt;

&lt;p&gt;Everything above prices local tokens &lt;em&gt;as if&lt;/em&gt; they'd run on the cloud. But one stage in the wider Cedar &amp;amp; Bloom stack already is hosted: SteadyPath's quiz runs on Anthropic Haiku, and instrumenting it captured &lt;strong&gt;4 real calls, 1,490 input and 1,861 output tokens&lt;/strong&gt;, actual &lt;code&gt;usage&lt;/code&gt; from the API, not a projection. Same metrics module, same JSONL, local and hosted side by side. That's the point of the backend-agnostic router. The moment you move a stage to the cloud, its real cost lands in the same ledger as the local estimate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest conclusion the data supports:&lt;/strong&gt; at Cedar &amp;amp; Bloom's real scale, batched cloud would cost tens of dollars a year, and a weekly run is under a dollar. Local's edge is &lt;em&gt;control, privacy, and no rate limits&lt;/em&gt;, and it only wins on &lt;strong&gt;cash&lt;/strong&gt; at roughly 10 times the volume, or once you bolt on a token-hungry agentic layer. The number that tells you which regime you're in is one you can now measure instead of argue about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/uxdw/resumable-llm-pipeline &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;resumable-llm-pipeline
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
rlp reset &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rlp run &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; rlp run       &lt;span class="c"&gt;# watch the 2nd run resume with 0 model calls&lt;/span&gt;
rlp benchmark                          &lt;span class="c"&gt;# your own local-vs-cloud crossover&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point the router at your models, feed the benchmark your real per-stage tokens and verified prices, and read your own break-even. &lt;strong&gt;Measure your crossover. Don't inherit someone else's headline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Companion piece: &lt;a href="https://dev.to/groundedarchitect/stop-shipping-ai-agents-you-cant-measure-evals-observability-from-scratch-35f5"&gt;Stop shipping AI Agents you can't measure: evals observability from scratch&lt;/a&gt;, on evals and observability as a CI gate. This pipeline's metrics core is the same telemetry idea, pointed at cost.)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built and measured at Cedar &amp;amp; Bloom. MIT-licensed. Written by Richard Atkins.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>llm</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop shipping AI agents you can't measure: evals + observability from scratch</title>
      <dc:creator>Richard Atkins</dc:creator>
      <pubDate>Sat, 18 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/groundedarchitect/stop-shipping-ai-agents-you-cant-measure-evals-observability-from-scratch-35f5</link>
      <guid>https://dev.to/groundedarchitect/stop-shipping-ai-agents-you-cant-measure-evals-observability-from-scratch-35f5</guid>
      <description>&lt;h2&gt;
  
  
  The demo, in one screen
&lt;/h2&gt;

&lt;p&gt;Here's an agent's eval scorecard on a green build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metric                  rate  baseline    delta
-----------------------------------------------
task_success          95.00%    95.00%   +0.00%
tool_correctness     100.00%   100.00%   +0.00%
schema_validity      100.00%   100.00%   +0.00%
groundedness         100.00%   100.00%   +0.00%
PASS: no regressions vs baseline.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I make one change. I stop the agent from actually &lt;em&gt;reading&lt;/em&gt; the documents it cites, so it answers from search snippets alone. That's one environment variable, &lt;code&gt;AES_DISABLE_FETCH=1&lt;/code&gt;. Re-run the exact same eval:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metric                  rate  baseline    delta
-----------------------------------------------
task_success          75.00%    95.00%  -20.00%  &amp;lt;-- REGRESSION
tool_correctness      15.00%   100.00%  -85.00%  &amp;lt;-- REGRESSION
schema_validity      100.00%   100.00%   +0.00%
groundedness          15.00%   100.00%  -85.00%  &amp;lt;-- REGRESSION
FAIL: 3 metric(s) regressed vs baseline: task_success, tool_correctness, groundedness
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command exits non-zero. &lt;strong&gt;CI goes red, and the PR is blocked.&lt;/strong&gt; One behavioural regression, caught by three independent metrics, before it ever reached a user.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00ittl6vb0j2c5ru0z91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F00ittl6vb0j2c5ru0z91.png" alt=" " width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would your agent have caught that? For most teams I talk to, the honest answer is no. Not because they don't care, but because the tooling that makes it easy is all paid SaaS, and the DIY version never quite becomes a priority. This post is that DIY version, built once so you can copy it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Everyone can &lt;em&gt;build&lt;/em&gt; an agent now. Almost no one can &lt;em&gt;prove&lt;/em&gt; it still works after the next prompt tweak, model bump, or refactor. The tools that help (Braintrust, LangSmith, DeepEval) are mostly paid platforms you bolt on later. For the deep version of &lt;em&gt;why&lt;/em&gt; evals matter and how to design domain-specific ones, Hamel Husain's &lt;a href="https://hamel.dev/blog/posts/evals/" rel="noopener noreferrer"&gt;Your AI Product Needs Evals&lt;/a&gt; is the canonical read. This post is the smallest running skeleton of that idea, wired into CI.&lt;/p&gt;

&lt;p&gt;There's no clean, vendor-neutral, framework-light reference that shows the whole loop: a working agent, traced, evaluated, and guardrailed, with &lt;strong&gt;evals running as CI&lt;/strong&gt; so a regression fails the build like a broken unit test. So I built one. The repo, &lt;a href="https://github.com/uxdw/agent-eval-starter" rel="noopener noreferrer"&gt;&lt;code&gt;agent-eval-starter&lt;/code&gt;&lt;/a&gt;, runs end-to-end with zero API keys and zero network on a deterministic &lt;code&gt;mock&lt;/code&gt; provider, so you can clone it and see all of the above in under five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;Four pieces, each deliberately boring:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fllldcn1vkckry86mos.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7fllldcn1vkckry86mos.png" alt=" " width="799" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A real agent loop&lt;/strong&gt; using a &lt;strong&gt;JSON-action protocol&lt;/strong&gt; rather than a vendor's native tool-calling. The agent emits &lt;code&gt;{"action": "...", ...}&lt;/code&gt; and the harness parses it. That keeps it portable across providers and trivially mockable, with no framework lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry tracing.&lt;/strong&gt; The same spans that draw the trace tree roll up into a cost, latency, and token scorecard, and export to any OTel backend. It isn't a bespoke tracer: the &lt;a href="https://opentelemetry.io/blog/2024/otel-generative-ai/" rel="noopener noreferrer"&gt;OpenTelemetry GenAI semantic conventions&lt;/a&gt; are standardising exactly this shape of LLM span, so you're not locked into my format.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An eval harness&lt;/strong&gt; that scores a committed, labelled dataset on four pass-rate metrics and compares against a committed &lt;code&gt;baseline.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails:&lt;/strong&gt; a deterministic banned-phrase post-filter, plus an optional LLM-as-verifier review pass.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The four metrics, each a pass-rate over the dataset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;task_success:&lt;/strong&gt; did it answer correctly, or correctly &lt;em&gt;abstain&lt;/em&gt; on the unanswerable cases? (Abstention counts as a correct answer.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;tool_correctness:&lt;/strong&gt; did it use tools honestly, citing only documents it actually fetched?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schema_validity:&lt;/strong&gt; is the final answer well-formed? (pydantic-validated)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;groundedness:&lt;/strong&gt; the hallucination guard. Is every claim backed by fetched text?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The insight worth stealing
&lt;/h2&gt;

&lt;p&gt;The single most useful thing I've learned shipping this stuff:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Prompt-level guardrails ("never say X", "always cite your sources") are unreliable. A deterministic post-filter is the dependable fallback, and an LLM-as-verifier is the second layer, not the first.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The model will, eventually, ignore the instruction. So the banned-phrase check runs in code, after generation, every time. It cannot be talked out of it. The LLM-as-verifier pass then catches the fuzzier failures a deterministic filter can't express. Two layers,&lt;br&gt;
cheap one first. It's why the demo works: &lt;code&gt;groundedness&lt;/code&gt; isn't a line in the system prompt asking nicely, it's a computed check over what the agent actually fetched.&lt;/p&gt;

&lt;h2&gt;
  
  
  The numbers (and their caveats)
&lt;/h2&gt;

&lt;p&gt;Everything above is measured on the repo's committed &lt;strong&gt;synthetic 20-case suite&lt;/strong&gt;, not a production workload. I'm flagging that on purpose. The credibility of an eval story is the honesty about what got measured.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Baseline pass-rates: task_success &lt;strong&gt;95%&lt;/strong&gt;, tool_correctness &lt;strong&gt;100%&lt;/strong&gt;, schema_validity &lt;strong&gt;100%&lt;/strong&gt;, groundedness &lt;strong&gt;100%&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;One seeded regression (disabling document fetch) drops &lt;strong&gt;3 of 4 metrics&lt;/strong&gt;, the suite exits non-zero, and CI posts the scorecard on the PR.&lt;/li&gt;
&lt;li&gt;Cost and latency per run: about &lt;strong&gt;$0 on the &lt;code&gt;mock&lt;/code&gt; provider&lt;/strong&gt; (no network). Point it at &lt;code&gt;claude&lt;/code&gt; or &lt;code&gt;ollama&lt;/code&gt; and the OpenTelemetry roll-up reports tokens, cost, and p95 latency from real calls, so accuracy and spend are tracked over time from the same spans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this proves the agent is &lt;em&gt;correct&lt;/em&gt;. As Edsger Dijkstra put it, testing "can be used to show the presence of bugs, but never to show their absence." What evals give you is the other thing production needs: a tripwire. The moment a change makes the agent measurably worse, the build stops.&lt;/p&gt;

&lt;p&gt;So the point isn't "95% is good." It's that the 95% is a number you can defend, version, and regress against. A gate, not a vibe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Take the pattern
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/uxdw/agent-eval-starter &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;agent-eval-starter
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;".[dev]"&lt;/span&gt;
agent-eval &lt;span class="nb"&gt;eval&lt;/span&gt;          &lt;span class="c"&gt;# score the suite -&amp;gt; scorecard&lt;/span&gt;
make demo                &lt;span class="c"&gt;# green, then a seeded regression turns it red&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Swap the corpus and dataset for your domain, point the provider at your model, and commit a new baseline. The pattern is the product; the example is just a runnable seed. &lt;strong&gt;Make your agent's accuracy a CI gate.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Companion piece: &lt;a href="https://dev.to/groundedarchitect/the-real-economics-of-a-production-llm-pipeline-resumability-cost-aware-routing-and-measuring-1ncf"&gt;The real economics of a Production LLM&lt;br&gt;
pipeline - resumability cost aware routing &amp;amp; measuring&lt;/a&gt;, on resumability, cost-aware routing, and measuring when local beats the API.)&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built and measured at Cedar &amp;amp; Bloom. MIT-licensed. Written by Richard Atkins.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>testing</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
