<?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: Вячеслав Барсуков</title>
    <description>The latest articles on DEV Community by Вячеслав Барсуков (@slavabars).</description>
    <link>https://dev.to/slavabars</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%2F4239002%2Ffdd69664-6001-4c94-9119-3490d1a0f266.png</url>
      <title>DEV Community: Вячеслав Барсуков</title>
      <link>https://dev.to/slavabars</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/slavabars"/>
    <language>en</language>
    <item>
      <title>Building a local-first decision engine that only calls the big model when it's worth it</title>
      <dc:creator>Вячеслав Барсуков</dc:creator>
      <pubDate>Wed, 11 Nov 2026 11:54:45 +0000</pubDate>
      <link>https://dev.to/slavabars/building-a-local-first-decision-engine-that-only-calls-the-big-model-when-its-worth-it-hdl</link>
      <guid>https://dev.to/slavabars/building-a-local-first-decision-engine-that-only-calls-the-big-model-when-its-worth-it-hdl</guid>
      <description>&lt;p&gt;Every AI agent I've built eventually hits the same wall: &lt;strong&gt;calling a big model on every request is too expensive, but a small local model is too dumb on the hard cases.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most teams pick one side and live with the consequences. I wanted a third option: run a small local model first, and escalate to a big one only when the expected benefit of escalation actually exceeds its cost.&lt;/p&gt;

&lt;p&gt;That project is called &lt;a href="https://github.com/slavabars/decision-cascade" rel="noopener noreferrer"&gt;decision-cascade&lt;/a&gt;. It's open source, it's on PyPI (&lt;code&gt;pip install decision-cascade&lt;/code&gt;), and this post is about what I learned building it — including the parts that didn't work at first.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with "escalate if confidence is low"
&lt;/h2&gt;

&lt;p&gt;My first version did the obvious thing: if the local model's &lt;code&gt;confidence &amp;lt; 0.6&lt;/code&gt;, send the request to a bigger model. Simple, works, and completely wrong in practice.&lt;/p&gt;

&lt;p&gt;Why wrong? Because &lt;strong&gt;confidence and benefit are not the same thing.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can be very unsure about a question where being wrong costs nothing.&lt;/li&gt;
&lt;li&gt;You can be quite sure (0.8) and still be wrong on a case where being wrong costs a lot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A confidence threshold ignores both the cost of an error and the price of the external call. So the cascade ends up escalating cheap-to-be-wrong cases and skipping expensive-to-be-wrong ones. That's backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed: benefit-aware escalation
&lt;/h2&gt;

&lt;p&gt;The current version compares two numbers for each request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Expected utility of the local answer&lt;/strong&gt; — accuracy adjusted by the cost of being wrong in this particular state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expected utility of escalation&lt;/strong&gt; — accuracy of the external model (estimated online via feedback) adjusted by the price of the call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the second is bigger than the first, escalate. Otherwise stay local.&lt;/p&gt;

&lt;p&gt;In code, the core of it looks roughly like this (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;should_escalate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;local_conf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error_cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_q&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;external_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;expected_local&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;local_conf&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;local_conf&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;error_cost&lt;/span&gt;
    &lt;span class="n"&gt;expected_external&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;external_q&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;external_price&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;expected_external&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;expected_local&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is that &lt;code&gt;external_q&lt;/code&gt; (the external model's accuracy) is &lt;strong&gt;not a constant&lt;/strong&gt;. It's updated online through &lt;code&gt;POST /v1/feedback&lt;/code&gt; as users report whether the external model was right. So the cascade adapts to the actual quality of whatever external service you plug in — Clef, Laya, Perplexity, your own endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard gate (because small data lies)
&lt;/h2&gt;

&lt;p&gt;Once I added a learned risk model &lt;code&gt;P(error | state, question)&lt;/code&gt;, things got better — until I fed it 8 examples and it overfit so badly it was worse than the original threshold.&lt;/p&gt;

&lt;p&gt;So I added a hard gate: &lt;strong&gt;if the learned risk model doesn't beat a simple confidence threshold on holdout data, the cascade falls back to the threshold automatically.&lt;/strong&gt; It's visible in the response as &lt;code&gt;usage.reason = "risk_gate"&lt;/code&gt;, so you can see exactly when it happens.&lt;/p&gt;

&lt;p&gt;This isn't glamorous, but it's the difference between "a research prototype" and "something you can actually run in production."&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmarks (with a 16GB GPU, n=200)
&lt;/h2&gt;

&lt;p&gt;I benchmarked against real releases of Clef-Flash (9.4B, GGUF Q4_K_M) and Laya (421M, ModernBERT-large) on two public datasets. Same hardware, same prompts, same evaluation script.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dataset&lt;/th&gt;
&lt;th&gt;decision-cascade 124M&lt;/th&gt;
&lt;th&gt;Clef-Flash 9.4B&lt;/th&gt;
&lt;th&gt;Laya 421M&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AG News — accuracy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.873&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.855&lt;/td&gt;
&lt;td&gt;0.662&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AG News — p50 latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;130 ms&lt;/td&gt;
&lt;td&gt;16 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RouterBench — accuracy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.542&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.442&lt;/td&gt;
&lt;td&gt;0.312&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RouterBench — p50 latency&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;21 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;170 ms&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A 124M model beating a 9.4B model sounds suspicious, so let me be precise about what this does and doesn't mean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It does mean&lt;/strong&gt; that on decision tasks (classification, escalation decisions), a small model with task-specific heads and calibrated escalation can outperform a much larger general model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not mean&lt;/strong&gt; the small model is better at free-form reasoning or generation. Clef wins easily there.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything is reproducible: &lt;code&gt;eval/competitors/reproduce_real.sh&lt;/code&gt; plus raw JSON in the releases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two demo games (because synthetic benchmarks aren't enough)
&lt;/h2&gt;

&lt;p&gt;Synthetic datasets don't answer "does this work where mistakes have a real cost?" So I built two games — fully local, no third-party assets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chess vs Stockfish:&lt;/strong&gt; 19% escalations close half the gap to "always search deep," at 38% of the full-search cost.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pac-Man-style maze:&lt;/strong&gt; cautious policy scores 94% of the maximum with 33% escalations — twice the greedy strategy.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The chess demo is the one I show people first. It makes escalation cost visible: you can literally watch the engine decide "this position is uncertain, but it doesn't matter — stay shallow."&lt;/p&gt;

&lt;h2&gt;
  
  
  Install and try it in 2 minutes
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip install decision-cascade
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fastest way to see it work, no GPU and no model downloads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DECISION_ENCODER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; uvicorn api.server.main:app --port 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This returns responses with &lt;code&gt;trained: false&lt;/code&gt; — that's honest, the default heads are random. For real quality, either download a pre-trained pack (v0.2.0) or run SFT on your own data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/slavabars/decision-cascade
&lt;span class="nb"&gt;cd&lt;/span&gt; decision-cascade
pip install -e &lt;span class="s2"&gt;".[train]"&lt;/span&gt;
python -m train.sft --config configs/sft/support.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trained weights land in &lt;code&gt;checkpoints/&lt;/code&gt; and are loaded automatically on server start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honest about the limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default heads are random.&lt;/strong&gt; No amount of clever cascade math saves you from untrained weights. You need SFT/RLCD or a weight pack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The risk model needs examples.&lt;/strong&gt; Out of the box it's no better than a threshold — and the hard gate admits this in the response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain calibration is on you.&lt;/strong&gt; AG News and RouterBench are public benchmarks, not your support tickets. Calibrate on your own data before trusting escalation rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cold start on CPU is slow.&lt;/strong&gt; First inference after a model load takes a few seconds. The hash encoder is the fastest way to test the pipeline without waiting for that.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why local-first matters (for me)
&lt;/h2&gt;

&lt;p&gt;Two reasons, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cost.&lt;/strong&gt; 70–90% of decisions in a typical product are routine. Escalating all of them is burning money for no quality gain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Privacy.&lt;/strong&gt; Some decisions shouldn't leave the building at all. A local cascade lets you keep sensitive cases on-prem while still escalating the ambiguous ones to a stronger model — if you choose to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The project speaks the SystemOne protocol, so it can act as a local gateway to Clef, Laya, or Perplexity Decider — or run entirely self-contained.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm working on next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A transformer encoder with attention masking over multiple states (for batching many decisions at once).&lt;/li&gt;
&lt;li&gt;Deeper integration examples with chat agents.&lt;/li&gt;
&lt;li&gt;More demo environments where escalation has a measurable cost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building something similar, I'd love to hear how you decide when to escalate. The math I'm using is not the only reasonable one, and I'm curious what trade-offs other people made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/slavabars/decision-cascade" rel="noopener noreferrer"&gt;github.com/slavabars/decision-cascade&lt;/a&gt;&lt;/p&gt;

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