DEV Community

Ali Süleyman TOPUZ
Ali Süleyman TOPUZ

Posted on Originally published at pub.towardsai.net on

More Agents, Smaller Gains: What Agensh, MAST and the Google Study Say About Your Orchestrator

Three papers went viral in my feed this month. Each one has a number that people are quoting without the sentence around it. So I read them, put the numbers side by side, and then ran my own small test in .NET with a local model.

Why I went looking

A few weeks ago I caught myself sketching a fourth agent onto a whiteboard. The pipeline already had a planner, a worker, and a reviewer. The reviewer kept letting bad output through, so my first instinct was to add a second reviewer.

I stopped, because I had written about exactly this trap before. When I audited my own agent stack, the most useful question turned out to be “who owns the loop?” Here the matching question is “who owns the coordination, and what does it cost?” I didn’t have a good answer, and the internet was offering me three loud ones:

  • Microsoft Research scaled a system called Agensh to 1,024 agents with no manager, and the headline was that more agents kept helping.
  • A Berkeley paper (MAST) was being quoted as “multi-agent systems fail 41% to 86.7% of the time.”
  • A Google Research study showed centralized multi-agent setups beating a single agent by about 81% on one benchmark and losing badly on another.

All three are real. None of them says what the shortest version of it says. Let me go through them in order, then show you the small harness I built so I could stop arguing with headlines and test the question on my own tasks.

Agensh: the curve is real, and it is flattening

Agensh comes from Microsoft Research (“Agensh: Scaling Organizational Intelligence to 1,024 Agents”, arXiv 2609.26781). JIN’s write-up, 1,024 Agents, No Manager, frames it well: Agensh removes the orchestrator, but the manager’s job doesn’t disappear. It moves into shared infrastructure. From the coverage I could read, that infrastructure is a Git workspace, a message bus, and a structured board where workers record findings, failed approaches, claims on subtasks, and intentions. Every worker runs the same loop: gather context, claim something, resolve overlaps by messaging, do the work. Roles like integrator and reviewer reportedly emerge from identical prompts as the population grows.

That is a genuinely interesting design. Here are the numbers people are sharing:

  • On ProgramBench (the hardest tasks they selected), the mean pass rate goes from 19.31% with one agent to 28.78% with 128. That is about 9.5 points, or a 49% relative gain.
  • On Pandoc, the pass rate goes from 33.89% with one agent to 55.06% with 1,024. That is about 21 points.

Now read the Pandoc curve more slowly. Roughly 17 of those 21 points arrive with the first 127 extra agents. The next 896 agents add about 4. I did the division because I wanted to feel it:

PANDOC, MARGINAL GAIN PER ADDED AGENT (approximate, from the paper's curve)
---------------------------------------------------------------------------
1 -> 128 agents ~17 points / 127 agents ~0.134 points per agent
128 -> 1,024 agents ~4 points / 896 agents ~0.004 points per agent
---------------------------------------------------------------------------
The last 896 agents are worth roughly 30x less each than the first 127.
Enter fullscreen mode Exit fullscreen mode

That is not a criticism of the system. Diminishing returns are normal. The problem is what’s missing next to the curve.

There is no orchestrated baseline at the same budget. The comparison is Agensh with N agents versus Agensh with one agent. The paper names orchestrator-worker systems (Codex subagents, Claude Code subagents and agent teams, Copilot fleet, Kimi Agent Swarm) as the thing it is moving away from, but I could not find a run where a conventional planner plus workers got the same compute and was measured on the same tasks. Without that, “no manager” and “more compute” are tangled together. I can’t tell how much of the 21 points is the design and how much is simply 1,024 times more attempts at the problem.

There is no cost data. No tokens, no dollars, no wall-clock per correct result. For a system whose whole pitch is scale, that is the number I need most. If 1,024 agents cost 1,024 times as much as one agent (it’s probably not that simple, since agents idle and share context), then the last 4 points on Pandoc are some of the most expensive points in software.

And going back to JIN’s framing: if the manager’s role moved into the board and the message bus, then the manager’s cost moved there too. Every worker reading the board, posting claims, and resolving overlaps is spending tokens on coordination. That spend is exactly the thing that grows with agent count, and it is exactly the thing that isn’t reported.

MAST: the 41% to 86.7% range is not a failure rate

This is the one I most want to correct, because I almost repeated it myself.

The paper is “Why Do Multi-Agent LLM Systems Fail?” by Mert Cemri and colleagues at UC Berkeley (arXiv 2503.13657). Its real contribution is MAST, a taxonomy of 14 failure modes in three categories: system design issues, inter-agent misalignment, and task verification. The current version builds it from 1,600+ annotated traces across seven frameworks, with high human agreement on the labels (Cohen’s kappa of 0.88).

The viral version is “multi-agent systems fail between 41% and 86.7% of the time.” That sentence is wrong in three ways:

  1. It’s a range across frameworks, not a distribution for “multi-agent systems.” Each number is one framework’s failure rate in that study. The 86.7% is AppWorld.
  2. Each framework ran on a different benchmark. Coding frameworks ran on coding tasks, math setups on math, AppWorld on its own app tasks. A framework failing 86.7% of a hard benchmark and another failing 41% of an easier one tells you about the benchmarks as much as the architectures.
  3. There’s no single-agent row in that comparison. You can’t use it to claim multi-agent is worse than single-agent, because the same tasks were not run with one agent next to it.

Jaroslaw Wasowski, in Adding Agents Doesn’t Fix Your Problem, gets this closer to right than most: he cites 86.7% as the worst of the frameworks, not an average, and uses it to argue that adding agents treats a symptom of bad decomposition and missing evaluation. I agree with his conclusion. I just don’t think the number carries as much weight as the argument does.

Even the category split moved between versions of the paper. The latest version puts system design issues at about 44%, inter-agent misalignment at about 32%, and task verification at about 24%. If the shares shift that much as the dataset grows, I’d treat any single percentage from this paper as a snapshot, not a constant.

What MAST is good for is a checklist. When my own pipeline fails, I now ask: was the role spec wrong, did two agents disagree about state, or did nobody verify the output before declaring done? That’s useful. “86.7% of agents fail” is not.

The Google study: the only one that controlled for budget

The third source is Google Research’s “Towards a Science of Scaling Agent Systems”, which JIN discusses in More Agents Isn’t the Upgrade You Think It Is. It is the most careful of the three, as far as I can tell:

  • 180 configurations, four benchmarks (Finance-Agent, BrowseComp-Plus, PlanCraft, Workbench), five architectures (single agent, plus independent, centralized, decentralized and hybrid multi-agent), three model families.
  • On Finance-Agent , a centralized multi-agent setup beat the single agent by about 81% (Google’s blog says 80.9%; JIN rounds it to 80.8%).
  • On PlanCraft , a sequential planning task, every multi-agent variant got worse, by 39% to 70%.
  • Errors spread differently: independent agents amplified errors about 17.2x , while a central coordinator held that to about 4.4x.

Two details from secondary coverage of the paper matter a lot for me, and I’d check them against the PDF before building on them. First, the authors reportedly matched the total reasoning-token budget across architectures, giving the single agent more steps to compensate. That is the control Agensh doesn’t have. Second, they describe a saturation point: once a single agent is already above roughly 45% accuracy on a task, adding agents tends to help less or hurt.

The lesson I take is not “multi-agent bad.” It’s that the shape of the task decides. Finance-Agent decomposes into parallel lookups. PlanCraft is a chain where each step needs the last one. And the 17.2x versus 4.4x figure is a quiet argument for keeping a coordinator, which is the opposite of the Agensh headline. Both can be true: a coordinator contains errors, and a coordinator is also a bottleneck at scale. Where your system sits on that trade-off depends on how many agents you actually run. Most of us run three, not 1,024.

Side by side

Here is the table I wanted before I started. Same columns for each source: how many agents, what pass rate or change, and what we know about cost.

+--------------------+-------------+--------------------------+------------------------------+
| SOURCE / TASK | AGENTS | PASS RATE / CHANGE | COST REPORTED? |
+--------------------+-------------+--------------------------+------------------------------+
| Agensh ProgramBench| 1 | 19.31% | No |
| | 128 | 28.78% (+9.5 pts) | No |
+--------------------+-------------+--------------------------+------------------------------+
| Agensh Pandoc | 1 | 33.89% | No |
| | ~128 | ~51% (+17 pts, approx.) | No |
| | 1,024 | 55.06% (+21 pts) | No |
+--------------------+-------------+--------------------------+------------------------------+
| Google Finance- | 1 vs | +80.9% relative for | Token budget reportedly |
| Agent | centralized | centralized | matched across architectures |
+--------------------+-------------+--------------------------+------------------------------+
| Google PlanCraft | 1 vs any | -39% to -70% relative | Same as above |
| | multi-agent | | |
+--------------------+-------------+--------------------------+------------------------------+
| MAST (per | varies | 41% to 86.7% FAILURE, | No, and different benchmark |
| framework) | | each on its own benchmark| per framework |
+--------------------+-------------+--------------------------+------------------------------+
Enter fullscreen mode Exit fullscreen mode

The column that bothers me is the last one. Two of three sources don’t tell me what the extra agents cost. So I built a way to measure it myself, at a scale a laptop can handle.

The experiment: one agent vs. a planner and two workers

I wanted the smallest possible version of the question that anyone can actually answer on their own machine: on the same tasks, with the same local model, does a planner plus two workers produce more correct answers per token than one agent?

I also added the baseline Agensh is missing. If the multi-agent setup spends four model calls per task, it is only fair to give the single agent a second call too. So there are three strategies:

STRATEGY AGENTS CALLS PER TASK WHAT IT DOES
------------------------------------------------------------------------
single 1 1 Solve, return JSON
single+check 1 2 Solve, then recompute and fix
planner+2 3 4 Plan into 2 subtasks, 2 workers
                                       solve, planner synthesizes
Enter fullscreen mode Exit fullscreen mode

The task set is deliberately boring, because boring is checkable. There are two shapes, mirroring the Google split:

  • split tasks have two independent parts (multiply two numbers, sort a list). This is the shape where decomposition should help.
  • chain tasks are five arithmetic steps in a row, each depending on the last. This is the PlanCraft-like shape where decomposition should hurt.

Every answer is JSON, checked by exact match in code. No LLM judges anything. The planner may declare a plan “sequential”, in which case worker 2 gets worker 1’s output. I didn’t want to rig the multi-agent setup to fail by forcing parallelism on a chain.

Cost is tracked from what Ollama returns on every call: prompt_eval_count (input tokens) and eval_count (output tokens). Because a local model has no invoice, my main cost metric is tokens per correct answer. If you pass prices, it also prints an illustrative dollar figure so you can see what the same token counts would cost on a hosted model.

Setup

You need Ollama and the .NET 8 SDK (or newer).

# Option A: native install (macOS: brew install ollama, Linux: see ollama.com)
ollama serve &
# Let the two workers actually run at the same time
export OLLAMA_NUM_PARALLEL=2

ollama pull qwen2.5:7b
ollama pull llama3.2:3b

# Option B: Docker
docker run -d --name ollama -p 11434:11434 \
  -e OLLAMA_NUM_PARALLEL=2 \
  -v ollama:/root/.ollama ollama/ollama
docker exec ollama ollama pull qwen2.5:7b
docker exec ollama ollama pull llama3.2:3b

dotnet new console -n AgentScaleBench
cd AgentScaleBench
# replace Program.cs with the file below, then:
dotnet run -c Release -- --model qwen2.5:7b --runs 3 --tasks 20
dotnet run -c Release -- --model llama3.2:3b --runs 3 --tasks 20

# optional: illustrative pricing per million tokens (input, output)
dotnet run -c Release -- --model qwen2.5:7b --runs 3 --price-in 0.15 --price-out 0.60
Enter fullscreen mode Exit fullscreen mode

If Ollama runs on another machine, set OLLAMA_URL=http://that-host:11434 or pass --endpoint.

The harness

No NuGet packages, just HttpClient and System.Text.Json. I kept it in one file so you can paste it.

// AgentScaleBench: one agent vs. one agent + self-check vs. planner + two workers,
// against a local Ollama model, on a small set of tasks with exact-match checkers.
// No NuGet packages. .NET 8+.
//
// Usage:
// dotnet run -c Release -- --model qwen2.5:7b --runs 3
// dotnet run -c Release -- --model llama3.2:3b --runs 3 --tasks 20 --price-in 0.15 --price-out 0.60
//
// Writes results.csv (one row per attempt) and prints an ASCII summary table.

using System.Diagnostics;
using System.Globalization;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
var cfg = Config.Parse(args);
var ollama = new Ollama(cfg);
var tasks = TaskSet.Generate(cfg.TaskCount, seed: 42);
Console.WriteLine($"Model: {cfg.Model} | tasks: {tasks.Count} | runs: {cfg.Runs} | endpoint: {cfg.Endpoint}");
var strategies = new IStrategy[] { new SingleAgent(), new SingleWithSelfCheck(), new PlannerTwoWorkers() };
var attempts = new List<Attempt>();
foreach (var strategy in strategies)
{
    for (var run = 0; run < cfg.Runs; run++)
    {
        foreach (var task in tasks)
        {
            var ledger = new Ledger();
            var sw = Stopwatch.StartNew();
            string? final = null;
            string? error = null;
            try { final = await strategy.SolveAsync(task, ollama, ledger, cfg.Seed + run); }
            catch (Exception ex) { error = ex.GetType().Name + ": " + ex.Message; }
            sw.Stop();
            var correct = final is not null && Checker.IsCorrect(task, final);
            attempts.Add(new Attempt(strategy.Name, strategy.Agents, task.Id, task.Kind, run, correct,
                ledger.Calls, ledger.PromptTokens, ledger.OutputTokens, sw.Elapsed.TotalSeconds, error));
            Console.Write(correct ? "." : "x");
        }
    }
    Console.WriteLine($" {strategy.Name}");
}
Report.WriteCsv(attempts, "results.csv");
Report.PrintTable(attempts, cfg);
// ---------------------------------------------------------------------------
// Strategies
// ---------------------------------------------------------------------------
interface IStrategy
{
    string Name { get; }
    int Agents { get; }
    Task<string?> SolveAsync(EvalTask task, Ollama llm, Ledger ledger, int seed);
}
sealed class SingleAgent : IStrategy
{
    public string Name => "single";
    public int Agents => 1;
    public Task<string?> SolveAsync(EvalTask task, Ollama llm, Ledger ledger, int seed) =>
        llm.ChatJsonAsync(Prompts.Solver, task.Prompt, ledger, seed)!;
}
// Same agent, second pass. This is the "spend the extra budget on one agent" baseline.
sealed class SingleWithSelfCheck : IStrategy
{
    public string Name => "single+check";
    public int Agents => 1;
    public async Task<string?> SolveAsync(EvalTask task, Ollama llm, Ledger ledger, int seed)
    {
        var draft = await llm.ChatJsonAsync(Prompts.Solver, task.Prompt, ledger, seed);
        var review = $"TASK:\n{task.Prompt}\n\nPROPOSED ANSWER:\n{draft}\n\n" +
                     "Recompute every step yourself. If the proposed answer is wrong, fix it. " +
                     "Return only the final JSON in the exact format the task asks for.";
        return await llm.ChatJsonAsync(Prompts.Solver, review, ledger, seed + 1);
    }
}
// Planner splits into exactly two subtasks, two workers solve them,
// planner synthesizes. Sequential plans pass worker 1's output to worker 2.
sealed class PlannerTwoWorkers : IStrategy
{
    public string Name => "planner+2";
    public int Agents => 3;
    public async Task<string?> SolveAsync(EvalTask task, Ollama llm, Ledger ledger, int seed)
    {
        var planJson = await llm.ChatJsonAsync(Prompts.Planner, task.Prompt, ledger, seed);
        var plan = JsonNode.Parse(planJson ?? "{}");
        var mode = plan?["mode"]?.GetValue<string>() ?? "parallel";
        var subtasks = plan?["subtasks"]?.AsArray().Select(n => n?.ToString() ?? "").ToList() ?? new();
        while (subtasks.Count < 2) subtasks.Add("Solve the whole task: " + task.Prompt);
        string? r1, r2;
        if (mode == "sequential")
        {
            r1 = await llm.ChatJsonAsync(Prompts.Worker, subtasks[0], ledger, seed + 1);
            r2 = await llm.ChatJsonAsync(Prompts.Worker,
                $"{subtasks[1]}\n\nRESULT OF THE PREVIOUS STEP:\n{r1}", ledger, seed + 2);
        }
        else
        {
            var t1 = llm.ChatJsonAsync(Prompts.Worker, subtasks[0], ledger, seed + 1);
            var t2 = llm.ChatJsonAsync(Prompts.Worker, subtasks[1], ledger, seed + 2);
            await Task.WhenAll(t1, t2);
            (r1, r2) = (t1.Result, t2.Result);
        }
        var synth = $"ORIGINAL TASK:\n{task.Prompt}\n\nWORKER 1 ({subtasks[0]}):\n{r1}\n\n" +
                    $"WORKER 2 ({subtasks[1]}):\n{r2}\n\n" +
                    "Combine the worker results into the final answer. " +
                    "Return only the final JSON in the exact format the original task asks for.";
        return await llm.ChatJsonAsync(Prompts.Planner, synth, ledger, seed + 3);
    }
}
static class Prompts
{
    public const string Solver =
        "You solve small exact tasks. Think carefully, then reply with JSON only, in the exact format requested.";
    public const string Planner =
        "You are a planner. When given a task, split it into exactly two subtasks for two workers. " +
        "Reply with JSON only: {\"mode\": \"parallel\" | \"sequential\", \"subtasks\": [\"...\", \"...\"]}. " +
        "Use \"sequential\" if the second subtask needs the first one's result. " +
        "Each subtask must be self-contained: copy every number and word it needs. " +
        "When asked to combine worker results, reply with the final JSON the original task asks for.";
    public const string Worker =
        "You are a worker. Solve exactly the subtask you are given. " +
        "Reply with JSON only: {\"result\": <your result>}.";
}
// ---------------------------------------------------------------------------
// Ollama client with token accounting
// ---------------------------------------------------------------------------
sealed class Ledger
{
    private int _calls, _prompt, _output;
    public int Calls => _calls;
    public int PromptTokens => _prompt;
    public int OutputTokens => _output;
    public void Add(int prompt, int output)
    {
        Interlocked.Increment(ref _calls);
        Interlocked.Add(ref _prompt, prompt);
        Interlocked.Add(ref _output, output);
    }
}
sealed class Ollama(Config cfg)
{
    private readonly HttpClient _http = new() { BaseAddress = new Uri(cfg.Endpoint), Timeout = TimeSpan.FromMinutes(5) };
    public async Task<string?> ChatJsonAsync(string system, string user, Ledger ledger, int seed)
    {
        var body = new
        {
            model = cfg.Model,
            stream = false,
            format = "json",
            messages = new[]
            {
                new { role = "system", content = system },
                new { role = "user", content = user }
            },
            options = new { temperature = cfg.Temperature, seed, num_ctx = 4096 }
        };
        var payload = JsonSerializer.Serialize(body);
        HttpResponseMessage resp;
        try
        {
            resp = await _http.PostAsync("/api/chat", new StringContent(payload, Encoding.UTF8, "application/json"));
        }
        catch (HttpRequestException)
        {
            // One retry for dropped keep-alive connections. Not counted as an extra model call.
            await Task.Delay(500);
            resp = await _http.PostAsync("/api/chat", new StringContent(payload, Encoding.UTF8, "application/json"));
        }
        using var _ = resp;
        resp.EnsureSuccessStatusCode();
        var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
        var prompt = json.TryGetProperty("prompt_eval_count", out var p) ? p.GetInt32() : 0;
        var output = json.TryGetProperty("eval_count", out var o) ? o.GetInt32() : 0;
        ledger.Add(prompt, output);
        return json.GetProperty("message").GetProperty("content").GetString();
    }
}
// ---------------------------------------------------------------------------
// Tasks and checking
// ---------------------------------------------------------------------------
sealed record EvalTask(string Id, string Kind, string Prompt, Dictionary<string, string> Expected);
static class TaskSet
{
    // Two shapes on purpose:
    // "split": two independent parts, the case where decomposition should help.
    // "chain": every step depends on the previous one, the case where it should hurt.
    public static List<EvalTask> Generate(int count, int seed)
    {
        var rng = new Random(seed);
        var list = new List<EvalTask>();
        for (var i = 0; i < count; i++)
            list.Add(i % 2 == 0 ? Split(rng, i) : Chain(rng, i));
        return list;
    }
    private static EvalTask Split(Random rng, int i)
    {
        int a = rng.Next(12, 99), b = rng.Next(12, 99);
        var nums = Enumerable.Range(0, 6).Select(_ => rng.Next(1, 200)).Distinct().ToList();
        var sorted = string.Join(",", nums.OrderBy(n => n));
        var prompt =
            $"Answer two independent questions.\n" +
            $"Part 1: What is {a} * {b}?\n" +
            $"Part 2: Sort these numbers in ascending order: {string.Join(", ", nums)}.\n" +
            "Reply with JSON: {\"p1\": <integer>, \"p2\": \"<comma-separated numbers, no spaces>\"}";
        return new EvalTask($"split-{i:00}", "split", prompt,
            new() { ["p1"] = (a * b).ToString(), ["p2"] = sorted });
    }
    private static EvalTask Chain(Random rng, int i)
    {
        var x = rng.Next(5, 40);
        var start = x;
        var steps = new List<string>();
        for (var s = 0; s < 5; s++)
        {
            switch (rng.Next(3))
            {
                case 0: { var k = rng.Next(3, 30); x += k; steps.Add($"add {k}"); break; }
                case 1: { var k = rng.Next(2, 15); x -= k; steps.Add($"subtract {k}"); break; }
                default: { var k = rng.Next(2, 4); x *= k; steps.Add($"multiply by {k}"); break; }
            }
        }
        var prompt =
            $"Start with {start}. Then, in order: {string.Join(", then ", steps)}.\n" +
            "Reply with JSON: {\"answer\": <integer>}";
        return new EvalTask($"chain-{i:00}", "chain", prompt, new() { ["answer"] = x.ToString() });
    }
}
static class Checker
{
    public static bool IsCorrect(EvalTask task, string modelJson)
    {
        try
        {
            var node = JsonNode.Parse(modelJson)?.AsObject();
            if (node is null) return false;
            foreach (var (key, expected) in task.Expected)
            {
                if (!node.TryGetPropertyValue(key, out var v) || v is null) return false;
                if (Normalize(v.ToString()) != Normalize(expected)) return false;
            }
            return true;
        }
        catch { return false; }
    }
    private static string Normalize(string s)
    {
        s = s.Trim().Trim('"').Replace(" ", "").Replace("[", "").Replace("]", "");
        return double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d) && !s.Contains(',')
            ? d.ToString(CultureInfo.InvariantCulture)
            : s;
    }
}
// ---------------------------------------------------------------------------
// Reporting
// ---------------------------------------------------------------------------
sealed record Attempt(string Strategy, int Agents, string TaskId, string Kind, int Run, bool Correct,
    int Calls, int PromptTokens, int OutputTokens, double Seconds, string? Error);
static class Report
{
    public static void WriteCsv(List<Attempt> attempts, string path)
    {
        var sb = new StringBuilder("strategy,agents,task,kind,run,correct,calls,prompt_tokens,output_tokens,seconds,error\n");
        foreach (var a in attempts)
            sb.AppendLine(string.Join(",", a.Strategy, a.Agents, a.TaskId, a.Kind, a.Run, a.Correct, a.Calls,
                a.PromptTokens, a.OutputTokens, a.Seconds.ToString("F2", CultureInfo.InvariantCulture),
                "\"" + (a.Error ?? "").Replace("\"", "'") + "\""));
        File.WriteAllText(path, sb.ToString());
    }
    public static void PrintTable(List<Attempt> attempts, Config cfg)
    {
        var pricing = cfg.PriceInPerM > 0 || cfg.PriceOutPerM > 0;
        var rows = new List<string[]>();
        var header = new List<string> { "STRATEGY", "AGENTS", "SLICE", "PASS", "PASS %", "CALLS/TASK", "TOKENS/TASK", "TOKENS/CORRECT", "SEC/CORRECT" };
        if (pricing) header.Add("$/CORRECT (illustr.)");
        foreach (var group in attempts.GroupBy(a => a.Strategy))
        {
            foreach (var slice in new[] { "all", "split", "chain" })
            {
                var g = group.Where(a => slice == "all" || a.Kind == slice).ToList();
                if (g.Count == 0) continue;
                var correct = g.Count(a => a.Correct);
                var tokens = g.Sum(a => (long)a.PromptTokens + a.OutputTokens);
                var seconds = g.Sum(a => a.Seconds);
                var row = new List<string>
                {
                    group.Key, g[0].Agents.ToString(), slice,
                    $"{correct}/{g.Count}",
                    (100.0 * correct / g.Count).ToString("F1", CultureInfo.InvariantCulture),
                    (g.Average(a => a.Calls)).ToString("F1", CultureInfo.InvariantCulture),
                    (tokens / (double)g.Count).ToString("F0", CultureInfo.InvariantCulture),
                    correct == 0 ? "n/a" : (tokens / (double)correct).ToString("F0", CultureInfo.InvariantCulture),
                    correct == 0 ? "n/a" : (seconds / correct).ToString("F1", CultureInfo.InvariantCulture)
                };
                if (pricing)
                {
                    var dollars = g.Sum(a => a.PromptTokens * cfg.PriceInPerM / 1e6 + a.OutputTokens * cfg.PriceOutPerM / 1e6);
                    row.Add(correct == 0 ? "n/a" : (dollars / correct).ToString("F6", CultureInfo.InvariantCulture));
                }
                rows.Add(row.ToArray());
            }
        }
        var all = new List<string[]> { header.ToArray() };
        all.AddRange(rows);
        var widths = Enumerable.Range(0, header.Count).Select(c => all.Max(r => r[c].Length)).ToArray();
        string Line() => "+" + string.Join("+", widths.Select(w => new string('-', w + 2))) + "+";
        string Fmt(string[] r) => "| " + string.Join(" | ", r.Select((v, c) => v.PadRight(widths[c]))) + " |";
        Console.WriteLine();
        Console.WriteLine(Line());
        Console.WriteLine(Fmt(header.ToArray()));
        Console.WriteLine(Line());
        string? last = null;
        foreach (var r in rows)
        {
            if (last is not null && r[0] != last) Console.WriteLine(Line());
            Console.WriteLine(Fmt(r));
            last = r[0];
        }
        Console.WriteLine(Line());
        var errors = attempts.Count(a => a.Error is not null);
        if (errors > 0) Console.WriteLine($"{errors} attempts threw errors (counted as failures). See results.csv.");
    }
}
sealed record Config(string Endpoint, string Model, int Runs, int TaskCount, double Temperature, int Seed,
    double PriceInPerM, double PriceOutPerM)
{
    public static Config Parse(string[] args)
    {
        string Get(string name, string fallback)
        {
            var i = Array.IndexOf(args, name);
            return i >= 0 && i + 1 < args.Length ? args[i + 1] : fallback;
        }
        double D(string name, string fb) => double.Parse(Get(name, fb), CultureInfo.InvariantCulture);
        return new Config(
            Get("--endpoint", Environment.GetEnvironmentVariable("OLLAMA_URL") ?? "http://localhost:11434"),
            Get("--model", "qwen2.5:7b"),
            int.Parse(Get("--runs", "3")),
            int.Parse(Get("--tasks", "20")),
            D("--temperature", "0.2"),
            int.Parse(Get("--seed", "7")),
            D("--price-in", "0"),
            D("--price-out", "0"));
    }
}
Enter fullscreen mode Exit fullscreen mode

A few choices I made on purpose:

  • Exact-match checking only. The moment an LLM grades the output, I’m measuring two models.
  • Errors count as failures. If the planner returns broken JSON, that’s a real cost of the architecture, not noise to filter out.
  • Seeds and low temperature (0.2) so reruns are close, plus --runs so you can see the variance instead of trusting one pass.
  • results.csv per attempt , so every number in the summary can be traced back to a row.

What I’m not giving you: a results table

I’m deliberately not publishing a results table here. That might sound like a cop-out in an article that complains about missing data, so let me explain.

The whole point of this piece is that a headline number travels without its context. If I post “planner+2 lost X% on chain tasks with qwen2.5:7b on my MacBook”, that number will get quoted without the prompts, the model, the hardware, the seed, or the 20-task sample size behind it. It would become exactly the kind of figure I spent the first half of this article picking apart.

The harness is more useful than any single table would be. Your model, your prompts, and your task shapes are what decide whether a second agent pays for itself. So instead of a table, here is how I read the output when I run it, and what I’d suggest you look at:

  1. Tokens per correct, not pass rate. A planner that wins 5 points of accuracy while spending 4x the tokens is a loss for most production budgets.
  2. single+check vs. planner+2. This is the comparison that tells you whether a “multi-agent” gain is about structure or just about spending more calls. If the self-check closes most of the gap, the extra agents were buying budget, not design.
  3. The split slice vs. the chain slice. If decomposition helps anywhere, it should be on split tasks. If it hurts anywhere, it should be on chain tasks. If your results don’t follow that pattern, that’s the interesting part, and worth a rerun with more tasks.
  4. The errors line. Broken planner JSON shows up as failed attempts in results.csv. With small local models this is often where the multi-agent setup loses, before any reasoning happens.
  5. Variance across runs. Run with --runs 3 or more and compare. If two runs of the same strategy differ by more than the gap between strategies, you don't have a finding yet.

Then swap in your own tasks. TaskSet.Generate is the only thing you need to change: give it real work from your domain with an exact checker, and the same three strategies run against it.

What I could not measure

Whoever runs this harness, me included, should be explicit about its limits, because this is where a small experiment and a big paper share the same weakness: it is easy to read past what’s missing.

  • Real money. Local inference has no bill. Tokens per correct answer is a proxy. The --price-in / --price-out dollar column is illustrative, and it assumes hosted pricing would be linear in tokens, which ignores caching discounts and batch pricing.
  • Electricity and hardware time. I report wall-clock seconds per correct answer, but that depends on my machine, on OLLAMA_NUM_PARALLEL, and on whether the model was already loaded. Treat it as relative within one run, not absolute.
  • Prompt caching distortion. Ollama can reuse a cached prompt prefix, which lowers prompt_eval_count on repeated system prompts. That probably flatters the multi-call strategies a little. I didn't try to correct for it.
  • Scale. Three agents is nothing like 128 or 1,024. This harness says nothing about Agensh’s regime, and it can’t reproduce its emergent roles or its board-based coordination.
  • Real tasks. Arithmetic and sorting are not ProgramBench or Pandoc. I chose them because I can check them exactly. Coding tasks would need a test runner and a sandbox, and that’s a different weekend.
  • Model breadth. Two small local models. Frontier models may decompose and synthesize much better, which could change the planner’s numbers in either direction.
  • The prompts. My planner prompt is one reasonable prompt, not an optimized one. A better planner prompt could close or widen the gap. I’d rather say that than pretend the architecture alone produced whatever you see.
  • Statistical confidence. With 20 tasks and 3 runs, small differences are noise. I’d want a few hundred tasks before claiming a gap under about 10 points.

What I’m doing with my whiteboard

I didn’t add the second reviewer. Here’s where I landed, at least for now:

  • I default to one agent and buy it a second pass before I add a second agent. If the self-check closes the gap, the extra agent was never about structure.
  • I only split work that is actually independent. If step two needs step one’s output, I keep it in one context. The Google PlanCraft numbers are the clearest warning in all three papers.
  • If I have more than two workers, I keep a coordinator. The 17.2x versus 4.4x error amplification gap is too big to ignore at the sizes I run.
  • I don’t quote a scaling curve without a cost column. Agensh may be the right design at 1,024 agents. I can’t tell, because nobody told me what the 1,024th agent costs.

The question I ask now isn’t “would another agent help?” It’s “what would one more call to the same agent buy me, and is the new agent cheaper than that?” That question is cheap to ask, and the harness above is how I ask it now.

Sources

Tags: AI Agents, Multi-Agent Systems, LLM, Dotnet, CSharp, Ollama, Software Architecture, Machine Learning, Benchmarking, Artificial Intelligence


Top comments (0)

⌂ 换网址