Agents Are Not Chatbots
The terms get conflated constantly, but the distinction matters for architecture decisions. A chatbot operates in a simple request-response loop: the user sends a message, the model generates a reply, the conversation continues. There is no persistent goal, no tool use, no autonomous decision-making.
An AI agent is fundamentally different. It maintains an objective, can observe its environment through tools, reasons about what to do next, acts by invoking functions or APIs, and evaluates the results before deciding whether to continue. Agents are goal-directed systems. They don’t just answer questions — they accomplish tasks.
This distinction shapes everything downstream. If you need a customer-facing Q&A interface, a chatbot is appropriate. If you need a system that researches a topic, drafts a report, reviews it for accuracy, and publishes it — that’s agent territory.
Microsoft’s Agent Framework, built on Semantic Kernel, provides the abstractions you need to build both single-agent and multi-agent systems in .NET.
The Microsoft Agent Framework
The Agent Framework ships as part of the Semantic Kernel ecosystem. At its core, it extends SK with agent-specific primitives while reusing everything you already know — the Kernel, plugins, AI service connectors, and filters.
Install the required packages:
dotnet add package Microsoft.SemanticKernel --version 1.34.0
dotnet add package Microsoft.SemanticKernel.Agents.Core --version 1.34.0
The framework introduces two primary abstractions:
ChatCompletionAgent— A single agent backed by a chat completion model. It holds a name, instructions (system prompt), and a Kernel with registered plugins.AgentGroupChat— An orchestration container that manages conversations between one or more agents, with configurable selection and termination strategies.
These building blocks are deliberately minimal. Microsoft designed the framework so that complex behavior emerges from composition, not from a sprawling API surface.
Defining Your First Agent
A ChatCompletionAgent wraps a configured Kernel with identity and purpose. Here is a straightforward example — a code review agent that analyzes C# code for common issues:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
// Build the kernel with your AI service
Kernel kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!)
.Build();
// Define the agent
ChatCompletionAgent reviewerAgent = new()
{
Name = "CodeReviewer",
Instructions = """
You are an expert .NET code reviewer. Analyze submitted C# code for:
- Potential null reference exceptions
- Missing disposal of IDisposable resources
- Async/await anti-patterns
- Security vulnerabilities
Provide specific, actionable feedback with corrected code examples.
""",
Kernel = kernel
};
To have a conversation with this single agent:
ChatHistory history = [];
history.AddUserMessage("Review this code:\n```csharp\nvar client = new HttpClient();\nvar result = client.GetStringAsync(url).Result;\n```");
await foreach (ChatMessageContent response in reviewerAgent.InvokeAsync(history))
{
Console.WriteLine(response.Content);
}
The agent uses the kernel’s configured AI service to generate responses, guided by its instructions. Nothing more, nothing less.
Registering Tools for Agents
Agents become genuinely useful when they can interact with the outside world. Tool registration works through the same plugin system that Semantic Kernel uses — you register plugins on the agent’s kernel, and the AI model decides when to call them.
public class GitPlugin
{
[KernelFunction("get_recent_commits")]
[Description("Retrieves the last N commits from the current git branch")]
public async Task<string> GetRecentCommitsAsync(
[Description("Number of commits to retrieve")] int count = 5)
{
var process = new Process
{
StartInfo = new ProcessStartInfo("git", $"log --oneline -n {count}")
{
RedirectStandardOutput = true,
UseShellExecute = false
}
};
process.Start();
return await process.StandardOutput.ReadToEndAsync();
}
[KernelFunction("get_file_diff")]
[Description("Gets the git diff for a specific file")]
public async Task<string> GetFileDiffAsync(
[Description("Path to the file")] string filePath)
{
var process = new Process
{
StartInfo = new ProcessStartInfo("git", $"diff {filePath}")
{
RedirectStandardOutput = true,
UseShellExecute = false
}
};
process.Start();
return await process.StandardOutput.ReadToEndAsync();
}
}
Register the plugin and enable automatic function calling:
Kernel toolKernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!)
.Build();
toolKernel.Plugins.AddFromType<GitPlugin>();
ChatCompletionAgent commitReviewAgent = new()
{
Name = "CommitReviewer",
Instructions = "Review recent git commits. Use the available tools to inspect commit history and file diffs. Summarize changes and flag potential issues.",
Kernel = toolKernel,
Arguments = new KernelArguments(
new OpenAIPromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
})
};
When this agent receives a message like “Review the last 3 commits,” it autonomously calls get_recent_commits, examines the results, optionally calls get_file_diff for specific files, and synthesizes a review. That’s the agent loop in action — observe, reason, act, evaluate.
Multi-Agent Patterns
Single agents handle focused tasks well. But many real-world workflows require specialization — different perspectives, different capabilities, or different stages of processing. That’s where multi-agent patterns come in.
Pattern 1: Writer/Editor (Two-Agent Collaboration)
The most intuitive multi-agent pattern. One agent produces content, another critiques and refines it.
ChatCompletionAgent writerAgent = new()
{
Name = "TechnicalWriter",
Instructions = """
You are a technical writer for .NET documentation.
Write clear, accurate content with code examples.
When the editor provides feedback, revise accordingly.
Say APPROVED when the editor approves your work.
""",
Kernel = kernel
};
ChatCompletionAgent editorAgent = new()
{
Name = "TechnicalEditor",
Instructions = """
You are a senior technical editor. Review the writer's output for:
- Technical accuracy
- Code correctness
- Clarity and conciseness
Provide specific feedback. Say APPROVED when the content meets standards.
""",
Kernel = kernel
};
Orchestrate them with AgentGroupChat:
AgentGroupChat chat = new(writerAgent, editorAgent)
{
ExecutionSettings = new()
{
TerminationStrategy = new ApprovalTerminationStrategy()
{
Agents = [editorAgent],
MaximumIterations = 8
}
}
};
chat.AddChatMessage(new ChatMessageContent(
AuthorRole.User,
"Write a 200-word explanation of dependency injection in .NET."));
await foreach (ChatMessageContent message in chat.InvokeAsync())
{
Console.WriteLine($"[{message.AuthorName}]: {message.Content}");
}
You need a termination strategy to prevent infinite loops. Here is a simple one that watches for the keyword “APPROVED”:
public class ApprovalTerminationStrategy : TerminationStrategy
{
protected override Task<bool> ShouldAgentTerminateAsync(
Agent agent, IReadOnlyList<ChatMessageContent> history,
CancellationToken cancellationToken = default)
{
bool isApproved = history.Last().Content?.Contains("APPROVED",
StringComparison.OrdinalIgnoreCase) ?? false;
return Task.FromResult(isApproved);
}
}
Pattern 2: Sequential Pipeline
Agents execute in a strict order. Each agent processes the accumulated conversation and hands off to the next. This pattern works well for staged processing like research, then draft, then review, then format.
ChatCompletionAgent researchAgent = new()
{
Name = "Researcher",
Instructions = "Gather and summarize relevant facts about the topic. Be thorough.",
Kernel = kernelWithSearchPlugin
};
ChatCompletionAgent drafterAgent = new()
{
Name = "Drafter",
Instructions = "Using the researcher's findings, draft a well-structured article.",
Kernel = kernel
};
ChatCompletionAgent factCheckerAgent = new()
{
Name = "FactChecker",
Instructions = "Verify the claims in the draft against the research. Flag inaccuracies. Say VERIFIED when satisfied.",
Kernel = kernel
};
AgentGroupChat pipeline = new(researchAgent, drafterAgent, factCheckerAgent)
{
ExecutionSettings = new()
{
SelectionStrategy = new SequentialSelectionStrategy(),
TerminationStrategy = new ApprovalTerminationStrategy
{
Agents = [factCheckerAgent],
MaximumIterations = 6
}
}
};
The SequentialSelectionStrategy ensures agents speak in the order they were added to the group chat. Predictable and easy to debug.
Pattern 3: Selector-Based Routing
For dynamic workflows where the next agent depends on context, use a KernelFunctionSelectionStrategy. The AI itself decides which agent should respond next.
KernelFunction selectionFunction = KernelFunctionFactory.CreateFromPrompt(
"""
Given the conversation so far, determine which agent should respond next.
Agents: {{$agents}}
Rules:
- If code needs to be written, select Coder
- If code needs review, select Reviewer
- If tests need to be written, select Tester
Respond with ONLY the agent name.
""");
AgentGroupChat dynamicChat = new(coderAgent, reviewerAgent, testerAgent)
{
ExecutionSettings = new()
{
SelectionStrategy = new KernelFunctionSelectionStrategy(
selectionFunction, kernel)
{
ResultParser = (result) => result.GetValue<string>()?.Trim() ?? "Coder"
},
TerminationStrategy = new MaxIterationTerminationStrategy(10)
}
};
This pattern is powerful but harder to predict. Use it when the workflow genuinely requires dynamic routing, not as a default choice.
Built-In Observability
The Agent Framework inherits Semantic Kernel’s filter pipeline, giving you observability without bolting on custom middleware. You can intercept every function invocation, every prompt sent to the model, and every response received.
public class AgentObservabilityFilter : IFunctionInvocationFilter
{
private readonly ILogger _logger;
public AgentObservabilityFilter(ILogger<AgentObservabilityFilter> logger)
{
_logger = logger;
}
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
_logger.LogInformation(
"Agent invoking function {Plugin}.{Function}",
context.Function.PluginName,
context.Function.Name);
var stopwatch = Stopwatch.StartNew();
await next(context);
stopwatch.Stop();
_logger.LogInformation(
"Function {Function} completed in {ElapsedMs}ms",
context.Function.Name,
stopwatch.ElapsedMilliseconds);
}
}
Register the filter on each agent’s kernel:
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(/* ... */);
builder.Services.AddSingleton<IFunctionInvocationFilter, AgentObservabilityFilter>();
Kernel observableKernel = builder.Build();
For production systems, pair this with OpenTelemetry to export traces, metrics, and logs to your monitoring infrastructure. Semantic Kernel emits traces following the OpenTelemetry gen_ai semantic conventions, so your agent interactions show up naturally in distributed tracing tools.
Single Agent vs Multi-Agent: Making the Decision
Not every problem needs multiple agents. In fact, most problems don’t. Here is a framework for deciding.
Use a single agent when:
- The task has a single, well-defined objective
- One system prompt can adequately describe the required behavior
- The tools needed are cohesive (they all serve the same purpose)
- Latency matters — each additional agent adds round trips to the AI service
Use multiple agents when:
- You need genuinely different perspectives (writer vs editor, optimist vs critic)
- Different stages require different tool sets that would confuse a single agent
- You want a separation of concerns — each agent has a focused responsibility
- The workflow benefits from iterative refinement across specialized roles
Real-World Trade-Offs
Multi-agent systems introduce complexity that single-agent setups avoid entirely.
Cost. Every agent turn is an API call. A three-agent pipeline with two iterations generates at least six LLM calls for a single user request. Budget accordingly.
Latency. Sequential agent conversations are inherently serial. If your writer/editor loop runs four turns, that’s four round-trip API calls before the user sees a result. Consider streaming the final output.
Debugging. When three agents collaborate and the output is wrong, identifying which agent introduced the error requires careful logging. The observability filters discussed above become essential, not optional.
Determinism. Multi-agent conversations are less predictable than single-agent interactions. The same input can produce different agent-to-agent dynamics. Build termination strategies with hard limits (MaximumIterations) to prevent runaway conversations.
Start with the simplest architecture that could work. Build a single agent with clear instructions and good tools. If you find that the instructions are becoming contradictory or the tool set is too large for the model to use reliably, that’s when decomposition into multiple agents earns its keep.
What’s Next
The Agent Framework continues to evolve rapidly within the Semantic Kernel ecosystem. Concepts like process orchestration (Microsoft.SemanticKernel.Process) and declarative agent definitions are on the roadmap.
For now, focus on mastering the core patterns covered here. Understand how Semantic Kernel’s architecture works, get comfortable with function calling and tool use, and read the official Agent Framework documentation to stay current with API changes.
The Microsoft.SemanticKernel.Agents.Core package on NuGet is the single dependency you need. If you’re new to the concept of agents, our practical definition of AI agents provides additional context on where the industry is heading.
Build agents that accomplish real tasks. Keep the architecture as simple as the problem allows. Add agents only when specialization genuinely improves outcomes.