← Back to list

Why My Multi-Agent Router Ignored Hard Bans (Fix: 41 Lines)

41 lines fixed a regression that cost me 4 failed test runs. I assumed each layer’s prose ban was strong enough to override Sonnet’s…

Javier Collipal Saavedra · 2026-07-10 05:38 · 0 claps · 5.9 min read paywalled
#llmops #claude-code #ai-engineering #multi-agent-systems #prompt-engineering
Open on Medium ↗
Wiki topics: LLM · Large Language Models AGT · AI Agents OPS · LLMOps & Inference ML · Machine Learning ✍️ · Writing & Creative

Why My Multi-Agent Router Ignored Hard Bans (Fix: 41 Lines)

41 lines fixed a regression that cost me 4 failed test runs. I assumed each layer’s prose ban was strong enough to override Sonnet’s training prior. That assumption cost me four failed test runs.

The first time, I blamed the prompt. The second time, I blamed myself. By the fourth time, I realized Claude Code was seeing something I couldn’t ban away.

⚠️ The Regression That Refused to Die

It was Wave 5 of my cartridge-v2 overhaul, July 2026. I counted 13 new agent cartridges in my ~/.claude/agents/ directory, a CLAUDE.md router I measured at 107 lines using wc -l, and 25 meta-eval rubric tests I'd run and verified passing. The sprint looked clean.

Then I watched my architect subagent field-test a real task. It routed orchestrator.py wiring to the retired integrator — four times in a row. The task: add a deep_research_node after research_node that hits Tavily a second time and writes deep_research_sources to PipelineState. Expected routing: node → llmops-expert, wiring → llmops-expert, review → adversarial.

The node routing was fine. The wiring kept going to integrator.

The regression was reproducible. Across 4 iterations, the architect routed orchestrator.py wiring to the retired integrator agent — consistently, not randomly. This wasn't a random hallucination. It was a pattern.

At 2 AM, I watched the fourth failure scroll by and realized I was fighting the model’s training data, not my code.

🛡️ Three Defensive Layers That All Failed in This Case

I had built three layers of defense. All three failed in this case.

+-----------------------------+-------------------------------------------------------------------+----------------------------------------------------+
| Layer                       | Mechanism                                                         | Failure Reason                                     |
+-----------------------------+-------------------------------------------------------------------+----------------------------------------------------+
| Slot 1 HARD BAN block       | Explicit instruction: "never route to these 7 names" in bold caps | Training prior overwhelmed prose negation          |
| Slot 4 Retired Agents table | Markdown table explaining retirement and new owner                | Model read but didn't internalize the reassignment |
| Slot 7 SELF-CRITIQUE        | Scan own output for banned names before returning                 | Self-correction step was also overridden by prior  |
+-----------------------------+-------------------------------------------------------------------+----------------------------------------------------+

Layer 1: Slot 1 HARD BAN block. The cartridge’s opening slot contained an explicit instruction: “never route to these 7 names.” I wrote it in bold caps. It looked unbreakable.

Layer 2: Slot 4 Retired Agents table. This was a clean Markdown table explaining why each agent was retired and who owned its responsibilities. For integrator, the table read: "Retired. Wiring ownership moved to llmops-expert. orchestrator.py routing handled by llmops-expert exclusively."

Layer 3: Slot 7 SELF-CRITIQUE. Before returning a routing decision, the architect had to scan its own output for banned names. If it spotted one, it was supposed to self-correct.

I checked my work. The bans were in the right slots. The syntax was correct. The table was clear.

But the LLM ignored it all.

🔬 Why Prose Bans Lose to Training Data

My hypothesis is that Sonnet’s training data associated “integrator wires orchestrator.py” with the integrator agent. That prior is baked into the model's weights, reinforced across millions of training examples.

Prose bans are just words. The training prior is a statistical pattern embedded in the model’s internal representations. When a router LLM has seen “integrator” paired with “wiring orchestrator.py” thousands of times, a sentence saying “don’t route to integrator” is a weak signal competing against a strong, pre-trained pattern.

The model doesn’t fail to read the ban. It reads it and still picks the wrong token because the training prior wins the probability race.

This is the same reason prompt engineering alone can’t fix certain hallucinations. If the model learned X → Y during training, a runtime instruction saying “don’t do X → Y” is a nudge, not a command.

For routing decisions with no strong training prior, a concise prose ban plus table may suffice and keep cartridge size smaller — the exemplar overhead pays off only when the LLM consistently overrides negative rules.

My case had a strong prior. So I needed a different approach.

🛠️ The 3-Shot Fix That Finally Worked

What actually worked was adding 3-shot positive exemplars to Slot 4, immediately after the retirement table. The cartridge grew from 185 to 226 lines — the exemplar block added 41 lines (I confirmed this with diff <original> <new> | grep '^>' | wc -l).

Here’s what I put in:

yaml# Positive exemplars for retired-agent routing
# Always route to the new owner, never the old name

Exemplar A: task: wiring orchestrator.py to add new LangGraph node correct_output: agent: llmops-expert task: wire deep_research_node into orchestrator.py

Exemplar B: task: emit TSDoc for a TypeScript utility module correct_output: agent: frontend-expert task: add TSDoc to utils/parser.ts

Exemplar C: task: read-only diagnostic scan of failed CI pipeline correct_output: agent: adversarial task: diagnose CI failure from logs, no code changes

Verification clause: every task-brief must have agent: set to exactly one of exactly these 13 strings and NOTHING ELSE. ```

I also appended the exhaustive 13-agent list verbatim.

The first field test after the change was Test 6–1: the same deep_research_node task. This time, the architect routed node → llmops-expert, wiring → llmops-expert, review → adversarial. Correct.

Test 6–2: flaky test_content_generator investigation. Adversarial diagnostic, llmops-expert fix-plan. Clean.

Test 6–3: TSDoc + security audit on 2 TypeScript files. Frontend-expert + adversarial. Clean.

Zero retired-agent misroutings across 3 tests. The regression was gone.

📈 Positive Exemplars Beat Negative Rules

Why do positive exemplars work when bans fail?

My hypothesis is about how LLMs generate structured output. When the model sees a prompt, it predicts the next token based on patterns in its training data. A prose ban modifies the probabilities indirectly, the model might still favor the banned token because the training prior is stronger.

A positive exemplar does something different. It shows the model the exact token sequence you want in the exact position where the wrong token would appear. The model sees agent: llmops-expert in the exemplar, and when it's time to generate agent:, the exemplar's output shape is the most recent pattern match. The training prior doesn't disappear, but the local context becomes the dominant influence.

Think of it as anchoring. The exemplar anchors the model to the correct output format and token values. The negative rule only rules out, while the exemplar rules in.

This principle generalizes beyond Claude Code. In the multi-agent systems I’ve tested, any router LLM with a strong training prior for the wrong answer benefits from positive exemplars in the same output shape you expect back. Negative rules alone will lose.

📊 What This Means for Multi-Agent Systems

If you’re building multi-agent systems on Claude Code, LangGraph, or any orchestrator that uses an LLM for role-routing, you need to distinguish two cases:

  • No strong prior: A concise prose ban plus a reference table works fine. Keep cartridge size small. — Strong prior exists: Add 3-shot positive exemplars in the output shape you want. The 41-line overhead is worth avoiding a regression that wastes hours of debugging.

Your first instinct when you see a regression will be to add more negative rules. Try exemplars instead. They cost less than an evening of debugging.

My cartridge-v2 sprint taught me this the hard way. I assumed three defensive layers were enough. The fourth field test showed me I was wrong.

Now every cartridge in my ~/.claude/agents/ directory has a Slot 4 exemplar block for any routing decision with a known training prior. The total sprint landed 35 items, with Codex verdicts of 2x needs-attention and 1x APPROVE across Waves 0-6. The meta-eval dataset of 24 cases now runs clean.

What I Still Don’t Know

But here’s what still bothers me: I still don’t know why the SELF-CRITIQUE layer, a trained scan for banned names before returning output, failed to self-correct despite being explicitly designed to catch this exact failure mode. The model read the banned name, saw its own output, and apparently overrode its own verification step. That’s a different kind of training prior problem, and I haven’t cracked it yet.

I’m testing right now whether exemplars degrade beyond 8,000 tokens of conversation history. The answer will determine whether this fix scales or breaks.

My prediction: it scales, but not linearly. Each new exemplar adds diminishing returns. Three was magic for me. Six might be worse.

I’ll find out next sprint. If you’ve hit a similar wall, try adding 3-shot exemplars, then run your shortest section heading test and tell me what happened.

Key lesson

I’m betting the SELF-CRITIQUE layer fails because its embedding space overlaps with the training prior — prove me wrong by sharing a case where self-critique caught a banned name after 3 exemplars. That counterexample would force a rewrite of my cartridge design rules.


메타데이터
post_id
990fb1b57d60
slug
why-my-multi-agent-router-ignored-hard-bans-fix-41-lines-990fb1b57d60
url
https://medium.com/@javiercollipalsaavedra/why-my-multi-agent-router-ignored-hard-bans-fix-41-lines-990fb1b57d60
canonical_url
https://medium.com/@javiercollipalsaavedra/why-my-multi-agent-router-ignored-hard-bans-fix-41-lines-990fb1b57d60
author_url
https://medium.com/@javiercollipalsaavedra
status
ok
fetched_at
2026-07-10 16:32:07