Lesson 17: Multi-Agent Systems | AI Engineering
Module 4: AI Agents & Tools

Multi-Agent Systems

Design agent teams with specialized roles, implement delegation patterns, build message-passing pipelines, and orchestrate multi-agent workflows for complex tasks.

Lesson 17 ~30 min Advanced

Why Multiple Agents?

The Case for Specialization

A single agent with 20 tools and a complex goal tends to get confused. Multi-agent systems split complex tasks across specialized agents — each with a focused role, limited tools, and a clear responsibility. This mirrors how human teams work: researcher, writer, editor, reviewer.

Benefits of multi-agent architectures:

  • Separation of concerns: Each agent has a focused system prompt and limited tools
  • Scalability: Add new agents without changing existing ones
  • Quality: Specialized prompts outperform generalist "do everything" prompts
  • Debuggability: Trace failures to a specific agent in the pipeline
  • Parallelism: Independent agents can run concurrently

Manager/Worker Pattern

Delegation Patterns

The manager agent receives the user request, breaks it into subtasks, delegates to specialized workers, and synthesizes results. Workers never talk to each other directly — all coordination flows through the manager. This prevents circular dependencies and makes the system predictable.

Pattern Topology Best For Limitation
Sequential Pipeline A → B → C Content creation, data processing No parallelism, each step blocks
Manager/Worker Manager → {W1, W2, W3} Research, analysis, complex queries Manager is a bottleneck
Debate/Consensus A ↔ B ↔ C → Vote Decision-making, quality assurance Expensive (many LLM calls)
Broadcast Input → {A, B, C} → Merge Multi-perspective analysis Results may conflict

Agent Communication

"""Message passing infrastructure for multi-agent systems."""
from dataclasses import dataclass, field
from typing import Optional
from enum import Enum
import uuid
from datetime import datetime

class MessageType(Enum):
    TASK = "task"           # Assignment from manager
    RESULT = "result"       # Completed work
    FEEDBACK = "feedback"   # Revision request
    QUERY = "query"         # Question to another agent
    STATUS = "status"       # Progress update

@dataclass
class AgentMessage:
    """Structured message between agents."""
    id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
    sender: str = ""
    receiver: str = ""
    type: MessageType = MessageType.TASK
    content: str = ""
    metadata: dict = field(default_factory=dict)
    timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
    parent_id: Optional[str] = None  # For threading conversations

class MessageBus:
    """Central message router for agent communication."""
    
    def __init__(self):
        self.queues: dict[str, list[AgentMessage]] = {}
        self.history: list[AgentMessage] = []
    
    def register_agent(self, agent_name: str):
        self.queues[agent_name] = []
    
    def send(self, message: AgentMessage):
        """Route message to receiver's queue."""
        self.history.append(message)
        if message.receiver in self.queues:
            self.queues[message.receiver].append(message)
        else:
            raise ValueError(f"Unknown agent: {message.receiver}")
    
    def receive(self, agent_name: str) -> list[AgentMessage]:
        """Get and clear pending messages for an agent."""
        messages = self.queues.get(agent_name, [])
        self.queues[agent_name] = []
        return messages
    
    def get_conversation(self, agent_a: str, agent_b: str) -> list[AgentMessage]:
        """Get all messages between two agents."""
        return [m for m in self.history 
                if (m.sender == agent_a and m.receiver == agent_b) or
                   (m.sender == agent_b and m.receiver == agent_a)]

3-Agent Content Pipeline

Researcher → Writer → Editor

Each agent has a single responsibility. The Researcher gathers information, the Writer creates a draft, and the Editor polishes and fact-checks. Messages flow sequentially, with the Editor able to send feedback back to the Writer for revisions.

"""Multi-Agent Content Pipeline — Researcher → Writer → Editor."""
import openai

client = openai.OpenAI()

class Agent:
    """Base agent with role, tools, and message handling."""
    
    def __init__(self, name: str, system_prompt: str, model: str = "gpt-4o"):
        self.name = name
        self.system_prompt = system_prompt
        self.model = model
        self.conversation: list[dict] = []
    
    def process(self, task: str, context: str = "") -> str:
        """Process a task and return the result."""
        messages = [
            {"role": "system", "content": self.system_prompt},
        ]
        if context:
            messages.append({"role": "user", "content": f"Context from previous agents:\n{context}"})
        messages.append({"role": "user", "content": task})
        
        response = client.chat.completions.create(
            model=self.model,
            messages=messages,
            temperature=0.7,
        )
        
        result = response.choices[0].message.content
        self.conversation.append({"task": task, "result": result})
        return result

# ─── Define Specialized Agents ────────────────────────────────

researcher = Agent(
    name="Researcher",
    system_prompt="""You are a research specialist. Your job is to:
1. Identify key topics and subtopics to investigate
2. Find relevant facts, statistics, and examples
3. Organize findings into structured notes
4. Flag areas that need more investigation

Output FORMAT: structured research notes with headers, bullet points, and source attribution.
Be thorough but concise. Focus on facts, not opinions."""
)

writer = Agent(
    name="Writer",
    system_prompt="""You are a professional technical writer. Your job is to:
1. Take research notes and transform them into clear, engaging prose
2. Structure content with logical flow (intro → body → conclusion)
3. Use concrete examples and analogies
4. Maintain a professional but accessible tone

Output FORMAT: well-structured article with headers, paragraphs, and code examples where relevant.
Never fabricate facts — only use what's in the research notes."""
)

editor = Agent(
    name="Editor",
    system_prompt="""You are a senior editor. Your job is to:
1. Check factual accuracy against the research notes
2. Improve clarity, conciseness, and flow
3. Fix grammatical issues and awkward phrasing
4. Ensure consistent tone and style
5. Rate the final quality (1-10) and list remaining issues

Output FORMAT: 
- EDITED_CONTENT: [the improved text]
- QUALITY_SCORE: [1-10]
- ISSUES: [any remaining problems]
- VERDICT: PUBLISH or NEEDS_REVISION"""
)

# ─── Pipeline Orchestrator ────────────────────────────────────

class ContentPipeline:
    """Orchestrates the Researcher → Writer → Editor flow."""
    
    def __init__(self, max_revisions: int = 2):
        self.max_revisions = max_revisions
        self.bus = MessageBus()
        self.bus.register_agent("Researcher")
        self.bus.register_agent("Writer")
        self.bus.register_agent("Editor")
    
    def run(self, topic: str) -> dict:
        """Run the full pipeline."""
        print(f"📋 Topic: {topic}\n")
        
        # Step 1: Research
        print("🔍 Researcher working...")
        research_notes = researcher.process(
            f"Research the following topic thoroughly: {topic}"
        )
        print(f"   ✓ Research complete ({len(research_notes)} chars)\n")
        
        # Step 2: Writing (with possible revisions)
        draft = None
        for revision in range(self.max_revisions + 1):
            if revision == 0:
                print("✍️  Writer working on first draft...")
                draft = writer.process(
                    f"Write a comprehensive article about: {topic}",
                    context=research_notes
                )
            else:
                print(f"✍️  Writer working on revision {revision}...")
                draft = writer.process(
                    f"Revise the article based on this editorial feedback:\n{feedback}",
                    context=f"Original research:\n{research_notes}\n\nCurrent draft:\n{draft}"
                )
            
            print(f"   ✓ Draft complete ({len(draft)} chars)\n")
            
            # Step 3: Editing
            print("📝 Editor reviewing...")
            edit_result = editor.process(
                "Review this article for quality, accuracy, and clarity.",
                context=f"Research notes:\n{research_notes}\n\nDraft:\n{draft}"
            )
            
            # Parse editor verdict
            if "PUBLISH" in edit_result or revision == self.max_revisions:
                print("   ✓ Editor approved!\n")
                break
            else:
                feedback = edit_result
                print(f"   ↩ Editor requested revision\n")
        
        return {
            "topic": topic,
            "research": research_notes,
            "final_draft": draft,
            "editor_notes": edit_result,
            "revisions": revision,
        }

# ─── Run the Pipeline ────────────────────────────────────────

pipeline = ContentPipeline(max_revisions=2)
result = pipeline.run("The impact of transformer architecture on modern NLP")
print(f"\n{'='*60}")
print(f"Final article ({len(result['final_draft'])} chars, {result['revisions']} revisions)")
print(result['final_draft'][:500] + "...")

Framework Comparison

Framework Architecture Strengths Weaknesses Best For
CrewAI Role-based crews with delegation Simple API, good defaults, role focus Limited control flow, opinionated Content pipelines, research teams
AutoGen Conversational agents with group chat Flexible communication, code execution Complex setup, verbose conversations Coding tasks, collaborative problem-solving
LangGraph State machine with typed state Precise control flow, persistence, streaming Steep learning curve, boilerplate Production workflows, complex state logic
Custom (this lesson) Whatever you design Full control, no dependencies, minimal Must build everything yourself Learning, specific requirements, lightweight
"""Framework comparison — same task in CrewAI vs custom."""

# ─── CrewAI Version (for reference) ──────────────────────────
# pip install crewai

from crewai import Agent as CrewAgent, Task, Crew

crew_researcher = CrewAgent(
    role="Research Analyst",
    goal="Find comprehensive information about {topic}",
    backstory="You are an expert researcher with access to the internet.",
    tools=[search_tool, read_tool],
)

crew_writer = CrewAgent(
    role="Technical Writer",
    goal="Write clear, engaging articles from research notes",
    backstory="You are a seasoned tech writer for a top publication.",
)

research_task = Task(
    description="Research {topic} thoroughly",
    agent=crew_researcher,
    expected_output="Structured research notes",
)

writing_task = Task(
    description="Write a comprehensive article from the research",
    agent=crew_writer,
    expected_output="Polished article",
    context=[research_task],  # Depends on research
)

crew = Crew(agents=[crew_researcher, crew_writer], tasks=[research_task, writing_task])
result = crew.kickoff(inputs={"topic": "quantum computing advances 2024"})

Advanced Patterns

Consensus Pattern

"""Consensus: Multiple agents vote on an answer."""

def consensus_answer(question: str, num_agents: int = 3) -> str:
    """Get multiple perspectives and find consensus."""
    perspectives = [
        "You are a conservative analyst who values caution and proven approaches.",
        "You are an innovative thinker who values novel solutions and experimentation.",
        "You are a pragmatic engineer who values simplicity and practicality.",
    ]
    
    answers = []
    for i in range(num_agents):
        agent = Agent(f"Agent_{i}", perspectives[i % len(perspectives)])
        answer = agent.process(question)
        answers.append(answer)
    
    # Synthesizer combines the answers
    synthesizer = Agent("Synthesizer", 
        "You synthesize multiple expert opinions into a balanced conclusion. "
        "Note where experts agree and where they disagree.")
    
    combined = "\n\n".join(f"Expert {i+1}: {a}" for i, a in enumerate(answers))
    return synthesizer.process(
        f"Question: {question}\n\nSynthesize these expert opinions:\n{combined}"
    )

Dynamic Agent Spawning

"""Manager that dynamically creates specialized agents."""

class DynamicManager:
    """Creates agents on-the-fly based on task requirements."""
    
    def __init__(self):
        self.planner = Agent("Planner",
            "You are a project planner. Given a complex task, break it into subtasks "
            "and specify what kind of specialist is needed for each. Output JSON:\n"
            '[{"task": "...", "specialist": "...", "skills": "..."}]')
    
    def execute(self, goal: str) -> str:
        # Step 1: Plan and identify needed specialists
        plan_json = self.planner.process(f"Break this into subtasks: {goal}")
        subtasks = json.loads(plan_json)
        
        # Step 2: Dynamically create agents for each subtask
        results = []
        for subtask in subtasks:
            specialist = Agent(
                name=subtask["specialist"],
                system_prompt=f"You are a {subtask['specialist']} with expertise in: "
                             f"{subtask['skills']}. Complete the assigned task thoroughly."
            )
            result = specialist.process(subtask["task"])
            results.append({"task": subtask["task"], "result": result})
        
        # Step 3: Synthesize results
        synthesizer = Agent("Synthesizer", "Combine these results into a coherent final output.")
        context = "\n\n".join(f"Task: {r['task']}\nResult: {r['result']}" for r in results)
        return synthesizer.process(f"Goal: {goal}\n\nResults:\n{context}")

Mini-Project: Multi-Agent Content Pipeline

🛠️ Build a Full Content Creation Pipeline

Create a multi-agent system that takes a topic and produces a polished, fact-checked article through specialized agent collaboration.

  1. Implement the MessageBus for structured agent communication
  2. Create 3 specialized agents:
    • Researcher: Gathers information (has web search tool)
    • Writer: Creates engaging content from research
    • Editor: Reviews, scores quality, requests revisions
  3. Implement the sequential pipeline with revision loop (max 2 revisions)
  4. Add the Editor's ability to send content back to Writer with specific feedback
  5. Log all messages through the MessageBus for observability
  6. Test with varied topics:
    • "Explain how CRISPR gene editing works" (science)
    • "The rise of Rust in systems programming" (tech)
    • "How to build a personal investment portfolio" (finance)
  7. Display execution trace: show all messages, agent decisions, revision count

Stretch Goals

  • Add a 4th agent: Fact-Checker that verifies claims against the research notes
  • Implement parallel research: spawn 3 Researchers for different subtopics, merge results
  • Add a consensus vote between Editor and Fact-Checker on whether to publish

Key Takeaways

  • Multi-agent systems beat single agents on complex tasks through specialization
  • The Manager/Worker pattern is the most common: one agent plans, others execute
  • Structured message passing enables tracing, debugging, and replay
  • Sequential pipelines (A → B → C) are simple and effective for content/data workflows
  • Revision loops with quality gates prevent low-quality output from reaching users
  • Start with custom code to understand the patterns, then adopt frameworks when needed