Skip to content

Multi-Agent Orchestration

Edit this page

Sometimes a single AI agent is not enough. You may need specialists for different domains, with an orchestrator that routes questions to the right expert. In this guide you will build a multi-agent system where a central orchestrator delegates tasks to specialist agents based on the content of the user's question.

Prerequisites

  • Symfony AI Platform component
  • Symfony AI Agent component

Step 1: Install Packages

Install the Platform and Agent components via Composer:

1
composer require symfony/ai-platform symfony/ai-agent

Step 2: Create Specialist Agents

Each specialist agent is a regular Agent with a SystemPromptInputProcessor that defines its area of expertise. Give each agent a descriptive name so the orchestrator can identify it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
use Symfony\AI\Agent\Agent;
use Symfony\AI\Agent\InputProcessor\SystemPromptInputProcessor;

$technical = new Agent(
    $platform,
    'gpt-5-mini',
    [new SystemPromptInputProcessor(
        'You are a technical support specialist. Help users resolve bugs and errors.',
    )],
    name: 'technical',
);

$billing = new Agent(
    $platform,
    'gpt-5-mini',
    [new SystemPromptInputProcessor(
        'You are a billing specialist. Help users with invoices and payments.',
    )],
    name: 'billing',
);

Step 3: Create an Orchestrator Agent

The orchestrator is another agent whose system prompt instructs it to analyze user questions and decide which specialist should handle them. It does not need a name since it acts as the entry point:

1
2
3
4
5
6
7
$orchestrator = new Agent(
    $platform,
    'gpt-5-mini',
    [new SystemPromptInputProcessor(
        'You are an agent orchestrator that routes user questions to specialized agents.',
    )],
);

Step 4: Configure Handoffs

A Handoff defines when a question should be routed to a specific agent. The when parameter accepts an array of keywords that trigger the routing. When the orchestrator detects these keywords in the user's question, it delegates to the matching agent:

1
2
3
4
5
6
7
8
9
10
11
12
use Symfony\AI\Agent\MultiAgent\Handoff;

$handoffs = [
    new Handoff(
        to: $technical,
        when: ['bug', 'error', 'exception', 'technical'],
    ),
    new Handoff(
        to: $billing,
        when: ['invoice', 'payment', 'billing', 'subscription'],
    ),
];

Step 5: Build the MultiAgent

The MultiAgent ties everything together. It takes the orchestrator, an array of handoffs, and a fallback agent that handles questions that do not match any specialist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\AI\Agent\MultiAgent\MultiAgent;

$fallback = new Agent(
    $platform,
    'gpt-5-mini',
    [new SystemPromptInputProcessor(
        'You are a general assistant. Help users with any non-specialized questions.',
    )],
    name: 'fallback',
);

$multiAgent = new MultiAgent(
    orchestrator: $orchestrator,
    handoffs: $handoffs,
    fallback: $fallback,
);

Step 6: Route Questions Automatically

Call the multi-agent with a MessageBag just like a regular agent. The orchestrator analyzes the question and routes it to the appropriate specialist automatically:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

// Technical question - routed to the technical agent
$messages = new MessageBag(
    Message::ofUser('I get a "Call to undefined method" error in my controller.'),
);
$result = $multiAgent->call($messages);
echo $result->getContent();

// General question - routed to the fallback agent
$messages = new MessageBag(
    Message::ofUser('Can you recommend a good pasta recipe?'),
);
$result = $multiAgent->call($messages);
echo $result->getContent();

Tip

You can add as many specialist agents as you need. Each handoff is evaluated independently, so the orchestrator can route to any number of domains. For debugging, pass a PSR-3 logger to the MultiAgent constructor to see which agent handles each request.

Step 7: Show Which Agent Answers

Routing is invisible from the outside: the caller asks a question and gets an answer, with no clue which specialist produced it. Iterating the multi-agent instead of reading its result lets you show that, because the delegated agents report their own steps into the very same execution. The routing itself arrives as a Progress update of the handoff stage, carrying the orchestrator's Decision as payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
use Symfony\AI\Agent\Execution\Update\Progress;
use Symfony\AI\Agent\Execution\Update\Result;
use Symfony\AI\Agent\MultiAgent\Handoff\Decision;

$messages = new MessageBag(
    Message::ofUser('I get a "Call to undefined method" error in my controller.'),
);

foreach ($multiAgent->call($messages) as $update) {
    if ($update instanceof Progress && 'handoff' === $update->getStage()) {
        $decision = $update->getPayload();
        \assert($decision instanceof Decision);

        echo $update->getMessage().' Reason: '.$decision->getReasoning().\PHP_EOL;

        continue;
    }

    if ($update instanceof Progress) {
        echo '  '.$update->getMessage().\PHP_EOL; // the specialist's own model requests and tool calls
    }

    if ($update instanceof Result) {
        echo $update->getResult()->getContent().\PHP_EOL;
    }
}

The message names the agent that actually runs, while the decision explains why the orchestrator picked it - the two differ when the orchestrator selects an agent that no handoff defines, in which case the fallback answers instead. See the orchestrator-iterable.php example for a runnable version of this loop.

Note

The orchestrator's own routing round is machinery, not answer: its model_request and tool_call updates are forwarded so you can show that a decision is being made, but the deltas that spell out the Decision are not. With the stream option, the delta updates you receive are therefore only those of the agent that answers.

Learn More

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version