{"slug": "i-added-more-ai-agents-to-the-problem-nothing-changed", "title": "I Added More AI Agents to the Problem. Nothing Changed.", "summary": "A developer built both single-agent and multi-agent versions of an LLM-powered customer support system and ran them through an identical eval suite, finding no difference across five properties including safety, intent accuracy, and groundedness. The multi-agent version cost five production types instead of one, 127 lines of code instead of 91, and at least two model calls per request instead of one, because the routing decision duplicated the existing intent classification and the specialists reused the same scoping, policy, and risk-gate objects. The developer concluded that splitting the caller changed who invokes the boundary but not what the boundary does.", "body_md": "*I built one agent and multi-agent versions, put them through the same tests, and learned what actually mattered.*\n\nPart 12 findings of an experiment: building an LLM-powered support agent with deterministic boundaries. The [companion repo](https://github.com/antoniolopescorreia/reliable-ai-support) contains the full code.\n\n\"We considered multi-agent and decided against the complexity\" is the most self-satisfied sentence in software architecture. It's also unfalsifiable, which is why it's so popular.\n\nSo I built the thing I was going to claim I didn't need. A triage agent that routes. A refund specialist owning the order tools and the approval gate. A knowledge specialist answering from the corpus. A coordinator holding them together.\n\nBoth versions implement the same interface, so the eval suite grades them without knowing which is which.\n\n```\n// AgentTeam: the coordinator, in full\npublic AgentRun run(EvalScenario scenario) {\n    AgentSession session = new AgentSession(scenario.customerId());\n    List<ScoredArticle> retrieved = knowledgeBase.search(Query.of(scenario.message()));\n    Handoff handoff = triage.route(scenario.message());\n\n    if (handoff.lane() == Lane.KNOWLEDGE) {\n        Answer answer = knowledgeSpecialist.answer(retrieved);\n        return new AgentRun(Classification.unclassified(), null,\n                answer.reply(), answer.fromKnowledge(), retrieved);\n    }\n\n    Result result = refundSpecialist.handle(session, handoff);\n    var classification = new Classification(\n            ActionType.PROCESS_REFUND, handoff.orderId(), \"requested via chat\");\n    return new AgentRun(classification, result, result.detail(), false, retrieved);\n}\n```\n\n`triage.route` calls the same `IntentClassifier` the single agent calls, and `refundSpecialist.handle` calls the same scoping, eligibility and gate objects. That reuse is the experiment being fair, not the experiment being rigged: change either one and the diff would measure my rewriting instead of the architecture.\n\n```\n$ ./gradlew architectureComparison\n\nPROPERTY             BASELINE  CANDIDATE    CHANGE\nsafety                  1.000      1.000    +0.000\ngate-outcome            1.000      1.000    +0.000\nintent-accuracy         0.875      0.875    +0.000\ngroundedness            1.000      1.000    +0.000\nanswered                0.667      0.667    +0.000\n\nFIXED   (0)\nBROKEN  (0)\n```\n\nNot a single scenario changes verdict. Not one property moves by a thousandth.\n\nWhat it cost, counted from the source: one production type became five, 91 lines of code became 127, one orchestration hop became two. On an LLM-backed stack, where each agent makes its own model call, one request would become at least two.\n\nThe routing decision *is* the intent classification. That already existed — the single agent has been doing it since post 6.\n\nAnd once routed, the specialists call the same scoping check, the same policy engine, and the same risk gate, in the same order. Not because I copied the code, but because that order is a business requirement. You cannot evaluate eligibility before you know the order is the customer's, and you cannot propose a refund before you know it's eligible.\n\nSplitting the caller changed who invokes the boundary. It didn't change what the boundary does — and the boundary is where every guarantee in this system lives.\n\n```\nflowchart LR\n    subgraph SA[\"Single agent\"]\n        direction LR\n        M1[\"Message\"] --> C1[\"Classify, retrieve\"] --> B1[\"Scope, eligibility, gate\"]\n    end\n    subgraph TM[\"Agent team\"]\n        direction LR\n        M2[\"Message\"] --> T[\"Triage<br/>(the same classify)\"] --> SP[\"Specialist\"] --> B2[\"Scope, eligibility, gate<br/>(the same objects)\"]\n    end\n    SA --> R[\"Identical on all<br/>5 eval properties\"]\n    TM --> R\n    classDef step fill:#eef2f6,stroke:#8fa3b8,color:#24313f\n    classDef same fill:#ecf2ed,stroke:#93b39d,color:#3d5344\n    class M1,C1,M2,T,SP step\n    class B1,B2,R same\n    style SA fill:#f7f9fb,stroke:#c5d1dc,color:#24313f\n    style TM fill:#f7f9fb,stroke:#c5d1dc,color:#24313f\n```\n\nI'm not arguing the pattern is useless. I use it daily in a different context: coding agents that delegate to subagents. One runs a broad search while another reads a diff, each with its own context window and tool set.\n\nThere, the split pays for itself immediately. The work is genuinely parallel, the contexts are genuinely separate, and a subagent burning through 40 files costs the parent nothing.\n\nNone of those conditions hold here. One customer message, one lane, sub-second work, one small tool set. The delegation would be a handoff with nothing to hand off.\n\nThe dishonest version of this post shows a strawman team and declares victory. So, plainly: what I built is the *structural* version of multi-agent. Separate responsibilities, a handoff, a coordinator.\n\nIt isn't agents that each make model calls and negotiate at runtime. That variant buys real things — per-role prompts, per-role tools, parallel execution. It also doubles the model calls, adds latency, and introduces a failure mode I don't have today: two agents disagreeing about what the customer wants.\n\nWhat it wouldn't do is move the deterministic boundary.\n\nFour things, and they're in the ADR so I can be held to them:\n\nThat last one is the real safeguard. `MultiAgentEquivalenceTest` runs both architectures on every build and asserts the difference is zero. The day it fails, this decision gets reopened by a test rather than by an argument.\n\nThe team stays in the repo — wired, tested, and not the default. Deleting it would turn evidence back into taste, and the whole point was to have grounds for the claim.\n\n*What's the architecture you rejected, and can you still run it?*", "url": "https://wpnews.pro/news/i-added-more-ai-agents-to-the-problem-nothing-changed", "canonical_source": "https://dev.to/tonal/i-added-more-ai-agents-to-the-problem-nothing-changed-1gph", "published_at": "2026-09-14 17:05:46+00:00", "updated_at": "2026-09-14 17:26:04.552995+00:00", "lang": "en", "topics": ["ai-agents", "large-language-models", "ai-tools", "developer-tools"], "entities": ["GitHub"], "alternates": {"html": "https://wpnews.pro/news/i-added-more-ai-agents-to-the-problem-nothing-changed", "markdown": "https://wpnews.pro/news/i-added-more-ai-agents-to-the-problem-nothing-changed.md", "text": "https://wpnews.pro/news/i-added-more-ai-agents-to-the-problem-nothing-changed.txt", "jsonld": "https://wpnews.pro/news/i-added-more-ai-agents-to-the-problem-nothing-changed.jsonld"}}