Multi-Agent Systems: Orchestrating Azure AI Agents with Semantic Kernel
A single agent can do a lot. But some tasks are too complex, too nuanced, or require too many different skills for one agent to handle well. That's when you need a team.
In this article, we'll explore multi-agent orchestration—how to build systems where specialized agents collaborate to accomplish what no single agent could do alone. We'll use Semantic Kernel as our orchestration layer, combining it with Azure AI Agent Service to create powerful agentic workflows.
Why Multi-Agent?
Before diving into code, let's understand when multi-agent systems make sense:
The Case for Specialization
Imagine building a content creation system. A single agent doing everything would need to:
- Research topics thoroughly
- Write engaging content
- Edit for grammar and style
- Fact-check claims
- Optimize for SEO
That's a lot to pack into one system prompt. The instructions become bloated, conflicting, and hard to tune. Worse, the model context gets cluttered with information that's irrelevant to the current subtask.
Multi-agent systems solve this through division of labor:
┌────────────────────────────────────────────────────────────────┐
│ Content Creation Pipeline │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Research │───▶│ Writer │───▶│ Editor │───▶│ SEO │ │
│ │ Agent │ │ Agent │ │ Agent │ │ Agent │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │ │ │ │ │
│ Focused on Focused on Focused on Focused on │
│ gathering compelling clarity and keywords │
│ accurate narrative correctness and reach │
│ information │
└────────────────────────────────────────────────────────────────┘
Benefits of Multi-Agent Architectures
- Specialization — Each agent does one thing well
- Separation of Concerns — Different system prompts, tools, and models
- Scalability — Add new agents without rewriting existing ones
- Debuggability — Trace issues to specific agents
- Flexibility — Swap agents, run in parallel, or reconfigure workflows
Semantic Kernel: The Orchestration Layer
Semantic Kernel is Microsoft's open-source SDK for building AI applications. While Azure AI Agent Service manages individual agents (threads, runs, tools), Semantic Kernel sits above it to coordinate multiple agents working together.
Why Semantic Kernel?
- Agent Abstractions — Works with Azure AI Agents, OpenAI, and custom implementations
- Chat Patterns — Built-in support for multi-agent conversations
- Selection Strategies — Control which agent speaks when
- Termination Strategies — Know when the task is complete
- Plugin System — Share capabilities across agents
Installing Semantic Kernel
dotnet add package Microsoft.SemanticKernel --version 1.25.0
dotnet add package Microsoft.SemanticKernel.Agents.Core --version 1.25.0-alpha
dotnet add package Microsoft.SemanticKernel.Agents.AzureAI --version 1.25.0-alpha
Note: Agent packages are in preview. Check NuGet for latest versions.
AgentGroupChat: Multi-Agent Conversations
The core abstraction for multi-agent orchestration is AgentGroupChat. It manages a conversation where multiple agents take turns responding.
Basic Setup
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.AzureAI;
using Microsoft.SemanticKernel.Agents.Chat;
using Azure.AI.Projects;
using Azure.Identity;
// Connect to Azure AI Foundry
var projectClient = new AIProjectClient(
new Uri(Environment.GetEnvironmentVariable("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT")!),
new DefaultAzureCredential()
);
// Create the underlying Azure AI agents
var researcherDef = await projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Researcher",
instructions: """
You are a thorough research specialist. Your job is to:
- Gather comprehensive information on topics
- Find relevant facts, statistics, and examples
- Identify key points and supporting evidence
- Present findings in a clear, organized manner
Focus on accuracy and completeness. Cite sources when possible.
When you've gathered sufficient information, summarize your findings.
"""
));
var writerDef = await projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Writer",
instructions: """
You are a skilled content writer. Your job is to:
- Take research and transform it into engaging content
- Write with clarity, flow, and reader engagement
- Structure content logically with compelling hooks
- Adapt tone and style to the target audience
Write complete, polished drafts. Don't leave placeholders.
"""
));
var editorDef = await projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Editor",
instructions: """
You are a meticulous editor. Your job is to:
- Review content for grammar, spelling, and punctuation
- Improve clarity and readability
- Ensure consistent tone and style
- Fact-check claims against the research
- Provide constructive feedback or final approval
Be specific about changes. When content is ready, say "APPROVED".
"""
));
// Wrap as Semantic Kernel agents
var researcher = new AzureAIAgent(researcherDef.Value, projectClient);
var writer = new AzureAIAgent(writerDef.Value, projectClient);
var editor = new AzureAIAgent(editorDef.Value, projectClient);
Creating the Group Chat
// Create a group chat with all agents
var chat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
// Agents take turns in order: researcher -> writer -> editor -> researcher...
SelectionStrategy = new SequentialSelectionStrategy(),
// Stop after 6 turns or when editor approves
TerminationStrategy = new AggregateTerminationStrategy(
new MaxTurnsTerminationStrategy(6),
new KeywordTerminationStrategy("APPROVED")
)
}
};
// Add the initial task
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User,
"Create a blog post about the benefits of AI agents in customer service. " +
"Target audience: business decision-makers. Length: 500-800 words."));
// Run the multi-agent workflow
Console.WriteLine("Starting multi-agent content creation...\n");
await foreach (var message in chat.InvokeAsync())
{
Console.WriteLine($"[{message.AuthorName}]");
Console.WriteLine(message.Content);
Console.WriteLine(new string('-', 60));
}
Console.WriteLine("\n✅ Content creation complete!");
Selection Strategies: Who Speaks Next?
The selection strategy determines which agent responds at each turn. Semantic Kernel provides several built-in options.
Sequential Selection
Agents take turns in a fixed order:
new SequentialSelectionStrategy()
Good for: Pipelines with defined stages (research → write → edit).
Round Robin Selection
Similar to sequential, but cycles continuously:
new RoundRobinSelectionStrategy()
Kernel Function Selection
Let an AI decide which agent should respond next:
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!,
new DefaultAzureCredential())
.Build();
var selectionFunction = KernelFunctionFactory.CreateFromPrompt(
"""
Examine the conversation and decide which agent should respond next.
Agents available:
- Researcher: Gathers information and facts
- Writer: Creates content from research
- Editor: Reviews and approves content
Rules:
- If no research exists yet, select Researcher
- If research exists but no draft, select Writer
- If a draft exists, select Editor
- If Editor requests changes, select Writer
- If Editor approves, the task is complete
Respond with ONLY the agent name.
""");
new KernelFunctionSelectionStrategy(selectionFunction, kernel)
{
ResultParser = result => result.GetValue<string>()?.Trim() ?? "Researcher"
}
Custom Selection Strategy
For complex logic, implement your own:
public class WorkflowSelectionStrategy : SelectionStrategy
{
private readonly Dictionary<string, string> _transitions = new()
{
["User"] = "Researcher",
["Researcher"] = "Writer",
["Writer"] = "Editor",
["Editor"] = "Writer" // Editor can request revisions
};
public override ValueTask<Agent> SelectAgentAsync(
IReadOnlyList<Agent> agents,
IReadOnlyList<ChatMessageContent> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
var lastSpeaker = lastMessage?.AuthorName ?? "User";
// Check for approval
if (lastSpeaker == "Editor" &&
lastMessage?.Content?.Contains("APPROVED") == true)
{
return ValueTask.FromResult<Agent>(null!); // Signal completion
}
// Get next agent based on workflow
var nextAgentName = _transitions.GetValueOrDefault(lastSpeaker, "Researcher");
var nextAgent = agents.First(a => a.Name == nextAgentName);
return ValueTask.FromResult(nextAgent);
}
}
Termination Strategies: Knowing When to Stop
Without proper termination, agents could chat forever. Termination strategies define when the conversation is complete.
Max Turns
Stop after a fixed number of agent responses:
new MaxTurnsTerminationStrategy(10)
Keyword Detection
Stop when an agent says a specific phrase:
new KeywordTerminationStrategy("APPROVED", "TASK_COMPLETE", "DONE")
{
// Only trigger when specific agents use the keyword
Agents = new[] { editor }
}
Kernel Function Termination
Let an AI decide when the task is complete:
var terminationFunction = KernelFunctionFactory.CreateFromPrompt(
"""
Review the conversation and determine if the task is complete.
The task is complete when:
- The Editor has explicitly approved the content
- OR a final polished piece has been delivered
Respond with ONLY "true" or "false".
""");
new KernelFunctionTerminationStrategy(terminationFunction, kernel)
{
ResultParser = result => result.GetValue<string>()?.Trim().ToLower() == "true"
}
Aggregate Strategies
Combine multiple strategies with AND/OR logic:
// Stop when ANY condition is met
new AggregateTerminationStrategy(
AggregateTerminationStrategy.AggregationMode.Any,
new MaxTurnsTerminationStrategy(10),
new KeywordTerminationStrategy("APPROVED")
)
// Stop when ALL conditions are met (less common)
new AggregateTerminationStrategy(
AggregateTerminationStrategy.AggregationMode.All,
new KeywordTerminationStrategy("FACT_CHECKED"),
new KeywordTerminationStrategy("APPROVED")
)
Complete Example: Research → Write → Edit Pipeline
Here's a production-ready multi-agent content creation system:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.AzureAI;
using Microsoft.SemanticKernel.Agents.Chat;
using Azure.AI.Projects;
using Azure.Identity;
namespace MultiAgentDemo;
public class ContentCreationPipeline
{
private readonly AIProjectClient _projectClient;
private readonly List<Agent> _agents = new();
private readonly List<string> _agentIds = new();
public ContentCreationPipeline()
{
_projectClient = new AIProjectClient(
new Uri(Environment.GetEnvironmentVariable("AZURE_AI_FOUNDRY_PROJECT_ENDPOINT")!),
new DefaultAzureCredential()
);
}
public async Task InitializeAsync()
{
Console.WriteLine("Initializing agents...");
// Research Agent - with Bing search tool
var researcherDef = await _projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Researcher",
instructions: """
You are a research specialist. When given a topic:
1. Identify key aspects that need to be covered
2. Gather relevant facts, statistics, and examples
3. Find compelling angles and insights
4. Organize findings in a clear structure
Present your research as a structured brief that a writer can use.
Include specific data points and examples.
Format your output as:
## Research Brief: [Topic]
### Key Points
- ...
### Statistics & Data
- ...
### Examples & Case Studies
- ...
### Suggested Angles
- ...
"""
));
_agentIds.Add(researcherDef.Value.Id);
_agents.Add(new AzureAIAgent(researcherDef.Value, _projectClient) { Name = "Researcher" });
// Writer Agent
var writerDef = await _projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Writer",
instructions: """
You are an expert content writer. Given research, create compelling content:
1. Start with a hook that grabs attention
2. Structure content with clear sections
3. Use conversational, engaging language
4. Include specific examples and data from the research
5. End with a clear call-to-action or conclusion
Write complete drafts—no placeholders or "insert here" notes.
Match the requested length and audience.
If the Editor requests changes, revise your draft accordingly.
Acknowledge what you changed in your response.
"""
));
_agentIds.Add(writerDef.Value.Id);
_agents.Add(new AzureAIAgent(writerDef.Value, _projectClient) { Name = "Writer" });
// Editor Agent
var editorDef = await _projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Editor",
instructions: """
You are a senior editor. Review content for:
1. **Accuracy** - Do claims match the research?
2. **Clarity** - Is the writing clear and readable?
3. **Engagement** - Does it hold attention?
4. **Structure** - Is it well-organized?
5. **Grammar** - Any errors or awkward phrasing?
If changes are needed:
- Be specific about what to fix
- Explain why the change improves the content
- Request a revision from the Writer
If the content is ready for publication:
- Summarize its strengths
- End your response with exactly: APPROVED
You must either request changes OR approve. Don't just comment.
"""
));
_agentIds.Add(editorDef.Value.Id);
_agents.Add(new AzureAIAgent(editorDef.Value, _projectClient) { Name = "Editor" });
Console.WriteLine($"✅ Created {_agents.Count} agents");
}
public async Task<string> CreateContentAsync(string topic, string audience, int wordCount)
{
var chat = new AgentGroupChat(_agents.ToArray())
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new SequentialSelectionStrategy(),
TerminationStrategy = new AggregateTerminationStrategy(
new MaxTurnsTerminationStrategy(8),
new ApprovalTerminationStrategy()
)
}
};
// Add the initial request
chat.AddChatMessage(new ChatMessageContent(
AuthorRole.User,
$"Create content about: {topic}\n" +
$"Target audience: {audience}\n" +
$"Target length: {wordCount} words\n\n" +
"Researcher: Start by gathering relevant information."
));
var finalContent = new StringBuilder();
var messageLog = new List<(string Agent, string Content)>();
await foreach (var message in chat.InvokeAsync())
{
Console.WriteLine($"\n[{message.AuthorName}]:");
var content = message.Content ?? "";
// Truncate display for long content
if (content.Length > 500)
{
Console.WriteLine(content[..500] + "...");
}
else
{
Console.WriteLine(content);
}
messageLog.Add((message.AuthorName ?? "Unknown", content));
// Track the latest Writer output as the final content
if (message.AuthorName == "Writer")
{
finalContent.Clear();
finalContent.Append(content);
}
}
Console.WriteLine($"\n✅ Pipeline complete! {messageLog.Count} messages exchanged.");
return finalContent.ToString();
}
public async Task CleanupAsync()
{
foreach (var agentId in _agentIds)
{
await _projectClient.DeleteAgentAsync(agentId);
}
Console.WriteLine("🧹 Agents cleaned up");
}
}
// Custom termination strategy for approval
public class ApprovalTerminationStrategy : TerminationStrategy
{
public override ValueTask<bool> ShouldTerminateAsync(
Agent agent,
IReadOnlyList<ChatMessageContent> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
// Only terminate if Editor approves
if (lastMessage?.AuthorName == "Editor" &&
lastMessage.Content?.Contains("APPROVED", StringComparison.OrdinalIgnoreCase) == true)
{
return ValueTask.FromResult(true);
}
return ValueTask.FromResult(false);
}
}
// Usage
class Program
{
static async Task Main()
{
var pipeline = new ContentCreationPipeline();
try
{
await pipeline.InitializeAsync();
var content = await pipeline.CreateContentAsync(
topic: "How AI agents are transforming customer service",
audience: "Business executives and IT leaders",
wordCount: 600
);
Console.WriteLine("\n" + new string('=', 60));
Console.WriteLine("FINAL CONTENT:");
Console.WriteLine(new string('=', 60));
Console.WriteLine(content);
}
finally
{
await pipeline.CleanupAsync();
}
}
}
Advanced Patterns
Pattern 1: Parallel Execution
Some tasks can be parallelized. Run multiple agents simultaneously:
public async Task<CombinedAnalysis> AnalyzeParallelAsync(string document)
{
// Create specialized analysis agents
var sentimentAgent = CreateAgent("SentimentAnalyzer", "Analyze emotional tone...");
var keywordAgent = CreateAgent("KeywordExtractor", "Extract key topics...");
var summaryAgent = CreateAgent("Summarizer", "Create a concise summary...");
// Run analyses in parallel
var sentimentTask = RunAgentAsync(sentimentAgent, document);
var keywordTask = RunAgentAsync(keywordAgent, document);
var summaryTask = RunAgentAsync(summaryAgent, document);
await Task.WhenAll(sentimentTask, keywordTask, summaryTask);
return new CombinedAnalysis
{
Sentiment = sentimentTask.Result,
Keywords = keywordTask.Result,
Summary = summaryTask.Result
};
}
private async Task<string> RunAgentAsync(Agent agent, string input)
{
var chat = new AgentGroupChat(agent)
{
ExecutionSettings = new() { TerminationStrategy = new MaxTurnsTerminationStrategy(1) }
};
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
var result = new StringBuilder();
await foreach (var message in chat.InvokeAsync())
{
result.Append(message.Content);
}
return result.ToString();
}
Pattern 2: Supervisor Agent
A supervisor agent that delegates to specialists:
var supervisor = await projectClient.CreateAgentAsync(new CreateAgentOptions(
model: "gpt-4o",
name: "Supervisor",
instructions: """
You are a project supervisor coordinating a team of specialists.
Your team:
- DataAnalyst: For analyzing data and creating visualizations
- Writer: For creating written content
- Researcher: For gathering information
- Coder: For writing and explaining code
When given a task:
1. Break it into subtasks
2. Assign each subtask to the appropriate specialist
3. Review their work and request revisions if needed
4. Compile the final deliverable
Use the format:
@DataAnalyst: [task description]
@Writer: [task description]
etc.
When all subtasks are complete, compile the final result and say COMPLETE.
"""
));
// Custom selection strategy that parses supervisor directives
public class SupervisorSelectionStrategy : SelectionStrategy
{
public override ValueTask<Agent> SelectAgentAsync(
IReadOnlyList<Agent> agents,
IReadOnlyList<ChatMessageContent> history,
CancellationToken cancellationToken = default)
{
var lastMessage = history.LastOrDefault();
if (lastMessage?.AuthorName == "Supervisor")
{
// Parse the @AgentName directive
var content = lastMessage.Content ?? "";
var match = Regex.Match(content, @"@(\w+):");
if (match.Success)
{
var targetName = match.Groups[1].Value;
var target = agents.FirstOrDefault(a => a.Name == targetName);
if (target != null) return ValueTask.FromResult(target);
}
}
// Default back to supervisor
return ValueTask.FromResult(agents.First(a => a.Name == "Supervisor"));
}
}
Pattern 3: Debate/Adversarial Pattern
Agents that challenge each other for better outcomes:
var proposer = CreateAgent("Proposer", """
Propose solutions to problems. Be creative and bold.
Support your proposals with reasoning.
""");
var critic = CreateAgent("Critic", """
Critically evaluate proposals. Find weaknesses and risks.
Suggest improvements or alternatives.
Be constructive but thorough.
""");
var synthesizer = CreateAgent("Synthesizer", """
After the Proposer and Critic have debated (at least 2 rounds each):
- Combine the best ideas
- Address the valid concerns raised
- Present a refined, practical solution
End with FINAL_SOLUTION when you have a complete answer.
""");
var debateChat = new AgentGroupChat(proposer, critic, synthesizer)
{
ExecutionSettings = new()
{
SelectionStrategy = new DebateSelectionStrategy(),
TerminationStrategy = new KeywordTerminationStrategy("FINAL_SOLUTION")
}
};
Sharing Context Across Agents
Agents in a group chat share the conversation history, but sometimes you need to share structured data or resources.
Using Chat History Effectively
// Prefix messages with structured markers for easy parsing
chat.AddChatMessage(new ChatMessageContent(
AuthorRole.User,
"""{
[TASK]
Create a technical blog post.
[REQUIREMENTS]
- Topic: Kubernetes best practices
- Audience: DevOps engineers
- Length: 1000 words
- Include: Code examples, diagrams descriptions
[CONSTRAINTS]
- No vendor-specific tools
- Focus on security
}"""
));
Shared Kernel for Common Functions
// Create a shared kernel with common plugins
var sharedKernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion("gpt-4o", endpoint, credential)
.Build();
// Add shared plugins
sharedKernel.ImportPluginFromType<DateTimePlugin>();
sharedKernel.ImportPluginFromType<WebSearchPlugin>();
sharedKernel.ImportPluginFromType<DatabasePlugin>();
// All agents can access these plugins through the kernel
var researcher = new ChatCompletionAgent
{
Name = "Researcher",
Instructions = "...",
Kernel = sharedKernel
};
Error Handling and Recovery
Multi-agent systems can fail in complex ways. Build in resilience:
public async Task<string> RunWithRetryAsync(AgentGroupChat chat, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
var result = new StringBuilder();
await foreach (var message in chat.InvokeAsync())
{
result.AppendLine($"[{message.AuthorName}]: {message.Content}");
}
return result.ToString();
}
catch (Exception ex) when (attempt < maxRetries)
{
Console.WriteLine($"Attempt {attempt} failed: {ex.Message}. Retrying...");
// Add a recovery message to the chat
chat.AddChatMessage(new ChatMessageContent(
AuthorRole.System,
$"Previous attempt encountered an error: {ex.Message}. " +
"Please continue from where we left off."
));
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt))); // Exponential backoff
}
}
throw new Exception("Max retries exceeded");
}
Best Practices
1. Clear Agent Boundaries
Each agent should have a distinct, non-overlapping responsibility:
// ❌ Bad: Overlapping responsibilities
var agent1 = "You write and edit content...";
var agent2 = "You edit content and fact-check...";
// ✅ Good: Clear boundaries
var writer = "You write content. Never edit or fact-check.";
var editor = "You edit for grammar and style. Don't rewrite.";
var factChecker = "You verify claims. Don't edit or write.";
2. Explicit Handoff Signals
Make it clear when an agent is done:
var instructions = """
When you complete your task:
- Summarize what you produced
- Say "READY FOR [NextAgent]" to signal handoff
If you need information from another agent:
- Say "NEED FROM [AgentName]: [specific request]"
""";
3. Limit Conversation Depth
Prevent infinite loops with hard limits:
new AggregateTerminationStrategy(
// Hard stop at 12 turns no matter what
new MaxTurnsTerminationStrategy(12),
// Normal termination on approval
new KeywordTerminationStrategy("APPROVED")
)
4. Log Everything
Multi-agent debugging is hard without logs:
await foreach (var message in chat.InvokeAsync())
{
var logEntry = new
{
Timestamp = DateTime.UtcNow,
Agent = message.AuthorName,
Role = message.Role.ToString(),
Content = message.Content,
TokenCount = EstimateTokens(message.Content)
};
_logger.LogInformation("Agent message: {@LogEntry}", logEntry);
// Also store for analysis
await _telemetry.TrackAgentMessageAsync(logEntry);
}
What's Next
We've built a multi-agent system, but we haven't tackled production concerns yet. In Part 4, we'll cover:
- State Management — Persisting conversations across sessions
- Session Patterns — Managing multiple concurrent users
- Observability — OpenTelemetry tracing for agent execution
- Cost Management — Token budgets and optimization
These patterns turn demos into production systems.
Summary
In this article, we learned:
- Why Multi-Agent — Specialization, scalability, and separation of concerns
- Semantic Kernel — The orchestration layer for agent coordination
- AgentGroupChat — Managing multi-agent conversations
- Selection Strategies — Controlling which agent speaks when
- Termination Strategies — Knowing when the task is complete
- Advanced Patterns — Parallel execution, supervisors, and debates
Your agents now work as a team. Let's make them production-ready.
Resources:
Next up: Part 4 — Production-Ready AI Agents: State Management, Sessions, and Telemetry
Top comments (0)