<?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: Mitar</title>
    <description>The latest articles on DEV Community by Mitar (@mitar_nik).</description>
    <link>https://dev.to/mitar_nik</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%2F3084356%2F94a90e85-62f7-4a34-93b1-74c5fb04b128.jpg</url>
      <title>DEV Community: Mitar</title>
      <link>https://dev.to/mitar_nik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mitar_nik"/>
    <language>en</language>
    <item>
      <title>Choosing the right AI model for your app: measure task success, then compare cost</title>
      <dc:creator>Mitar</dc:creator>
      <pubDate>Thu, 29 Oct 2026 14:54:39 +0000</pubDate>
      <link>https://dev.to/mitar_nik/choosing-the-right-ai-model-for-your-app-measure-task-success-then-compare-cost-5gcc</link>
      <guid>https://dev.to/mitar_nik/choosing-the-right-ai-model-for-your-app-measure-task-success-then-compare-cost-5gcc</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; The evaluation approach in this post applies to any programming language or application stack. The code examples use C# and .NET because that's what I'm most familiar with.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Say you want to add an AI chat to a website. Users should be able to ask questions about their documents, look up information, and generate downloadable files.&lt;/p&gt;

&lt;p&gt;The model with the lowest token price is an obvious place to start. Until it tells a user that a report is ready, and the report contains the wrong numbers.&lt;/p&gt;

&lt;p&gt;Consider this request:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find my Q1 expenses in connected storage. Create a downloadable JSON report with one row per month, in USD, and a total. Keep your reply to one sentence and include the download link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Two models return exactly the same sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your Q1 expense report is ready: &lt;code&gt;[download](./report.json)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both files open. Both contain the right monthly amounts. One has a total of USD 360. The other has a total of USD 370.&lt;/p&gt;

&lt;p&gt;Comparing the chat messages would give these models a tie. Inspecting the files gives us a failure we can actually act on.&lt;/p&gt;

&lt;p&gt;That's the starting point for choosing a model: define what a completed task looks like, measure how often the application achieves it, and compare the cost of doing so.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use public benchmarks to build a shortlist
&lt;/h2&gt;

&lt;p&gt;Public benchmarks are useful when deciding which models deserve a trial. They're less useful as the final acceptance test for a feature.&lt;/p&gt;

&lt;p&gt;A general knowledge score doesn't tell me whether a model can select the right storage tool, recover from an expired link, and produce a correct spreadsheet through my application's tool loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://arxiv.org/abs/2211.09110" rel="noopener noreferrer"&gt;Stanford's HELM research&lt;/a&gt; takes a multi-scenario, multi-metric approach to model evaluation. That's a useful principle to borrow: look at several capabilities and their trade-offs, rather than treating one score as a complete description of a model.&lt;/p&gt;

&lt;p&gt;For this website, I'd shortlist a few candidates with the capabilities I need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tool calling supported by the provider and SDK I use.&lt;/li&gt;
&lt;li&gt;Enough context for realistic conversations and retrieved documents.&lt;/li&gt;
&lt;li&gt;The required input modalities, if users upload images or scanned PDFs.&lt;/li&gt;
&lt;li&gt;A deployment option that meets the project's operational requirements.&lt;/li&gt;
&lt;li&gt;Plausible pricing and latency for the expected traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then I'd run those candidates through the website's actual chat workflow.&lt;/p&gt;

&lt;p&gt;The thing under evaluation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;model + instructions + tool definitions + retrieval + application code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model doesn't inherently have access to my users' storage. My application provides that access through tools. Likewise, file generation might mean calling a deterministic report builder, executing code in a sandbox, or returning structured data that my backend renders into a file. Those are different implementations, with different failure modes and costs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents" rel="noopener noreferrer"&gt;Anthropic's guide to agent evaluations&lt;/a&gt; makes this distinction explicit: an agent evaluation measures the model and its surrounding harness working together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn feature requirements into cases you can grade
&lt;/h2&gt;

&lt;p&gt;For the expense example, the fixture contains three monthly amounts: January USD 120, February USD 80, and March USD 160. The expected total is USD 360.&lt;/p&gt;

&lt;p&gt;I would define success before running any candidate:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The application reads the correct user's expense document.&lt;/li&gt;
&lt;li&gt;A downloadable file exists and the current user can retrieve it.&lt;/li&gt;
&lt;li&gt;The JSON contains exactly the expected months and amounts, the correct currency, and the correct total.&lt;/li&gt;
&lt;li&gt;The reply contains one sentence and a working link to that artifact.&lt;/li&gt;
&lt;li&gt;The workflow finishes within the agreed time and tool-call budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The expected amounts belong in the evaluation fixture. They should not be included in the candidate's prompt as an answer key.&lt;/p&gt;

&lt;p&gt;Now expand that into a small dataset:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Case&lt;/th&gt;
&lt;th&gt;Input or environment&lt;/th&gt;
&lt;th&gt;Expected outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Normal export&lt;/td&gt;
&lt;td&gt;One matching expense document&lt;/td&gt;
&lt;td&gt;Correct, downloadable report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ambiguous document&lt;/td&gt;
&lt;td&gt;Two plausible Q1 files&lt;/td&gt;
&lt;td&gt;Ask which file to use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing quarter&lt;/td&gt;
&lt;td&gt;Storage only contains Q2&lt;/td&gt;
&lt;td&gt;Explain that Q1 wasn't found; don't invent it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage failure&lt;/td&gt;
&lt;td&gt;Read tool returns a timeout&lt;/td&gt;
&lt;td&gt;Report the failure; don't claim an export succeeded&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conversation update&lt;/td&gt;
&lt;td&gt;User requests a different output currency&lt;/td&gt;
&lt;td&gt;Follow the defined currency-conversion policy, or ask for the missing rate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web lookup&lt;/td&gt;
&lt;td&gt;User asks for today's published exchange rate&lt;/td&gt;
&lt;td&gt;Retrieve an authoritative source and cite supporting evidence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No lookup needed&lt;/td&gt;
&lt;td&gt;User asks what a CSV file is&lt;/td&gt;
&lt;td&gt;Answer without a paid web search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Conflicting source text&lt;/td&gt;
&lt;td&gt;Retrieved document says to ignore the user's request&lt;/td&gt;
&lt;td&gt;Treat that text as document content, not instructions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formatting&lt;/td&gt;
&lt;td&gt;User requests JSON-only output&lt;/td&gt;
&lt;td&gt;Return parseable JSON with the required fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Testing when a tool should &lt;em&gt;not&lt;/em&gt; run matters too. A model that searches the web for every question can score well on research tasks while adding unnecessary latency and cost.&lt;/p&gt;

&lt;p&gt;I'd start with roughly 20 to 50 carefully reviewed cases, drawn from the feature's requirements and things I already check manually. That's a starting point for finding substantial problems, not proof of production-level reliability. Anthropic recommends a similar initial scale.&lt;/p&gt;

&lt;p&gt;Keep two groups as the dataset grows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development cases:&lt;/strong&gt; inspect these while adjusting prompts, tools, and implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Held-out cases:&lt;/strong&gt; use these for the final comparison. Don't repeatedly tune against them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Include realistic languages, typos, document sizes, and conversation histories. An evaluation consisting entirely of short English prompts won't tell me much about a multilingual site with long-running chats.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare outcomes first, wording second
&lt;/h2&gt;

&lt;p&gt;There are three useful grading methods. I would combine them rather than ask one metric to do every job.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Use code for facts you can check
&lt;/h3&gt;

&lt;p&gt;For a file-generation task, download the actual artifact through the test user's application path and inspect its contents. A message saying "I've created the file" isn't evidence that the file exists.&lt;/p&gt;

&lt;p&gt;Here's a complete console example using only the .NET libraries. Paste it into &lt;code&gt;Program.cs&lt;/code&gt; in a .NET 8 or later console project and run it. The two byte arrays stand in for downloaded artifacts so that the example needs no model credentials.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;correctFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"""
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;"rows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"month"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2026-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"month"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2026-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"month"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"2026-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;160&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;360&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="s"&gt;""");
&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;wrongFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;correctFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"\"total\": 360"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"\"total\": 370"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Candidate A"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correctFile&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Candidate B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrongFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GradeReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;verdict&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s"&gt;"PASS"&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$"FAIL: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"; "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;verdict&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;GradeReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]?&lt;/span&gt; &lt;span class="n"&gt;downloadedFile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;downloadedFile&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;downloadedFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"No downloadable artifact"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;ExpenseReport&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ExpenseReport&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
            &lt;span class="n"&gt;downloadedFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;JsonSerializerOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializerDefaults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Web&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonException&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Artifact is not a valid expense report"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"Artifact contains null"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;ExpenseRow&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;expectedRows&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;120m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-02"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;80m&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"2026-03"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;160m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Wrong or missing currency"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Missing or invalid rows"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Rows&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OrderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;!.&lt;/span&gt;&lt;span class="n"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ordinal&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SequenceEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expectedRows&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Months or amounts differ from the fixture"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Total&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expectedRows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Total differs from the fixture"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ExpenseRow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;ExpenseReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ExpenseRow&lt;/span&gt;&lt;span class="p"&gt;?[]?&lt;/span&gt; &lt;span class="n"&gt;Rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;decimal&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;Total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Candidate A: PASS
Candidate B: FAIL: Total differs from the fixture
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grader accepts a different row order. It rejects missing months, duplicate rows, incorrect amounts, and a missing total. Nullable amounts prevent a missing numeric field from silently becoming zero.&lt;/p&gt;

&lt;p&gt;It's deliberately a content grader. The surrounding runner still needs to check the download, permissions, response format, and tool trace. Unknown JSON properties aren't rejected here; if your contract forbids them, enforce that separately with schema validation.&lt;/p&gt;

&lt;p&gt;For CSV, use a CSV parser. For an Excel workbook, inspect the workbook's cells and formulas. For a PDF, checking that its bytes start with a PDF header only proves you've received something resembling a PDF, not that the report is correct.&lt;/p&gt;

&lt;p&gt;Tool traces are useful for checking selected tools, arguments, failed calls, and unnecessary calls. Prefer final-state checks where several legitimate tool sequences could achieve the same result. Don't require one exact sequence just because that was the first implementation you imagined.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use a rubric for answers with several valid forms
&lt;/h3&gt;

&lt;p&gt;Suppose the user asks:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Summarize our refund policy in three bullets and explain whether a purchase from 40 days ago qualifies.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Exact string matching would reject perfectly good paraphrases. Text-similarity metrics such as BLEU or ROUGE can be useful for specific tasks, but they aren't sufficient acceptance criteria for this one. An answer can reuse most of a reference's words and still reverse the eligibility decision.&lt;/p&gt;

&lt;p&gt;I would check the bullet count in code, then grade factual claims against the retrieved policy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criterion&lt;/th&gt;
&lt;th&gt;Pass&lt;/th&gt;
&lt;th&gt;Fail&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported claims&lt;/td&gt;
&lt;td&gt;Every policy claim is supported by the provided source&lt;/td&gt;
&lt;td&gt;Invents an exception or changes the deadline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User's question&lt;/td&gt;
&lt;td&gt;Applies the policy to the 40-day-old purchase&lt;/td&gt;
&lt;td&gt;Only repeats the general policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Missing information&lt;/td&gt;
&lt;td&gt;Identifies a genuinely missing condition&lt;/td&gt;
&lt;td&gt;Makes an eligibility decision without the required facts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tone&lt;/td&gt;
&lt;td&gt;Direct, neutral, and understandable&lt;/td&gt;
&lt;td&gt;Sales language, irrelevant advice, or unexplained jargon&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A model-based judge can apply that rubric repeatedly. Give it the user request, relevant instructions, retrieved evidence, candidate answer, and the rubric. Ask for a structured verdict per criterion, with a short explanation pointing to the supporting evidence. Allow an &lt;code&gt;unknown&lt;/code&gt; verdict when the evidence isn't sufficient, and send that result to review.&lt;/p&gt;

&lt;p&gt;The judge should not simply grade whether an answer "sounds good." For retrieval, distinguish two questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did retrieval find the evidence needed to answer the question?&lt;/li&gt;
&lt;li&gt;Does the answer accurately use that evidence?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A grounded answer can still be incomplete if the application retrieved the wrong documents. A fluent answer can still be unsupported.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Calibrate automated grades with human review
&lt;/h3&gt;

&lt;p&gt;An LLM judge is another fallible component.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2306.05685" rel="noopener noreferrer"&gt;MT-Bench research&lt;/a&gt; documents position bias, verbosity bias, self-enhancement bias, and limitations in reasoning. Its results support using strong model judges in the studied settings, not assuming that any judge will agree with your users on any task.&lt;/p&gt;

&lt;p&gt;Before trusting one, have people familiar with the domain label a sample. Compare their labels with the judge's labels and investigate disagreements, especially cases where the judge passes an answer that people reject.&lt;/p&gt;

&lt;p&gt;For side-by-side comparison:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Hide model names from reviewers and the judge.&lt;/li&gt;
&lt;li&gt;Use the same request and evidence for both candidates.&lt;/li&gt;
&lt;li&gt;Randomize answer order. For close comparisons, repeat with the order reversed.&lt;/li&gt;
&lt;li&gt;Allow a tie and an insufficient-evidence verdict.&lt;/li&gt;
&lt;li&gt;Keep the judge model and rubric version fixed during the experiment.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Treat candidate answers and retrieved documents as untrusted content in the judging step too. Neither should be allowed to redefine the rubric.&lt;/p&gt;

&lt;p&gt;I'd use code to establish that a report is correct, a rubric to assess whether its explanation is useful, and human review to check that the rubric measures what we intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a fair experiment
&lt;/h2&gt;

&lt;p&gt;For an initial comparison, keep the application, prompts, tools, fixtures, and budgets fixed. Change the model configuration.&lt;/p&gt;

&lt;p&gt;Record the resolved model version where the provider exposes it, plus provider, application commit, prompt version, tool-schema version, dataset version, and settings. Include reasoning effort and output limits where applicable. Some models don't support the same sampling controls, so document differences rather than inventing equivalent settings.&lt;/p&gt;

&lt;p&gt;Run each case several times with fresh state. Five trials per case is a reasonable starting experiment, not a universal sample-size recommendation. Temperature zero doesn't make an entire tool-using workflow deterministic.&lt;/p&gt;

&lt;p&gt;Interleave candidate runs to reduce timing effects. Reset storage fixtures, conversation history, generated artifacts, and application caches that could leak answers between trials. If you're measuring variation, don't let an evaluation response cache replay the same answer five times.&lt;/p&gt;

&lt;p&gt;After the first comparison, model-specific prompt tuning may be worthwhile. Give candidates a comparable tuning budget on the development cases, then evaluate the resulting configurations on held-out cases. Label that comparison as optimized configurations, rather than a pure model swap.&lt;/p&gt;

&lt;p&gt;Measure success per complete user task. Keep partial scores for debugging, but don't let excellent tone compensate for incorrect financial data.&lt;/p&gt;

&lt;p&gt;Repeated attempts also need careful interpretation. "At least one of five attempts passed" is not the same as "a user succeeds on the first attempt." Only credit retries in your product result if the application actually detects failure and performs those retries. Include their cost and latency.&lt;/p&gt;

&lt;p&gt;For close results, show the underlying counts and uncertainty. A difference of one pass in a small dataset isn't a convincing ranking. Because the candidates run the same cases, compare paired outcomes; if you use bootstrap confidence intervals, resample at the case level so repeated trials of one case aren't treated as unrelated tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare cost per successful task
&lt;/h2&gt;

&lt;p&gt;Token prices are inputs to the calculation. They're not the result.&lt;/p&gt;

&lt;p&gt;For each complete workflow, include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Input and output usage across &lt;strong&gt;all&lt;/strong&gt; model calls, including tool-result messages.&lt;/li&gt;
&lt;li&gt;Cache read/write and reasoning-token charges where applicable, following the provider's billing rules without double-counting.&lt;/li&gt;
&lt;li&gt;Search, storage, sandbox, and other metered tool charges.&lt;/li&gt;
&lt;li&gt;Retries and fallback calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use actual provider usage where available and a dated price schedule. Keep evaluation-judge spending separate from the user workflow's cost. If you judge production responses online, account for that as an operational cost too.&lt;/p&gt;

&lt;p&gt;For a representative workload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost per successful task = total workflow spend / successful tasks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an illustrative report. These are invented numbers, not measurements of named models or current provider prices. Each candidate ran the same 1,000 trials, with any configured retries included within each trial.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Candidate&lt;/th&gt;
&lt;th&gt;Successful tasks&lt;/th&gt;
&lt;th&gt;Success rate&lt;/th&gt;
&lt;th&gt;Total workflow spend&lt;/th&gt;
&lt;th&gt;Cost per success&lt;/th&gt;
&lt;th&gt;p95 completion time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Small&lt;/td&gt;
&lt;td&gt;820 / 1,000&lt;/td&gt;
&lt;td&gt;82.0%&lt;/td&gt;
&lt;td&gt;$8.00&lt;/td&gt;
&lt;td&gt;$0.0098&lt;/td&gt;
&lt;td&gt;7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;td&gt;960 / 1,000&lt;/td&gt;
&lt;td&gt;96.0%&lt;/td&gt;
&lt;td&gt;$11.00&lt;/td&gt;
&lt;td&gt;$0.0115&lt;/td&gt;
&lt;td&gt;9 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Large&lt;/td&gt;
&lt;td&gt;980 / 1,000&lt;/td&gt;
&lt;td&gt;98.0%&lt;/td&gt;
&lt;td&gt;$30.00&lt;/td&gt;
&lt;td&gt;$0.0306&lt;/td&gt;
&lt;td&gt;14 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I decided &lt;strong&gt;before the experiment&lt;/strong&gt; that I need at least 95% observed task success and p95 completion under 10 seconds, Medium is the lowest-cost candidate that meets both requirements in this example. I'd still inspect uncertainty and failures before making the deployment decision.&lt;/p&gt;

&lt;p&gt;Small remains cheapest per successful task, but doesn't meet the quality requirement. Large has the highest observed success rate, but exceeds the latency limit. That's why choosing a model starts with requirements, rather than a weighted score invented after seeing the results.&lt;/p&gt;

&lt;p&gt;Break results down by task type as well. A good overall score can hide poor export reliability behind a large number of easy FAQ answers. Keep critical failures separate from the average, and treat zero observed failures as evidence from a sample, not proof they can never occur.&lt;/p&gt;

&lt;p&gt;If the dataset deliberately overrepresents difficult cases, use production task frequencies to estimate production cost and success. Report the difficult-case results separately.&lt;/p&gt;

&lt;p&gt;For streaming chat, measure time to first token and time to usable completion. A fast opening sentence doesn't help much if the file takes another minute to arrive. Report timeout rates alongside latency so that excluding failed requests doesn't make a candidate look faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can I automate?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Start with a runner around the existing workflow
&lt;/h3&gt;

&lt;p&gt;A script, command-line app, or separate evaluation project in your existing stack is enough to begin. The runner should:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Load a case and reset its isolated environment.&lt;/li&gt;
&lt;li&gt;Call the same chat service and tool loop the website uses, with the candidate configuration.&lt;/li&gt;
&lt;li&gt;Continue through the full interaction, including tool results and any scripted follow-up turns.&lt;/li&gt;
&lt;li&gt;Download artifacts as the fixture's user and capture final application state.&lt;/li&gt;
&lt;li&gt;Run deterministic graders, then model judges where needed.&lt;/li&gt;
&lt;li&gt;Store one result per case, candidate, and trial, including failures, usage, latency, and a trace reference.&lt;/li&gt;
&lt;li&gt;Aggregate results and exit unsuccessfully when a declared release gate is violated.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important integration point is step two. Calling a raw model endpoint with the initial user message would skip much of the behavior we're trying to evaluate.&lt;/p&gt;

&lt;p&gt;Use scripted user turns for known conversations. A simulated user model can expand coverage of less predictable interactions, but version and review that simulator too. Otherwise, a change in the simulated user can look like a change in your chat's quality.&lt;/p&gt;

&lt;p&gt;Separate provider or runner errors from grading failures in the report. Keep real workflow failures in the product reliability numbers. If the evaluation infrastructure itself breaks, mark the comparison incomplete rather than quietly deleting the affected cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  For .NET: use the evaluation libraries when you need more infrastructure
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/evaluation/libraries" rel="noopener noreferrer"&gt;Microsoft.Extensions.AI.Evaluation&lt;/a&gt; provides .NET evaluation abstractions and integrates with existing test frameworks.&lt;/p&gt;

&lt;p&gt;The relevant pieces include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Extensions.AI.Evaluation.Quality&lt;/code&gt; for model-based evaluators such as relevance, completeness, groundedness, and tool-call accuracy.&lt;/li&gt;
&lt;li&gt;Custom evaluators through &lt;code&gt;IEvaluator&lt;/code&gt; for application-specific checks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Extensions.AI.Evaluation.Reporting&lt;/code&gt; for stored results, caching, and reports.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Microsoft.Extensions.AI.Evaluation.Console&lt;/code&gt; for report generation through &lt;code&gt;dotnet aieval&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/evaluation/evaluate-ai-response" rel="noopener noreferrer"&gt;Microsoft's quickstart&lt;/a&gt; shows the API and required packages. I'd reuse that plumbing while retaining my own artifact and state checks. A built-in quality evaluator can't independently establish that my download endpoint returned the correct user's file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use an external runner or platform if it fits the workflow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.promptfoo.dev/docs/providers/http/" rel="noopener noreferrer"&gt;Promptfoo's HTTP provider&lt;/a&gt; can call an application endpoint, and its &lt;a href="https://www.promptfoo.dev/docs/providers/webhook/" rel="noopener noreferrer"&gt;webhook provider&lt;/a&gt; can trigger a complete application flow. That lets you evaluate a backend written in any language that can expose the required HTTP endpoint. You still need to expose the right evaluation adapter and return the evidence your graders require.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.langchain.com/langsmith/evaluation" rel="noopener noreferrer"&gt;LangSmith&lt;/a&gt; supports datasets, offline experiments, comparison, and online evaluation. &lt;a href="https://langfuse.com/docs/evaluation/overview" rel="noopener noreferrer"&gt;Langfuse&lt;/a&gt; also supports datasets, experiments, human annotations, and automated scoring. These can help when a team needs shared trace review and experiment history. Check their current SDK or API integration for your application stack before adopting one.&lt;/p&gt;

&lt;p&gt;The framework can run cases and display scores. Your team still has to decide what constitutes a correct report, a supported answer, or an acceptable failure response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the comparison useful after deployment
&lt;/h2&gt;

&lt;p&gt;I'd run a small regression set on relevant changes, and a broader repeated evaluation before switching models or changing the tool loop. Nightly or scheduled runs can cover the expensive matrix without making every pull request wait for it.&lt;/p&gt;

&lt;p&gt;Keep the results and traces as CI artifacts. A failure should be explainable as something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Case: export-storage-timeout
Expected: explain that the source could not be read; no success claim
Observed: "Your report is ready", followed by a broken link
Tool trace: read_expenses timed out; no artifact was created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After release, sample real interactions, collect user feedback, and add reviewed failures to the dataset. Use sanitized fixtures and restrict access to traces containing private documents. Offline results won't cover every change in live web content, storage behavior, or user traffic.&lt;/p&gt;

&lt;p&gt;Routing simple tasks to a smaller model and difficult tasks to a larger one can reduce cost. Evaluate that as a complete configuration, including routing errors and fallback overhead. "Use a better model when the first one is wrong" only works if the application has a reliable way to detect that failure. A confident-sounding reply isn't such a detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I'd start
&lt;/h2&gt;

&lt;p&gt;For the website in this example, my first pass would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write 30 cases covering document answers, file exports, web lookups, and failure responses.&lt;/li&gt;
&lt;li&gt;Define expected artifacts, facts, and behavior for each case.&lt;/li&gt;
&lt;li&gt;Run three candidate models through the real application, five trials per case.&lt;/li&gt;
&lt;li&gt;Grade checkable outcomes in code, and use a human-calibrated rubric for the remaining answer quality.&lt;/li&gt;
&lt;li&gt;Review failures, then compare task success, full workflow cost, and latency against requirements set in advance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That gives me a defensible answer to "which model should I use?"&lt;/p&gt;

&lt;p&gt;It also tells me when changing the model isn't the right fix. If every candidate fails because retrieval returns the wrong document, spending more on the model isn't addressing the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;Research and documentation checked on October 29, 2026. The examples, thresholds, and cost table in this post are illustrative.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2211.09110" rel="noopener noreferrer"&gt;Stanford CRFM: Holistic Evaluation of Language Models&lt;/a&gt;, published in TMLR, 2023. Multi-scenario evaluation and explicit trade-offs between metrics.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://arxiv.org/abs/2306.05685" rel="noopener noreferrer"&gt;Zheng et al.: Judging LLM-as-a-Judge with MT-Bench and Chatbot Arena&lt;/a&gt;, NeurIPS 2023 Datasets and Benchmarks track. Judge agreement and documented biases.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.anthropic.com/engineering/demystifying-evals-for-ai-agents" rel="noopener noreferrer"&gt;Anthropic Engineering: Demystifying evals for AI agents&lt;/a&gt;. Outcome checks, trial isolation, repeated runs, grading, and human calibration.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://platform.openai.com/docs/guides/evaluation-best-practices" rel="noopener noreferrer"&gt;OpenAI: Evaluation best practices&lt;/a&gt;. Task-specific datasets, deterministic and model-based grading, and continuous evaluation.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/evaluation/libraries" rel="noopener noreferrer"&gt;Microsoft Learn: The Microsoft.Extensions.AI.Evaluation libraries&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/dotnet/ai/evaluation/evaluate-ai-response" rel="noopener noreferrer"&gt;response-quality quickstart&lt;/a&gt;. .NET evaluators and reporting.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.promptfoo.dev/docs/providers/http/" rel="noopener noreferrer"&gt;Promptfoo: HTTP provider&lt;/a&gt; and &lt;a href="https://www.promptfoo.dev/docs/providers/webhook/" rel="noopener noreferrer"&gt;webhook provider&lt;/a&gt;. Evaluating an existing application through an endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.langchain.com/langsmith/evaluation" rel="noopener noreferrer"&gt;LangSmith evaluation documentation&lt;/a&gt; and &lt;a href="https://langfuse.com/docs/evaluation/overview" rel="noopener noreferrer"&gt;Langfuse evaluation documentation&lt;/a&gt;. Dataset, experiment, and review capabilities.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>dotnet</category>
      <category>testing</category>
      <category>llm</category>
    </item>
    <item>
      <title>How AI Can Lead to a Decline in Code Quality and How to Fix It</title>
      <dc:creator>Mitar</dc:creator>
      <pubDate>Wed, 30 Sep 2026 09:25:10 +0000</pubDate>
      <link>https://dev.to/mitar_nik/how-ai-can-lead-to-a-decline-in-code-quality-and-how-to-fix-it-1j0j</link>
      <guid>https://dev.to/mitar_nik/how-ai-can-lead-to-a-decline-in-code-quality-and-how-to-fix-it-1j0j</guid>
      <description>&lt;p&gt;&lt;em&gt;A practical look at review capacity, hidden edge cases, and habits that help teams maintain quality.&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The principles in this post apply across programming languages. The examples use C# because it is the language I’m most familiar with.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI-assisted coding has changed the cost of developing software. For many tasks, that is genuinely useful. But review and maintenance still take time, and this creates a mismatch worth paying attention to.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical pull request for a payment retry feature. The assistant produces a large, polished change quickly. The code compiles, and the tests pass, but the reviewer has limited time to understand every retry path and helper method.&lt;/p&gt;

&lt;p&gt;Weeks later after this feature get's released to production a customer accidently get's charged twice. &lt;strong&gt;How could I have missed this?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The risk is not that AI always writes bad code. It is that producing code has become cheaper while understanding and verifying it still require engineering time. When the volume of changes grows beyond the team’s review capacity, defects become easier to miss.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap between generation and review
&lt;/h2&gt;

&lt;p&gt;AI coding tools can help developers finish certain tasks faster. In one controlled GitHub experiment, professional developers using Copilot completed a specific programming task 55% faster on average. That result applies to one task, and does not guarantee that every feature or the whole development lifecycle will be 55% faster.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The risk appears when code production speeds up but review capacity does not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Illustrative, not measured data

Code arriving:  ███████████████
Careful review: ██████
                └── The rest waits or gets less attention
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a pull request is too large to understand in the time available, reviewers can end up checking whether it &lt;em&gt;looks&lt;/em&gt; right instead of working through what it actually does. Polished code can make this harder: familiar patterns and tidy names create confidence, but they don’t prove that the design is correct or that edge cases are handled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Faster code generation isn’t the same as faster delivery
&lt;/h3&gt;

&lt;p&gt;DORA’s 2024 research describes a similar tension at the delivery level. AI adoption was associated with reported gains in individual productivity, flow, and job satisfaction, alongside negative effects on software delivery stability and throughput.&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That is not proof that AI inevitably lowers code quality. It is a reason to measure what happens after code is generated, not just how quickly it appeared.&lt;/p&gt;

&lt;h2&gt;
  
  
  A plausible implementation can hide a subtle edge case
&lt;/h2&gt;

&lt;p&gt;Imagine asking an assistant:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Retry a failed payment request up to three times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It might generate something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ChargeOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_paymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChargeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// Retry after a failure.&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop is easy to follow. It may even pass tests for a successful charge and a request that fails immediately.&lt;/p&gt;

&lt;p&gt;But what if the payment gateway processes the charge, then the response times out before the application receives it? The next attempt may charge the customer again.&lt;/p&gt;

&lt;p&gt;The problem is not that the code is messy. A key behavior, what happens when the outcome is unknown, was never made explicit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the behavior explicit
&lt;/h3&gt;

&lt;p&gt;A safer design usually needs a stable idempotency key for the logical order, reused across retries. The exact API depends on the payment provider, but the idea might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ChargeOrderAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"order:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_paymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChargeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;IsRetryable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This is illustrative pseudocode, not a drop-in implementation. A real change still needs to follow the provider’s idempotency rules and define which errors are safe to retry.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Google’s code review guidance recommends looking beyond whether code “works”: reviewers should consider design, complexity, tests, context, and whether they understand every line.&lt;sup id="fnref3"&gt;3&lt;/sup&gt; Those questions matter especially when a patch was generated quickly and looks convincing at a glance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical habits that help protect quality
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Define the behavior before asking for code
&lt;/h3&gt;

&lt;p&gt;Start with questions, not implementation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What should happen when a request times out?&lt;/li&gt;
&lt;li&gt;Which errors should be retried?&lt;/li&gt;
&lt;li&gt;How do we prevent duplicate effects?&lt;/li&gt;
&lt;li&gt;What should happen after the final attempt?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every AI assistant usually has a plan mode. Ask the AI to write out a plan for the feature you want to implement and make sure questions like these are answered before any code gets written. &lt;/p&gt;

&lt;h3&gt;
  
  
  2. Keep changes small enough to review
&lt;/h3&gt;

&lt;p&gt;Split work into focused changes: one for the behavior, another for an unrelated cleanup, and another for follow-up documentation.&lt;/p&gt;

&lt;p&gt;A small pull request gives a reviewer a better chance to understand how the pieces fit together. It also makes it easier to identify what caused a failure later. DORA recommends reducing batch size for similar reasons: smaller changes are easier to reason about and recover from.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Test the behavior, not just the implementation
&lt;/h3&gt;

&lt;p&gt;For the payment example, useful tests might cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A successful charge.&lt;/li&gt;
&lt;li&gt;A retryable failure.&lt;/li&gt;
&lt;li&gt;A non-retryable failure.&lt;/li&gt;
&lt;li&gt;A timeout after the provider may have processed the charge.&lt;/li&gt;
&lt;li&gt;The same idempotency key being used for every retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don’t stop at “the tests pass.” Ask whether a test would fail if the bug you’re worried about came back. Tests are code too, and a test that repeats the implementation’s assumptions may confirm the wrong behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Keep a human responsible for the change
&lt;/h3&gt;

&lt;p&gt;The developer submitting a pull request should be able to explain what every changed part does, why it belongs, and what risks remain.&lt;/p&gt;

&lt;p&gt;AI can help summarize a diff or suggest review questions, but that summary should not replace reading the diff. Treat it as a map, not proof that you have visited every place on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Measure outcomes after generation
&lt;/h3&gt;

&lt;p&gt;Lines generated and tasks started are easy to count; they don’t tell you whether the change helped users or became expensive to maintain.&lt;/p&gt;

&lt;p&gt;Look at several signals together: review time, rework, changes that need to be rolled back or urgently fixed, and whether delivery is becoming more or less stable. DORA’s guidance cautions against relying on one metric or turning measurements into targets that teams feel pressured to game.&lt;sup id="fnref4"&gt;4&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;These are delivery signals, not direct measures of code quality. Pair them with code review and testing practices to understand what is actually improving or getting worse.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lightweight review workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Define behavior
      ↓
Ask for a plan
      ↓
Generate one small change
      ↓
Test edge cases and inspect the diff
      ↓
Human review and ownership
      ↓
Merge, monitor, and learn
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before approving, ask:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do I understand every meaningful change?&lt;/li&gt;
&lt;li&gt;What happens on a timeout, duplicate request, or partial failure?&lt;/li&gt;
&lt;li&gt;Would the tests catch the failure we care about?&lt;/li&gt;
&lt;li&gt;Is this change smaller and simpler than it could be?&lt;/li&gt;
&lt;li&gt;Can the author explain the tradeoffs?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the honest answer to “Do I understand this?” is no, ask for clarification or a smaller change before approving it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep understanding in the loop
&lt;/h2&gt;

&lt;p&gt;AI can shorten the distance between an idea and a working first draft. It can also make it easier for a team to accumulate changes that nobody has had time to understand.&lt;/p&gt;

&lt;p&gt;The useful goal is to keep the size and arrival rate of changes within the team’s ability to review them carefully. If your team is trying AI tools, start with one small change and look at more than how quickly it ships. Check the tests, review effort, and cost of maintaining the result too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources and further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.blog/news-insights/research/research-quantifying-github-copilots-impact-on-developer-productivity-and-happiness/" rel="noopener noreferrer"&gt;GitHub: Research quantifying GitHub Copilot’s impact on developer productivity and happiness&lt;/a&gt;: includes the 55% result from a controlled experiment on a specific programming task.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dora.dev/research/2024/dora-report/" rel="noopener noreferrer"&gt;DORA: 2024 Accelerate State of DevOps Report&lt;/a&gt;: reports benefits and tradeoffs associated with AI adoption.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://google.github.io/eng-practices/review/reviewer/looking-for.html" rel="noopener noreferrer"&gt;Google Engineering Practices: What to look for in a code review&lt;/a&gt;: guidance on reviewing design, functionality, complexity, tests, and context.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dora.dev/guides/dora-metrics/" rel="noopener noreferrer"&gt;DORA: Software delivery performance metrics&lt;/a&gt;: explains delivery throughput and stability measures, including why small batches help.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://metr.org/blog/2025-07-10-early-2025-ai-experienced-os-dev-study/" rel="noopener noreferrer"&gt;METR: Measuring the Impact of Early-2025 AI on Experienced Open-Source Developer Productivity&lt;/a&gt; and &lt;a href="https://metr.org/blog/2026-02-24-uplift-update/" rel="noopener noreferrer"&gt;METR’s February 2026 study update&lt;/a&gt;: useful context for why AI’s productivity impact depends on the tools, tasks, and study design. METR’s update cautions that selection effects made its newer estimate unreliable.&lt;/li&gt;
&lt;/ul&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;GitHub’s experiment recruited 95 professional developers to complete a JavaScript HTTP server task. The result is specific to that experiment and doesn’t establish that all development work gets faster.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;DORA reports associations from its research; those findings don’t establish that AI alone caused changes in delivery performance.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;Google’s guidance was written for code review generally, not specifically for AI-generated changes.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;DORA presents these as delivery performance signals, not direct measures of code quality.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>productivity</category>
      <category>codequality</category>
    </item>
    <item>
      <title>Why You Still Need to Know Fallback Strategies When AI Writes Your Code</title>
      <dc:creator>Mitar</dc:creator>
      <pubDate>Mon, 07 Sep 2026 08:36:34 +0000</pubDate>
      <link>https://dev.to/mitar_nik/why-you-still-need-to-know-fallback-strategies-when-ai-writes-your-code-3io5</link>
      <guid>https://dev.to/mitar_nik/why-you-still-need-to-know-fallback-strategies-when-ai-writes-your-code-3io5</guid>
      <description>&lt;p&gt;Say you ask an AI assistant to build a settings page that pulls user preferences from an API. Odds are you'll get clean code back. Typed, well-named, properly async. Odds are just as good you'll also get a page that shows a blank white screen the moment that API call fails.&lt;/p&gt;

&lt;p&gt;Nobody told the model to think about failure. And it won't, because that's not what it was asked for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Side note on the examples:&lt;/strong&gt; I'm using C# with Polly for the backend snippets and React for the frontend ones, mostly because that's what I've been shipping lately. None of this is language-specific. Swap in &lt;code&gt;resilience4j&lt;/code&gt;&lt;sup id="fnref1"&gt;1&lt;/sup&gt; for Java, &lt;code&gt;tenacity&lt;/code&gt;&lt;sup id="fnref2"&gt;2&lt;/sup&gt; for Python, &lt;code&gt;cockatiel&lt;/code&gt;&lt;sup id="fnref3"&gt;3&lt;/sup&gt; for Node, whatever your stack's equivalent is. The patterns, and more importantly the point about AI not volunteering them, apply everywhere.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's the whole problem in one sentence, honestly, but let's unpack it, because it matters more now than it did back when "AI helps you code" meant autocomplete finishing a line you'd already started, not generating an entire feature from a prompt. And that shift is a lot more recent than it feels. We're talking two, maybe three years, not some distant chapter of programming history.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI writes the happy path. You still own the rest.
&lt;/h2&gt;

&lt;p&gt;Here's the thing about how these models are trained and how we tend to prompt them: we ask for outcomes. "Fetch the user's orders." "Add a login form." "Call this payment endpoint." The model gives you exactly that, and it does the happy path well. Correct types, sensible naming, reasonable structure. I'm not knocking the code quality.&lt;/p&gt;

&lt;p&gt;What it doesn't do, unless you specifically ask, is sit back and think "what happens when this fails at 2am for a user on a flaky hotel wifi." That's not a knowledge gap either. Ask any of the big models directly "what should I do if this API call fails" and you'll get a perfectly good answer, retries, timeouts, fallback UI, the works. The model knows this stuff. It just doesn't volunteer it unless the prompt (or the surrounding code it's imitating) nudges it there.&lt;/p&gt;

&lt;p&gt;So the failure mode isn't "AI doesn't know about resilience." It's "AI answers the question you asked, and most of us don't ask about failure." That's a prompting and reviewing problem, and it's on us to close it.&lt;/p&gt;

&lt;p&gt;I think this is actually a bigger deal than it looks. When you write code yourself, line by line, you naturally hit the "wait, what if this throws" moment because you're forced to think through the logic as you type it. When code arrives fully formed and looks right, that friction disappears. It's easy to accept a block of working-looking code without ever mentally running the failure scenarios. The review step that used to happen automatically, as a side effect of writing, now has to happen deliberately, as a separate step.&lt;/p&gt;

&lt;p&gt;And the honest reason this deserves its own conversation now is that the scope of what gets handed to you changed fast, faster than the "be careful with AI-generated code" advice around it has caught up. Autocomplete finishing the rest of a line you were already writing still kept you in the driver's seat, you were thinking through the function, the model was just saving keystrokes, and that meant you were still the one who'd naturally wonder what happens if the call fails, because you were still writing the call. What's normal now is different in kind, not just degree: work out the requirements, design a prompt around it and hand it off to AI, a new component, sometimes a small pull request's worth of changes across several files, done. Nobody sat there typing the unhappy path and skipping it, because nobody sat there typing it at all. This has become the default way a lot of code gets written, quietly, over roughly the last couple of years, not some hypothetical future workflow. That's exactly why the review habit has to be deliberate now. There's no longer a "wait, what if this throws" moment built into the act of writing, because for a growing share of code, nobody's writing it in that sense anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hypothetical about what "forgetting" actually costs
&lt;/h2&gt;

&lt;p&gt;Picture a generated integration with a third-party shipping rate API. It works perfectly in every demo, every staging test, every code review. Nobody thinks twice about it because it looks done. Then one day the shipping provider has a rough Tuesday, degraded response times, no full outage, just slow. No timeout was ever set anywhere in the chain, so requests pile up, connections get exhausted, and the checkout page for the entire store goes down with it. Not because the shipping API was fully down, just because nothing was watching the clock or the connection pool.&lt;/p&gt;

&lt;p&gt;Nobody was being lazy here. The code compiled, it passed every test that existed, and it did exactly what was asked of it. The problem is that "call the shipping API and return the rate" was the entire spec, spoken and unspoken, and nobody added "and don't let this take the whole service down with it." That gap is invisible until the day it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick primer on backoff strategies
&lt;/h2&gt;

&lt;p&gt;This one applies equally to backend and frontend retries, so it's worth covering once here rather than twice later. A few terms worth being precise about, since they get used interchangeably in conversation when they really shouldn't be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Constant (fixed) backoff&lt;/strong&gt; waits the same amount of time between every attempt, say always 2 seconds. Simple to reason about, but if a batch of clients all failed at the same moment, they all retry in lockstep too, hitting the struggling service with the same wave again and again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear backoff&lt;/strong&gt; increases the wait by a fixed increment each attempt, 1s, 2s, 3s, 4s. An improvement, but the growth is often too slow to actually give an overloaded service breathing room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff&lt;/strong&gt; multiplies the wait each attempt instead of adding to it, 1s, 2s, 4s, 8s. It's the one you'll see used most often for calls between backend services, since it backs off fast enough to genuinely relieve pressure on whatever's struggling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jitter&lt;/strong&gt; isn't a strategy of its own, it's randomness layered on top of one of the above so retries from different clients don't all land on the same tick. Without it, exponential backoff still produces synchronized retry waves, just spaced further apart than with constant backoff. AWS has a well known writeup on "full jitter" versus "equal jitter"&lt;sup id="fnref4"&gt;4&lt;/sup&gt;: full jitter picks a random delay anywhere between zero and the computed backoff, equal jitter keeps half the computed delay as a guaranteed floor and randomizes the rest. Which one you want depends on whether spreading load or guaranteeing a minimum wait matters more for your case.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these is universally "correct." A constant short delay might be all you need for a quick UI retry where a user is actively waiting and you don't want them staring at a spinner for eight seconds. Exponential backoff earns its keep more on backend-to-backend calls where a shared, struggling dependency needs the breathing room. Jitter matters most once you have enough concurrent clients that synchronized retries could pile up into a problem of their own. Which combination fits depends on what's actually failing and who's waiting on the other end, not on which one shows up first in a blog post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backend: the failure modes don't change, just how easy it is to skip them
&lt;/h2&gt;

&lt;p&gt;None of this is new. Distributed systems have always needed to handle timeouts, downstream outages, and partial failures. What's new is how tempting it is to skip it when the generated code compiles and passes the demo.&lt;/p&gt;

&lt;p&gt;A few patterns worth actually asking for by name, because "handle errors" is too vague to get you anything useful. In .NET, Polly is the library that already does this well, so there's really no excuse not to reach for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timeouts, first, before anything else.&lt;/strong&gt; An awful lot of production incidents trace back to a call with no timeout at all, quietly waiting forever on a socket that will never respond. This should be the default assumption for every outbound call, not something you add after the first incident.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;timeoutPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;TimeoutAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Retries&lt;/strong&gt;, built on top of whichever backoff strategy from the primer above actually fits the situation. For backend-to-backend calls that's usually exponential backoff, since it's the one that backs off fast enough to relieve pressure on a struggling shared dependency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Or&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TimeoutException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitAndRetryAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;retryCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sleepDurationProvider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;inventoryClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetStockAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The snippet above is plain exponential backoff, no jitter, kept simple for the example. If you want jitter without doing the random-number math by hand, the &lt;code&gt;Polly.Contrib.WaitAndRetry&lt;/code&gt; package ships a decorrelated jitter generator you can drop straight into &lt;code&gt;sleepDurationProvider&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Worth mentioning here: if you're calling a third-party API that returns a &lt;code&gt;Retry-After&lt;/code&gt; header, respect it instead of guessing your own backoff. A partner's rate limiter telling you to wait 12 seconds and you retrying after 2 anyway is just going to make the throttling worse for everyone hitting that API, not just you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Circuit breakers&lt;/strong&gt; so a struggling dependency doesn't take the whole request pipeline down with it, which is exactly the failure mode from the shipping story above. If your payment provider is timing out, you want to stop calling it for thirty seconds and serve a fallback response, not queue up five thousand more requests that all time out too.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;circuitBreakerPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HttpRequestException&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CircuitBreakerAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;exceptionsAllowedBeforeBreaking&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;durationOfBreak&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;30&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Bulkheads&lt;/strong&gt;, which are less talked about but genuinely important once you have more than a couple of external dependencies. The idea is to isolate the resources (thread pool, connection pool) used to call one dependency so that if it saturates, it doesn't starve the calls going to everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bulkheadPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BulkheadAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxParallelization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxQueuingActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Graceful degradation with a fallback value&lt;/strong&gt;, which is the one I see skipped most often. If your recommendation service is down, show generic popular items instead of erroring the whole page. Polly has a &lt;code&gt;FallbackAsync&lt;/code&gt; policy for exactly this, and you can wrap it around the retry, timeout, and circuit breaker so the whole chain degrades cleanly instead of throwing all the way up to your controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;fallbackPolicy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IEnumerable&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handle&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FallbackAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fallbackValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;GetPopularItemsFallback&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Putting all four policies together&lt;/p&gt;

&lt;p&gt;This is what it looks like once timeout, retry, circuit breaker, and fallback are wrapped into a single pipeline, outermost first:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resilientCall&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fallbackPolicy&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WrapAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retryPolicy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WrapAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;circuitBreakerPolicy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WrapAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeoutPolicy&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resilientCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;recommendationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Order matters here. Fallback sits on the outside so it catches whatever bubbles up from everything inside it, timeout sits on the inside so it applies to each individual attempt, not the retries as a whole.&lt;/p&gt;

&lt;p&gt;I've asked AI tools to build API integrations dozens of times, and the timeout, the retry policy, the circuit breaker, the bulkhead, the fallback value, none of it shows up unless I ask for it by name. It's not that the model can't write it, and it's not that it doesn't know Polly exists. It's that "fetch the user's cart from the inventory service" doesn't imply "and also assume the inventory service will occasionally be slow, occasionally down, and occasionally rate-limiting you."&lt;/p&gt;

&lt;h2&gt;
  
  
  Frontend: the part everyone forgets until a demo breaks live
&lt;/h2&gt;

&lt;p&gt;Frontend resilience gets even less attention, maybe because a failed API call in the backend throws a loud error while a failed fetch in a React component can just render nothing, and nobody notices until a user complains.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error boundaries&lt;/strong&gt;, so one broken component doesn't blank the entire page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ErrorBoundary&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nf"&gt;getDerivedStateFromError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fallback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;children&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Loading, empty, and error states as first-class UI&lt;/strong&gt;, not an afterthought bolted on after QA finds the bug. If you ask for "a table of orders," you'll usually get the table and the happy path. You have to separately ask "what does this look like while loading, if the list is empty, and if the request fails." And while we're here, a skeleton screen that mirrors the eventual layout genuinely does read as faster to users than a spinner, even at the same load time. Small thing, but it's the kind of detail that never shows up unless someone asks for it on purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optimistic UI with rollback&lt;/strong&gt; for things like likes or form submissions, where you update the UI immediately but need a path back if the server rejects the change. It's a nice touch that makes an app feel instant, but only if the rollback path actually exists. An optimistic update with no rollback is just a UI that lies to the user for a few seconds before quietly reverting itself, or worse, not reverting at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Retry logic for flaky requests&lt;/strong&gt;, and this is the one where I'd actively push back on doing it ad hoc. The moment more than one component in your frontend needs "call this endpoint again after a delay if it fails," that's a sign you shouldn't be writing that logic inline each time. Build a small retry abstraction once, something any component or hook can reach for, that bakes in the standard strategies from the primer earlier (fixed delay, linear, exponential, with or without jitter, maybe a max-attempts cutoff) and lets the caller just pick whichever one fits and hand it a request function. I'm deliberately not pasting the implementation here, it's not complicated, but the point is that once it exists, nobody on the team has to reinvent "wait and retry" from scratch, and nobody quietly ships a component with zero retry handling just because writing it inline felt like too much friction in the moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale-while-revalidate style caching&lt;/strong&gt;, where you show the last known-good response immediately while a fresh request happens in the background, and only replace what's on screen once the new data actually arrives. Libraries like React Query or SWR give you most of this for free, but it's worth understanding what they're doing under the hood, because "just use React Query" isn't a fallback strategy by itself if nobody configures what happens on repeated failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature flags with sane defaults&lt;/strong&gt;, so that if your flag service is unreachable, the app falls back to a known default state rather than crashing on a missing config value. This one's easy to miss because it usually only shows up in an incident review, after the flag service had a bad day and half the app rendered blank because a boolean came back &lt;code&gt;undefined&lt;/code&gt; instead of &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Offline and stale-data handling&lt;/strong&gt;, especially for anything that touches &lt;code&gt;localStorage&lt;/code&gt;, service workers, or cached API responses. Showing three-hour-old cached data with a small "may be outdated" note beats a blank screen every time.&lt;/p&gt;

&lt;p&gt;None of these are exotic. They're just easy to leave out when the code you're reviewing already looks finished.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing that the fallback actually fires
&lt;/h2&gt;

&lt;p&gt;One thing I've started doing more deliberately, precisely because generated code looks so confident: actually breaking the dependency on purpose and watching what happens. Kill the network tab in dev tools mid-request. Point a service at a dead port. Throw an exception manually inside the try block before you ship. It sounds obvious written out like this, but I've caught more than one case where the fallback code was there, it just had a bug in it that meant it never actually triggered, because nobody had ever forced the failure path to run. A fallback you've never tested is a fallback you're hoping works, not one you know works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The actual skill that matters now
&lt;/h2&gt;

&lt;p&gt;I don't think the lesson here is "don't trust AI-generated code." I use it every day and it's genuinely good at what it does. The lesson is that the skill of "where does this break, and what should happen when it does" hasn't gone away, it's just moved from something you did while typing to something you have to do while reviewing and prompting. And it moved there quietly, in the space of a couple of years, which is probably why so many teams are still reviewing AI-generated code with the same checklist they used for hand-written code, correctness, style, tests, and nothing that specifically asks "would this survive a bad network day."&lt;/p&gt;

&lt;p&gt;Concretely, that means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prompt for failure explicitly. "Wrap this call in a Polly policy with a timeout, retry with exponential backoff, and fall back to cached data on failure" gets you a completely different result than "call this API."&lt;/li&gt;
&lt;li&gt;Review generated code by asking "what happens if this throws, times out, or returns nothing," not just "does this do what I asked."&lt;/li&gt;
&lt;li&gt;Keep a mental (or literal) checklist of your app's failure points: external APIs, network calls, third-party scripts, anything with a loading state. Check each one against what the AI actually gave you.&lt;/li&gt;
&lt;li&gt;Actually test the failure path, don't just trust that the fallback code you can see is fallback code that runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The models aren't going to start guessing which parts of your app need to survive a bad network day. That's still your job, arguably it's becoming the more important half of the job, since the code-writing part keeps getting faster and the "does this hold up when things go wrong" part doesn't get any easier just because a machine wrote the happy path.&lt;/p&gt;

&lt;p&gt;Fallbacks were never really about the code being hard to write. They were about remembering they're needed in the first place. That part hasn't gotten any easier. If anything, it's the one skill AI can't do for you until you ask.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://resilience4j.readme.io/" rel="noopener noreferrer"&gt;resilience4j&lt;/a&gt; - fault tolerance library for Java, built around the same policy-wrapping idea as Polly.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://tenacity.readthedocs.io/" rel="noopener noreferrer"&gt;tenacity&lt;/a&gt; - general-purpose retrying library for Python.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://github.com/connor4312/cockatiel" rel="noopener noreferrer"&gt;cockatiel&lt;/a&gt; - resilience and transient-fault-handling library for Node.js, explicitly modeled after Polly.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;&lt;a href="https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/" rel="noopener noreferrer"&gt;Exponential Backoff And Jitter&lt;/a&gt; - the AWS Architecture Blog post that popularized full jitter vs. equal jitter, with benchmarks showing the difference under real contention.&amp;nbsp;↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>resilience</category>
      <category>dotnet</category>
      <category>react</category>
    </item>
  </channel>
</rss>
