DEV Community

Zaher Khateeb
Zaher Khateeb

Posted on

I Paid an LLM to Sabotage My Multi-Agent System (On Purpose)

The most dangerous agent in a multi-agent system isn't the one that crashes. It's the one that keeps talking.

I've spent the last year and a half building multi-agent infrastructure, and somewhere along the way I noticed something uncomfortable: the topology you pick for your agent mesh — who talks to whom — is quietly one of the biggest resilience decisions you make. And almost nobody tests it. We chaos-test our microservices, we kill pods in staging, we run game days for our databases. Then we wire five LLM agents together in whatever shape the framework tutorial suggested and ship it.

So for the Nebius Serverless AI Builders Challenge I built Balagan (Hebrew for "chaos" — if you know, you know): a chaos engineering harness for agent meshes. It injects faults into multi-agent systems and measures what happens to the quality of their collective decisions. This post covers what I built, why serverless was genuinely the right substrate for it rather than a checkbox, and what the heatmap says.

Repo: https://github.com/zahere/balagan · runs in 60 seconds offline in --mock mode, or end-to-end on Nebius Serverless.

The experiment

Three topologies, same task, same model:

  • Flat — all-to-all debate, two rounds, strict majority vote.
  • Hierarchical — four workers answer independently; one aggregator issues the final verdict.
  • Ring — sequential relay; each agent refines its predecessor's reasoning; the last agent decides.

Three conditions per topology:

  • Baseline — everyone behaves.
  • Crash-stop — one random agent is silent for the whole trial.
  • Byzantine — one random agent stays chatty but is covertly instructed to work out the right answer and argue for the opposite.

That last one is my favorite implementation detail: the traitor is just a system prompt. No forked model, no weight surgery — one prompt swap turns an honest verifier into a confident, articulate saboteur. If that doesn't slightly unsettle you about production agent systems, read it again.

The panel is five Qwen2.5-7B-Instruct agents at temperature 0, and everything is seeded — the dataset regenerates byte-identically, the victim of each trial is drawn from a per-trial RNG, and 25 trials fill every (topology × fault) cell. The task is deliberately easy — claim verification with objective ground truth on a deterministic synthetic dataset. That's methodology, not laziness: when the fault-free condition sits at 96–100% accuracy, every point of degradation you see under faults is attributable to the fault, not to the task being hard.

Why serverless, actually

Here's the shape of the workload: 225 independent trials, each a burst of 5–10 LLM calls, embarrassingly parallel, needed exactly once per model. That's a terrible fit for a rented GPU box (idle between bursts, babysitting required) and a perfect fit for two serverless primitives doing what they're each for:

  • A Nebius Serverless Endpoint serves Qwen2.5-7B-Instruct via vLLM — the GPU lives exactly as long as the model needs to be hot, and it's an OpenAI-compatible URL, so the harness doesn't know or care that it's on Nebius.
  • A Nebius Serverless Job runs the sweep itself — a CPU-only container that fans out trials with bounded concurrency, then releases compute the moment the sweep completes. Note the split: every GPU minute I pay for is a minute the model is actually serving tokens. The orchestration costs cents.

And the punchline the timing data handed me: the full 225-trial sweep — 1,400 endpoint calls, 339k tokens — took 63 seconds against one L40S. The endpoint's ~7-minute cold start took longer than the entire experiment it served. At that ratio, the economics aren't "GPU box vs serverless," they're "why would anyone babysit a lease for a one-minute burst." The measured bill for everything in this post — two full 225-trial sweeps, the demo run, a smoke test, and several kill-and-recover demos — was $3.64 at the billing snapshot: $3.50 of GPU endpoint, four cents of CPU sweep jobs, and less than a cent of Object Storage for the checkpoint layer the whole resilience story depends on. Call it six dollars all-in with the video-recording session.

The bug that would have killed the demo

My first version checkpointed to local disk. Reasonable, boring, wrong.

A Serverless Job's filesystem is released when the job VM goes away. So a local checkpoint survives a crash inside the process — and not the one failure that actually matters here, a cancelled job. My centerpiece demo would have printed resume: 0 already complete on camera and I would have deserved it.

The fix reframes the whole design: Object Storage is the checkpoint of record. And since S3 has no append, rewriting one big JSONL from a dozen concurrent workers would be a lost-update race waiting to happen. So each trial becomes its own object:

s3://<bucket>/balagan/full/trials/<trial_id>.json
Enter fullscreen mode Exit fullscreen mode

Resume is now a LIST call. Every write is atomic. Re-running a trial overwrites only itself. What started as a workaround for a missing filesystem turned out to be the more correct design — which is a lesson I keep re-learning about serverless: the constraint is usually telling you something true.

(The Nebius cookbook's own train-and-serve example uses Object Storage as the handoff from a training Job to a serving Endpoint. Balagan runs the same pattern inverted — the Endpoint feeds the Job, and the bucket carries the experiment's memory instead of a model's weights.)

The unexpected dividend: the sweep is now preemption-proof, so it runs on preemptible capacity with one flag — a preempted job resumes exactly like a cancelled one. The resilience feature turned out to be the cost feature.

A chaos harness had better survive chaos

Every endpoint call carries exponential-backoff retry. A trial that dies records an error row instead of taking down the sweep. And with the checkpoint outside the job, you can do this: launch the sweep as a Serverless Job, cancel it mid-run, resubmit the identical spec — and watch the first log line:

[balagan] run 'kill-demo': 225 trials total | resume: 140 already complete | 85 to run | mode: endpoint
Enter fullscreen mode Exit fullscreen mode

The resubmitted job then ran exactly those 85 — not one finished trial re-ran, not one was lost.

The moment after the kill: identical resubmit, and the checkpoint in Object Storage answers — 140 already complete, 85 to run.
The moment after the kill: identical resubmit, and the checkpoint in Object Storage answers — 140 already complete, 85 to run.

I killed my own benchmark mid-flight and it shrugged. A resilience benchmark that is itself resilient felt like the only honest way to build this.

(One production note that earns its keep: the sweep got so fast against a healthy endpoint that I had to add a deliberately slow kill-demo profile just to have time to kill it. Chaos engineering problem #1: your system recovering faster than you can break it.)

What the heatmap says

The resilience heatmap: 225 trials, 25 per cell, zero trial-level errors. Green survives; red is where a topology meets the fault it can't absorb.
The resilience heatmap: 225 trials, 25 per cell, zero trial-level errors. Green survives; red is where a topology meets the fault it can't absorb.

225 trials, 25 per cell, zero trial-level errors:

Topology baseline crash byzantine
flat 96% 100% 100%
hierarchical 100% 80% 92%
ring 96% 92% 88%

Three things jump out.

Flat majority voting absorbed everything I threw at it — 100% under both a crashed agent and a live saboteur. It also posted 96% on the fault-free baseline: one trial ended with the vote deadlocked, no majority either way. Five agents arguing is a distributed system even when nobody's attacking it.

Hierarchical's single point of failure isn't a metaphor — it's arithmetic. One random victim among five agents kills the aggregator once in five trials, an undecided mesh scores as wrong, and the measured accuracy under crash is: 80%. Exactly the structural prediction. But here's the twist I didn't expect — the byzantine condition hurt the hierarchy less than plain crash did (92% vs 80%). A saboteur in the aggregator seat turns out to be an imperfect liar, and honest worker reports drag it toward the truth besides. In a hierarchy, an incompetent traitor is survivable; a dead boss is not.

Ring is the fault-type victim. It degrades monotonically — 96% → 92% → 88% — and it's weakest under byzantine, because whoever speaks last owns the verdict, and a saboteur landing late in the chain gets the final word with nobody left to overrule it.

And the price tag on flat's immunity: 9.3 LLM calls and ~2,600 tokens per trial, versus hierarchical's 4.7 calls and ~930 tokens. Fault-tolerance-by-majority costs roughly 2.8× the tokens. That's a real engineering trade, and now it has numbers.

The price of resilience: each line drops from fault-free accuracy to worst-fault accuracy. Flat barely drops — and sits at 2.8× the tokens.
The price of resilience: each line drops from fault-free to worst-fault accuracy. Flat barely drops — at 2.8× the tokens.

One more chart earned its place, because it inverts an instinct: in agent meshes, getting faster can be a failure symptom. A hierarchical trial whose aggregator is dead returns quickly — there's nobody left to deliberate. A ring with a crashed member is a shorter ring. If you alert on latency spikes but not latency drops, a whole class of mesh failures walks right past your monitoring.

After the
Latency under fault: dead aggregators answer fast. If you only alert on latency spikes, this failure class walks past you.

Because a single 225-trial sweep costs about a minute, I ran the whole thing twice — second time with a hardened verdict parser — as a stability check (committed in results/stability_check/). Individual cells wobble by ±1 trial, which is what n=25 sampling does, but the invariant reproduced exactly: ten aggregator deaths across the two runs, ten total losses. The second run also taught me something the first missed: one flat/crash trial ended in a 2–2 tie, because killing one of five agents doesn't just remove a voter — it removes oddness. And a census of every failure across both runs: faulted meshes almost never decide wrong; they fail to decide at all. Except under byzantine — those failures are confidently wrong. Your monitoring can catch silence; it takes more to catch confidence.

The one-sentence takeaway: fault type matters more than fault count. One faulty agent out of five moved accuracy anywhere from zero points to twenty depending on which failure mode and which topology — a topology that shrugs off a crashed agent can be quietly poisoned by a Byzantine one, and vice versa.

Lessons learned

  1. On serverless, your checkpoint cannot live on the box. The job VM is cattle. Object Storage, one object per trial, resume by LIST — sixty-odd lines, and it's the difference between a benchmark and a demo.
  2. Deterministic mock mode paid for itself instantly. The entire pipeline is verifiable offline in seconds for $0, which meant my paid GPU minutes debugged integration, never logic. (The one bug that reached Nebius — an openai/httpx version conflict — was caught by a 10-trial smoke job in the first five minutes, exactly what smoke tests are for.)
  3. Byzantine agents are one prompt away. Sit with that before you wire untrusted context into your mesh.
  4. Keep the fault-free baseline at the ceiling. My first task mix included letter-counting claims — tokenization-hostile, so a 7B model flubs them sober. That would have confounded "the fault broke it" with "the task was hard." Easy task, clean signal.
  5. The surprise was the ranking flip. I expected byzantine to be strictly worse than crash everywhere. For hierarchical it was the opposite — at temperature 0, an LLM instructed to lie consistently still leaks the truth, while a dead aggregator decides nothing, always. Falsified expectations are what the harness is for.

Run it yourself

First, find your own system in the grid: if you run a supervisor/orchestrator framework, you live in the hierarchical row; a sequential chain or pipeline is the ring row; a debate-or-swarm-with-a-vote is the flat row. Your crashed agent is the teammate that timed out or hit a rate limit; your byzantine agent is the one whose context got poisoned by injected input. This isn't a metaphor — the repo ships a LangGraph adapter that runs the same three topologies as compiled StateGraphs (lg-flat, lg-hierarchical, lg-ring), so "bring your own topology" is literal.

pip install ., balagan run --config configs/demo.yaml --mock for the free tour; the runbook takes you to the full Nebius deployment in ~30 minutes. Swap in your own topology by implementing one async function — or one StateGraph. I'd genuinely love to see someone's production mesh shape in that heatmap.

Balagan is v0.1 of a bigger idea — next up: mid-protocol crash points, partition faults, and formally verifying topology protocols before empirically breaking them. If chaos engineering for agent systems is your kind of problem, the repo is open and so are my DMs.

Top comments (0)