ai · 13 min read

Multi-Agent AI Systems - Orchestrating Collaborative Intelligence

Design and build sophisticated multi-agent AI systems where autonomous agents collaborate to solve complex problems.

Fortan Pireva · 15 November 2024

The future of AI isn't just about making individual agents smarter - it's about making them work together. Multi-agent systems (MAS) represent a paradigm shift from monolithic AI to distributed, collaborative intelligence. Let's explore how to architect and implement systems where multiple AI agents coordinate to solve complex problems.

Understanding Multi-Agent Systems

A multi-agent system consists of multiple autonomous agents that interact, coordinate, and collaborate to achieve individual or collective goals. Unlike single-agent systems, MAS can:

  • Distribute workload across specialized agents
  • Solve problems too complex for any single agent
  • Exhibit emergent behavior through agent interactions
  • Scale horizontally by adding more agents
  • Provide resilience through redundancy

Core Architecture Patterns

1. Centralized Coordination (Orchestrator Pattern)

A central orchestrator manages agent interactions:

from typing import List, Dict, Any
from dataclasses import dataclass
from enum import Enum

class AgentStatus(Enum):
    IDLE = "idle"
    WORKING = "working"
    WAITING = "waiting"
    COMPLETED = "completed"
    FAILED = "failed"

@dataclass
class Agent:
    id: str
    role: str
    capabilities: List[str]
    status: AgentStatus = AgentStatus.IDLE
    current_task: Optional[str] = None

class Orchestrator:
    def __init__(self):
        self.agents: Dict[str, Agent] = {}
        self.task_queue: List[Task] = []
        self.completed_tasks: List[Task] = []

    def register_agent(self, agent: Agent):
        """Register an agent with the orchestrator"""
        self.agents[agent.id] = agent
        print(f"Agent {agent.id} registered with role: {agent.role}")

    def assign_task(self, task: Task) -> Optional[Agent]:
        """Assign task to most suitable available agent"""
        # Find agents with required capabilities
        capable_agents = [
            agent for agent in self.agents.values()
            if agent.status == AgentStatus.IDLE
            and any(cap in agent.capabilities for cap in task.required_capabilities)
        ]

        if not capable_agents:
            self.task_queue.append(task)
            return None

        # Select best agent (simple: first available)
        selected_agent = capable_agents[0]
        selected_agent.status = AgentStatus.WORKING
        selected_agent.current_task = task.id

        return selected_agent

    def coordinate(self, task: Task):
        """Coordinate multiple agents for complex task"""
        # Break task into subtasks
        subtasks = self.decompose_task(task)

        # Assign subtasks to agents
        assignments = {}
        for subtask in subtasks:
            agent = self.assign_task(subtask)
            if agent:
                assignments[subtask.id] = agent.id

        # Monitor execution
        results = self.monitor_execution(assignments)

        # Aggregate results
        return self.aggregate_results(results)

    def decompose_task(self, task: Task) -> List[Task]:
        """Break complex task into subtasks"""
        # Use LLM to decompose task
        decomposition_prompt = f"""
        Task: {task.description}

        Break this task into smaller, independent subtasks that can be
        executed by different specialized agents.

        For each subtask, specify:
        - Description
        - Required capabilities
        - Dependencies on other subtasks

        Return as structured JSON.
        """

        # Get decomposition from LLM
        # Parse and create subtask objects
        pass

    def monitor_execution(self, assignments: Dict[str, str]) -> Dict[str, Any]:
        """Monitor agent execution"""
        results = {}
        while assignments:
            for task_id, agent_id in list(assignments.items()):
                agent = self.agents[agent_id]

                if agent.status == AgentStatus.COMPLETED:
                    results[task_id] = agent.get_result()
                    assignments.pop(task_id)
                    agent.status = AgentStatus.IDLE

                elif agent.status == AgentStatus.FAILED:
                    # Handle failure - retry or reassign
                    self.handle_failure(task_id, agent_id)
                    assignments.pop(task_id)

        return results

2. Decentralized Peer-to-Peer

Agents communicate directly without central coordination:

import asyncio
from typing import Set

class Message:
    def __init__(self, sender: str, receiver: str, content: Any, msg_type: str):
        self.sender = sender
        self.receiver = receiver
        self.content = content
        self.type = msg_type
        self.timestamp = datetime.now()

class P2PAgent:
    def __init__(self, agent_id: str, role: str):
        self.id = agent_id
        self.role = role
        self.peers: Set[str] = set()
        self.inbox: asyncio.Queue = asyncio.Queue()
        self.knowledge_base: Dict[str, Any] = {}

    def connect_to_peer(self, peer_id: str):
        """Establish connection with another agent"""
        self.peers.add(peer_id)
        self.broadcast_message({
            "type": "peer_connected",
            "peer_id": self.id
        })

    async def send_message(self, receiver: str, content: Any, msg_type: str):
        """Send message to specific agent"""
        message = Message(self.id, receiver, content, msg_type)
        # Route message to receiver
        await self.route_message(message)

    async def broadcast_message(self, content: Any):
        """Broadcast message to all peers"""
        for peer_id in self.peers:
            await self.send_message(peer_id, content, "broadcast")

    async def process_messages(self):
        """Process incoming messages"""
        while True:
            message = await self.inbox.get()
            await self.handle_message(message)

    async def handle_message(self, message: Message):
        """Handle different message types"""
        if message.type == "request":
            response = await self.process_request(message.content)
            await self.send_message(
                message.sender,
                response,
                "response"
            )

        elif message.type == "share_knowledge":
            self.update_knowledge(message.content)

        elif message.type == "collaboration_request":
            await self.handle_collaboration(message)

    async def collaborate(self, task: str, required_roles: List[str]):
        """Initiate collaboration with peers"""
        # Find suitable peers
        collaborators = await self.find_collaborators(required_roles)

        # Propose collaboration
        collaboration_id = f"collab_{uuid.uuid4()}"
        proposals = []

        for peer_id in collaborators:
            proposal = await self.send_message(
                peer_id,
                {
                    "collaboration_id": collaboration_id,
                    "task": task,
                    "initiator": self.id
                },
                "collaboration_request"
            )
            proposals.append(proposal)

        # Wait for responses
        responses = await self.collect_responses(collaboration_id)

        # Form team
        team = [peer for peer, accepted in responses.items() if accepted]

        return CollaborationSession(collaboration_id, team, task)

    async def find_collaborators(self, required_roles: List[str]) -> List[str]:
        """Find peers with required capabilities"""
        suitable_peers = []

        for peer_id in self.peers:
            # Query peer capabilities
            response = await self.send_message(
                peer_id,
                {"query": "capabilities"},
                "request"
            )

            if any(role in response.get("roles", []) for role in required_roles):
                suitable_peers.append(peer_id)

        return suitable_peers

3. Hierarchical Organization

Agents organized in hierarchical structure:

class HierarchicalAgent:
    def __init__(self, agent_id: str, role: str, level: int):
        self.id = agent_id
        self.role = role
        self.level = level  # 0 = worker, 1 = manager, 2 = director
        self.subordinates: List[HierarchicalAgent] = []
        self.manager: Optional[HierarchicalAgent] = None

    def assign_subordinate(self, agent: 'HierarchicalAgent'):
        """Add agent as subordinate"""
        self.subordinates.append(agent)
        agent.manager = self

    def delegate_task(self, task: Task):
        """Delegate task to subordinates"""
        if not self.subordinates:
            # Execute task directly
            return self.execute(task)

        # Decompose and assign to subordinates
        subtasks = self.decompose_task(task)
        results = []

        for subtask, subordinate in zip(subtasks, self.subordinates):
            result = subordinate.delegate_task(subtask)
            results.append(result)

        # Aggregate results
        return self.aggregate_subordinate_results(results)

    def report_to_manager(self, report: Dict[str, Any]):
        """Report progress to manager"""
        if self.manager:
            self.manager.receive_report(self.id, report)

    def receive_report(self, subordinate_id: str, report: Dict[str, Any]):
        """Receive report from subordinate"""
        # Process report
        if report.get("needs_help"):
            self.provide_assistance(subordinate_id, report["issue"])

        # Aggregate and report upward if needed
        if self.should_escalate(report):
            self.report_to_manager({
                "from": subordinate_id,
                "issue": report,
                "escalated_by": self.id
            })

Communication Protocols

1. Message Passing

from enum import Enum
from dataclasses import dataclass

class MessageType(Enum):
    REQUEST = "request"
    RESPONSE = "response"
    INFORM = "inform"
    QUERY = "query"
    PROPOSE = "propose"
    ACCEPT = "accept"
    REJECT = "reject"

@dataclass
class AgentMessage:
    sender: str
    receiver: str
    message_type: MessageType
    content: Dict[str, Any]
    conversation_id: str
    timestamp: datetime

class MessageBus:
    def __init__(self):
        self.queues: Dict[str, asyncio.Queue] = {}
        self.message_history: List[AgentMessage] = []

    def register_agent(self, agent_id: str):
        """Register agent's message queue"""
        self.queues[agent_id] = asyncio.Queue()

    async def send(self, message: AgentMessage):
        """Send message to recipient"""
        if message.receiver not in self.queues:
            raise ValueError(f"Agent {message.receiver} not registered")

        await self.queues[message.receiver].put(message)
        self.message_history.append(message)

    async def receive(self, agent_id: str) -> AgentMessage:
        """Receive message for agent"""
        if agent_id not in self.queues:
            raise ValueError(f"Agent {agent_id} not registered")

        return await self.queues[agent_id].get()

    def get_conversation(self, conversation_id: str) -> List[AgentMessage]:
        """Retrieve full conversation"""
        return [
            msg for msg in self.message_history
            if msg.conversation_id == conversation_id
        ]

2. Shared Blackboard

Agents read from and write to shared knowledge space:

from threading import Lock

class Blackboard:
    def __init__(self):
        self.data: Dict[str, Any] = {}
        self.locks: Dict[str, Lock] = {}
        self.subscribers: Dict[str, List[callable]] = {}

    def write(self, key: str, value: Any, agent_id: str):
        """Write data to blackboard"""
        if key not in self.locks:
            self.locks[key] = Lock()

        with self.locks[key]:
            old_value = self.data.get(key)
            self.data[key] = {
                "value": value,
                "written_by": agent_id,
                "timestamp": datetime.now()
            }

            # Notify subscribers
            self.notify_subscribers(key, value, old_value)

    def read(self, key: str) -> Any:
        """Read data from blackboard"""
        return self.data.get(key, {}).get("value")

    def subscribe(self, key: str, callback: callable):
        """Subscribe to changes on a key"""
        if key not in self.subscribers:
            self.subscribers[key] = []
        self.subscribers[key].append(callback)

    def notify_subscribers(self, key: str, new_value: Any, old_value: Any):
        """Notify subscribers of changes"""
        if key in self.subscribers:
            for callback in self.subscribers[key]:
                callback(key, new_value, old_value)

class BlackboardAgent:
    def __init__(self, agent_id: str, blackboard: Blackboard):
        self.id = agent_id
        self.blackboard = blackboard

    def contribute_knowledge(self, key: str, value: Any):
        """Write knowledge to blackboard"""
        self.blackboard.write(key, value, self.id)

    def access_knowledge(self, key: str) -> Any:
        """Access shared knowledge"""
        return self.blackboard.read(key)

    def watch_for_changes(self, key: str):
        """Subscribe to knowledge changes"""
        self.blackboard.subscribe(key, self.on_knowledge_updated)

    def on_knowledge_updated(self, key: str, new_value: Any, old_value: Any):
        """Handle knowledge updates"""
        print(f"Agent {self.id}: {key} updated from {old_value} to {new_value}")
        # React to changes

Consensus and Decision Making

1. Voting Mechanism

from collections import Counter

class VotingSystem:
    def __init__(self, agents: List[Agent]):
        self.agents = agents
        self.votes: Dict[str, Any] = {}

    async def call_vote(self, proposal: Dict[str, Any]) -> Dict[str, Any]:
        """Conduct vote among agents"""
        self.votes = {}

        # Collect votes
        vote_tasks = [
            self.get_agent_vote(agent, proposal)
            for agent in self.agents
        ]
        await asyncio.gather(*vote_tasks)

        # Tally votes
        return self.tally_votes()

    async def get_agent_vote(self, agent: Agent, proposal: Dict[str, Any]):
        """Get individual agent's vote"""
        vote = await agent.vote_on_proposal(proposal)
        self.votes[agent.id] = vote

    def tally_votes(self) -> Dict[str, Any]:
        """Count votes and determine outcome"""
        vote_counts = Counter(self.votes.values())
        total_votes = len(self.votes)

        majority_threshold = total_votes // 2 + 1
        winning_vote = vote_counts.most_common(1)[0]

        return {
            "outcome": winning_vote[0],
            "vote_count": winning_vote[1],
            "is_majority": winning_vote[1] >= majority_threshold,
            "detailed_votes": dict(vote_counts)
        }

class VotingAgent:
    async def vote_on_proposal(self, proposal: Dict[str, Any]) -> str:
        """Agent evaluates proposal and votes"""
        # Analyze proposal
        analysis = await self.analyze_proposal(proposal)

        # Make decision based on agent's goals and constraints
        if analysis["benefits"] > analysis["costs"]:
            return "approve"
        elif analysis["benefits"] < analysis["costs"]:
            return "reject"
        else:
            return "abstain"

2. Consensus Through Dialogue

class ConsensusAgent:
    def __init__(self, agent_id: str, initial_position: Any):
        self.id = agent_id
        self.position = initial_position
        self.confidence = 1.0

    async def negotiate(self, other_agents: List['ConsensusAgent'], rounds: int = 5):
        """Negotiate with other agents to reach consensus"""
        for round_num in range(rounds):
            # Share position
            positions = [agent.position for agent in other_agents] + [self.position]

            # Evaluate positions
            evaluation = await self.evaluate_positions(positions)

            # Update position if convinced
            if evaluation["should_update"]:
                self.position = evaluation["new_position"]
                self.confidence = evaluation["confidence"]

            # Check for consensus
            if self.check_consensus(other_agents):
                return True

        return False

    async def evaluate_positions(self, positions: List[Any]) -> Dict[str, Any]:
        """Evaluate other agents' positions"""
        prompt = f"""
        My current position: {self.position}
        Other positions: {positions}

        Should I update my position based on the arguments presented?
        Consider:
        1. Strength of arguments
        2. Evidence provided
        3. Consensus building

        Return: should_update (bool), new_position (if updating), confidence (0-1)
        """

        # Get evaluation from LLM
        # Return structured decision
        pass

    def check_consensus(self, other_agents: List['ConsensusAgent']) -> bool:
        """Check if consensus is reached"""
        positions = [agent.position for agent in other_agents] + [self.position]
        # Check if all positions are similar enough
        return len(set(positions)) == 1

Coordination Patterns

1. Task Allocation (Contract Net Protocol)

class ContractNetManager:
    async def announce_task(self, task: Task, agents: List[Agent]):
        """Announce task and collect bids"""
        # Announce task
        bids = await self.collect_bids(task, agents)

        # Evaluate bids
        winner = self.select_winner(bids)

        # Award contract
        if winner:
            await self.award_contract(winner, task)
            return winner

        return None

    async def collect_bids(self, task: Task, agents: List[Agent]) -> List[Bid]:
        """Collect bids from capable agents"""
        bids = []

        for agent in agents:
            if agent.can_perform(task):
                bid = await agent.submit_bid(task)
                if bid:
                    bids.append(bid)

        return bids

    def select_winner(self, bids: List[Bid]) -> Optional[Agent]:
        """Select best bid"""
        if not bids:
            return None

        # Evaluate bids (cost, quality, time)
        scored_bids = [
            (bid, self.score_bid(bid))
            for bid in bids
        ]

        winner = max(scored_bids, key=lambda x: x[1])
        return winner[0].agent

    def score_bid(self, bid: Bid) -> float:
        """Score bid based on multiple criteria"""
        # Normalize and weight different factors
        cost_score = 1.0 / (bid.cost + 1)  # Lower cost is better
        quality_score = bid.quality  # Higher quality is better
        time_score = 1.0 / (bid.estimated_time + 1)  # Faster is better

        return (
            0.4 * cost_score +
            0.4 * quality_score +
            0.2 * time_score
        )

class BiddingAgent:
    async def submit_bid(self, task: Task) -> Optional[Bid]:
        """Evaluate task and submit bid"""
        # Assess capability
        capability = self.assess_capability(task)

        if capability < 0.5:  # Not capable enough
            return None

        # Estimate cost and time
        cost = self.estimate_cost(task)
        time = self.estimate_time(task)

        return Bid(
            agent=self,
            task=task,
            cost=cost,
            quality=capability,
            estimated_time=time
        )

2. Formation of Coalitions

class CoalitionAgent:
    def __init__(self, agent_id: str, capabilities: List[str]):
        self.id = agent_id
        self.capabilities = capabilities
        self.current_coalition: Optional[Coalition] = None

    async def form_coalition(self, task: Task, available_agents: List['CoalitionAgent']):
        """Form coalition to accomplish task"""
        required_capabilities = task.required_capabilities

        # Find complementary agents
        coalition_members = self.find_complementary_agents(
            available_agents,
            required_capabilities
        )

        # Propose coalition
        coalition = Coalition(task, coalition_members)

        # Get agreement from all members
        if await coalition.get_consensus():
            self.current_coalition = coalition
            return coalition

        return None

    def find_complementary_agents(
        self,
        agents: List['CoalitionAgent'],
        required_capabilities: List[str]
    ) -> List['CoalitionAgent']:
        """Find agents that together cover required capabilities"""
        selected = [self]
        covered_capabilities = set(self.capabilities)

        for agent in agents:
            if agent.id == self.id:
                continue

            # Check if agent adds new capabilities
            new_capabilities = set(agent.capabilities) - covered_capabilities

            if new_capabilities and len(selected) < 10:  # Max coalition size
                selected.append(agent)
                covered_capabilities.update(agent.capabilities)

            # Check if all capabilities covered
            if all(cap in covered_capabilities for cap in required_capabilities):
                break

        return selected if covered_capabilities.issuperset(required_capabilities) else []

class Coalition:
    def __init__(self, task: Task, members: List[CoalitionAgent]):
        self.task = task
        self.members = members
        self.agreements: Dict[str, bool] = {}

    async def get_consensus(self) -> bool:
        """Get agreement from all coalition members"""
        for member in self.members:
            agreed = await member.agree_to_coalition(self)
            self.agreements[member.id] = agreed

        return all(self.agreements.values())

    async def execute_task(self):
        """Execute task collaboratively"""
        # Assign subtasks to members
        assignments = self.assign_subtasks()

        # Execute in parallel
        results = await asyncio.gather(*[
            member.execute_subtask(subtask)
            for member, subtask in assignments.items()
        ])

        # Aggregate results
        return self.aggregate_results(results)

Emergent Behavior and Self-Organization

class SelfOrganizingSystem:
    def __init__(self, agents: List[Agent]):
        self.agents = agents
        self.environment = Environment()

    def simulate(self, steps: int):
        """Simulate system to observe emergent behavior"""
        for step in range(steps):
            # Each agent observes environment
            for agent in self.agents:
                observations = agent.observe(self.environment)

                # Agent makes local decision
                action = agent.decide(observations)

                # Agent acts on environment
                agent.act(action, self.environment)

            # Environment updates
            self.environment.update()

            # Analyze emergent patterns
            self.analyze_patterns(step)

    def analyze_patterns(self, step: int):
        """Detect emergent patterns in agent behavior"""
        # Analyze agent positions, states, interactions
        patterns = {
            "clusters": self.detect_clusters(),
            "coordination_level": self.measure_coordination(),
            "efficiency": self.measure_efficiency()
        }

        print(f"Step {step}: {patterns}")

    def detect_clusters(self) -> int:
        """Detect agent clustering"""
        # Implementation depends on agent properties
        pass

    def measure_coordination(self) -> float:
        """Measure how well agents coordinate"""
        # Analyze agent interactions and outcomes
        pass

Real-World Applications

1. Distributed Problem Solving

# Scientific research collaboration
research_system = MultiAgentSystem()
research_system.add_agents([
    ResearchAgent("literature_reviewer", "Literature Review"),
    ResearchAgent("data_analyst", "Data Analysis"),
    ResearchAgent("hypothesis_generator", "Hypothesis Generation"),
    ResearchAgent("experiment_designer", "Experiment Design"),
    ResearchAgent("writer", "Paper Writing")
])

result = await research_system.collaborate_on_task(
    "Investigate the effects of XYZ on ABC"
)

2. Autonomous Supply Chain

# Supply chain optimization
supply_chain = MultiAgentSystem()
supply_chain.add_agents([
    SupplierAgent("supplier_1"),
    ManufacturerAgent("manufacturer_1"),
    DistributorAgent("distributor_1"),
    RetailerAgent("retailer_1"),
    LogisticsAgent("logistics_1")
])

# Agents negotiate and coordinate automatically
supply_chain.optimize_operations()

3. Smart City Management

# Traffic management system
city_system = MultiAgentSystem()
city_system.add_agents([
    TrafficLightAgent(intersection_id) for intersection_id in intersections
] + [
    EmergencyVehicleAgent(vehicle_id) for vehicle_id in emergency_vehicles
])

# Agents coordinate to optimize traffic flow
city_system.run_real_time_optimization()

Best Practices

1. Design for Scalability

  • Use asynchronous communication
  • Implement message queuing
  • Design for horizontal scaling

2. Handle Failures Gracefully

  • Implement timeouts
  • Retry mechanisms
  • Fallback strategies

3. Monitor and Debug

  • Log agent interactions
  • Track message flows
  • Visualize agent states

4. Balance Autonomy and Control

  • Define clear agent boundaries
  • Set coordination protocols
  • Maintain system-level oversight

Conclusion

Multi-agent systems represent the cutting edge of AI architecture. By distributing intelligence across specialized agents that can communicate, coordinate, and collaborate, we can:

  • Solve problems too complex for single agents
  • Build resilient, fault-tolerant systems
  • Scale intelligently by adding agents
  • Model real-world organizational structures

The key to successful multi-agent systems is thoughtful design of:

  • Agent roles and capabilities
  • Communication protocols
  • Coordination mechanisms
  • Consensus and decision-making processes

Start simple, with 2-3 agents, master the coordination patterns, then scale to more complex systems. The future of AI is collaborative, distributed, and emergent.


Build your multi-agent system today. Start with a clear problem, design specialized agents, implement communication protocols, and watch emergent intelligence unfold.