<?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: Dmitry Glhf</title>
    <description>The latest articles on DEV Community by Dmitry Glhf (@enorth).</description>
    <link>https://dev.to/enorth</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%2F3692962%2F5ab630ee-bc43-4b6d-9e26-d069fbdc8653.jpg</url>
      <title>DEV Community: Dmitry Glhf</title>
      <link>https://dev.to/enorth</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/enorth"/>
    <language>en</language>
    <item>
      <title>Does going multi-agent pay off, and can you auto-pick a pattern per task?</title>
      <dc:creator>Dmitry Glhf</dc:creator>
      <pubDate>Mon, 15 Jun 2026 12:14:32 +0000</pubDate>
      <link>https://dev.to/enorth/does-going-multi-agent-pay-off-and-can-you-auto-pick-a-pattern-per-task-15ld</link>
      <guid>https://dev.to/enorth/does-going-multi-agent-pay-off-and-can-you-auto-pick-a-pattern-per-task-15ld</guid>
      <description>&lt;p&gt;I ran six multi-agent patterns across three benchmarks and three models. By "a team of agents" I mean setups like critic-actor or an orchestrator with subordinates. Almost everywhere, the team lost to a single agent. It lost on accuracy and on cost, and on cost sometimes by 4x.&lt;/p&gt;

&lt;p&gt;This could have been a dull "MAS isn't worth it, let's go home" note. But there was an exception. The harder the task is for the model, the better the team's chances of winning. So I have two questions. Does the system's topology affect the result at all? And if it does, can we automatically pick a suitable pattern for a specific task? The answer to the second one is no, for now, and the most interesting part is exactly &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;I show the examples on the &lt;a href="https://github.com/ITMO-NSS-team/fedotmas-harness" rel="noopener noreferrer"&gt;FEDOT.MAS&lt;/a&gt; multi-agent framework, and all the experiment code (patterns, runs, and plots) lives in the repo. I'm measuring a tool I write myself. But the conclusions mostly come out against multi-agent, so this is more self-criticism than advertising.&lt;/p&gt;

&lt;h2&gt;
  
  
  MAS patterns
&lt;/h2&gt;

&lt;p&gt;Each pattern is a way to wire several model calls into one system in a particular order. For comparison I used the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;single.&lt;/strong&gt; One agent, one pass. The baseline everything else is compared against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;chain.&lt;/strong&gt; A two-step chain. One agent breaks the task down, the other solves it from the breakdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;voting.&lt;/strong&gt; Two agents solve the task independently, and a third judge merges their answers into one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eval_optimizer.&lt;/strong&gt; A generator proposes an answer; a critic accepts it or sends it back for revision.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;orchestrator.&lt;/strong&gt; At each step a coordinator decides which subordinate works next, and keeps going until it says "done".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;blackboard.&lt;/strong&gt; Agents write to a shared board. These are a researcher, a skeptic-verifier, and an assembler.
The framework supports all these patterns and their combinations, and a system is assembled from primitives via an overloaded &lt;code&gt;+&lt;/code&gt; operator. Here's the eval_optimizer pattern:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fedotmas.adapters.pydantic_ai&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PydanticAI&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fedotmas.sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Condition&lt;/span&gt;

&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PydanticAI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;openrouter:openai/gpt-oss-20b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;takes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Solve the task; if the critic left a comment, take it into account.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task: {task}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Comment (if any): {verdict}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;critic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;critic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;takes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;revise&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Review the solution: approve if correct, otherwise revise.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Task: {task}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Solution: {draft}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;system&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;critic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;into&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# draft -&amp;gt; verdict, and round and round until the critic says approve
&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Condition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eq&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;approve&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# start point and run
&lt;/span&gt;&lt;span class="n"&gt;start_point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;verdict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;answer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_point&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other five are assembled similarly. One interesting note: a node in the system can be a single LLM agent, an entire MAS, or just a plain mechanical Python function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment setup
&lt;/h2&gt;

&lt;p&gt;For the tests I took several standard benchmarks of varying difficulty from different domains. GSM8K is grade-school math. MMLU is multiple-choice questions across domains from chemistry to history. LogiQA is logic problems. As executor models I used gpt-oss-20b, ministral-8b, and llama-3.1-8b. I didn't take the whole benchmark, just a 100-question subsample. For the multi-domain benchmarks I used a bootstrap sample with a fixed seed. Caveat: at this stage I only considered benchmarks that don't require tools for the agents.&lt;/p&gt;

&lt;p&gt;I gave each role a simple, direct prompt with no special tuning per pattern, and kept the prompts at the same level across all benchmarks. This was to measure the system architecture, not prompt engineering.&lt;/p&gt;

&lt;p&gt;We'll also need a reference point: a per-task oracle. This is as if an omniscient someone picked the ideal pattern for every task. It's the ceiling that shows how much you could squeeze out of selection in principle.&lt;/p&gt;

&lt;p&gt;One important caveat. The oracle chooses in hindsight, from already-computed runs. So there's an assumption that on a rerun the chosen pattern would give roughly the same accuracy. That's why the choice is taken over the average of three runs, not one, so it reflects the pattern's stable behavior on the task, not a one-off lucky hit.&lt;/p&gt;

&lt;h2&gt;
  
  
  First result: easy tasks
&lt;/h2&gt;

&lt;p&gt;Let's start easy. On GSM8K and MMLU the picture is the same. The single agent sits on the cost/accuracy frontier, and there's nowhere to push it from there.&lt;/p&gt;

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

&lt;p&gt;On GSM8K with gpt-oss-20b, single gives 0.94 for 398 tokens per task. The best multi-agent pattern gives 0.95, but at 1692 tokens. A one-point difference sits within the noise, and you pay 4x more. On MMLU it's exactly the same. Any "smart" layer on top of a single pass either doesn't help or slightly hurts, while always costing 2-4x more.&lt;/p&gt;

&lt;p&gt;On GSM8K with gpt-oss-20b, single gives 0.94 for 398 tokens per task. The best multi-agent pattern gives 0.95, but at 1692 tokens. A one-point difference sits within the noise, and you pay 4x more. On MMLU it's exactly the same. Any "smart" layer on top of a single pass either doesn't help or slightly hurts, while always costing 2-4x more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The harder the task, the more selection makes sense
&lt;/h2&gt;

&lt;p&gt;Now the interesting part. If it were only "MAS isn't worth it", the article would end here. But let's look at that same gpt-oss-20b across benchmarks in increasing difficulty.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tic6fcuzua4sjl8ak5s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0tic6fcuzua4sjl8ak5s.png" alt="What happens as the benchmark gets harder" width="800" height="514"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The model is the same, but the gap between the best fixed pattern and the per-task oracle grows. On GSM8K it's +3 points, on MMLU already +6, and on LogiQA +9. So the harder the task is for the model, the more you could gain by picking a pattern per individual task.&lt;/p&gt;

&lt;p&gt;This headroom reflects how hard the task is for the model, not the model's size. The small ministral-8b on easy GSM8K gives only +1 point of headroom, because it's strong there. But llama-3.1-8b of the same class, yet weak on these tasks (0.63-0.80), gives +11…16 points. It's not about parameter count, but about how hard the task is for that specific executor. In some cases a critic helps, in others a single pass is enough.&lt;/p&gt;

&lt;p&gt;So there's something to choose between.&lt;/p&gt;

&lt;h2&gt;
  
  
  If selection matters, let's select automatically
&lt;/h2&gt;

&lt;p&gt;An obvious solution suggests itself. Put a selector in front of the system, an agent that picks a pattern from the task text. In FEDOT.MAS the selector is a single agent with a limited choice from a menu of patterns. Did it improve quality?&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

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

&lt;p&gt;Here are the numbers on LogiQA with gpt-oss-20b. The single agent gives 0.77, random pattern choice 0.73, the selector only 0.71, and the oracle 0.89. The selector didn't just fall short of the ceiling. It came out worse than both the single agent and a random guess. And it's like that everywhere. In none of the columns did the selector beat the single agent. It only beat random choice where the options contained an obvious outlier, like the looping orchestrator.&lt;/p&gt;

&lt;p&gt;It's not that the selector is "dumb". It's about information. Which pattern solves this particular task on this particular model is an empirical fact that lives in the results matrix, not in the task text. You can't derive it from the wording alone. Tasks look the same but get solved by different patterns for reasons that simply aren't in the text. So a smarter judge won't save you here either. The problem isn't intelligence, it's the absence of signal at the input.&lt;/p&gt;

&lt;p&gt;What to do about it? One option is to train the selector on examples like "this kind of task on this model is best solved by this pattern". That's fine-tuning already, and a topic for a separate discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions and what's next
&lt;/h2&gt;

&lt;p&gt;What did we learn, and what might be useful in practice?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An orchestrator on a weak model is dangerous. Without a hard step limit it loops and burns tokens.&lt;/li&gt;
&lt;li&gt;Pattern selection only pays off on tasks that are hard for the model.
It's worth honestly noting a weak spot in the measurement. In patterns with verifier checking, the verifier is the same model as the generator, so it essentially judges itself, with no independent signal. That's why the honest next step leads to where multi-agent has a principled way to win. That's code with real test execution, like HumanEval, where the critic stops being a "second opinion" and becomes a test runner with a real signal. Or hard benchmarks like GAIA2 that require tools and interaction with the outside world.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, all the results are on GitHub and can be reproduced on other models.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How Finite State Machines Make an Agent More Reliable, and Where pydantic-graph Fits In</title>
      <dc:creator>Dmitry Glhf</dc:creator>
      <pubDate>Thu, 04 Jun 2026 21:23:30 +0000</pubDate>
      <link>https://dev.to/enorth/how-finite-state-machines-make-an-agent-more-reliable-and-where-pydantic-graph-fits-in-2imp</link>
      <guid>https://dev.to/enorth/how-finite-state-machines-make-an-agent-more-reliable-and-where-pydantic-graph-fits-in-2imp</guid>
      <description>&lt;p&gt;When you write a library, sooner or later you run into the engine. Not the pretty external interface, not the wrappers, but the part inside that drives the process through its states: it generated something, checked it, decided what to do next, repeated. A couple of flags, a &lt;code&gt;while&lt;/code&gt; loop, a big &lt;code&gt;if&lt;/code&gt; in the middle, and a month later you can't even remember which transitions are possible in there and why one of the branches is unreachable.&lt;/p&gt;

&lt;p&gt;Recently I was building exactly this kind of engine and stumbled onto a library that does the job noticeably more cleanly. It's called pydantic-graph. Hardly anyone writes about it, even though the whole of pydantic-ai (the agent framework from the authors of Pydantic) is built on top of it. Below I'll walk through it with a concrete example: a reliability harness for weak language models.&lt;/p&gt;

&lt;p&gt;Let me clarify the term up front, since it's everywhere right now. A harness isn't just MCP, skills, and memory. It's also robustness, including for very small models. It's that second part I'm using as the example. But the article isn't so much about models as about the approach itself. The main idea is simple: this is a convenient way to assemble an engine for anything that has states and transitions, without drowning in your own loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: the model is capable, but trips over small things
&lt;/h2&gt;

&lt;p&gt;The example I worked everything out on is this:&lt;/p&gt;

&lt;p&gt;Take a small model (in my case it's Llama 3.1 8B Instruct) and ask it to solve problems from HumanEval+ (a code benchmark for LLMs). Low-resource models don't exactly fail on such tasks: they succeed about half the time. Off the cuff, with no scaffolding at all, this model scores a pass@1 of around 0.61. Roughly speaking, about two attempts out of five end up in the trash, even though the solution is usually almost correct and stumbles on something trivial.&lt;/p&gt;

&lt;p&gt;I ran 20 samples per problem across all 164 problems and looked through the error log. Here are the characteristic examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NameError: name 'encode_cyclic' is not defined&lt;/code&gt;. The model called a helper function that it forgot to write out itself.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max() arg is an empty sequence&lt;/code&gt;. The empty input isn't handled, and the solution crashes on it.&lt;/li&gt;
&lt;li&gt;The most common case: the code runs without a single complaint, but returns the wrong thing on a hidden test. The logic diverged from the requirement by one step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All these failures share a common trait, and it matters for everything that follows. Each of them is visible automatically, and once an error is visible without a human in the loop, you can react to it without knowing anything about the model's internals and without touching its weights. That observation is where the next idea came from.&lt;/p&gt;

&lt;p&gt;The same 164 problems, 20 samples per problem, &lt;code&gt;temp=0.2&lt;/code&gt;. Figures are for hidden tests (plus), with public tests (base) in parentheses. The pass@k curve (number of attempts):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;k&lt;/th&gt;
&lt;th&gt;plus&lt;/th&gt;
&lt;th&gt;base&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;pass@1&lt;/td&gt;
&lt;td&gt;0.607&lt;/td&gt;
&lt;td&gt;0.665&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pass@2&lt;/td&gt;
&lt;td&gt;0.654&lt;/td&gt;
&lt;td&gt;0.712&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pass@5&lt;/td&gt;
&lt;td&gt;0.698&lt;/td&gt;
&lt;td&gt;0.758&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pass@10&lt;/td&gt;
&lt;td&gt;0.722&lt;/td&gt;
&lt;td&gt;0.782&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The idea: the engine drives the agent
&lt;/h2&gt;

&lt;p&gt;The idea is actually trivial. We treat the agent as a black box and run it through a series of special nodes that work with that box. Generated an answer, checked it; if it's bad, we append to the prompt what the problem was and generate again. Once the attempt budget runs out, we return the best of the variants and honestly mark it as imperfect.&lt;/p&gt;

&lt;p&gt;The crucial condition this was all built around: in this loop, only the model should &lt;em&gt;think&lt;/em&gt;, and only in one place. Generation is so far the single step where there's any nondeterminism. Checking, the "fix or give up" decision, and assembling the next prompt are ordinary, predictable code.&lt;/p&gt;

&lt;p&gt;A construction like this is natural to describe as a finite state machine. Nodes are states, edges are transitions, with strict types at the seams. This is where the library enters the frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  pydantic-graph: a state graph built on types
&lt;/h2&gt;

&lt;p&gt;It installs separately; you don't need to drag all of pydantic-ai along with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add pydantic-graph&lt;span class="o"&gt;==&lt;/span&gt;1.105.0  &lt;span class="c"&gt;# version at the time of writing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's worth knowing about this library: it's a state-graph engine built entirely on types. A node is a class, and a transition is expressed by the return type of a method. It sounds unusual, but in practice it's convenient, because the type checker and the IDE can see the topology of the graph: you can't silently draw an impossible edge.&lt;/p&gt;

&lt;p&gt;The library itself doesn't have to know anything about language models. Yes, pydantic-ai is built on top of it, but at its core it's a general-purpose state machine. It suits any process with states: an order-processing pipeline, a state machine in a game, retries when calling an external service. What won me over is that it reads like ordinary Python without hidden magic, while the graph stays verifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent contract: a prompt in, a string out
&lt;/h2&gt;

&lt;p&gt;For the engine to work with any agent, it needs a minimal shared language. As a first approximation I decided to limit it to a single function: a prompt goes in, a string comes out.&lt;/p&gt;

&lt;p&gt;This is the narrow waist through which anything can be threaded into the engine. In this design, &lt;code&gt;AgentFn&lt;/code&gt; doesn't depend on scale, so you can wrap it around any chunk of the system. You can put the harness on a single agent inside a multi-agent system, in which case &lt;code&gt;AgentFn&lt;/code&gt; is exactly that agent, while the planner and the other executors stay outside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t1aoc71a4d9tmty3dc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5t1aoc71a4d9tmty3dc0.png" alt="Harness on a single agent:  raw `AgentFn` endraw  is just the executor you need; the rest of the team is outside." width="800" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or you can wrap it around the whole team. The planner, the executors, all the internal orchestration get hidden inside, and from the outside the harness still sees a single agent: a prompt in, a string out. The engine doesn't care what's going on outside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw56f6cjl7quj48846ju1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw56f6cjl7quj48846ju1.png" alt="The same contract around the whole team: the entire multi-agent system hides behind a single  raw `AgentFn` endraw ." width="800" height="423"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the graph is built
&lt;/h2&gt;

&lt;p&gt;The working nodes return into a central decision node (&lt;code&gt;Decide&lt;/code&gt;).&lt;/p&gt;

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

&lt;p&gt;Here's what one node looks like in code. Pay attention to the signature of the &lt;code&gt;run&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;That &lt;code&gt;-&amp;gt; Validate&lt;/code&gt; is the detail that kept me on the library. It's not a comment or a line in a config; it's a real type annotation. The type checker knows that from the generation node you can only get to the validation node. If I mistakenly return a node that shouldn't be reachable at that point, it gets flagged in the IDE before I even run anything.&lt;/p&gt;

&lt;p&gt;Everything is assembled with a builder.&lt;/p&gt;

&lt;p&gt;A quick word on the roles of the nodes. The generation node is the only place with the model. The validation node is deterministic: it reduces the messy semantics of an answer to a simple verdict: passed or not, what kind of error, a short reason, the fraction of checks passed. The decision node (&lt;code&gt;Decide&lt;/code&gt;) is pure logic over that verdict, the history of attempts, and the remaining budget, with no model inside. The repair node assembles the next prompt from the last error and calls generation again. The fallback node fires when the attempts run out: it returns the best-scoring variant and marks the result as degraded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validators: swappable checks in a single slot
&lt;/h2&gt;

&lt;p&gt;The validation node (&lt;code&gt;Validate&lt;/code&gt;) doesn't know in advance what counts as a correct answer. It accepts any callback of the form "string in, verdict out," and that turns it into a slot you can plug anything into.&lt;/p&gt;

&lt;p&gt;There's room to get creative here. The cheapest checks are mechanical: parsing, a linter, type checking, validating JSON against a schema, the plain "does it even run?" They're universal and work on any string, but the signal they give is weak: they only catch format issues and crashes. My sense is that the strong signal is always domain-specific (for code, that's running the tests). On top of that you can hang a model in the judge role, either one judge or a whole ensemble.&lt;/p&gt;

&lt;p&gt;What's more, a combination of validators is itself a validator, from the engine's point of view. So no matter how many there are, to the engine it's always a single slot. The complexity hides in the combinator, and the graph stays the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repair node and where it can grow
&lt;/h2&gt;

&lt;p&gt;Right now my repair node is fairly simple: here's the error, regenerate taking the feedback into account. A kind of prompt engineering. But you can build in different repair strategies. For example, route by error type: if it crashed at runtime, try a bigger model. Or run several models supplied by the user and take the best candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came out in the end?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;pass@1 on HumanEval+&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Baseline&lt;/td&gt;
&lt;td&gt;0.607 ± 0.034&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;With harness&lt;/td&gt;
&lt;td&gt;0.623 ± 0.035&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The harness raises pass@1 from 0.607 to 0.623. The deviation came out to ±0.034, and at first glance the gain dissolves into it. But both modes were run on the same 164 problems, so what you should look at is the paired per-problem difference, and that comes to +0.016 ± 0.007, i.e. consistently positive, if small.&lt;/p&gt;

&lt;p&gt;What I liked along the way, beyond the numbers themselves: all of it turned out to be extensible and strictly typed. The graph reads directly in the IDE, and the transitions can't be mixed up unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The idea of applying finite state machines in development isn't new in itself. But the library that implements it struck me, personally, as interesting and underexposed. I won't claim everyone needs it. Nor will I risk saying that finite-state logic is the one right solution for the idea in this article.&lt;/p&gt;

&lt;p&gt;If the model is already fine-tuned to answer reliably in the required format, the corresponding harness validator simply stops doing anything, and the absence of a gain here is a perfectly logical outcome. If you have a completely linear pipeline with no branching, pydantic-graph will be overkill, since a plain function is enough.&lt;/p&gt;

&lt;p&gt;But if the process branches, has states, and it matters to you that the IDE holds its topology for you, the library fits well. And, to repeat, it doesn't have to be about language models at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ITMO-NSS-team/harness-research" rel="noopener noreferrer"&gt;All the code is packaged into a library and available on GitHub.&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Automating machine learning with AI agents</title>
      <dc:creator>Dmitry Glhf</dc:creator>
      <pubDate>Tue, 06 Jan 2026 01:08:59 +0000</pubDate>
      <link>https://dev.to/enorth/automating-machine-learning-with-ai-agents-1i5c</link>
      <guid>https://dev.to/enorth/automating-machine-learning-with-ai-agents-1i5c</guid>
      <description>&lt;p&gt;When solving competitions on Kaggle, you start to notice a pattern. It's easy to create a baseline: upload the data, run CatBoost or LightGBM, and get the baseline metric. It takes half an hour. But to get into the top solutions, you need to try dozens of preprocessing options, hundreds of feature combinations, and thousands of hyperparameter sets.&lt;/p&gt;

&lt;p&gt;Existing AutoML systems don't help much. They work according to a fixed scenario: they try a predetermined set of algorithms, select the best one according to the metric, and return the result. AutoGluon trains several models and creates a multi-level ensemble, but each run starts from scratch. TPOT generates a pipeline through a genetic algorithm, but does not learn from the mistakes of previous runs.&lt;/p&gt;

&lt;p&gt;The main problem is that these systems do not reason. They do not analyze why a particular approach worked or failed. They do not adapt to the specifics of the task. They do not accumulate experience between runs. Each new task is like the first for them.&lt;/p&gt;

&lt;p&gt;Humans work differently. If a data scientist sees unbalanced classes, they immediately know that stratification and threshold selection are needed. If they have seen a similar task before, they apply what worked then. If the first attempt fails, they analyze why and try a different approach.&lt;/p&gt;

&lt;p&gt;With the advent of language models, it became possible to create a system that works closer to humans. LLMs can analyze data, reason about method selection, and learn from examples. But one model is not enough. It can miss an obvious mistake or get stuck on the wrong approach. We need an architecture that allows the system to check itself and accumulate experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea with two agents
&lt;/h2&gt;

&lt;p&gt;The first version was simple: one agent receives data, trains the model, and returns predictions. The problem quickly became apparent. LLM sometimes skips data checks or forgets to process gaps.&lt;/p&gt;

&lt;p&gt;The solution came from reinforcement learning. In Actor-Critic methods, one agent acts, while the second evaluates these actions. Why not apply this approach to AutoML?&lt;/p&gt;

&lt;p&gt;The Actor receives data and a set of tools for analyzing, processing, and training models. It explores the dataset, decides what steps are needed, and generates a solution. The Critic looks at the result from the outside and checks if everything is done correctly. If the Critic finds problems, the Actor receives feedback and tries again.&lt;/p&gt;

&lt;p&gt;This architecture solves a key problem: one agent may make mistakes, but two agents with different roles will catch most of them.&lt;/p&gt;

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

&lt;p&gt;The diagram shows how the system works. Actor has access to five specialized MCP servers with tools for working with data and models. Critic works without tools, only analyzing Actor's reports. There is an iterative exchange between them: the decision from Actor goes to Critic for evaluation, and feedback is returned. After each iteration, the experience is stored in memory, from where it is retrieved when working with similar tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools for the agent
&lt;/h2&gt;

&lt;p&gt;LLM can reason on its own, but it needs tools to work with data. I have divided them into several categories: data preview, statistical analysis, processing, and model training.&lt;/p&gt;

&lt;p&gt;For example, the preview tool returns structured information:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"shape"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"columns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sepal_length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sepal_width"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"petal_length"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"petal_width"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"species"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dtypes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"sepal_length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Float64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"species"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"String"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sample"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"sepal_length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"species"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"setosa"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent sees the dimension, column types, and first rows. This is sufficient to make decisions about further steps.&lt;/p&gt;

&lt;p&gt;An important point for data processing: the agent must apply the same transformations to train and test. If the categorical feature was encoded as {“red”: 0, “blue”: 1} in the training sample, the same encoding must be used in the test sample. To do this, mappings are saved in JSON files:&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="n"&gt;mapping_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_mapping.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mapping_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mapping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is critical for categorical classification. If the target variable was encoded as numbers, the model will return numbers, but we need the original class labels.&lt;/p&gt;

&lt;p&gt;Each training tool returns three things: the path to the model, the path to predictions, and metrics on train. Paths are generated with a timestamp and UUID, so the agent can experiment with multiple algorithms simultaneously without conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model Context Protocol
&lt;/h2&gt;

&lt;p&gt;But what happens if you need to add more tools, for example, for a specialized domain? When the number of tools exceeds ten, problems arise with their support, management, and scaling.&lt;/p&gt;

&lt;p&gt;Model Context Protocol, and in particular the &lt;a href="https://gofastmcp.com/getting-started/welcome" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; framework, solves this problem. MCP allows you to package tools into separate servers that the agent calls as needed.&lt;/p&gt;

&lt;p&gt;I created five MCP servers: file_operations for working with files, data_preview for CSV previews, data_analysis for statistics, data_processing for transformations, and machine_learning for training models and ensemble predictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation of decisions
&lt;/h2&gt;

&lt;p&gt;Actor generates a solution in the form of a structured report with four sections: data analysis, preprocessing, model training, and results. Critic receives this report and analyzes each section separately.&lt;/p&gt;

&lt;p&gt;Instead of one LLM judge, I used four specialized ones. The first checks the quality of data analysis: whether the agent studied the distribution, checked for gaps, and detected anomalies. The second looks at preprocessing: whether gaps are processed correctly, categories are encoded correctly, and there is no data leakage. The third evaluates the choice of model and hyperparameters. The fourth analyzes the results and overall methodology.&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="n"&gt;judges&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;LLMJudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluate data_analysis: Is exploration thorough?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;LLMJudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluate preprocessing: Are steps appropriate?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;LLMJudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluate model_training: Is selection justified?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;LLMJudge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Evaluate results: Are metrics calculated correctly?&lt;/span&gt;&lt;span class="sh"&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;Each judge returns a score between 0 and 1 with justification. The average score is compared to the acceptance threshold (usually 0.75). If higher, the decision is accepted. Otherwise, Critic forms feedback from all judges' comments and passes it to Actor for the next iteration.&lt;/p&gt;

&lt;p&gt;This approach is more stable than a single judge. A single LLM may be too strict or miss an obvious error. Four specialized judges smooth out subjectivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolated workspace
&lt;/h2&gt;

&lt;p&gt;When an agent works with files, it should not be given access to the entire file system. Isolation is necessary. I created a dedicated directory for each session in &lt;code&gt;~/.scald/actor/&lt;/code&gt; with three subdirectories: data for copies of the source data, output for intermediate files, and workspace for models and predictions.&lt;/p&gt;

&lt;p&gt;The source CSV files are copied to data dir. All tools work only within these directories. The agent cannot accidentally overwrite important files or read other people's data.&lt;/p&gt;

&lt;p&gt;After completion, all artifacts are copied to the session directory with a timestamp, and the workspace is cleared. After some time, you can open this directory and understand what exactly the agent did: which models it trained (load them from .pkl), what metrics it received, and what steps it performed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory and learning
&lt;/h2&gt;

&lt;p&gt;After each iteration, the system saves the experience. The Actor report and Critic evaluation are recorded in the ChromaDB vector database. When the agent receives a new task, the system searches for similar past solutions based on semantic similarity using the &lt;a href="https://jina.ai/embeddings" rel="noopener noreferrer"&gt;Jina&lt;/a&gt; embedding model.&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;actor_solution&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_solution&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;critic_evaluation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;critic_evaluation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;iteration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;iteration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Search for similar
&lt;/span&gt;&lt;span class="n"&gt;actor_memory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;critic_memory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;actor_report&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;actor_solution&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;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;top_k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&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 solutions found are passed to the agent as context. If the system has previously solved a similar classification task with unbalanced data, it will remember what helped then.&lt;/p&gt;

&lt;p&gt;Interestingly, it is not only successful solutions that are useful. When Critic says, “you forgot to process the gaps,” this is valuable information for future tasks. Semantic search finds such cases as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Main cycle
&lt;/h2&gt;

&lt;p&gt;When all components are ready, all that remains is to put them together. The cycle runs until the maximum number of iterations is reached or the Critic makes a decision. At each iteration, the Actor solves the problem taking into account feedback, the Critic evaluates the solution, the experience is stored in memory, and relevant context is extracted for the next attempt.&lt;/p&gt;

&lt;p&gt;It is interesting to observe how the Actor learns from feedback. The first iteration is usually simple: basic preprocessing and one model. The Critic finds problems: “you did not check the class balance” or “you did not do feature engineering.” The second iteration is more accurate: the agent adds the missing steps, tries several models, and creates an ensemble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems during development
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Encoding the target variable
&lt;/h3&gt;

&lt;p&gt;The first version failed on categorical classification. The agent encoded the target into numbers, trained the model, but forgot to decode the predictions back. The output was numbers instead of class labels.&lt;/p&gt;

&lt;p&gt;The solution required explicit instructions in the system prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If you encode target column, you MUST DECODE predictions before returning.
Use decode_categorical_label with the mapping path from encoding step.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Unique file names
&lt;/h3&gt;

&lt;p&gt;When the agent experiments with multiple models, the files overwrite each other. I tried to delegate this to the agent via prompt, but LLM does not always generate unique names. The correct solution turned out to be to do this at the tool level with timestamp and UUID.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was the result?
&lt;/h2&gt;

&lt;p&gt;To fully solve the initial tasks of deep analysis, preprocessing, and training on data, it is necessary to continue developing the system by adding specialized agents and tools. Nevertheless, the system is already working: running it on a dataset for several iterations gives predictions, and all intermediate results are saved.&lt;/p&gt;

&lt;p&gt;I tested the system on tasks from OpenML. On the christine dataset, it showed an F1-score of 0.743, outperforming Random Forest (0.713) by 4% and surpassing AutoGluon with FLAML, which failed to cope with this task at all. On cnae-9, the result was 0.980 against 0.945 for the best competitor, FLAML, which is 3.5% better.&lt;/p&gt;

&lt;p&gt;There were also failures. On the Australian dataset, the system showed 0.836, losing to AutoGluon (0.860) and other baseline methods. Interestingly, on blood-transfusion, the result of 0.756 was better than Random Forest (0.712) and AutoGluon (0.734), but worse than FLAML (0.767).&lt;/p&gt;

&lt;p&gt;The cost of running varies from $0.14 to $3.43 depending on the complexity of the task and the number of iterations. The running time is unpredictable: from a minute to half an hour.&lt;/p&gt;

&lt;p&gt;In fact, the value of the result is not that the metric is better or worse than a particular AutoML framework. The value lies in the fact that modern LLMs allow for more intelligent automation of routine tasks, while the modularity of MCP paves the way for the creation of an ecosystem of specialized agents. This solves the original “fixed scenario” problem of classic AutoML: now the system can be adapted to any task by simply connecting the necessary agents, while maintaining a single cycle of iterative improvement and experience accumulation.&lt;/p&gt;

&lt;p&gt;The limitations are clear. The system is good for tabular data with gradient boosting algorithms. For deep learning or time series, other tools are needed. Quality depends heavily on the size of the underlying LLM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;For those who want to try it, installation is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;scald
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can use it either from the terminal via CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scald &lt;span class="nt"&gt;--train&lt;/span&gt; data/train.csv &lt;span class="nt"&gt;--test&lt;/span&gt; data/test.csv &lt;span class="nt"&gt;--target&lt;/span&gt; price &lt;span class="nt"&gt;--task-type&lt;/span&gt; regression
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or via Python API:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scald&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Scald&lt;/span&gt;

&lt;span class="n"&gt;scald&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Scald&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;predictions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scald&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;train&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/train.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# .csv or dataframe
&lt;/span&gt;    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/test.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;target_column&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;task_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;classification&lt;/span&gt;&lt;span class="sh"&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;To work, you need an API key from a provider compatible with OpenAI (for example, &lt;a href="https://openrouter.ai/" rel="noopener noreferrer"&gt;OpenRouter&lt;/a&gt;). You will also need a key from &lt;a href="https://jina.ai/embeddings" rel="noopener noreferrer"&gt;Jina&lt;/a&gt; for embeddings in the memory system (the service provides a large number of free tokens upon registration).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/dmitryglhf/scald" rel="noopener noreferrer"&gt;All code is packaged in a library and available on GitHub.&lt;/a&gt;&lt;/p&gt;

</description>
      <category>machinelearning</category>
      <category>automation</category>
      <category>python</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
