π€ Tools vs Sub-Agents
The most important architectural decision in multi-agent systems: when to use a simple C# function tool vs a full LLM-powered sub-agent.
Both tools and sub-agents extend what an agent can do β but they have fundamentally different cost, latency, and capability profiles. Choosing wrong leads to either over-engineered systems (agent for everything) or brittle ones (expecting a C# function to reason for you).
Quick Decision Guide
| Question | β Use a Tool | β Use a Sub-Agent |
|---|---|---|
| Does it need LLM reasoning? | β No β deterministic output | β Yes β judgment required |
| Does it benefit from a focused system prompt? | β No persona needed | β Domain expertise via instructions |
| Does it call external APIs or databases? | β Perfect fit | β Overkill β use a tool |
| Does it need to make multi-step decisions? | β Fixed logic only | β Planning + tool-calling loop |
| Does it need to call other tools itself? | β A tool cannot call tools | β Agents can have their own tools |
| Is speed critical (<100 ms)? | β No LLM overhead | β Adds an LLM round-trip |
| Will multiple agents reuse it? | β Share one C# function | β Share an AIAgent instance |
| Does the output vary with context/tone? | β Returns fixed data | β Adapts output to situation |
Side-by-Side Code Comparison
Same task (get weather) implemented both ways β so you can see the difference clearly.
β As a Tool (correct)
Weather lookup is deterministic β always the same result for the same city. No reasoning needed.
// A tool is just a C# method.
// AIFunctionFactory.Create() wraps it.
string GetWeather(
[Description("City name")] string city)
=> $"Weather in {city}: 22Β°C, sunny.";
AIAgent agent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: "Answer weather questions.",
tools: [AIFunctionFactory.Create(GetWeather)]);
// Agent calls GetWeather() internally,
// then synthesises the answer.
Console.WriteLine(await agent.RunAsync(
"What's the weather in Paris?"));
- 1 LLM call + 1 C# function call
- Fast, cheap, predictable
β As a Sub-Agent (overkill)
Creating a full agent just to look up weather is wasteful β it adds an extra LLM round-trip with no benefit.
// β DON'T do this for simple lookups!
AIAgent weatherAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: "Look up weather.",
name: "WeatherAgent",
description: "Gets weather data.");
AIAgent orchestrator = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: "Answer questions.",
tools: [weatherAgent.AsAIFunction()]);
// 2 LLM calls for something a C# string
// could handle in 0 ms. Avoid this.
Console.WriteLine(await orchestrator.RunAsync(
"What's the weather in Paris?"));
- 2 LLM calls β double the cost & latency
- No quality improvement over a simple tool
When a Sub-Agent IS Worth the Extra LLM Call
// β
Sub-agent IS justified here: the task requires domain reasoning,
// a focused persona, and its own tool-calling loop.
using System.ComponentModel;
using Azure.AI.OpenAI;
using Azure.Identity;
using Microsoft.Agents.AI;
using Microsoft.Extensions.AI;
// These are TOOLS β deterministic lookups, no reasoning needed.
string GetCustomerOrders(
[Description("Customer email")] string email)
=> $"Orders for {email}: #1042 (delivered), #1087 (processing), #1101 (shipped).";
string GetProductDetails(
[Description("Product ID")] string productId)
=> $"Product {productId}: Premium Widget, Β£49.99, In Stock: 12 units, Rating: 4.7/5.";
string ApplyDiscount(
[Description("Order ID")] string orderId,
[Description("Discount percentage (1-20)")] int percent)
=> $"Applied {percent}% discount to order #{orderId}. New total: Β£{49.99 * (1 - percent / 100.0):F2}.";
var azureClient = new AzureOpenAIClient(
new Uri(Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!),
new AzureCliCredential());
// β
Sub-agent IS worth the extra LLM call here because:
// 1. Needs a specialist persona (empathetic, policy-aware)
// 2. Makes multi-step decisions: look up account β assess situation β decide on discount
// 3. The discount decision requires judgment β not a fixed rule
AIAgent retentionAgent = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are a customer retention specialist. Your goal is to keep customers happy
and prevent churn. When a customer is unhappy:
1. Look up their order history first β understand their purchase pattern.
2. Check the specific product they're concerned about.
3. Use your best judgment to offer a discount (5-20%) based on their loyalty.
Long-term customers with multiple orders deserve more generous offers.
4. Apply the discount if the customer accepts.
Be warm, empathetic, and proactive β don't wait for the customer to ask.
""",
name: "RetentionAgent",
description: "Handles unhappy customers and applies retention discounts. Provide the customer email and their complaint.",
tools:
[
AIFunctionFactory.Create(GetCustomerOrders),
AIFunctionFactory.Create(GetProductDetails),
AIFunctionFactory.Create(ApplyDiscount)
]);
// The orchestrator (main agent) delegates retention cases to the specialist.
AIAgent supportOrchestrator = azureClient
.GetChatClient("gpt-4o-mini")
.AsAIAgent(
instructions: """
You are the main customer support agent. Handle general questions yourself.
For unhappy customers who mention cancelling, refunds, or disappointment β
delegate to RetentionAgent immediately with their email and complaint details.
""",
tools: [retentionAgent.AsAIFunction()]);
// The orchestrator recognises this as a retention case and delegates.
Console.WriteLine(await supportOrchestrator.RunAsync(
"My email is jane@example.com. I've been a customer for 2 years but my last order " +
"was broken and I'm thinking of cancelling my subscription."));
πΊοΈ Pattern Catalog
An orchestrator classifies input and routes to the right specialist. See Customer Support Triage.
User β [Orchestrator]
βββ billing? β [BillingAgent]
βββ technical? β [TechnicalAgent]
βββ account? β [AccountAgent]
When to use: Input can be classified into categories, each requiring different expertise or persona.
Each agent processes and enriches the output of the previous one. See Research Pipeline.
Input β [Stage1Agent] β [Stage2Agent] β [Stage3Agent] β Output
(research) (fact-check) (write)
When to use: Complex task can be decomposed into sequential refinement steps, each requiring different expertise.
An orchestrator fans out to multiple specialists simultaneously and synthesises their independent perspectives.
ββ [SecurityAgent] βββ
Input β [Coordinator] ββ€β [PerformanceAgent] βββ synthesise β Output
ββ [StyleAgent] βββ
When to use: Task needs multiple independent reviews (code review, multi-perspective analysis). Use Task.WhenAll to fan out in parallel for speed.
A top-level orchestrator delegates to sub-orchestrators that themselves coordinate their own specialists. Enables very large-scale systems.
[TopOrchestrator]
βββ [SalesOrchestrator]
β βββ [LeadAgent]
β βββ [PricingAgent]
βββ [SupportOrchestrator]
βββ [BillingAgent]
βββ [TechnicalAgent]
When to use: System is large enough that a single orchestrator would have too many tools. Decompose into sub-orchestrators by department or domain.
β οΈ Anti-Patterns to Avoid
β The "God Tool"
A tool function that does complex reasoning internally β calling an LLM, parsing responses, making decisions.
// β Wrong: a "tool" that secretly calls an LLM
string AnalyseCustomerSentiment(string text)
{
// calls OpenAI internally β hidden LLM!
var response = openAiClient.Complete(text);
return response.Choices[0].Text;
}
Fix: Make it a sub-agent with proper instructions and tool registration. Hidden LLM calls bypass observability and error handling.
β Flat Everything
One giant agent with 20+ tools, no sub-agents, handling everything in one context window.
// β Wrong: one agent, 20 tools, 3000 word
// system prompt. Context is polluted.
AIAgent agent = client.AsAIAgent(
instructions: "...3000 words...",
tools: [tool1, tool2, ..., tool20]);
Fix: Group related tools into specialist sub-agents. Smaller context β better focus β higher quality.
β οΈ Agent for Everything
Using a full sub-agent for tasks that a C# function could handle in milliseconds at zero LLM cost.
// β οΈ Overkill for a simple string format:
AIAgent formatterAgent = client.AsAIAgent(
name: "DateFormatterAgent",
instructions: "Format dates as DD/MM/YYYY.");
Fix: DateTime.ToString("dd/MM/yyyy") β a tool that calls a C# method. Costs nothing.
β οΈ Vague Sub-Agent Descriptions
The orchestrator uses the description to decide when to call a sub-agent. Vague descriptions cause mis-routing.
// β Vague β the orchestrator won't know when to call this:
name: "HelperAgent", description: "Helps with things."
// β
Specific β clear routing signal:
name: "BillingAgent",
description: "Handles invoice queries, payment problems, " +
"refund requests, and subscription billing issues."
Rule of Thumb
π΅ Use a sub-agent if the task requires a persona, multi-step planning, or judgment.
π‘ When in doubt: start with a tool. Promote it to a sub-agent when you find yourself writing complex logic inside the tool body that would benefit from LLM reasoning.
See These Patterns in Action
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
On This Page
- π Quick Decision Guide
- βοΈ Side-by-Side Comparison
- β When Sub-Agents Shine
- πΊοΈ Pattern Catalog
- β οΈ Anti-Patterns
π§ Router Example π¬ Pipeline Example