It was 11 PM on a Tuesday in Dubai, and I was watching a single-LLM pipeline hallucinate its way through a government compliance report.
Not the fun kind of hallucination. Not the kind where the model invents a plausible-sounding startup name or fabricates a citation you can laugh off. This was the kind where a 70-page regulatory document got summarized into confident, beautifully formatted, completely wrong conclusions. The client demo was in fourteen hours. I remember staring at the terminal output, coffee going cold, thinking: this architecture is fundamentally broken.
That was the night I stopped trying to make a single prompt do everything.
Why I Stopped Believing in the One-Agent Solution#
Here is the thing nobody tells you when you are building your first LLM application. A single model call is seductive. You write one prompt, you get one response, you ship it. Clean. Simple. Elegant, even.
But then the real world shows up.
I had a research workflow that needed to search the web, extract and cross-reference content, analyze patterns in structured data, draft a coherent report, and then, critically, fact-check its own work. That is not one job. That is five jobs. Asking one prompt to handle all of them is like asking your restaurant's head chef to also wait tables, manage the books, wash the dishes, and park the cars. Sure, they could attempt it. But the risotto is going to suffer.
Multi-agent systems solve this the way any well-run kitchen does, by decomposing the work into specialized roles, each one focused enough to be excellent at its piece.
But I am getting ahead of myself. Let me show you what this actually looks like in code.
Core Architecture Patterns#
The Orchestrator Pattern#
The first pattern I reach for, and honestly the one I still use most, is the orchestrator. Think of it like a film director. The director does not act, does not operate the camera, does not design the sets. But they coordinate everyone who does.
class ResearchOrchestrator:
def __init__(self):
self.researcher = ResearcherAgent()
self.analyst = AnalystAgent()
self.writer = WriterAgent()
self.reviewer = ReviewerAgent()
async def execute(self, query: str) -> Report:
research = await self.researcher.gather(query)
analysis = await self.analyst.process(research)
draft = await self.writer.compose(analysis)
return await self.reviewer.validate(draft)
Each agent operates independently with a focused responsibility. The orchestrator coordinates the workflow, deciding what happens in what order, passing outputs downstream. It never tries to do the actual work itself.
This seems obvious on paper. It was not obvious to me the first three times I tried to build these systems.
State Management with LangGraph#
This is where things get interesting, and where I wish I had had better tooling two years ago.
LangGraph gives you a proper foundation for stateful agent workflows. Instead of passing context around in dictionaries and hoping nothing gets lost, which was my approach for longer than I would like to admit, you get an actual state graph with typed transitions and conditional routing.
from langgraph.graph import StateGraph, END
workflow = StateGraph(ResearchState)
workflow.add_node("research", research_node)
workflow.add_node("analyze", analyze_node)
workflow.add_node("write", write_node)
workflow.add_node("review", review_node)
workflow.add_edge("research", "analyze")
workflow.add_edge("analyze", "write")
workflow.add_conditional_edges(
"review",
should_revise,
{"revise": "write", "complete": END}
)
See that should_revise conditional edge? That is the part that changed how I think about these systems. In my early implementations, the pipeline was strictly linear. Research, analyze, write, done. No feedback loops. No quality gates. The output was whatever the last agent produced, for better or worse.
The conditional edge means the reviewer agent can send work back to the writer. It can say: "This does not meet the quality bar, try again." It is the difference between an assembly line and a writers' room. One pushes product forward regardless of quality, the other iterates until the work is actually good.
I cannot overstate how much this single capability improved output quality for our enterprise deployments. The first draft is almost never the best draft. That is true for humans, and it turns out it is true for agents too.
Critical Design Decisions#
Agent Autonomy vs. Control#
This is the question I get asked most at AI meetups in Dubai. I do not think there is a clean answer yet.
More autonomy enables creative problem-solving. An agent that can decide how to research a topic, choosing between web search, database queries, or API calls based on the query, will often find better paths than one locked into a rigid sequence. But more autonomy also means less predictability. And for government and enterprise clients in the UAE, the kind of clients I work with daily, unpredictability is not a feature. It is a liability.
So here is the framework I have landed on, at least for now:
- Bounded autonomy: Agents can make decisions within defined parameters. They pick from an approved set of tools, not the entire internet.
- Human-in-the-loop checkpoints: Critical decisions, anything involving financial data, legal interpretation, or external communications, require human approval before proceeding.
- Rollback capabilities: Any agent action can be undone, because it will need to be.
Is this the perfect balance? No. I still lose sleep over edge cases where an agent's bounded autonomy was not bounded enough. But it is a starting point that has kept us out of trouble on production deployments. In this domain, staying out of trouble matters more than being clever.
Error Handling, or Learning to Expect Failure#
Agents fail. Networks timeout. APIs rate-limit. LLM providers have outages at the worst possible moment. If there is one thing building distributed systems for fifteen years has taught me, it is this: everything that can break will break, usually on a Thursday afternoon right before a client presentation.
Design for failure from day one:
class ResilientAgent:
async def execute_with_retry(self, task, max_retries=3):
for attempt in range(max_retries):
try:
return await self._execute(task)
except TransientError as e:
if attempt == max_retries - 1:
raise
await asyncio.sleep(2 ** attempt)
Exponential backoff. Retry budgets. Circuit breakers. These are not exciting. Nobody writes conference talks about their retry logic. But they are the difference between a system that works in a demo and a system that works at 3 AM when you are asleep and a batch job is processing ten thousand documents for a ministry deadline.
I learned this the hard way. More than once.
Observability, Because You Cannot Debug What You Cannot See#
This one took me longer to internalize than I would like to admit. Early on, I treated logging as an afterthought, something to add once the core system worked. That was a mistake. When you have four agents passing state between them, and the final output is wrong, you need to know which agent made the bad decision and why.
Here is what I instrument now, on every multi-agent system, before writing any business logic:
- Agent decisions and reasoning traces, the full chain of thought, not just the final answer
- State transitions and timing, where did the pipeline spend its time? Where did it stall?
- Token usage and costs, because agent chains can make dozens of LLM calls per workflow, and your finance team will want to know why the API bill tripled
- Success and failure rates, broken down by agent, by task type, by time of day
Think of it like the black box on an airplane. You hope you never need to review the recordings. But when something goes wrong at altitude, that data is the only thing standing between you and a mystery.
Production Lessons, What I Wish Someone Had Told Me#
After deploying multi-agent systems for government and enterprise clients across the UAE and MENA region, here are the lessons that cost me the most time and stress to learn. I am sharing them so maybe they will cost you less.
Start simple. I cannot emphasize this enough. Begin with two agents before adding a third. Each new agent does not just add one more component. It adds coordination overhead, failure modes, and debugging complexity that scale non-linearly. My most successful deployments started with a two-agent system that worked reliably, then grew from there. My biggest headaches came from systems where I designed five agents on a whiteboard before writing a single line of code.
Define clear interfaces. Agents communicate through structured data, typed Pydantic models, JSON schemas, validated payloads, never free-form text. I broke this rule once on a prototype. The research agent passed natural language summaries to the analyst agent. It worked beautifully in testing. In production, one slightly malformed summary cascaded into three downstream failures that took a full day to diagnose. Schema validation is not glamorous, but it prevents the kind of cascading failures that make you question your career choices.
Monitor costs obsessively. A single user query can trigger a multi-agent workflow that makes thirty or forty LLM calls. Multiply that by a few hundred concurrent users, and you are looking at API bills that will make your CFO call an emergency meeting. Implement per-workflow budgets and circuit breakers. Set hard limits. Alert on anomalies. I have a Slack bot that pings me any time a single workflow exceeds its cost threshold, and it has saved us from some genuinely expensive runaway loops.
Test with adversarial inputs. Edge cases in multi-agent systems compound. What happens when the research agent returns empty results? What if the analyst agent produces an analysis that contradicts the source data? What if the reviewer agent gets stuck in an infinite revision loop? You need to answer these questions before your users discover them for you. Because they will discover them, at the worst possible time, in the most embarrassing possible way.
Where We Go From Here#
Multi-agent systems are becoming the standard pattern for complex AI applications. The tooling is maturing fast. LangGraph, CrewAI, AutoGen, and a new framework seemingly every week. That is exciting. It also means the landscape is shifting under our feet, and what counts as best practice today might be outdated in six months.
I do not have this fully figured out. Nobody does, not yet. What I know is that treating agents as distributed systems components, with all the rigor that implies for reliability, monitoring, and graceful degradation, has served me well. The hardest problems are not the AI parts. They are the same problems distributed systems engineers have been wrestling with for decades: coordination, failure handling, observability, and managing complexity as systems grow.
If you are building multi-agent systems, or thinking about it, I would genuinely love to hear what you are learning. The patterns I have shared here are the ones that have worked for me, on the specific kinds of enterprise and government projects I work on in this region. Your context might be different. Your lessons might be better.
We are all still writing the playbook on this one.