What is Function Calling?
Function calling — also known as tool use — is the mechanism that bridges the gap between an LLM generating text and an LLM doing things. At the protocol level, the model does not simply return a string. Instead, it can return a structured JSON object that says: “I need to call function X with arguments Y.”
Your application receives that request, executes the function, and returns the result to the model. The model then incorporates that result into its reasoning and either calls another function or produces a final text response.
This is how AI agents work. Without function calling, an LLM is a sophisticated text predictor. With function calling, it becomes an orchestrator that can query databases, call APIs, perform calculations, and interact with the world through your code.
For a broader view of the Semantic Kernel architecture that makes this possible, see Semantic Kernel Architecture Deep Dive.
How Function Calling Works at the Protocol Level
When you send a chat completion request with function definitions, the exchange follows this pattern:
1. Your app → LLM: "What's the weather in Seattle?" + [function definitions]
2. LLM → Your app: tool_call { name: "get_weather", arguments: { "city": "Seattle" } }
3. Your app: executes get_weather("Seattle") → "72°F, partly cloudy"
4. Your app → LLM: tool_result { "72°F, partly cloudy" }
5. LLM → Your app: "The weather in Seattle is 72°F and partly cloudy."
The model never executes the function. It decides which function to call and what arguments to pass. Your application is the execution environment. This separation is fundamental to understanding the security model.
The KernelFunction Attribute
Semantic Kernel maps C# methods to AI-callable functions through the [KernelFunction] attribute. The model reads the function name, description, parameter names, and parameter descriptions to decide when and how to call each function.
using Microsoft.SemanticKernel;
using System.ComponentModel;
public class WeatherPlugin
{
[KernelFunction("get_current_weather")]
[Description("Gets the current weather conditions for a specified city")]
public async Task<string> GetCurrentWeatherAsync(
[Description("The city name, e.g. 'Seattle' or 'London'")] string city,
[Description("Temperature unit: 'celsius' or 'fahrenheit'")] string unit = "fahrenheit")
{
// In production, call a real weather API here
return $"""
{{
"city": "{city}",
"temperature": 72,
"unit": "{unit}",
"conditions": "partly cloudy",
"humidity": 65
}}
""";
}
}
Every detail matters here. The function name tells the model what this function does. The [Description] on the method tells it when to use it. The [Description] on each parameter tells it how to construct the arguments. Vague descriptions produce vague behavior. Be precise.
Return Types
Kernel functions can return:
string— passed directly to the model as the tool result- Complex objects — serialized to JSON automatically by Semantic Kernel
Task<T>— async functions are fully supported (and recommended)void/Task— the model receives confirmation that the function executed
For most scenarios, returning a JSON-formatted string or a serializable object gives the model the richest context to work with.
Organizing Functions with Plugins
A plugin is a logical grouping of related functions. Think of it as a namespace for capabilities that the model can discover and invoke.
public class DatabasePlugin
{
private readonly IDbConnection _connection;
public DatabasePlugin(IDbConnection connection)
{
_connection = connection;
}
[KernelFunction("query_customers")]
[Description("Searches for customers by name or email. Returns up to 10 matching results.")]
public async Task<List<CustomerSummary>> QueryCustomersAsync(
[Description("Search term to match against customer name or email")] string searchTerm)
{
// Parameterized query — never concatenate user/model input into SQL
var results = await _connection.QueryAsync<CustomerSummary>(
"SELECT TOP 10 Id, Name, Email FROM Customers WHERE Name LIKE @term OR Email LIKE @term",
new { term = $"%{searchTerm}%" });
return results.ToList();
}
[KernelFunction("get_customer_orders")]
[Description("Gets the recent orders for a specific customer by their customer ID")]
public async Task<List<OrderSummary>> GetCustomerOrdersAsync(
[Description("The unique customer ID (integer)")] int customerId)
{
var orders = await _connection.QueryAsync<OrderSummary>(
"SELECT TOP 20 Id, OrderDate, Total, Status FROM Orders WHERE CustomerId = @id ORDER BY OrderDate DESC",
new { id = customerId });
return orders.ToList();
}
}
Register plugins with the kernel:
kernel.Plugins.AddFromType<WeatherPlugin>("Weather");
kernel.Plugins.AddFromType<DatabasePlugin>("Database");
The plugin name (“Weather”, “Database”) gives the model additional context about the function group. When the model sees Weather-get_current_weather and Database-query_customers, it understands the domain boundary.
Auto Function Calling vs. Manual Control
Semantic Kernel provides two modes for handling function calls.
Automatic Invocation (Recommended for Most Cases)
With FunctionChoiceBehavior.Auto(), Semantic Kernel handles the entire tool-calling loop automatically. When the model requests a function call, SK invokes the C# method, sends the result back, and continues until the model produces a final response.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: "https://your-resource.openai.azure.com/",
apiKey: "your-api-key");
Kernel kernel = builder.Build();
kernel.Plugins.AddFromType<WeatherPlugin>("Weather");
kernel.Plugins.AddFromType<DatabasePlugin>("Database");
OpenAIPromptExecutionSettings settings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var result = await kernel.InvokePromptAsync(
"Find customer John Smith and tell me about his recent orders",
new(settings));
Console.WriteLine(result);
Behind the scenes, the model will:
- Call
Database-query_customerswithsearchTerm: "John Smith" - Receive the customer list, identify the correct customer ID
- Call
Database-get_customer_orderswith that customer ID - Produce a natural language summary of the orders
All of this happens in a single InvokePromptAsync call. Semantic Kernel manages the round-trips.
Manual Control
For scenarios where you need to inspect, modify, or approve function calls before execution, use FunctionChoiceBehavior.Auto(autoInvoke: false):
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
var chatService = kernel.GetRequiredService<IChatCompletionService>();
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage("Delete all orders for customer 42");
OpenAIPromptExecutionSettings settings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(autoInvoke: false)
};
var response = await chatService.GetChatMessageContentAsync(chatHistory, settings, kernel);
// Inspect tool calls before executing
foreach (var toolCall in response.Items.OfType<FunctionCallContent>())
{
Console.WriteLine($"Model wants to call: {toolCall.PluginName}-{toolCall.FunctionName}");
Console.WriteLine($"Arguments: {string.Join(", ", toolCall.Arguments ?? [])}");
// Implement approval logic here
Console.Write("Allow this function call? (y/n): ");
if (Console.ReadLine()?.Trim().ToLower() == "y")
{
var functionResult = await toolCall.InvokeAsync(kernel);
chatHistory.Add(functionResult.ToChatMessage());
}
else
{
chatHistory.AddToolMessage(
[new TextContent("Function call denied by user.")]);
}
}
Manual control is essential for destructive operations — deletes, updates, financial transactions — where you need human-in-the-loop approval.
Multi-Step Tool Calling Chains
The real power of function calling emerges when the model chains multiple calls. Consider a travel assistant that needs to check weather, find flights, and calculate costs:
public class TravelPlugin
{
[KernelFunction("get_weather_forecast")]
[Description("Gets the 5-day weather forecast for a destination city")]
public async Task<string> GetForecastAsync(
[Description("Destination city name")] string city,
[Description("Travel date in yyyy-MM-dd format")] string date)
{
// Call weather API
return $$"""{"city":"{{city}}","date":"{{date}}","forecast":"sunny","high":78,"low":62}""";
}
[KernelFunction("search_flights")]
[Description("Searches for available flights between two cities on a given date")]
public async Task<string> SearchFlightsAsync(
[Description("Departure city")] string from,
[Description("Destination city")] string to,
[Description("Date in yyyy-MM-dd format")] string date)
{
// Call flight search API
return $$"""
[
{"airline":"Alaska","flight":"AS123","price":289,"departure":"08:00","arrival":"11:30"},
{"airline":"Delta","flight":"DL456","price":342,"departure":"14:15","arrival":"17:45"}
]
""";
}
[KernelFunction("calculate_trip_budget")]
[Description("Calculates total estimated trip cost including flights, hotel, and meals")]
public Task<string> CalculateBudgetAsync(
[Description("Flight cost in USD")] decimal flightCost,
[Description("Number of nights")] int nights,
[Description("Hotel budget per night in USD")] decimal hotelPerNight)
{
var total = flightCost + (nights * hotelPerNight) + (nights * 75); // $75/day meals
return Task.FromResult($$"""
{"flight":{{flightCost}},"hotel":{{nights * hotelPerNight}},"meals":{{nights * 75}},"total":{{total}}}
""");
}
}
When a user asks “Plan a 3-day trip from Seattle to San Francisco next week,” the model will orchestrate calls to get_weather_forecast, search_flights, and calculate_trip_budget in a logical sequence — using the output of earlier calls to inform the arguments of later ones.
Error Handling
Functions can and will fail in production. Network timeouts, database errors, invalid input — your kernel functions need to handle these gracefully because the model will receive the error information and attempt to reason about it.
[KernelFunction("get_stock_price")]
[Description("Gets the current stock price for a given ticker symbol")]
public async Task<string> GetStockPriceAsync(
[Description("Stock ticker symbol, e.g. 'MSFT' or 'AAPL'")] string ticker)
{
if (string.IsNullOrWhiteSpace(ticker) || ticker.Length > 5)
{
return """{"error": "Invalid ticker symbol. Must be 1-5 characters."}""";
}
try
{
var price = await _stockService.GetPriceAsync(ticker.ToUpperInvariant());
return $$"""{"ticker":"{{ticker.ToUpperInvariant()}}","price":{{price}},"currency":"USD"}""";
}
catch (HttpRequestException ex)
{
return $$"""{"error": "Unable to fetch stock price. Service unavailable.", "detail": "{{ex.Message}}"}""";
}
}
Returning structured error objects (rather than throwing exceptions) gives the model enough context to either retry with corrected arguments or inform the user about the failure. Unhandled exceptions will bubble up as tool errors, which most models handle poorly.
Security Considerations
Function calling changes your threat model. The AI model now controls which functions are called and what arguments are passed. This demands defensive design.
Principle of least privilege. Only register functions that the AI needs for its current task. Do not register an “admin” plugin alongside a customer-facing chat plugin.
Input validation. Treat every parameter as untrusted input — because it is. The model’s arguments are influenced by user prompts, which means prompt injection can affect function arguments.
[KernelFunction("search_products")]
[Description("Searches the product catalog by keyword")]
public async Task<List<Product>> SearchProductsAsync(
[Description("Search keyword (max 100 characters)")] string keyword)
{
// Validate and sanitize
if (string.IsNullOrWhiteSpace(keyword))
return [];
keyword = keyword.Trim();
if (keyword.Length > 100)
keyword = keyword[..100];
// Use parameterized queries — never interpolate model output into SQL
return await _productRepo.SearchAsync(keyword);
}
Rate limiting. A model that calls your functions in a loop can generate significant load. Set limits on the maximum number of tool round-trips per request using the FunctionChoiceBehavior configuration.
Audit logging. Log every function invocation with its arguments and results. This is essential for debugging unexpected AI behavior and for compliance in regulated environments.
Complete Working Example
Here is a full, self-contained example that ties everything together. The scenario: a customer support assistant that can look up orders and check weather for delivery estimates.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using System.ComponentModel;
// --- Plugin Definitions ---
public class OrderPlugin
{
[KernelFunction("lookup_order")]
[Description("Looks up an order by order number and returns its status and details")]
public Task<string> LookupOrderAsync(
[Description("The order number, e.g. 'ORD-12345'")] string orderNumber)
{
// Simulated lookup — replace with real database call
return Task.FromResult($$"""
{
"orderNumber": "{{orderNumber}}",
"status": "shipped",
"carrier": "FedEx",
"tracking": "7489201384",
"destination": "Seattle, WA",
"estimatedDelivery": "2026-03-02"
}
""");
}
[KernelFunction("list_recent_orders")]
[Description("Lists the most recent orders for a customer email address")]
public Task<string> ListRecentOrdersAsync(
[Description("Customer email address")] string email)
{
return Task.FromResult($$"""
[
{"orderNumber":"ORD-12345","date":"2026-02-25","total":89.99,"status":"shipped"},
{"orderNumber":"ORD-12300","date":"2026-02-20","total":45.00,"status":"delivered"}
]
""");
}
}
public class DeliveryPlugin
{
[KernelFunction("check_delivery_weather")]
[Description("Checks weather conditions at the delivery destination that might affect delivery timing")]
public Task<string> CheckDeliveryWeatherAsync(
[Description("Destination city and state, e.g. 'Seattle, WA'")] string destination)
{
return Task.FromResult($$"""
{
"destination": "{{destination}}",
"conditions": "rain expected",
"severity": "minor",
"deliveryImpact": "No expected delays"
}
""");
}
}
// --- Application Setup ---
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: "gpt-4o",
endpoint: Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")!,
apiKey: Environment.GetEnvironmentVariable("AZURE_OPENAI_KEY")!);
Kernel kernel = builder.Build();
kernel.Plugins.AddFromType<OrderPlugin>("Orders");
kernel.Plugins.AddFromType<DeliveryPlugin>("Delivery");
// --- Conversation with Auto Tool Calling ---
OpenAIPromptExecutionSettings settings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto()
};
var result = await kernel.InvokePromptAsync(
"I'm jane@example.com. Can you check on my latest order and tell me if the weather will delay it?",
new(settings));
Console.WriteLine(result);
When this runs, the model will call Orders-list_recent_orders to find Jane’s orders, then Orders-lookup_order to get shipping details on the most recent one, then Delivery-check_delivery_weather to check conditions at the destination. The final response synthesizes all three results into a coherent answer.
That is function calling in practice — the model reasons about what to do, and your C# code handles how to do it.
Observability with Filters
To see what is happening inside the tool-calling loop, implement an IFunctionInvocationFilter. This is the recommended approach for logging, telemetry, and debugging in production applications.
public class ToolCallLoggingFilter(ILogger<ToolCallLoggingFilter> logger)
: IFunctionInvocationFilter
{
public async Task OnFunctionInvocationAsync(
FunctionInvocationContext context,
Func<FunctionInvocationContext, Task> next)
{
logger.LogInformation(
"Tool call: {Plugin}-{Function} with args: {Args}",
context.Function.PluginName,
context.Function.Name,
context.Arguments);
var sw = Stopwatch.StartNew();
await next(context);
sw.Stop();
logger.LogInformation(
"Tool result: {Plugin}-{Function} completed in {Elapsed}ms",
context.Function.PluginName,
context.Function.Name,
sw.ElapsedMilliseconds);
}
}
Register it in your DI container and every tool call will be traced — invaluable for diagnosing why the model chose certain functions or why a multi-step chain produced unexpected results.
For building complete RAG systems that combine function calling with retrieval, see Retrieval-Augmented Generation Explained for .NET Architects. For a hands-on end-to-end walkthrough, the Build a RAG Chatbot Workshop walks through every step from project setup to deployment.
Summary
Function calling transforms an LLM from a text generator into an orchestrator of your application logic. Semantic Kernel provides the C# infrastructure to make this practical: attribute-based function registration, automatic invocation loops, plugin organization, and filters for observability.
The implementation pattern is consistent. Define your functions with clear [KernelFunction] and [Description] attributes. Register them as plugins. Enable FunctionChoiceBehavior.Auto(). Validate every input. Handle every error. Log every invocation.
With that foundation in place, your AI application stops generating text about your data and starts actually working with it.
For more on the Semantic Kernel ecosystem, visit the official documentation and the Microsoft.SemanticKernel NuGet package.