← Blog

Building Multi-Agent Systems with Semantic Kernel

A deep dive into creating intelligent multi-agent systems using Microsoft Semantic Kernel for orchestrating complex AI workflows.

0
Table of contents

The first agent most teams build is one model with one long prompt and every tool attached. It works in the demo. Then the tool list grows, the prompt grows with it, and the model starts calling the wrong function, skipping steps or approving its own mistakes. The fix that has worked best for me is the same one you would use with people: give each agent one job, and put a simple process around how they talk to each other.

This post walks through that pattern with Microsoft Semantic Kernel in C#. The example is the kind of task I automate around ERPNext: find items running low in a warehouse and prepare purchase orders for them. Three agents share the work. A Planner breaks the request into steps, a Builder carries them out with tools, and a Reviewer checks the result before anything is handed to a person.

The building blocks

  • Kernel: the container that holds your AI services (the chat model) and your plugins. Every agent runs on a kernel.
  • Plugins: plain C# classes whose methods are marked [KernelFunction]. The description you write on each method is what the model reads when it decides whether to call it.
  • ChatCompletionAgent: an agent with a name, instructions and a kernel. Its instructions act as the system prompt.
  • AgentGroupChat: a conversation between several agents, with one strategy deciding who speaks next and another deciding when the conversation is over.

Start with the tools. They are ordinary classes that wrap the ERP’s REST API, which keeps the AI side thin and easy to test.

Plugins.cs · tools the Builder can call
using System.ComponentModel;
using Microsoft.SemanticKernel;

public sealed class InventoryPlugin(ErpClient erp)
{
    [KernelFunction, Description("Items in a warehouse below their reorder level.")]
    public Task<IReadOnlyList<LowStockItem>> GetLowStockItemsAsync(
        [Description("Warehouse name, e.g. 'Stores - Dhaka'")] string warehouse)
        => erp.GetLowStockAsync(warehouse);
}

public sealed class BuyingPlugin(ErpClient erp)
{
    [KernelFunction, Description("Creates a DRAFT purchase order and returns its name.")]
    public Task<string> CreateDraftPurchaseOrderAsync(
        string supplier, OrderLine[] lines)
        => erp.CreateDraftPurchaseOrderAsync(supplier, lines);
}

Notice that the only write action creates a draft. Agents can be wrong, so the design makes being wrong cheap: a draft that a person reviews and submits, never a posted document.

Give each agent one job

Each agent gets short, specific instructions. Only the Builder gets the tools, through its own clone of the kernel, so the Planner and Reviewer can’t call them even by accident.

Agents.cs · three agents, one kernel each
var kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion("gpt-4o-mini", config["OpenAI:ApiKey"]!);
Kernel kernel = kernelBuilder.Build();

Kernel toolKernel = kernel.Clone();
toolKernel.Plugins.AddFromObject(new InventoryPlugin(erp), "Inventory");
toolKernel.Plugins.AddFromObject(new BuyingPlugin(erp), "Buying");

ChatCompletionAgent planner = new()
{
    Name = "Planner",
    Instructions = """
        You plan stock replenishment. Turn the request into short numbered
        steps for the Builder. Never call tools and never invent numbers.
        """,
    Kernel = kernel,
};

ChatCompletionAgent builder = new()
{
    Name = "Builder",
    Instructions = """
        You carry out the Planner's steps with your tools. Report exactly what
        each tool returned. Purchase orders are always drafts.
        """,
    Kernel = toolKernel,
    Arguments = new KernelArguments(new OpenAIPromptExecutionSettings
    {
        FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(),
    }),
};

ChatCompletionAgent reviewer = new()
{
    Name = "Reviewer",
    Instructions = """
        Check the Builder's result against the request: every low-stock item
        is covered, quantities reach the reorder quantity, one draft per
        supplier. Reply APPROVED if it is right, otherwise list what to fix.
        """,
    Kernel = kernel,
};
Diagram: a user request enters an AgentGroupChat where a selection strategy picks between Planner, Builder and Reviewer; the Builder calls the ERPNext REST API; a termination strategy ends the chat when the Reviewer approves, producing a draft order
The three agents inside one group chat. Only the Builder touches the ERP, and the result is a draft a person submits.

Deciding who speaks next

The selection strategy picks the next speaker after every turn. For a fixed order you can use SequentialSelectionStrategy, which simply goes round the list. I prefer a small prompt, because it lets the Builder go back to work when the Reviewer asks for changes without the Planner starting over.

Selection.cs · a prompt that returns one agent name
KernelFunction selectNext = AgentGroupChat.CreatePromptFunctionForStrategy(
    """
    Choose who speaks next: Planner, Builder or Reviewer.
    - The Planner speaks first.
    - After the Planner, the Builder.
    - After the Builder, the Reviewer.
    - If the Reviewer asked for changes, the Builder.
    Reply with the name only.

    History:
    {{$history}}
    """,
    safeParameterNames: "history");

This prompt runs on every turn, so it should be cheap. A small model is plenty, and the strategy only needs the last few messages, not the whole conversation.

Knowing when to stop

The termination strategy ends the conversation. It doesn’t need a model at all: the Reviewer was told to say APPROVED, so a string check is enough. MaximumIterations is the safety net that stops two agents from arguing forever.

Chat.cs · wiring it together and running it
#pragma warning disable SKEXP0110 // AgentGroupChat is experimental

sealed class ApprovalTermination : TerminationStrategy
{
    protected override Task<bool> ShouldAgentTerminateAsync(
        Agent agent, IReadOnlyList<ChatMessageContent> history,
        CancellationToken ct)
        => Task.FromResult(history[^1].Content?.Contains("APPROVED") ?? false);
}

AgentGroupChat chat = new(planner, builder, reviewer)
{
    ExecutionSettings = new()
    {
        SelectionStrategy = new KernelFunctionSelectionStrategy(selectNext, kernel)
        {
            ResultParser = r => r.GetValue<string>()?.Trim() ?? "Planner",
            HistoryVariableName = "history",
        },
        TerminationStrategy = new ApprovalTermination
        {
            Agents = [reviewer],   // only the Reviewer can end the chat
            MaximumIterations = 10,
        },
    },
};

chat.AddChatMessage(new ChatMessageContent(AuthorRole.User,
    "Reorder anything running low in Stores - Dhaka."));

await foreach (ChatMessageContent message in chat.InvokeAsync())
{
    logger.LogInformation("{Agent}: {Text}", message.AuthorName, message.Content);
}

What matters in production

Getting three agents to talk is the easy part. Keeping them dependable on real business data is where the time goes:

  • Few tools per agent. A model choosing between three well-described functions is far more reliable than one choosing between thirty. If an agent needs many tools, it is probably two agents.
  • Descriptions are prompts. The Description on each function and parameter is what the model reads. Say what it returns, what units it uses and when not to call it.
  • Write actions create drafts. Anything that posts money, stock or payroll stays a draft until a person approves it. This one rule removes most of the risk.
  • Log every turn. Save who said what, every tool call and its arguments. When a run goes wrong, the transcript tells you which agent to fix.
  • Cap the cost. Limit iterations, keep the selection prompt on a small model, and trim the history each strategy sees.
  • Test with real requests. Keep a set of past requests with known good outcomes and rerun them whenever you change a prompt or a model.
The agents don’t need to be clever. They need narrow jobs, honest tools and a process that makes mistakes cheap.

Takeaways

  • Split one overloaded agent into roles: plan, act, review. Each prompt gets shorter and each failure gets easier to trace.
  • In Semantic Kernel, give tools only to the agent that needs them by giving it its own kernel.
  • Use a model to choose the next speaker if the order varies, and plain code to decide when to stop.
  • Design for review: the system prepares the work, a person signs it off.

Newsletter

Enjoyed this? Get the next one by email

A short email when I publish something new on AI agents, ERP automation or research. No spam; unsubscribe in one click.

Comments (0)

You’ll sign in when you post. Only your name and photo are shown.

Comments are checked by AI before they appear. Read the rules
  • Be respectful: no insults, harassment, hate or threats. Disagreeing is fine.
  • No spam: no ads, self-promotion, SEO or affiliate links, crypto or gambling.
  • Nothing sexual, violent or illegal, and no scams or malware.
  • Don't share anyone's personal details (phone numbers, addresses, private emails).
  • Keep it a real comment: no gibberish, test text or messages aimed at the AI.

Loading comments…

More from Omor Al Tanim

See all posts →