top of page

Tutorial 7: Multi-Agent Workflows

  • Contributor
  • 4 days ago
  • 3 min read

Multi-agent systems sound impressive in talks. In practice they're often overkill, sometimes useful. This tutorial walks through when they help and how to build them.

What You'll Build

A multi-agent workflow where specialized agents handle different parts of a task.

Step 1: Decide If You Need Multi-Agent (15 min)

Multi-agent makes sense when:

  • Different parts of the task need genuinely different capabilities

  • The team is large enough to maintain multiple agents

  • Tool sets are too large for one agent to handle well

  • Clear handoff points exist

Multi-agent is overkill when:

  • Single agent + good tools works

  • The "specialties" overlap significantly

  • The complexity outweighs the benefit

Default to single agent. Move to multi-agent when single agent demonstrably can't handle it.

Step 2: Identify the Specialties (15 min)

For a customer service workflow:

Agent: Triage
  Role: Classify incoming issue; route to specialist
  Tools: classify_issue, route_to_specialist

Agent: Billing Specialist  
  Role: Handle billing questions and issues
  Tools: look_up_account, look_up_invoices, process_refund

Agent: Technical Specialist
  Role: Handle technical problems
  Tools: search_kb, check_service_status, run_diagnostics

Agent: Escalation
  Role: Hand off to human support
  Tools: create_ticket, page_oncall

Each agent has a focused role and tool set.

Step 3: Define the Handoffs (15 min)

def handoff_to(target_agent, context, original_question):
    return {
        "target": target_agent,
        "context": context,  # What's been learned
        "task": original_question,
    }

# Triage agent has a tool
TRIAGE_TOOLS = [
    {
        "name": "handoff_to_billing",
        "description": "Hand off this issue to the billing specialist agent",
        # ...
    },
    {
        "name": "handoff_to_technical",
        # ...
    },
]

Handoffs are explicit. The triage agent chooses.

Step 4: Orchestrate the Workflow (30 min)

def orchestrate(user_message):
    # Triage first
    triage_result = triage_agent(user_message)
    
    if triage_result.get("handoff"):
        handoff = triage_result["handoff"]
        
        if handoff["target"] == "billing":
            return billing_agent(handoff["task"], context=handoff["context"])
        elif handoff["target"] == "technical":
            return technical_agent(handoff["task"], context=handoff["context"])
        elif handoff["target"] == "escalation":
            return escalation_agent(handoff["task"], context=handoff["context"])
    
    # No handoff needed; triage handled it
    return triage_result["response"]

The orchestrator routes. Each agent handles its specialty.

Step 5: Share Context Carefully (15 min)

When agent B takes over from A, B needs context but not all of it:

def prepare_handoff_context(triage_messages):
    # Don't pass all triage messages — too much noise
    # Pass a summary
    
    summary_prompt = f"""
    Summarize this interaction so a specialist can pick up:
    
    {format_messages(triage_messages)}
    
    Output:
    - User's original question
    - Key facts established
    - Specific item user is asking about (account ID, order, etc.)
    """
    
    return call_llm(summary_prompt)

Specialists shouldn't redo work; shouldn't be confused by irrelevant detail.

Step 6: Handle Re-Routing (15 min)

Specialist realizes the wrong agent was called:

# Billing agent
def billing_agent_tools():
    return [
        # ... billing tools
        {
            "name": "handoff_to_technical",
            "description": "If the issue is actually technical, hand off to technical specialist",
            # ...
        }
    ]

Each specialist can re-route. Prevents getting stuck with the wrong agent.

Step 7: Avoid Infinite Handoff (10 min)

def orchestrate(user_message, max_handoffs=3):
    handoff_count = 0
    
    while handoff_count < max_handoffs:
        # ... run current agent
        
        if result.get("handoff"):
            handoff_count += 1
            # Continue with new agent
        else:
            return result
    
    # Max handoffs; escalate to human
    return escalation_agent(user_message)

Without limits, agents could hand off back and forth.

Step 8: Test the Workflows (varies)

For each handoff pattern:

def test_billing_handoff():
    result = orchestrate("My invoice is wrong, can you fix it?")
    assert "billing" in result.trace[0]["handoff"]

def test_technical_handoff():
    result = orchestrate("My app won't load")
    assert "technical" in result.trace[0]["handoff"]

def test_recovery_from_misrouting():
    # User says something ambiguous
    result = orchestrate("It's broken")
    # Should ask for clarification, not just guess
    assert "clarif" in result.response.lower()

Step 9: Centralize Logging (15 min)

For multi-agent, especially:

def log_agent_action(agent_name, action, details):
    log({
        "agent": agent_name,
        "action": action,
        "details": details,
        "workflow_id": current_workflow_id(),
    })

Trace the workflow across agents.

Step 10: Compare to Single-Agent (varies)

For the same use case, compare:

  • Single agent with all the tools

  • Multi-agent with specialization

Measure: quality, latency, cost, maintainability.

Often single agent wins on the first three; multi-agent wins on maintainability for complex domains.

What You Just Did

You built a multi-agent workflow with handoffs, context sharing, re-routing, and orchestration. Useful for genuinely specialized work. Skip when single agent suffices.

Common Failure Modes

Multi-agent for everything. Overkill; complexity hurts.

Loss of context in handoff. Specialist asks user to repeat themselves.

Infinite handoff. Agents bounce work back and forth.

No re-routing. Wrong agent gets stuck on the task.

Per-agent costs. Each handoff costs an LLM call; expensive at scale.

bottom of page