Multi-Agent Systems: Coordination, Communication, Consensus


Introduction





A single LLM agent has limitations: finite context windows, single perspective bias, and vulnerability to cascading errors. Multi-agent systems address these by distributing work across specialized agents that communicate, coordinate, and reach consensus. This article covers the architectural patterns for building effective multi-agent systems.





Agent Roles and Specialization





Each agent should have a narrow, defined role:






@dataclass


class AgentSpec:


name: str


role: str


system_prompt: str


tools: list[dict]


model: str




class Agent:


def __init__(self, spec: AgentSpec, llm_fn):


self.spec = spec


self.llm = llm_fn


self.message_history = [{"role": "system", "content": spec.system_prompt}]




async def process(self, task: str, context: str = "") -> str:


messages = self.message_history + [{"role": "user", "content": f"{context}\n\nTask: {task}"}]


response = self.llm(messages, tools=self.spec.tools)


self.message_history.append({"role": "assistant", "content": response})


return response




# Define specialized agents


researcher = Agent(AgentSpec(


name="Researcher",


role="Information retrieval and fact-checking",


system_prompt="You are a research specialist. Find accurate information and verify facts. Cite all sources.",


tools=[search_tool, web_scrape_tool],


model="claude-sonnet-4-20260512",


))




writer = Agent(AgentSpec(


name="Writer",


role="Content creation and editing",


system_prompt="You are a technical writer. Produce clear, well-structured content. Adapt tone to the audience.",


tools=[grammar_check_tool, style_guide_tool],


model="claude-sonnet-4-20260512",


))




critic = Agent(AgentSpec(


name="Critic",


role="Quality assurance and review",


system_prompt="You are a critical reviewer. Find errors, inconsistencies, and areas for improvement. Be thorough.",


tools=[],


model="claude-opus-4-20260512", # Stronger model for evaluation


))







Communication Protocols





Agents communicate through structured messages:






from enum import Enum


from datetime import datetime




class MessageType(Enum):


TASK = "task"


RESULT = "result"


QUERY = "query"


RESPONSE = "response"


FEEDBACK = "feedback"


COORDINATION = "coordination"




@dataclass


class AgentMessage:


id: str


sender: str


recipients: list[str]


type: MessageType


content: str


metadata: dict = None


timestamp: str = None




def __post_init__(self):


if self.timestamp is None:


self.timestamp = datetime.now().isoformat()




class MessageBus:


def __init__(self):


self.queues: dict[str, list[AgentMessage]] = {}


self.broadcast_log: list[AgentMessage] = []




def send(self, message: AgentMessage):


for recipient in message.recipients:


if recipient not in self.queues:


self.queues[recipient] = []


self.queues[recipient].append(message)


if not message.recipients:


self.broadcast_log.append(message)




def receive(self, agent_name: str) -> list[AgentMessage]:


return self.queues.pop(agent_name, [])




def broadcast(self, message: AgentMessage):


for agent in self.queues:


self.queues[agent].append(message)


self.broadcast_log.append(message)







Orchestration Patterns





Sequential Handoff





One agent passes results to the next in a pipeline:






class SequentialOrchestrator:


def __init__(self, agents: list[Agent], bus: MessageBus):


self.agents = agents


self.bus = bus




async def run(self, initial_task: str) -> str:


current_output = initial_task


for i, agent in enumerate(self.agents):


result = await agent.process(current_output)


current_output = result




if i < len(self.agents) - 1:


self.bus.send(AgentMessage(


id=f"handoff_{i}",


sender=agent.spec.name,


recipients=[self.agents[i+1].spec.name],


type=MessageType.TASK,


content=result,


))


return current_output







Debate and Consensus





Multiple agents work on the same problem and then converge:






class DebateOrchestrator:


def __init__(self, agents: list[Agent], rounds: int = 3):


self.agents = agents


self.rounds = rounds




async def debate(self, problem: str) -> str:


positions = {a.spec.name: await a.process(problem) for a in self.agents}




for round_num in range(self.rounds):


# Share positions


summary = "\n".join(f"{name}: {pos}" for name, pos in positions.items())




# Each agent critiques and refines


new_positions = {}


for agent in self.agents:


critique = await agent.process(


f"Round {round_num + 1}. Review these positions and refine your own:\n{summary}",


context=f"Your previous position: {positions[agent.spec.name]}"


)


new_positions[agent.spec.name] = critique




positions = new_positions




# Final consensus


consensus_agent = self.agents[0]


summary = "\n".join(f"{name}: {pos}" for name, pos in positions.items())


consensus = await consensus_agent.process(


f"Based on all perspectives, produce a final consensus answer:\n{summary}"


)


return consensus







Manager-Worker Pattern





A manager agent decomposes tasks and delegates to workers:






class ManagerWorkerOrchestrator:


def __init__(self, manager: Agent, workers: list[Agent], bus: MessageBus):


self.manager = manager


self.workers = workers


self.bus = bus




async def run(self, task: str) -> str:


# Manager creates a plan


plan = await self.manager.process(f"Decompose this task into sub-tasks and assign to workers: {task}")


sub_tasks = self._parse_plan(plan)




# Distribute to workers


worker_futures = []


for sub_task in sub_tasks:


worker = self._select_worker(sub_task)


future = worker.process(sub_task["description"])


worker_futures.append(future)




# Collect results


results = await asyncio.gather(*worker_futures)




# Manager synthesizes final output


results_text = "\n".join(f"{st['id']}: {r}" for st, r in zip(sub_tasks, results))


final = await self.manager.process(


f"Synthesize these results into a coherent output:\n{results_text}",


context=f"Original task: {task}"


)


return final




def _select_worker(self, sub_task: dict) -> Agent:


skill = sub_task.get("required_skill", "general")


for worker in self.workers:


if skill in worker.spec.role.lower():


return worker


return self.workers[0]







Consensus with Voting





When agents disagree, a voting mechanism resolves conflicts:






def weighted_vote(proposals: list[dict], agent_weights: dict[str, float]) -> dict:


"""Weighted voting: each agent's vote is weighted by its reliability."""


vote_counts = {}


for proposal in proposals:


key = proposal["solution"]


weight = agent_weights.get(proposal["agent"], 1.0)


vote_counts[key] = vote_counts.get(key, 0) + weight




winner = max(vote_counts, key=vote_counts.get)


return {"winner": winner, "confidence": vote_counts[winner] / sum(vote_counts.values())}







Conclusion





Multi-agent systems distribute intelligence across specialized agents. Sequential handoff is suitable for linear pipelines. Debate and consensus produces higher-quality answers by challenging assumptions. Manager-worker scales to complex tasks by decomposing and delegating. Choose your coordination pattern based on the task: sequential for well-defined steps, debate for decisions requiring multiple perspectives, and manager-worker for complex projects with parallelizable sub-tasks.