Skip to main content

Challenge 23: Multi-Agent Orchestration

Estimated Time

60 min | Cost: $5-10 (estimated) | Domain: Implement Agentic Solutions (5-10%)

Preview

Multi-agent frameworks are rapidly evolving. APIs shown here may change. This challenge covers concepts tested on the exam with current SDK patterns.

Exam skills covered

  • Implement complex agents with Semantic Kernel Agent Framework
  • Design multi-agent solutions with orchestration patterns
  • Test and deploy agent solutions

Overview

Multi-agent systems use multiple specialized agents collaborating to solve complex problems. Key patterns:

  • Sequential (pipeline): Agents process in order, each building on previous output
  • Parallel (fan-out/fan-in): Multiple agents work simultaneously, results aggregated
  • Handoff: One agent transfers control to another based on context
  • Supervisor: A coordinator agent delegates to worker agents

Semantic Kernel provides the Agent Framework for building multi-agent solutions in Azure.

Prerequisites

  • Azure subscription with Azure OpenAI access
  • Azure OpenAI with GPT-4o deployed
  • Python 3.10+ or .NET 8
  • Packages: semantic-kernel>=1.0.0 (Python) or Microsoft.SemanticKernel (C#)

Implementation

Task 1: Create Specialized Agents with Semantic Kernel

import os
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.agents import ChatCompletionAgent, AgentGroupChat
from semantic_kernel.agents.strategies import SequentialSelectionStrategy, TerminationStrategy
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion

# Configure kernel with Azure OpenAI
kernel = Kernel()
kernel.add_service(AzureChatCompletion(
deployment_name="gpt-4o",
endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_key=os.environ["AZURE_OPENAI_KEY"]
))

# Create specialized agents
researcher = ChatCompletionAgent(
kernel=kernel,
name="Researcher",
instructions=(
"You are a research analyst. Given a topic, provide factual information, "
"data points, and key findings. Be thorough and cite sources when possible. "
"Focus on gathering information, not making recommendations."
)
)

writer = ChatCompletionAgent(
kernel=kernel,
name="Writer",
instructions=(
"You are a technical writer. Take research findings and create clear, "
"well-structured content. Use headings, bullet points, and concise language. "
"Transform raw research into polished documentation."
)
)

reviewer = ChatCompletionAgent(
kernel=kernel,
name="Reviewer",
instructions=(
"You are a quality reviewer. Evaluate the written content for accuracy, "
"clarity, and completeness. Provide specific feedback. "
"Say 'APPROVED' when the content meets quality standards."
)
)

# Define termination condition
class ApprovalTermination(TerminationStrategy):
async def should_agent_terminate(self, agent, history):
if history:
last_message = history[-1].content
return "APPROVED" in last_message.upper()
return False

# Create group chat with sequential strategy
chat = AgentGroupChat(
agents=[researcher, writer, reviewer],
selection_strategy=SequentialSelectionStrategy(),
termination_strategy=ApprovalTermination(maximum_iterations=6)
)

async def run_multi_agent():
await chat.add_chat_message(
message="Create a brief guide about Azure AI Agent Service architecture and use cases."
)

async for response in chat.invoke():
print(f"\n{'='*60}")
print(f"[{response.name}]:")
print(f"{'='*60}")
print(response.content[:500])

asyncio.run(run_multi_agent())

Task 2: Implement Agent Handoff Pattern

from semantic_kernel.agents import ChatCompletionAgent
from semantic_kernel.contents import ChatMessageContent, AuthorRole

# Specialized agents for different domains
billing_agent = ChatCompletionAgent(
kernel=kernel,
name="BillingAgent",
instructions=(
"You handle billing inquiries: invoices, payments, pricing questions. "
"If the user asks about technical issues, say: HANDOFF:TechnicalAgent"
)
)

technical_agent = ChatCompletionAgent(
kernel=kernel,
name="TechnicalAgent",
instructions=(
"You handle technical support: errors, configuration, troubleshooting. "
"If the user asks about billing, say: HANDOFF:BillingAgent"
)
)

triage_agent = ChatCompletionAgent(
kernel=kernel,
name="TriageAgent",
instructions=(
"You are a triage agent. Analyze the user's request and route to the correct agent. "
"For billing questions respond: HANDOFF:BillingAgent "
"For technical questions respond: HANDOFF:TechnicalAgent"
)
)

async def handoff_orchestrator(user_message: str):
"""Simple handoff orchestration"""
agents = {
"TriageAgent": triage_agent,
"BillingAgent": billing_agent,
"TechnicalAgent": technical_agent
}

current_agent = triage_agent
history = [ChatMessageContent(role=AuthorRole.USER, content=user_message)]
max_handoffs = 3

for i in range(max_handoffs):
print(f"\n[Routing to: {current_agent.name}]")

response = await current_agent.invoke(history)
response_text = str(response)
print(f"[{current_agent.name}]: {response_text[:200]}")

if "HANDOFF:" in response_text:
target = response_text.split("HANDOFF:")[1].strip().split()[0]
if target in agents:
current_agent = agents[target]
continue

return response_text

return "Max handoffs reached."

# Test handoff
asyncio.run(handoff_orchestrator("My API calls are returning 429 errors"))
asyncio.run(handoff_orchestrator("I need a copy of last month's invoice"))

Task 3: Test and Evaluate Multi-Agent Output

import json
from datetime import datetime

# Evaluation framework for multi-agent systems
class AgentEvaluator:
def __init__(self):
self.results = []

async def evaluate_run(self, chat, test_input, expected_keywords):
"""Evaluate a multi-agent run against expected outcomes"""
start_time = datetime.now()

await chat.add_chat_message(message=test_input)

messages = []
async for response in chat.invoke():
messages.append({
"agent": response.name,
"content": response.content,
"timestamp": datetime.now().isoformat()
})

duration = (datetime.now() - start_time).total_seconds()

# Check if expected keywords appear in final output
final_content = messages[-1]["content"] if messages else ""
keywords_found = [kw for kw in expected_keywords if kw.lower() in final_content.lower()]

result = {
"input": test_input,
"num_turns": len(messages),
"agents_involved": [m["agent"] for m in messages],
"duration_seconds": duration,
"keywords_found": len(keywords_found),
"keywords_expected": len(expected_keywords),
"coverage": len(keywords_found) / len(expected_keywords) if expected_keywords else 0,
"terminated_properly": "APPROVED" in (messages[-1]["content"] if messages else "")
}

self.results.append(result)
return result

def summary(self):
avg_turns = sum(r["num_turns"] for r in self.results) / len(self.results)
avg_coverage = sum(r["coverage"] for r in self.results) / len(self.results)
success_rate = sum(1 for r in self.results if r["terminated_properly"]) / len(self.results)

print(f"\nEvaluation Summary ({len(self.results)} tests)")
print(f" Avg turns per conversation: {avg_turns:.1f}")
print(f" Avg keyword coverage: {avg_coverage:.0%}")
print(f" Proper termination rate: {success_rate:.0%}")

# Run evaluation
evaluator = AgentEvaluator()

test_cases = [
("Explain Azure AI Search pricing tiers", ["basic", "standard", "free"]),
("How to configure vector search", ["vector", "index", "embedding"]),
]

async def run_evaluation():
for test_input, keywords in test_cases:
result = await evaluator.evaluate_run(chat, test_input, keywords)
print(f"Test: {test_input[:40]}... Coverage: {result['coverage']:.0%}")
evaluator.summary()

asyncio.run(run_evaluation())

Expected Output

============================================================
[Researcher]:
============================================================
Azure AI Agent Service is a managed platform for building AI agents...
Key features: thread management, tool execution, run lifecycle...

============================================================
[Writer]:
============================================================
# Azure AI Agent Service Guide
## Architecture
The service uses a thread-based architecture...
## Use Cases
1. Customer support automation
2. Document analysis and Q&A...

============================================================
[Reviewer]:
============================================================
The content is well-structured and accurate. APPROVED.

[Routing to: TriageAgent]
[TriageAgent]: HANDOFF:TechnicalAgent
[Routing to: TechnicalAgent]
[TechnicalAgent]: HTTP 429 indicates rate limiting. Check your TPM quota...

Break & fix

ScenarioSymptomRoot CauseFix
Infinite agent loopAgents keep passing to each otherNo termination condition metAdd maximum_iterations limit; ensure termination keywords are clear
Wrong agent selectedIrrelevant responsesTriage instructions too vagueAdd explicit routing rules with examples in instructions
Context lost between agentsAgent ignores prior contextHistory not passed properlyEnsure full message history is shared via AgentGroupChat
High token usageExpensive runsEach agent gets full historySummarize history before passing; limit context window
Handoff target not foundKeyError on agent lookupTypo in HANDOFF target nameValidate handoff targets against registered agent names

Knowledge Check

1. What is the 'sequential' orchestration pattern in multi-agent systems?

2. In Semantic Kernel Agent Framework, what does AgentGroupChat provide?

3. What is the 'handoff' pattern in multi-agent architecture?

4. How should you prevent infinite loops in multi-agent orchestration?

5. What is the primary advantage of multi-agent systems over single-agent systems?

Cleanup

az group delete --name rg-ai102-agents --yes --no-wait

Learn More