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.


