Agents represent the evolution from AI assistants that respond to AI systems that reason and act. An agent doesn't just answer questions—it breaks down problems, uses tools, collaborates with other agents, and drives toward goals autonomously.
In Part 4, we built production RAG systems. Now we'll explore Semantic Kernel's agent framework—from single agents with tools to multi-agent orchestration.
What Makes an Agent?
An agent combines three capabilities:
- Reasoning: Understanding context and planning actions
- Tool Use: Executing functions to interact with the world
- Memory: Maintaining state across interactions
Semantic Kernel provides two main agent types:
- ChatCompletionAgent: Single agent with conversation and tools
- AgentGroupChat: Multiple agents collaborating on tasks
ChatCompletionAgent: Your First Agent
Let's build a customer support agent that can look up orders, process refunds, and handle inquiries:
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
// Create the kernel with plugins
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: config["AzureOpenAI:Endpoint"]!,
credential: new DefaultAzureCredential())
.Build();
// Register plugins
kernel.Plugins.AddFromObject(new OrderPlugin(orderService), "Orders");
kernel.Plugins.AddFromObject(new CustomerPlugin(customerService), "Customers");
kernel.Plugins.AddFromObject(new RefundPlugin(paymentService), "Refunds");
// Create the agent
var supportAgent = new ChatCompletionAgent
{
Name = "SupportAgent",
Instructions = """
You are a helpful customer support agent for TechShop.
Your capabilities:
- Look up order status and details
- Check customer account information
- Process refunds for eligible orders
- Answer questions about policies
Guidelines:
- Always verify the customer's identity before sharing order details
- Be empathetic but professional
- If you can't help, escalate to a human agent
- Never share sensitive payment information
Available tools will help you access customer and order data.
""",
Kernel = kernel,
Arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
Running Agent Conversations
// Create a conversation history
var history = new ChatHistory();
// Simulate customer interaction
history.AddUserMessage("Hi, I need to check on my order. It's ORD-789456");
// Get agent response
await foreach (var response in supportAgent.InvokeAsync(history))
{
Console.WriteLine($"[{supportAgent.Name}]: {response.Content}");
history.Add(response);
}
// Continue the conversation
history.AddUserMessage("It's been 2 weeks and hasn't arrived. Can I get a refund?");
await foreach (var response in supportAgent.InvokeAsync(history))
{
Console.WriteLine($"[{supportAgent.Name}]: {response.Content}");
history.Add(response);
}
// The agent will:
// 1. Call get_order_status to check the order
// 2. See it's overdue
// 3. Call process_refund if eligible
// 4. Respond with confirmation
Streaming Agent Responses
For real-time UX, stream responses as they generate:
public async IAsyncEnumerable<string> StreamAgentResponseAsync(
ChatCompletionAgent agent,
ChatHistory history,
string userMessage)
{
history.AddUserMessage(userMessage);
await foreach (var chunk in agent.InvokeStreamingAsync(history))
{
if (!string.IsNullOrEmpty(chunk.Content))
{
yield return chunk.Content;
}
}
}
// Usage in an API endpoint
app.MapPost("/chat", async (ChatRequest request, ChatCompletionAgent agent) =>
{
return Results.Stream(async stream =>
{
var writer = new StreamWriter(stream);
await foreach (var chunk in StreamAgentResponseAsync(agent, history, request.Message))
{
await writer.WriteAsync(chunk);
await writer.FlushAsync();
}
}, "text/event-stream");
});
Multi-Agent Orchestration with AgentGroupChat
Real power comes from multiple specialized agents working together. Let's build a content creation team:
// Research Agent - finds information
var researcher = new ChatCompletionAgent
{
Name = "Researcher",
Instructions = """
You are a thorough research specialist.
Your job is to find accurate, relevant information on topics.
Always cite sources and distinguish between facts and opinions.
Focus on gathering comprehensive data before conclusions.
""",
Kernel = kernelWithSearchPlugin // Has web search tools
};
// Writer Agent - creates content
var writer = new ChatCompletionAgent
{
Name = "Writer",
Instructions = """
You are a skilled content writer.
Your job is to take research and create engaging, well-structured content.
Use clear language, compelling narratives, and proper formatting.
Always incorporate the research provided - don't make up facts.
""",
Kernel = kernel
};
// Editor Agent - reviews and improves
var editor = new ChatCompletionAgent
{
Name = "Editor",
Instructions = """
You are a meticulous editor.
Your job is to review content for:
- Factual accuracy (cross-reference with research)
- Grammar and style
- Clarity and flow
- Engagement and readability
Provide specific, actionable feedback.
When content is publication-ready, say "APPROVED".
""",
Kernel = kernel
};
// Create the group chat
var groupChat = new AgentGroupChat(researcher, writer, editor);
Selection Strategies
How does the system know which agent should speak next?
// 1. Sequential - each agent speaks in order
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new SequentialSelectionStrategy()
}
};
// 2. Round Robin - cycles through agents
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new RoundRobinSelectionStrategy()
}
};
// 3. Kernel Function - LLM decides who speaks next
var selectionFunction = kernel.CreateFunctionFromPrompt("""
Based on the conversation, determine which agent should respond next.
Agents:
- Researcher: When facts or information are needed
- Writer: When content needs to be created or revised
- Editor: When content needs review or final approval
Conversation so far:
{{$history}}
Last message was from: {{$lastAgent}}
Who should speak next? Respond with just the agent name:
""");
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new KernelFunctionSelectionStrategy(selectionFunction, kernel)
{
ResultParser = result => result.GetValue<string>()?.Trim() ?? "Writer"
}
}
};
Termination Strategies
When should the conversation end?
// 1. Maximum turns
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new MaximumIterationTerminationStrategy(10)
}
};
// 2. Keyword-based termination
var terminationFunction = kernel.CreateFunctionFromPrompt("""
Review the conversation and determine if the task is complete.
The task is complete when the Editor has said "APPROVED".
Conversation:
{{$history}}
Is the task complete? Answer 'yes' or 'no':
""");
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new KernelFunctionTerminationStrategy(terminationFunction, kernel)
{
ResultParser = result =>
result.GetValue<string>()?.Trim().Equals("yes", StringComparison.OrdinalIgnoreCase) ?? false
}
}
};
// 3. Combined strategies
var groupChat = new AgentGroupChat(researcher, writer, editor)
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new AggregatorTerminationStrategy(
new MaximumIterationTerminationStrategy(15),
new KernelFunctionTerminationStrategy(terminationFunction, kernel))
}
};
Running the Group Chat
// Kick off with a task
groupChat.AddChatMessage(new ChatMessageContent(
AuthorRole.User,
"Write a 500-word blog post about the benefits of AI pair programming for developers."));
// Let agents collaborate
Console.WriteLine("=== Agent Collaboration Starting ===\n");
await foreach (var message in groupChat.InvokeAsync())
{
var agentName = message.AuthorName ?? "Unknown";
Console.WriteLine($"--- {agentName} ---");
Console.WriteLine(message.Content);
Console.WriteLine();
}
Console.WriteLine("=== Collaboration Complete ===");
// Get the final history
var finalHistory = await groupChat.GetChatMessagesAsync().ToListAsync();
var finalContent = finalHistory.Last(m => m.AuthorName == "Writer").Content;
Console.WriteLine($"\nFinal Article:\n{finalContent}");
Agent Filters for Observability
Monitor and control agent behavior with filters:
public class AgentLoggingFilter : IAutoFunctionInvocationFilter
{
private readonly ILogger _logger;
private readonly Stopwatch _stopwatch = new();
public AgentLoggingFilter(ILogger<AgentLoggingFilter> logger) => _logger = logger;
public async Task OnAutoFunctionInvocationAsync(
AutoFunctionInvocationContext context,
Func<AutoFunctionInvocationContext, Task> next)
{
_stopwatch.Restart();
_logger.LogInformation(
"Agent invoking function {Plugin}.{Function} with args: {Args}",
context.Function.PluginName,
context.Function.Name,
JsonSerializer.Serialize(context.Arguments));
try
{
await next(context);
_stopwatch.Stop();
_logger.LogInformation(
"Function {Function} completed in {ElapsedMs}ms. Result: {Result}",
context.Function.Name,
_stopwatch.ElapsedMilliseconds,
context.Result.GetValue<object>()?.ToString()?[..Math.Min(200, context.Result.ToString()!.Length)]);
}
catch (Exception ex)
{
_logger.LogError(ex, "Function {Function} failed", context.Function.Name);
throw;
}
}
}
// Register the filter
builder.Services.AddSingleton<IAutoFunctionInvocationFilter, AgentLoggingFilter>();
Safety Filter
Prevent dangerous operations:
public class SafetyFilter : IAutoFunctionInvocationFilter
{
private readonly HashSet<string> _dangerousFunctions = new()
{
"delete_account",
"transfer_funds",
"modify_permissions"
};
public async Task OnAutoFunctionInvocationAsync(
AutoFunctionInvocationContext context,
Func<AutoFunctionInvocationContext, Task> next)
{
var functionName = context.Function.Name;
if (_dangerousFunctions.Contains(functionName))
{
// Require confirmation for dangerous operations
context.Result = new FunctionResult(
context.Function,
"This operation requires human approval. Please confirm with the user.");
return; // Don't call the actual function
}
await next(context);
}
}
Human-in-the-Loop Patterns
Keep humans in control for critical decisions:
public class HumanApprovalPlugin
{
private readonly IApprovalService _approvalService;
[KernelFunction("request_approval")]
[Description("Requests human approval for an action before proceeding")]
public async Task<ApprovalResult> RequestApprovalAsync(
[Description("The action requiring approval")] string action,
[Description("Why this action is needed")] string justification,
[Description("Risk level: low, medium, high")] string riskLevel)
{
var request = new ApprovalRequest
{
Action = action,
Justification = justification,
RiskLevel = Enum.Parse<RiskLevel>(riskLevel, ignoreCase: true),
RequestedAt = DateTime.UtcNow
};
// This could send a Slack message, email, or push notification
var approval = await _approvalService.RequestApprovalAsync(request);
return new ApprovalResult
{
Approved = approval.IsApproved,
ApprovedBy = approval.ApproverName,
Message = approval.IsApproved
? "Approval granted. You may proceed."
: $"Approval denied. Reason: {approval.DenialReason}"
};
}
}
// In agent instructions:
var agent = new ChatCompletionAgent
{
Instructions = """
Before performing any action with risk level 'high', you MUST:
1. Call request_approval with the action details
2. Wait for approval
3. Only proceed if approved
Never skip the approval step for high-risk actions.
"""
};
Interactive Approval Flow
public class InteractiveAgent
{
private readonly ChatCompletionAgent _agent;
private readonly IUserInterface _ui;
public async Task<string> ProcessWithApprovalAsync(string userRequest)
{
var history = new ChatHistory();
history.AddUserMessage(userRequest);
await foreach (var response in _agent.InvokeAsync(history))
{
// Check if agent is requesting approval
if (response.Content?.Contains("APPROVAL_REQUIRED:") == true)
{
var approvalRequest = ParseApprovalRequest(response.Content);
// Show to user
var approved = await _ui.RequestApprovalAsync(
$"The agent wants to: {approvalRequest.Action}\n" +
$"Reason: {approvalRequest.Justification}\n\n" +
"Do you approve?");
// Continue with approval result
history.Add(response);
history.AddUserMessage(approved
? "APPROVED - proceed with the action"
: "DENIED - do not proceed");
// Get final response
await foreach (var finalResponse in _agent.InvokeAsync(history))
{
return finalResponse.Content ?? "";
}
}
return response.Content ?? "";
}
return "";
}
}
Agent Patterns for Production
Supervisor Pattern
One agent coordinates others:
var supervisor = new ChatCompletionAgent
{
Name = "Supervisor",
Instructions = """
You are a supervisor agent that coordinates a team of specialists.
Your team:
- DataAnalyst: For data queries and analysis
- ContentWriter: For creating written content
- QualityChecker: For reviewing and validating work
Your job:
1. Understand the user's request
2. Delegate to the appropriate specialist(s)
3. Review their work
4. Provide the final response to the user
Always explain your delegation decisions.
""",
Kernel = kernel
};
var dataAnalyst = new ChatCompletionAgent { Name = "DataAnalyst", /* ... */ };
var contentWriter = new ChatCompletionAgent { Name = "ContentWriter", /* ... */ };
var qualityChecker = new ChatCompletionAgent { Name = "QualityChecker", /* ... */ };
var supervisedChat = new AgentGroupChat(supervisor, dataAnalyst, contentWriter, qualityChecker)
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new KernelFunctionSelectionStrategy(
kernel.CreateFunctionFromPrompt("""
The Supervisor always speaks first and last.
Between, specialists speak when delegated to.
Conversation: {{$history}}
Who speaks next?
"""),
kernel)
}
};
Debate Pattern
Agents argue different perspectives:
var optimist = new ChatCompletionAgent
{
Name = "Optimist",
Instructions = "You argue for the benefits and opportunities in every situation. Be enthusiastic but factual."
};
var skeptic = new ChatCompletionAgent
{
Name = "Skeptic",
Instructions = "You identify risks, challenges, and potential problems. Be critical but constructive."
};
var moderator = new ChatCompletionAgent
{
Name = "Moderator",
Instructions = """
You synthesize the debate between Optimist and Skeptic.
After they've each spoken twice, summarize:
- Key benefits identified
- Key risks identified
- Balanced recommendation
"""
};
var debate = new AgentGroupChat(optimist, skeptic, moderator)
{
ExecutionSettings = new AgentGroupChatSettings
{
SelectionStrategy = new SequentialSelectionStrategy(),
TerminationStrategy = new MaximumIterationTerminationStrategy(5)
}
};
Specialist Router
Route to specialized agents based on intent:
public class AgentRouter
{
private readonly Dictionary<string, ChatCompletionAgent> _specialists;
private readonly Kernel _kernel;
public async Task<ChatCompletionAgent> RouteAsync(string userMessage)
{
var classificationPrompt = $"""
Classify this user message into one category:
- billing: Payment, invoices, subscriptions
- technical: Product issues, bugs, how-to
- sales: Pricing, features, comparisons
- general: Other inquiries
Message: {userMessage}
Category (one word):
""";
var category = await _kernel.InvokePromptAsync<string>(classificationPrompt);
return _specialists.GetValueOrDefault(
category?.Trim().ToLower() ?? "general",
_specialists["general"]);
}
}
Testing Agents
public class AgentTests
{
[Fact]
public async Task SupportAgent_LooksUpOrder_WhenAsked()
{
// Arrange
var mockOrderPlugin = new Mock<IOrderPlugin>();
mockOrderPlugin
.Setup(p => p.GetOrderAsync("ORD-123", It.IsAny<CancellationToken>()))
.ReturnsAsync(new Order { Id = "ORD-123", Status = "Shipped" });
var kernel = CreateTestKernel();
kernel.Plugins.AddFromObject(mockOrderPlugin.Object, "Orders");
var agent = new ChatCompletionAgent
{
Name = "Support",
Instructions = "Help customers with order inquiries.",
Kernel = kernel,
Arguments = new KernelArguments(new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
// Act
var history = new ChatHistory();
history.AddUserMessage("What's the status of order ORD-123?");
var responses = await agent.InvokeAsync(history).ToListAsync();
// Assert
mockOrderPlugin.Verify(p => p.GetOrderAsync("ORD-123", It.IsAny<CancellationToken>()), Times.Once);
Assert.Contains("Shipped", responses.Last().Content);
}
[Fact]
public async Task AgentGroupChat_CompletesTask_WithTermination()
{
// Arrange
var agents = CreateTestAgents();
var groupChat = new AgentGroupChat(agents.ToArray())
{
ExecutionSettings = new AgentGroupChatSettings
{
TerminationStrategy = new MaximumIterationTerminationStrategy(5)
}
};
// Act
groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "Write a haiku about testing."));
var messages = await groupChat.InvokeAsync().ToListAsync();
// Assert
Assert.NotEmpty(messages);
Assert.True(messages.Count <= 5);
}
}
Series Conclusion
Across this five-part series, we've built a complete understanding of Semantic Kernel:
- Fundamentals: Kernel architecture, services, functions, and templates
- Plugins: Native functions, OpenAPI, and MCP integration
- Memory: Vector stores, embeddings, and semantic search
- RAG: Production retrieval patterns and evaluation
- Agents: Autonomous reasoning with multi-agent orchestration
Semantic Kernel provides the building blocks for sophisticated AI applications. The patterns we've explored—from simple chat completions to multi-agent collaboration—form a foundation you can extend for your specific use cases.
What's Next for You
- Start small: Build a single-agent assistant with 2-3 plugins
- Add memory: Give your agent persistent knowledge with RAG
- Scale up: Experiment with multi-agent patterns for complex workflows
- Measure: Implement evaluation to track and improve quality
- Secure: Add filters, approval flows, and monitoring for production
The AI agent landscape is evolving rapidly. Semantic Kernel's abstractions help you stay flexible as new models, protocols, and patterns emerge.
Happy building! 🚀
This concludes our 5-part series on Semantic Kernel.
Top comments (0)