π§ Customer Support Triage
A routing orchestrator classifies incoming support tickets and delegates to specialist sub-agents β each with their own domain tools, instructions, and expertise.
This is a realistic production pattern. A single TriageAgent receives every customer message, determines the category (billing, technical, or account), and routes to the right specialist. Each specialist has its own focused system prompt and dedicated tools, so it never has to deal with concerns outside its domain.
Each specialist is a full
AIAgent registered as a tool via .AsAIFunction().
When to Use This Pattern
| Use a Sub-Agent when⦠| Use a Tool (function) when⦠|
|---|---|
| The task needs its own focused system prompt / persona | The operation is deterministic (database lookup, API call) |
| The task requires multi-step LLM reasoning internally | No LLM reasoning is needed β just data retrieval or mutation |
| The domain is complex enough to benefit from specialist context | The result is always the same given the same inputs |
| You want the sub-task to be independently testable and reusable | Speed matters β tools run in milliseconds without an LLM call |
| The sub-agent may itself call other tools or sub-agents | The function is shared across multiple agents as a utility |
NuGet Packages
dotnet add package Microsoft.Agents.AI.OpenAI --prerelease
dotnet add package Azure.AI.OpenAI --prerelease
dotnet add package Azure.Identity
Full Example β Customer Support Triage System
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// ββ Tool functions (deterministic β no LLM needed, so these are plain C# functions) ββ
string GetInvoice(
[Description("The invoice ID to look up")] string invoiceId)
=> $"Invoice #{invoiceId}: $149.99 due 2026-02-01. Status: Overdue.";
string ProcessRefund(
[Description("The invoice ID to refund")] string invoiceId)
=> $"Refund for invoice #{invoiceId} has been initiated. Allow 3-5 business days.";
string GetServiceStatus(
[Description("The service name, e.g. 'API', 'Dashboard', 'Auth'")] string service)
=> $"{service} is currently experiencing degraded performance in EU-West region.";
string ResetPassword(
[Description("The customer account email address")] string email)
=> $"Password reset email sent to {email}. Link expires in 24 hours.";
string GetAccountDetails(
[Description("The customer account email address")] string email)
=> $"Account for {email}: Plan=Pro, Seats=5, Renewal=2026-06-01, MFA=Enabled.";
// ββ Azure OpenAI client ββ
var azureClient = new AzureOpenAIClient(
new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
new AzureCliCredential());
// ββ Specialist Agent 1: Billing ββ
// Sub-agent because: needs domain expertise, multi-step reasoning (empathy + policy + refund decision)
AIAgent billingAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are a billing specialist with deep knowledge of invoicing and refund policies.
Always be empathetic and professional. When looking up invoices or processing refunds,
use your tools. Explain charges clearly and offer solutions proactively.
Never promise a refund before checking the invoice first.
""",
name: "BillingAgent",
description: "Handles billing issues: invoice queries, payment problems, refund requests, subscription charges.",
tools:
[
AIFunctionFactory.Create(GetInvoice),
AIFunctionFactory.Create(ProcessRefund)
]);
// ββ Specialist Agent 2: Technical Support ββ
// Sub-agent because: needs systematic troubleshooting reasoning and domain knowledge
AIAgent technicalAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are a technical support engineer. Diagnose problems methodically:
1. Check service status first, 2. Gather symptoms, 3. Provide step-by-step fixes.
Use your tools to check live service status. If an outage is detected, acknowledge it
immediately and give an ETA if possible. Always end with a follow-up offer.
""",
name: "TechnicalAgent",
description: "Handles technical issues: bugs, outages, API errors, integration problems, performance issues.",
tools:
[
AIFunctionFactory.Create(GetServiceStatus)
]);
// ββ Specialist Agent 3: Account Management ββ
// Sub-agent because: handles sensitive account operations with its own compliance instructions
AIAgent accountAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are an account management specialist. Help customers with account settings,
security, and plan management. Always verify the customer's email before making changes.
For security-sensitive operations (password reset, MFA), confirm the action explicitly
before proceeding. Log all actions taken.
""",
name: "AccountAgent",
description: "Handles account issues: password resets, MFA setup, plan upgrades, user management, profile changes.",
tools:
[
AIFunctionFactory.Create(ResetPassword),
AIFunctionFactory.Create(GetAccountDetails)
]);
// ββ Triage Orchestrator ββ
// This agent classifies and routes β it does NOT handle domain topics itself.
AIAgent triageAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are the first point of contact for customer support. Your job is to:
1. Greet the customer warmly.
2. Understand their issue.
3. Route to the correct specialist agent β do NOT try to resolve domain issues yourself.
Routing rules:
- Billing questions (invoices, payments, refunds, charges) β BillingAgent
- Technical problems (bugs, outages, API errors, slow performance) β TechnicalAgent
- Account issues (login, password, MFA, plan, profile) β AccountAgent
After the specialist responds, relay their answer to the customer in a friendly tone.
If the issue spans multiple domains, call each relevant agent and synthesise the answers.
""",
tools:
[
billingAgent.AsAIFunction(),
technicalAgent.AsAIFunction(),
accountAgent.AsAIFunction()
]);
// ββ Run the triage system ββ
// Example 1: Billing issue β routed to BillingAgent
Console.WriteLine("=== Ticket 1: Billing ===");
Console.WriteLine(await triageAgent.RunAsync(
"Hi, I was charged $149.99 for invoice #INV-2042 but I thought I cancelled. Can I get a refund?"));
Console.WriteLine();
// Example 2: Technical issue β routed to TechnicalAgent
Console.WriteLine("=== Ticket 2: Technical ===");
Console.WriteLine(await triageAgent.RunAsync(
"The Dashboard has been loading slowly for the past hour. Is there an outage?"));
Console.WriteLine();
// Example 3: Mixed issue β triage calls BOTH AccountAgent and BillingAgent
Console.WriteLine("=== Ticket 3: Mixed ===");
Console.WriteLine(await triageAgent.RunAsync(
"I can't log in to my account (user@example.com) and I also need to check my current plan details."));
Architecture Explained
Customer message
β
βΌ
[TriageAgent] β "routing brain" β classifies and delegates
ββ billing keywords β [BillingAgent] β GetInvoice(), ProcessRefund()
ββ technical issues β [TechnicalAgent] β GetServiceStatus()
ββ account problems β [AccountAgent] β ResetPassword(), GetAccountDetails()
β
Each specialist returns a resolution
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
[TriageAgent] relays result to customer in a friendly tone
Why Tools for Data Lookups?
Notice that GetInvoice(), GetServiceStatus(), and similar helpers
are plain C# functions, not sub-agents. They are fast, deterministic, and
require no reasoning β they just look up data. The LLM reasoning happens around the
data, inside the specialist agent's instructions. This keeps costs low and latency minimal.
Contrast this with the specialist agents themselves β they are sub-agents because each one needs a focused system prompt, domain expertise, and the ability to make multi-step decisions (e.g. check invoice, then decide whether refund policy applies, then process the refund, then draft a reply). That reasoning chain benefits from a dedicated LLM context and persona.
Key Design Decisions
- The TriageAgent does NOT solve domain problems itself β its instructions explicitly say "do not resolve domain issues yourself". This keeps responsibilities clear and prevents the orchestrator from hallucinating domain knowledge.
- Rich
descriptionstrings on sub-agents β the triage LLM reads these to decide routing. Poor descriptions cause mis-routing. Treat them like job titles + responsibilities. - Mixed-issue support β Ticket 3 shows the orchestrator calling two sub-agents and synthesising a combined answer. This happens automatically.
- Each specialist is independently testable β you can call
billingAgent.RunAsync(...)directly in unit tests without the triage layer.
Next Steps
All Examples
- π€ Hello Agent
- π§ Function Tools
- π¬ Multi-Turn Conversations
- β‘ Streaming Responses
- π¦ Structured Output
- π Sequential Workflows
- πΈοΈ Multi-Agent Orchestration
- π¦ Ollama β Local AI
- π₯οΈ LM Studio β Local AI
- π§ Agent Memory
- π RAG
- π MCP Tools
- π OpenTelemetry
- π§ Customer Support Triage
- π¬ Research Pipeline
- π€ Tools vs Sub-Agents
Concepts Used
π€ Tools vs Sub-Agents Guide π Agents Documentation