Function Calling and Tool Use with Semantic Kernel

Intermediate Original .NET 9 Microsoft.SemanticKernel 1.34.0
By Rajesh Mishra · Feb 28, 2026 · Verified: Feb 28, 2026 · 15 min read

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.

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:

  1. Call Database-query_customers with searchTerm: "John Smith"
  2. Receive the customer list, identify the correct customer ID
  3. Call Database-get_customer_orders with that customer ID
  4. 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.

⚠ Production Considerations

  • Auto function calling with many registered plugins can lead to unexpected function invocations — audit which plugins are registered and remove any that expose destructive operations without confirmation.
  • Function calling adds round-trip latency per tool call. Multi-step chains with 4-5 calls can push response times past user tolerance thresholds — consider caching and pre-computation for commonly requested data.

🧠 Architect’s Note

Function calling is the inflection point where AI moves from generating text to orchestrating your business logic. Treat every KernelFunction as a public API surface — validate inputs, handle errors gracefully, and log every invocation. The model decides when and how to call your code, so defensive programming is not optional.

AI-Friendly Summary

Summary

This article explains how to implement function calling and tool use in .NET applications using Semantic Kernel 1.34.0. It covers the KernelFunction attribute, plugin organization, auto vs. manual function calling behavior, multi-step tool chains, error handling, and security considerations. Includes a complete working example with weather and database plugins.

Key Takeaways

  • Function calling lets the AI model request execution of your C# methods with structured arguments
  • KernelFunction attribute maps C# methods to the OpenAI function calling schema automatically
  • FunctionChoiceBehavior.Auto() enables fully automatic tool invocation round-trips
  • Plugin grouping via KernelPlugin organizes related functions for the model's discovery
  • Always validate inputs inside kernel functions — the model controls the arguments

Implementation Checklist

  • Install Microsoft.SemanticKernel 1.34.0
  • Create plugin classes with [KernelFunction] and [Description] attributes
  • Register plugins with kernel.Plugins.AddFromType<T>()
  • Set FunctionChoiceBehavior.Auto() in execution settings
  • Add input validation inside every exposed kernel function
  • Implement error handling with try/catch inside functions
  • Test multi-step function chains with realistic prompts
  • Audit exposed functions for security implications

Frequently Asked Questions

What is function calling in AI?

Function calling is a protocol-level capability where an LLM, instead of generating a plain text response, returns a structured request to invoke a specific function with specific arguments. The model does not execute the function itself — your application receives the request, runs the function, and returns the result to the model for continued reasoning. This allows AI to interact with databases, APIs, and business logic in a controlled way.

How does Semantic Kernel handle tool use?

Semantic Kernel maps C# methods decorated with [KernelFunction] attributes to the OpenAI function calling schema. When you enable FunctionChoiceBehavior.Auto(), the kernel automatically intercepts tool call requests from the model, invokes the corresponding C# method, and feeds the result back. This round-trip can happen multiple times in a single conversation turn.

Can the AI call multiple C# functions in sequence?

Yes. When FunctionChoiceBehavior.Auto() is enabled, the model can request multiple function calls in a single response (parallel tool calls) or chain function calls across multiple round-trips. Semantic Kernel handles the entire loop — invoking functions, collecting results, and returning them to the model until it produces a final text response.

How do I secure exposed functions?

Only register functions that are safe for AI invocation. Validate all input parameters inside your function implementations. Use the KernelFunction description to constrain the model's understanding of each function's purpose. For sensitive operations, implement confirmation patterns where the function returns a preview that requires explicit user approval before execution.

Related Articles

Was this article useful?

Feedback is anonymous and helps us improve content quality.

Discussion

Engineering discussion powered by GitHub Discussions.

#Semantic Kernel #Function Calling #Tool Use #Plugins #.NET AI