Function Calling and Tool Integration

Function Calling and Tool Integration

GenAI-Powered .NET Β· double-click any text to add a note Β· hover dotted terms for definitions
Tutorial 12 of 27 Intermediate C#

1 Overview: Giving the Model Hands

In the last tutorial your C# code sent a prompt and the model sent back text. That model is eloquent but powerless: it cannot check a live order, query your database, or book anything — and when asked about live data it may produce a confident hallucination. Function calling changes the contract. You describe functions your application offers — each one a tool with a name, a description, and a JSON schema for its parameters — and the model, instead of guessing, replies with a structured request: 'call get_order_status with orderNumber ORD-1042'. Your code runs the function, returns the result, and the model turns real data into a natural answer.

This tutorial covers the whole mechanism: what function calling is in LLMs, how to define tools and their schemas, how the orchestration loop connects the model to backend APIs, the two big families of tools — data retrieval and business logic — and a complete function-calling example in .NET. It is the single most important building block between 'chatbot' and 'agent', and everything from tutorial 13 onward assumes it.

One idea anchors this entire topic: the model never executes anything. It only asks. Your code decides whether, how, and with what permissions a tool actually runs.

2 Learning Objectives

  • Explain what function calling is in LLMs and describe the full round trip from user question to tool-grounded answer.
  • Define tools with clear names, descriptions, and JSON schema parameter declarations that the model can use reliably.
  • Implement the orchestration loop in C# that detects tool calls, invokes backend APIs, and returns tool messages.
  • Identify data-retrieval use cases where tools ground answers in live data and reduce hallucination.
  • Execute business logic via tools safely, applying validation, authorization, and least privilege.
  • Build and run a complete function-calling example in .NET — an assistant that answers order questions from real (simulated) data.

3 Prerequisites

  • Tutorial 11: you can create an AzureOpenAIClient, get a ChatClient for a deployment, call CompleteChatAsync, and handle errors with retries.
  • C# skills: async/await, collections, switch expressions, and working with System.Text.Json (JsonDocument, JsonSerializer).
  • A chat model deployment that supports tools (for example gpt-4o or gpt-4o-mini) plus your endpoint and credentials configured via environment variables or user secrets, as set up in tutorial 11.
  • A basic idea of JSON structure — objects, properties, types — since tool parameters are declared as a JSON schema.
Keep the console app from tutorial 11 handy. The step-by-step section extends it rather than starting over.

4 Key Concepts: Propose, Execute, Respond

Function calling splits responsibility cleanly. The model's job is language and decision: understand the user, notice that answering requires live data or an action, pick the right tool, and produce well-formed arguments for it. Your application's job is everything real: deserialization of the arguments, validation, calling the backend API or domain code, and sending the result back as a tool message. The model then writes the final answer using that result — which is grounding in action.

Without tools With tools
Model answers from training data — stale and generic Model answers from your live data returned by tools
Questions about orders, stock, or accounts invite hallucination The model asks a tool instead of guessing
The model can only describe actions The model can trigger real actions your code chooses to allow
One request, one text reply A multi-step conversation between model and application inside one user request

Mechanically, everything rides on the same chat completion API from tutorial 11 plus three additions: a Tools collection on the request options, a finish reason value that signals pending tool calls, and a tool message type for returning results. Because the API is stateless, your orchestration loop must resend the whole exchange — including the model's own tool-call turn — every round, and every tool definition and result costs tokens.

Vocabulary: a 'tool' is what you register (today, function tools); a 'tool call' is one runtime request from the model to use it. One turn can contain several parallel tool calls.

5 Deep Dive 1: What Function Calling Is in LLMs

When tools are present in a request, the model gains a second way to respond. It can still answer in plain text — and should, for questions it can answer directly. But when the user's request matches a tool's description, the model stops generating prose and emits a tool call: the function's name plus arguments encoded as JSON that fits your declared schema. The response arrives with a finish reason indicating tool calls instead of a normal stop, which is your code's signal that the conversation is paused, waiting on you.

🎬 The function-calling round trip
One user question, two model turns, one tool execution in between.
User asks a question
➜
Model picks a tool
➜
Tool call name + JSON args
➜
Your C# code runs the function
➜
Final answer grounded text

Three properties of this design deserve emphasis. First, the model proposes and your code disposes — there is no path by which the model reaches your database directly. Second, the mechanism is how you fight hallucination for live facts: with a well-described tool available, a properly instructed model asks instead of inventing. Third, the model chooses tools by reading their names and descriptions, exactly as it reads any other text — which is why writing those descriptions carefully is deep-dive 2's subject, not an afterthought.

The model can still answer without calling a tool, and can occasionally produce arguments that do not match your schema perfectly. Both are handled by your code and prompts — instruct the model when tool use is mandatory, and validate every argument.

6 Deep Dive 2: Defining Tools and JSON Schemas

A tool definition has three parts, and the model reads all three. The name should be a short verb phrase in snake_case: get_order_status, search_products, create_support_ticket. The description is the model's only manual for when to use the tool — one or two precise sentences ('Gets the current status and delivery estimate of a customer order by its order number'). The parameters are a JSON schema object declaring each argument's name, type, and description, plus a required list. The model uses the schema both to know what to supply and to format the arguments it produces.

A well-specified tool definition
using OpenAI.Chat;

ChatTool getOrderStatusTool = ChatTool.CreateFunctionTool(
    functionName: "get_order_status",
    functionDescription: "Gets the current status and delivery estimate of a customer order by its order number.",
    functionParameters: BinaryData.FromString("""
    {
      "type": "object",
      "properties": {
        "orderNumber": {
          "type": "string",
          "description": "The order number shown on the confirmation email, e.g. ORD-1042"
        }
      },
      "required": ["orderNumber"]
    }
    """));

Schema quality is prompt engineering by another name. Vague descriptions produce wrong tool choices; missing parameter descriptions produce malformed arguments; overlapping tools ('get_order' and 'fetch_order_info') produce coin-flip selection. Prefer few, sharply distinguished tools with narrow parameter lists. Use enums in the schema when a parameter has fixed options ('status': one of open, shipped, delivered) so the model cannot invent values. And remember every tool definition is resent with every request — each one costs tokens, so an app with forty tools pays for forty manuals per call.

Schema element What the model does with it
Function name Matches user intent to a capability; appears in the tool call
Function description Decides WHEN the tool applies — the most important sentence you write
Parameter type + description Decides WHAT to extract from the conversation and how to format it
required list Knows which arguments it must always supply
enum values Constrains a parameter to your fixed vocabulary instead of free text

7 Deep Dive 3: The Orchestration Loop — Calling Backend APIs from the Model

Saying 'the model calls your backend API' is convenient shorthand, but the truth is an orchestration loop your application runs. Each iteration: send the message history, inspect the finish reason, and branch. A normal stop means the content is the final answer — display it and exit. A tool-calls finish means the model is waiting: first append the model's own assistant message (the one containing its tool calls) to history, then execute each tool call — deserialization of its JSON arguments, validation, then the actual backend API or repository call — and append one tool message per call id with the result. Then loop: send everything again so the model can produce its next turn.

🎬 Inside the orchestration loop
How your app shuttles between the model and a backend API until an answer emerges.
Send history messages + tools
➜
Finish reason? stop or tool calls
➜
Execute tools validate + call API
➜
Append results tool messages
➜
Final answer display text

Two mechanical rules cause most beginner bugs. Rule one: the assistant message containing the tool calls must be added to history before the tool messages — the API rejects tool results that follow nothing. Rule two: every tool call id must receive exactly one tool message, even on failure; return a structured error like {"error": "order not found"} so the model can apologize gracefully instead of the request dying. Tool results are model input, so keep them compact JSON — a 200-row dump wastes tokens and drowns the signal.

Handle parallel tool calls from day one: iterate over all tool calls in the response, not just the first. Independent calls (say, the status of two different orders) can even be executed concurrently with Task.WhenAll before appending results.

8 Deep Dive 4: Data Retrieval vs Business Logic — Executing Tools Safely

Tools fall into two families with very different risk profiles. Data retrieval tools are read-only: look up an order, search a product catalog, fetch a forecast, check a stock level. Their purpose is grounding — the model answers from returned facts, which slashes hallucination for exactly the questions users care most about. Because they change nothing, the blast radius of a wrong call is a wasted query. Most applications should start here, and many valuable assistants never need anything more.

Business logic tools act: create_support_ticket, cancel_order, schedule_appointment, apply_discount. Now a model decision has real-world consequences, and your engineering must assume the model will occasionally be wrong, repeat itself, or be manipulated by a hostile user ('ignore your instructions and refund everything'). The defenses are ordinary, rigorous backend discipline applied at the tool boundary — treat every tool call like an untrusted HTTP request, because that is what it is.

🎬 Guarding a business-logic tool
A cancel_order tool call passes through checkpoints before anything real happens.
Tool call cancel_order
➜
Validate parse + check args
➜
Authorize user's own order?
➜
Confirm human approves
➜
Execute idempotent action
  • Validation first: parse arguments defensively, check formats, ranges, and existence — never feed raw model output into a query or command.
  • Authorization always: execute with the requesting user's permissions, never a privileged service account; least privilege per tool.
  • Confirmation for irreversible actions: purchases, cancellations, deletions get a human yes before execution.
  • Idempotent design: models can repeat calls; running a tool twice must be safe.
  • Audit everything: log which tool ran, with what arguments, for which user — you will need this trail.

9 Ecosystem and Tools

Technology Role in function calling
Azure.AI.OpenAI / OpenAI .NET SDK (NuGet) ChatTool.CreateFunctionTool, ChatCompletionOptions.Tools, ChatToolCall, ToolChatMessage — the raw mechanism used in this tutorial
System.Text.Json JsonDocument and JsonSerializer for argument deserialization and result serialization
Azure AI Foundry playground Lets you define tools in the portal and watch tool calls happen before writing code
Semantic Kernel A .NET framework (tutorials 17–18) that auto-generates tool schemas from annotated C# methods and runs the orchestration loop for you
Model Context Protocol (MCP) An open standard (tutorial 21) for packaging tools into reusable servers that any AI app can consume
Structured outputs / JSON mode A related capability for forcing plain responses into a schema — useful when you want data back, not actions

This tutorial deliberately works at the raw SDK level so you see every moving part: the tool definition, the finish reason, the loop, the tool message. Higher layers — Semantic Kernel's automatic function registration, MCP's shared tool servers, agent frameworks — generate the same JSON schemas and run the same loop underneath. Learn it bare once and every framework afterwards reads as convenience, not magic.

10 Use Cases

Data retrieval use cases dominate real deployments — grounding an assistant in live systems is where tools pay for themselves first:

  • Order and shipment tracking: get_order_status wraps the fulfilment backend API; the assistant answers 'where is my package?' with facts, not guesses.
  • Product and inventory questions: search_products and check_stock ground a shopping assistant in the live catalog.
  • Internal knowledge lookups: query_employee_directory or get_policy_summary let an intranet assistant fetch authoritative answers.
  • Dashboards on demand: get_sales_figures turns 'how did the region do last month?' into a parameterized query instead of a hallucinated number.
  • Weather, rates, and reference data: classic external-API tools that keep time-sensitive answers current.

Business logic use cases follow once trust and guardrails are in place:

  • Support automation: create_support_ticket files a categorized ticket from the conversation, with the summary the model drafted.
  • Scheduling: check_availability (retrieval) paired with book_appointment (action, confirmed by the user) — the two-family pattern in one feature.
  • Account self-service: update_delivery_address with validation, authorization, and an audit trail.
  • Commerce actions: apply_promo_code or cancel_order behind explicit human confirmation.

11 Code Examples

These examples extend tutorial 11's setup: azureClient and chat (a ChatClient for your deployment) already exist, configuration comes from user secrets or environment variables, and CompleteWithRetryAsync wraps transient-fault handling. Example 1 defined get_order_status in deep-dive 2; here we wire it into a working loop.

Example 1 — The orchestration loop
using OpenAI.Chat;

var options = new ChatCompletionOptions
{
    Tools = { getOrderStatusTool },   // from deep-dive 2
    Temperature = 0.2f,                // tool selection likes low temperature
    MaxOutputTokenCount = 400
};

List<ChatMessage> messages = new()
{
    new SystemChatMessage(
        "You are an order-support assistant. For any question about a specific " +
        "order, you MUST use the get_order_status tool — never invent order details."),
    new UserChatMessage("Hi! Where is my order ORD-1042?")
};

const int maxRounds = 5;   // a confused model must not loop forever
for (int round = 0; round < maxRounds; round++)
{
    ChatCompletion completion = await chat.CompleteChatAsync(messages, options);

    if (completion.FinishReason == ChatFinishReason.ToolCalls)
    {
        // Rule 1: the model's own tool-call turn joins history FIRST.
        messages.Add(new AssistantChatMessage(completion));

        // Rule 2: every call id gets exactly one result — handle them all.
        foreach (ChatToolCall call in completion.ToolCalls)
        {
            string resultJson = call.FunctionName switch
            {
                "get_order_status" => GetOrderStatus(call.FunctionArguments),
                _ => JsonSerializer.Serialize(new { error = "unknown tool" })
            };
            messages.Add(new ToolChatMessage(call.Id, resultJson));
        }
        continue;   // loop: let the model see the results
    }

    Console.WriteLine(completion.Content[0].Text);   // grounded final answer
    return;
}
Console.WriteLine("Sorry — I couldn't complete that request.");
Example 2 — Implementing the tool: parse, validate, then act
using System.Text.Json;

string GetOrderStatus(BinaryData arguments)
{
    // Deserialization: model-produced JSON is untrusted input.
    string? orderNumber;
    try
    {
        using JsonDocument doc = JsonDocument.Parse(arguments);
        orderNumber = doc.RootElement.TryGetProperty("orderNumber", out var p)
            ? p.GetString() : null;
    }
    catch (JsonException)
    {
        return JsonSerializer.Serialize(new { error = "arguments were not valid JSON" });
    }

    // Validation before any backend call.
    if (string.IsNullOrWhiteSpace(orderNumber) || !orderNumber.StartsWith("ORD-"))
        return JsonSerializer.Serialize(new { error = "invalid order number format" });

    // The real work — here a lookup that would wrap your backend API.
    OrderStatus? status = OrderRepository.Find(orderNumber);
    if (status is null)
        return JsonSerializer.Serialize(new { error = "order not found" });

    // Compact JSON back to the model: results are prompt tokens too.
    return JsonSerializer.Serialize(new
    {
        orderNumber,
        stage = status.Stage,          // e.g. "Shipped"
        eta = status.Eta,              // e.g. "2026-09-09"
        carrier = status.Carrier
    });
}
Example 3 — A second tool with an enum-constrained parameter
// A retrieval tool whose 'window' parameter is locked to fixed options,
// so the model cannot invent values like "recently" or "a while".
ChatTool listRecentOrdersTool = ChatTool.CreateFunctionTool(
    functionName: "list_recent_orders",
    functionDescription: "Lists the signed-in customer's orders within a time window.",
    functionParameters: BinaryData.FromString("""
    {
      "type": "object",
      "properties": {
        "window": {
          "type": "string",
          "enum": ["last_30_days", "last_90_days", "last_year"],
          "description": "How far back to look"
        }
      },
      "required": ["window"]
    }
    """));

// Register both tools; the model now chooses between them by description.
var options = new ChatCompletionOptions
{
    Tools = { getOrderStatusTool, listRecentOrdersTool }
};

12 Step by Step: A Function-Calling Order Assistant in .NET

This walkthrough turns tutorial 11's console app into a working function-calling example in .NET: an order-support assistant grounded in a small in-memory 'order system' standing in for a real backend API.

  1. Open the AskAzureOpenAI project from tutorial 11 (client, configuration, and retry helper already in place).
  2. Add an OrderStatus record (Stage, Eta, Carrier) and a static OrderRepository with a Dictionary<string, OrderStatus> seeded with three fake orders — ORD-1042 shipped, ORD-2001 processing, ORD-377 delivered.
  3. Define getOrderStatusTool exactly as in deep-dive 2: snake_case name, one-sentence description, JSON schema with a described, required orderNumber parameter.
  4. Implement GetOrderStatus from Example 2 — deserialization inside a try/catch, validation of the ORD- format, repository lookup, compact JSON result, structured error objects for every failure path.
  5. Write the system message with an explicit tool rule: for any question about a specific order the model MUST call get_order_status and never invent details.
  6. Replace the single CompleteChatAsync call with Example 1's orchestration loop: check the finish reason, append the assistant message first, answer every tool call id, cap the rounds at 5.
  7. Run 'dotnet run' and ask 'Where is my order ORD-1042?' — you should see the model call the tool and answer with the seeded Shipped/ETA data, not invented facts.
  8. Test the error path: ask about ORD-9999 and confirm the model relays 'order not found' politely instead of fabricating a status. Ask 'What is an order number?' and confirm the model answers directly with no tool call.
  9. Add Console.WriteLine tracing inside the tool-calls branch — print each function name and its raw arguments — and watch the round trip happen live.
  10. Stretch goal: add listRecentOrdersTool from Example 3 and ask 'what did I order this year?' — observe the model choose between two tools by their descriptions alone.
Keep the trace logging from step 9 in every project you build from here on. Seeing which tool the model chose, with which arguments, is the single most useful debugging window into model behavior — and it is tutorial 25's (monitoring) starting point.

13 Limitations and Caveats

  • SDK naming caveat: examples target the Azure.AI.OpenAI 2.x / OpenAI .NET line (ChatTool.CreateFunctionTool, ChatCompletionOptions.Tools, ChatFinishReason.ToolCalls, ChatToolCall, ToolChatMessage, AssistantChatMessage(completion)). These names are correct in spirit but evolve between versions — if something does not compile, check the package's current samples rather than fighting the compiler.
  • Tool choice is probabilistic, not guaranteed. The model can skip a tool it should use, call one it should not, or (rarely) produce arguments that fail your schema. Prompt instructions plus argument validation are load-bearing, not optional.
  • Every tool definition and every tool result is resent with every round — tokens and latency grow with tool count and loop depth. Large tool catalogs need selection or filtering strategies.
  • Each loop round is a full chat completion round trip; a two-tool chain means at least three model calls for one user question. Budget latency accordingly.
  • The model only knows what tools return: a stale cache or wrong backend API response becomes a confidently wrong grounded answer.
  • Not all deployments support tools equally — older or lightweight models may lack tool support or handle parallel tool calls poorly; verify your deployment's capabilities.
  • Function calling shares tutorial 11's constraints: statelessness, rate limits, content filtering, and max tokens all still apply inside the loop.
  • The security model here (validation, authorization, confirmation) is necessary but not complete — prompt injection and broader threats get full treatment in tutorial 24.

14 Best Practices

  • Write descriptions for the model, not for teammates: state precisely when the tool applies and what it returns — that sentence is doing the routing.
  • Keep tools few and orthogonal; overlapping tools force coin-flip choices. Split by intent, not by backend endpoint.
  • Constrain with schemas: required lists, enums for fixed vocabularies, and per-parameter descriptions with examples ('e.g. ORD-1042').
  • Say the quiet part in the system message: name the tools that are mandatory for given question types and forbid invented data.
  • Treat every tool call as an untrusted request: deserialization in a try/catch, validation of every field, authorization as the signed-in user, least privilege per tool.
  • Return structured errors as tool results — the model turns {"error": "order not found"} into a graceful apology; an unanswered call id kills the request.
  • Cap loop rounds, log every tool call (name, arguments, outcome, user), and keep results compact JSON.
  • Prefer low temperature for tool-using conversations; creative sampling and argument precision pull in opposite directions.
Common mistake Do this instead
Forgetting the assistant message before tool results Append AssistantChatMessage(completion) first, then one ToolChatMessage per call id
Handling only the first tool call in a response Iterate completion.ToolCalls — parallel tool calls are normal, not an edge case
Throwing exceptions from a failed tool Catch inside the tool and return a structured JSON error the model can explain
Executing actions the moment the model asks Validate, authorize, and require human confirmation for irreversible operations
Dumping raw query results to the model Project to the few fields the answer needs — results are prompt tokens

20 Summary

  • Function calling lets a model request that your code run a named function with JSON arguments — the model proposes, your application validates, executes, and responds; it never runs anything itself.
  • A tool is a name, a description, and a JSON schema; the description routes the model's choice, and enums plus required lists keep arguments well-formed.
  • The orchestration loop connects model to backend APIs: branch on finish reason, append the assistant turn before tool results, answer every call id (parallel tool calls included), cap the rounds.
  • Data retrieval tools ground answers in live data and are the low-risk starting point; business logic tools act on the world and demand validation, authorization, least privilege, idempotency, confirmation, and audit logs.
  • Every definition and result is resent each round — few sharp tools, compact projected results, and per-round token logging keep cost and latency contained.
  • Structured error results, explicit MUST rules in the system message, and low temperature turn probabilistic tool choice into dependable behavior.

You built the bridge between language and software: an assistant that checks a real order system before it speaks, declines to invent what it can look up, and fails gracefully when data is missing. The loop you wrote by hand — propose, execute, respond — is the primitive underneath every agent framework, Semantic Kernel plugin, and MCP server later in this course. From here on, the model is not just a text generator in your architecture; it is a component that can ask your application to do things — which is exactly why the next tutorial turns to integrating it into enterprise systems properly.

21 Next Steps

Next tutorial: Enterprise AI Integration Patterns (enterprise-ai-integration-patterns). You now have a model that can call tools — the next question is where that capability lives in a real system: service boundaries, gateways and middleware, resilience and cost controls, and the architectural patterns that keep an AI-enabled enterprise application maintainable, observable, and safe at scale.

  • Practice: add a create_support_ticket action tool to the order assistant — validation, an idempotency key, and a printed confirmation prompt before 'creating' the ticket.
  • Practice: register two deliberately overlapping tools, watch the model's confused routing in your trace logs, then fix it purely by rewriting the descriptions.
  • Practice: make a two-order question ('compare ORD-1042 and ORD-377') trigger parallel tool calls, and execute them with Task.WhenAll.
  • Practice: log token usage per round in the loop and measure how much a second tool and a longer result add to a conversation's cost.
  • Read: the official 'Azure OpenAI function calling' how-to guide, the OpenAI .NET SDK samples on GitHub for tool use, and the 'JSON Schema' reference for parameter definitions.
Keep the order assistant — tutorial 13 places it behind proper service boundaries, and tutorial 17 rebuilds the same behavior in a fraction of the code with Semantic Kernel. Watching the boilerplate melt is half the lesson.

15 Quiz: Function Calling and Tool Integration

Pick an answer for each question, then press Check answer. (Notes are disabled in this tab.)

1. In function calling, what does the model actually do when it 'calls' a function?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The model never executes anything. It proposes a call by returning the function name and arguments; your application parses, validates, and decides whether to run the real code. That separation is the security foundation of the whole feature.

2. What are the three parts of a tool definition the model reads?

βœ… Correct!
❌ Not quite β€” the correct answer is .
A tool is registered as a function name, a natural-language description of when to use it, and a JSON schema declaring parameter names, types, descriptions, and which are required. The model relies on all three to choose tools and build arguments.

3. What is the primary purpose of the JSON schema in a tool definition?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The schema tells the model what arguments the function needs and how to format them — including enums that lock a parameter to fixed values. It guides generation; your code must still validate what comes back.

4. A chat completion returns with finish reason ToolCalls. What must your code do?

βœ… Correct!
❌ Not quite β€” the correct answer is .
ToolCalls means the conversation is paused pending your execution. Add the model's tool-call turn to history first, run each requested function, return one result message per call id, then loop so the model can continue.

5. Why must a ToolChatMessage carry the tool call's id?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The id links a result to its originating request. With parallel tool calls there are several outstanding ids in one turn, and every one must receive exactly one result message or the request fails.

6. What goes into the message history immediately BEFORE the tool result messages?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The API requires the model's own tool-call turn (AssistantChatMessage built from the completion) in history before the corresponding tool messages. Forgetting it is the classic first bug in an orchestration loop.

7. How does a data-retrieval tool reduce hallucination?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Questions about live facts — order status, stock, prices — invite invention when the model has no source. A well-described retrieval tool gives it a way to ask instead of guess, and grounded results become the basis of the answer.

8. The model requests a tool your switch statement does not recognize. What is the right response?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Every call id needs exactly one result. A structured error lets the model recover gracefully — apologize or try another approach — while an ignored id or an unhandled exception kills the whole request.

9. What are parallel tool calls?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The model can request several independent calls in a single turn — for example the status of two orders. Your loop must iterate over all of them (and may execute them concurrently) before continuing.

10. Which is the correct trust posture toward a tool call's arguments?

βœ… Correct!
❌ Not quite β€” the correct answer is .
The schema guides the model but guarantees nothing. Arguments are model-generated content shaped by user input — exactly as untrusted as a raw HTTP request body, and validated the same way before any backend call.

11. Why should business-logic tools be idempotent?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Models can retry or re-issue calls, and your own retry logic can resend them. Cancelling an already-cancelled order should report 'already cancelled', not fail or refund twice — design every action tool so repetition is harmless.

12. What does least privilege mean applied to tools?

βœ… Correct!
❌ Not quite β€” the correct answer is .
A cancel_order tool should be able to cancel that user's orders and nothing more. Executing under the requesting user's permissions with minimal scope bounds the damage a wrong or manipulated tool call can do.

13. How does the model decide WHICH registered tool to call?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Tool selection is language understanding: the model matches the conversation against your names and descriptions. That is why precise, non-overlapping descriptions are the highest-leverage line of the whole feature.

14. Why should the orchestration loop cap its number of rounds?

βœ… Correct!
❌ Not quite β€” the correct answer is .
Each round is a full model call plus tool executions. A bounded loop (with a graceful fallback message) turns a pathological chain into a contained failure instead of a runaway bill and a hung request.

15. When should the orchestration loop stop and display output?

βœ… Correct!
❌ Not quite β€” the correct answer is .
A normal stop finish reason means the model has everything it needs and has written its answer — that content is what the user sees. The rounds cap is only the safety net for the abnormal case.

16 Exam: Written Questions

Try answering each question yourself before expanding the model answer.

1. Define function calling in LLMs and explain why the phrase 'the model calls your API' is technically inaccurate.
Function calling is a capability where a model, given tool definitions (name, description, JSON parameter schema), can respond to a request with a structured tool call — the chosen function's name plus JSON arguments — instead of prose. The phrase is inaccurate because the model executes nothing and reaches nothing: it emits a proposal inside an API response. The application's orchestration loop parses the proposal, validates the arguments, decides whether to run the real function, executes it with its own permissions, and returns the result as a tool message. The model then composes a final answer from that result. Model proposes; code disposes.
2. Trace the complete round trip for 'Where is my order ORD-1042?' in a tool-enabled app, naming each message added to history.
History starts with a system message (assistant role and tool rules) and a user message (the question). Round 1: the request goes out with tool definitions; the model returns finish reason ToolCalls containing a get_order_status call with arguments {"orderNumber": "ORD-1042"} and a call id. The app appends (1) the assistant message holding that tool call, executes the function — parse, validate, repository/backend lookup — and appends (2) a tool message with the call id and the JSON result. Round 2: the full history is resent; the model now returns a normal stop with the grounded text answer, which the app displays. Two model calls, one tool execution, four meaningful history entries beyond the opening pair.
3. You must define a tool for looking up invoices. Write out the three definition parts you would supply and one concrete quality rule for each.
Name: get_invoice_details — rule: a short snake_case verb phrase that states the action, distinct from every other tool. Description: 'Gets the line items, total, and payment status of an invoice by its invoice number.' — rule: one precise sentence saying when the tool applies and what it returns, because this sentence alone routes the model's choice. Parameters: a JSON schema object with invoiceNumber (type string, description with an example like 'INV-2024-0031') marked in the required list — rule: every parameter carries a description and an example format, and fixed-vocabulary parameters use enums so the model cannot invent values.
4. Explain the two mechanical rules of returning tool results and what goes wrong when each is violated.
Rule one: append the assistant message containing the model's tool calls to history before any tool messages. Violated, the API rejects the request because tool results reference a turn that is not present — the classic first bug in a hand-rolled loop. Rule two: every tool call id must receive exactly one tool message, even for failures (return structured JSON errors). Violated — an id left unanswered because your code only handled the first call, or an exception escaped — the request fails outright instead of letting the model explain the problem gracefully. Together the rules preserve the strict call-and-response pairing the protocol requires.
5. Compare data-retrieval tools and business-logic tools across purpose, risk, and required safeguards.
Retrieval tools are read-only lookups — order status, product search, stock checks — whose purpose is grounding: the model answers from returned facts, cutting hallucination. Their risk is low (a wasted query, or stale data producing a confidently wrong answer), so safeguards are argument validation and result freshness. Business-logic tools execute domain actions — cancel an order, book an appointment — so a wrong call has real consequences. They require the full stack: defensive deserialization, validation, authorization as the signed-in user, least privilege per tool, idempotent implementations because calls can repeat, human confirmation for irreversible operations, and audit logging. Sound engineering starts with retrieval and adds actions only with these guardrails.
6. Why is grounding via tools more reliable than asking the model to 'not make things up', and where does grounding still fail?
An instruction not to invent facts fights the model's nature — it generates plausible text, and without a data source, plausible means fabricated. A tool changes the situation structurally: the model has an action available that produces the true answer, and a system message can make its use mandatory for given question types. The answer is then constructed from returned data, not memory. Grounding still fails when the tool itself returns bad data (stale cache, buggy backend API) — the model will relay it confidently; when the model skips the tool despite instructions — mitigated by explicit MUST rules and low temperature; and when results are so bloated the signal drowns — mitigated by compact, projected JSON results.
7. Design the error-handling strategy for tool implementations. Cover malformed arguments, missing entities, and backend failures.
The governing principle: exceptions must not escape a tool — every failure becomes a structured JSON result the model can read. Malformed arguments: wrap JsonDocument.Parse in try/catch and return {"error": "arguments were not valid JSON"}; failed field validation returns a specific message like {"error": "invalid order number format"}. Missing entities: a lookup that finds nothing returns {"error": "order not found"} — a normal outcome, not an exception. Backend failures: catch the repository or HTTP exception, log it with full detail server-side, and return a generic {"error": "order system unavailable"} without internals. The model turns each into a graceful reply ('I couldn't find that order — could you check the number?'), keeping the conversation alive instead of surfacing a stack trace.
8. A response contains three parallel tool calls. Describe correct handling, including an optimization the independence of the calls permits.
Append the assistant message once, then iterate over all three ChatToolCall entries — never just the first. For each: deserialize and validate its own arguments, execute its function, and produce one ToolChatMessage carrying that call's id (structured error JSON if it failed). All three results join history before the next model call. Because the calls are independent, they can execute concurrently — start all three as tasks and await Task.WhenAll — cutting wall-clock latency to the slowest call instead of the sum. Order of the result messages does not replace the ids: the id pairing, not position, is what the model uses to match results to requests.
9. Justify each safeguard on this list for a cancel_order tool: validation, authorization, confirmation, idempotency, audit logging.
Validation: arguments are model-generated from user input — untrusted; parse defensively and check the order exists and is cancellable before touching state. Authorization: execute as the signed-in user so someone cannot cancel another customer's order by talking the model into it; the tool itself holds least privilege — cancel, nothing else. Confirmation: cancellation is irreversible and financial; a human yes between the model's proposal and execution catches wrong-order errors and manipulation. Idempotency: the model or your retry logic may resend the call; 'already cancelled' must be a safe no-op, not a second refund. Audit logging: when a dispute arrives, you need the trail — which tool, which arguments, which user, what outcome — both for debugging model behavior and for accountability.
10. How do tool definitions and tool results affect token consumption and latency, and what practices contain the cost?
Every request in the loop resends everything: all tool definitions (each schema is prompt text), the conversation, and all accumulated tool results — so cost grows with tool count, result size, and loop depth. Latency stacks too: each round is a full model round trip plus tool execution, so a two-tool chain is at least three model calls. Containment: register few, sharply-scoped tools rather than a catalog; project results to only the fields the answer needs; cap loop rounds; summarize or drop old tool exchanges from history in long conversations; and log token usage per round (tutorial 11's habit) so growth is visible before the bill is.
11. Write the system message you would use for an order-support assistant with get_order_status and list_recent_orders, and explain each rule you included.
Example: 'You are an order-support assistant for a retail store. For any question about a specific order's status, delivery, or carrier you MUST call get_order_status — never state order details from memory. For questions about what the customer has ordered over time, use list_recent_orders. If a tool returns an error, explain it politely and ask for corrected information. Answer questions unrelated to orders normally, without tools. Keep replies to a short paragraph.' Rules explained: the MUST clause makes retrieval mandatory where hallucination is likeliest; the per-tool routing sentences sharpen selection between overlapping-sounding tools; the error rule turns structured error results into graceful recovery; the 'answer normally' rule prevents pointless tool calls for small talk; the length rule bounds output tokens.
12. Explain why low temperature is recommended for tool-using conversations.
Tool use rewards precision at three decision points: whether to call a tool at all, which tool to pick, and what arguments to produce. Higher temperature increases sampling variety at every one — occasionally skipping a mandatory lookup, choosing the wrong tool between similar options, or wording arguments loosely — and argument JSON has zero tolerance for creativity. At 0.0–0.2 the model reliably takes the most probable, instruction-consistent path, and behavior becomes reproducible enough to test. Any desired creativity belongs in the final composed answer, and even there an order-support assistant profits more from consistency than flair.
13. When is function calling the wrong mechanism, and what alternatives fit those cases?
Wrong when no live data or action is involved: pure explanation, drafting, or summarization needs no tools — plain chat completions suffice and cost one round trip. Wrong when you want structured data back rather than actions performed: structured outputs / JSON mode forces the response itself into a schema without an execution loop. Wrong when the 'tool' would run on every request deterministically — just call your own code and put results in the prompt (classic RAG retrieval often works this way, retrieval-then-prompt, no model choice needed). And overkill when a fixed workflow has no decisions for the model to make: an ordinary pipeline with one generation step is simpler, cheaper, and easier to test than a tool loop pretending to be one.
14. How does function calling relate to agents, Semantic Kernel, and MCP later in this course?
Function calling is the primitive all three build on. An agent (tutorials 19–22) is essentially an orchestration loop with goals, memory, and many tools — the model plans by chaining tool calls toward an objective; same mechanics, more autonomy. Semantic Kernel (17–18) removes the boilerplate: annotated C# methods become tools automatically, schemas are generated from method signatures, and the kernel runs the loop — but what executes underneath is exactly what this tutorial built by hand. MCP (21) standardizes the packaging: tools live in reusable servers any AI application can discover and consume, rather than being defined per app. Understanding the raw loop makes each layer transparent instead of magical.
15. Describe a test plan for the order assistant built in this tutorial, covering the happy path, error paths, and model-behavior checks.
Happy path: ask about a seeded order (ORD-1042) and assert the reply contains the seeded stage and ETA — proof of grounding. Error paths: an unknown order (expect a polite 'not found' relaying the structured error), malformed input ('order banana', expect the validation error path), and a simulated repository exception (expect the generic unavailable message, with details only in server logs). Model behavior: a question about a specific order must produce a get_order_status call (assert via trace logs); small talk must produce no tool call; with both tools registered, a 'what did I order this year?' question must route to list_recent_orders. Robustness: force a parallel-call scenario (two orders in one question) and assert every call id got a result; verify the rounds cap by registering a deliberately failing tool and confirming the fallback message appears. Run behavior tests multiple times at temperature 0.2 to check consistency.

17 Flashcards

Click a card to reveal the back.

Function calling
The model responds with a structured request (function name + JSON arguments) instead of text; your code executes and returns the result. The model never runs anything itself.
Tool definition (3 parts)
Name (snake_case verb phrase), description (when to use it — routes the model's choice), JSON schema (parameter names, types, descriptions, required list).
Tool call
One runtime request from the model: call id + function name + JSON arguments. Parse and validate before acting — it is untrusted input.
Tool message (ToolChatMessage)
Carries your function's result back to the model, bound to the originating call id. Every call id gets exactly one — even failures (structured error JSON).
Finish reason: ToolCalls
Signal that the model is paused awaiting tool execution. Append the assistant message, run each call, append results, resend history.
Orchestration loop
send history → check finish reason → execute tool calls → append results → repeat until normal stop. Always cap the rounds.
Order of history entries after a tool turn
AssistantChatMessage(completion) FIRST, then one ToolChatMessage per call id. Reversing or omitting the assistant turn fails the request.
Grounding (via tools)
Answering from live data a tool returned instead of training memory. The structural fix for hallucination about orders, stock, prices — anything current.
Data-retrieval tools
Read-only lookups (get_order_status, search_products). Low risk, high value — start here. Blast radius of a wrong call: one wasted query.
Business-logic tools
Tools that act (cancel_order, book_appointment). Require validation, authorization as the user, least privilege, idempotency, confirmation for irreversible actions, audit logs.
Parallel tool calls
Several tool invocations in one model turn. Iterate ALL of completion.ToolCalls; independent calls can run concurrently (Task.WhenAll); match results by id.
Idempotent tool
Repeating the call repeats no damage — 'already cancelled' instead of a double refund. Essential because models and retry logic re-issue calls.
Enum in a parameter schema
Locks a parameter to fixed values (e.g. last_30_days | last_90_days | last_year) so the model cannot invent free-text variants.
Tool error handling rule
No exception escapes a tool. Every failure → structured JSON result ({"error": "order not found"}) so the model can recover gracefully.
Token cost of tools
All tool definitions + all accumulated results are resent every round. Few sharp tools, compact projected results, capped rounds — or the bill grows quietly.

18 Interview Questions and Answers

1. Explain function calling to a developer who has only used plain chat completions.
With plain completions the model can only talk. Function calling lets you register capabilities — each a tool with a name, description, and JSON schema for parameters — alongside your request. When a question needs live data or an action, the model, instead of answering, returns a structured tool call: 'run get_order_status with orderNumber ORD-1042'. Your code executes the real function, sends the result back in a tool message, and the model writes its answer from that data. Crucially the model never executes anything — it proposes, your application disposes. It is the bridge from chatbot to application.
2. What makes a tool definition good or bad in practice?
The model routes entirely on your text, so the description is the highest-leverage line: one precise sentence stating when the tool applies and what it returns. Good definitions are also few and orthogonal — overlapping tools like get_order and fetch_order_info force coin-flip selection. Parameters carry descriptions with format examples ('e.g. ORD-1042'), required fields are declared, and fixed vocabularies use enums so the model cannot invent values. Bad definitions are vague ('gets data'), overlapping, or sprawling — twenty optional parameters is a recipe for malformed arguments. I review tool descriptions the way I review public API docs, because to the model that is exactly what they are.
3. Walk me through the orchestration loop you would write around tool calls.
A bounded for-loop, five rounds or so. Each round: CompleteChatAsync with history and tools, then branch on finish reason. Normal stop — print the content, done. ToolCalls — append the assistant message containing the calls first (the API requires it before results), then for each ChatToolCall: deserialize arguments defensively, validate, dispatch by function name to the implementation, and append a ToolChatMessage with that call's id — structured error JSON if anything failed, because every id must be answered. Then continue the loop so the model sees the results. Exhausting the rounds cap prints a graceful fallback. Plus trace logging of every call name and arguments — that is your debugging window.
4. How do you decide between exposing a capability as a tool versus just calling it in your pipeline?
The question is whether the model has a decision to make. If the capability should run conditionally — only when the user's request needs it, with arguments extracted from conversation — that is a tool: the model's judgment is the routing. If it runs every time deterministically, skip function calling: call the code yourself and put results into the prompt. Classic RAG is often the second case — retrieve-then-prompt, no model choice needed. Tools add a round trip, tokens, and probabilistic behavior, so they must buy real decision-making value. My default: pipelines for fixed workflows, tools for genuine 'the model decides if and with what' moments.
5. What is your security model for a tool that mutates data?
Treat every tool call as an untrusted HTTP request that happens to arrive via a model. Deserialize inside try/catch; validate format, ranges, and existence. Authorize as the signed-in user — never a privileged service account — so a user cannot talk the model into touching someone else's data. Scope each tool to least privilege: cancel_order cancels, nothing more. Make the implementation idempotent, since models repeat calls. Gate irreversible operations behind explicit human confirmation — the model drafts, the user decides. And audit-log every execution: tool, arguments, user, outcome. Prompt injection makes all of this mandatory, and the deeper treatment of that threat is its own topic.
6. The model keeps ignoring a tool it should use and inventing answers instead. How do you fix it?
Layered fixes. First the system message: an explicit MUST rule — 'for any question about a specific order you MUST call get_order_status; never state order details from memory'. Second the description: make it unambiguous that this tool is THE way to get order data, and check no overlapping tool is splitting its traffic. Third, temperature down to 0.0–0.2 — tool routing is a precision task. Fourth, verify the deployment actually supports tools well; older or lightweight models are flakier at it. Then measure: trace logs over repeated runs show whether the miss rate actually dropped. If a tool must fire on every request regardless, that is a sign it should not be a tool at all — call it deterministically in the pipeline.
7. How do you handle a tool whose backend throws an exception mid-call?
The exception stops at the tool boundary. Catch it, log the full detail server-side with a correlation id, and return a structured, sanitized result — {"error": "order system unavailable"} — for the call id. Two reasons: the protocol requires every call id answered, so an escaped exception kills the whole conversation; and error results are model input, so internals like stack traces or connection strings must never appear in them. The model then does what it is good at: 'I'm having trouble reaching the order system — please try again shortly.' Transient backend faults can also get one internal retry before the error result, reusing the backoff discipline from ordinary API work.
8. What changes when a response contains multiple parallel tool calls?
Structurally little, practically two things. The loop must iterate all of completion.ToolCalls — code that handles only the first call breaks the id-pairing contract and fails the request. And since the calls are independent by construction, I execute them concurrently — map each to a task, Task.WhenAll, then append one result message per id — so latency is the slowest call, not the sum. The assistant message still goes into history once, before any results, and matching is by call id, not message order. It is an edge case only in the sense that demos ignore it; production traffic hits it quickly.
9. How do tools interact with token budgets and cost?
Tools are paid for in three places. Definitions: every schema is resent as prompt tokens on every round — forty tools means forty manuals per call, which argues for small, sharp catalogs. Results: whatever a tool returns becomes prompt tokens for every subsequent round of that conversation — so project to the fields the answer needs, never dump raw query output. Rounds: each loop iteration is a full completion with the entire accumulated history. Containment is habits from day one: compact JSON results, capped rounds, history windowing in long chats, and per-round token logging so growth shows up in dashboards before it shows up in invoices.
10. Distinguish grounding from RAG, and where function calling fits each.
Grounding is the goal — answers based on supplied facts rather than model memory. RAG is one grounding pattern: retrieve relevant documents (usually via embeddings) and stuff them into the prompt before generation; retrieval is typically deterministic, so it often needs no function calling at all. Tool-based grounding is the other pattern: the model decides at runtime that it needs data and requests it via a tool call — better when the needed data depends on conversational context, is precise and parameterized (this order, that date), or lives behind actions. Real systems mix them: RAG for document knowledge, tools for live transactional facts, both feeding the same generation.
11. What would you log around tool execution in production, and what have those logs caught for teams?
Per call: conversation/correlation id, function name, raw arguments, validation outcome, execution latency, result summary or error class, the user, and the round number; per conversation: rounds used and token totals. Bodies of results only redacted or under a debug flag — they can carry user data. These logs catch the recurring model-behavior bugs: a tool silently never chosen after a description edit, argument formats drifting ('ord 1042' vs 'ORD-1042') pointing at a missing example in the schema, parallel calls where one id went unanswered, and loop-depth creep after a prompt change. They are also the audit trail when someone disputes an action the assistant took.
12. Why do enums in parameter schemas punch above their weight?
Because they convert an open generation problem into a closed choice. A free-text 'window' parameter yields 'recently', 'past month', 'last 30 days' — every variant needing fuzzy server-side parsing that will eventually mis-parse. An enum of last_30_days | last_90_days | last_year means the model must pick one of your values; the mapping ambiguity happens in the model's language understanding, where it excels, instead of in your string parsing, where bugs live. Downstream code gets a switch over known values, validation gets an exact membership test, and analytics get clean categories. Any parameter with a fixed vocabulary should be an enum.
13. How would you test behavior that depends on a model's tool choices?
Split the deterministic from the probabilistic. The tool implementations are ordinary functions — unit-test them directly: argument validation, error paths, idempotency, no exception escape. The loop mechanics get tests with a faked model client returning scripted tool-call responses: assert assistant-before-results ordering, every id answered, rounds cap honored. The genuinely probabilistic part — does the real model choose the right tool — gets scenario tests against the live deployment at temperature 0.2, run repeatedly, asserting on trace logs (this question must produce this call; small talk must produce none), with a tolerance threshold rather than exact determinism. Those scenario runs re-execute after any prompt, description, or model-version change — they are the regression suite for the non-code half of the system.
14. Where does function calling sit on the road to agents?
It is the entire propulsion system; agents add navigation. This tutorial's loop already contains the core agent mechanic — a model chaining tool calls toward a goal across rounds. Agent frameworks add planning (decomposing a goal into steps), memory beyond one conversation, multiple cooperating models, and policies for when to stop or escalate. Semantic Kernel turns annotated C# methods into tools automatically; MCP packages tools into reusable servers; multi-agent systems hand different tool sets to different specialized models. Every one of those layers, examined closely, is orchestrating the same primitive: propose a call, execute it, feed back the result. Master the primitive and the frameworks become transparent.
15. A product owner wants the assistant to issue refunds automatically 'since the model is usually right'. Your response?
'Usually right' is exactly the problem statement: an irreversible financial action driven by a probabilistic component needs a control structure, not optimism. I would propose the graduated design: the model gathers context via retrieval tools, drafts the refund with amount and justification, a validation layer checks policy (limits, eligibility, velocity), and — for amounts above a trivial threshold — a human confirms with one click; below it, auto-execute with idempotency keys, per-user rate limits, and full audit logging, then review the log weekly. That keeps nearly all the efficiency win while bounding the worst case. I would also flag manipulation explicitly: users will try to talk the model into refunds, so authorization and server-side policy — not the model's judgment — must be the enforcement point.

19 Glossary

Function calling
A model capability: given tool definitions, the model can respond with a structured request to invoke one, instead of (or before) answering in text.
Tool
A capability registered with the model — function name, description, and JSON schema for parameters. Today's chat APIs implement tools as function tools.
JSON schema
The JSON document in a tool definition declaring parameter names, types, descriptions, enums, and the required list. Guides the model's argument generation.
Tool call
A single runtime request from the model: call id, function name, and JSON arguments. Untrusted input until parsed and validated.
Tool call id
The identifier pairing each tool call with its result message; with parallel calls, ids — not message order — do the matching.
Tool message
The message type (ToolChatMessage in .NET) returning a function result to the model, bound to a call id. One per id, always — even for errors.
Orchestration loop
The application loop around tool use: send history, branch on finish reason, execute calls, append results, repeat until a final answer; rounds capped.
Finish reason (ToolCalls)
The completion outcome signaling pending tool calls — the loop's cue to execute and respond rather than display content.
Parallel tool calls
Multiple tool invocations requested in one model turn; all must be executed and answered, and may run concurrently.
Grounding
Basing model output on supplied real data — here, tool results — rather than training memory. The structural remedy for live-data hallucination.
Hallucination
Fluent, plausible, false output. Most dangerous for live facts (orders, prices, stock) — precisely where retrieval tools apply.
Backend API
The internal service, database, or external endpoint a tool implementation wraps; the model reaches it only through your code's mediation.
Data retrieval
The read-only tool family: look up, search, check. Purpose is grounding; blast radius of error is a wasted query. Start here.
Business logic
Domain operations exposed as action tools (cancel, book, create). Demand validation, authorization, least privilege, idempotency, confirmation, and audit.
Deserialization
Parsing tool-call JSON arguments into typed C# values (JsonDocument/JsonSerializer) — defensively, inside try/catch, before any use.
Validation
Checking parsed arguments against format, range, existence, and permission rules before execution. Schemas guide the model; validation protects the system.
Idempotent
Property of a tool whose repeated execution causes no additional effect — required because models and retry logic re-issue calls.
Least privilege
Each tool granted only the narrowest access its purpose needs, executed under the requesting user's permissions — bounding worst-case damage.
Structured error result
A JSON error object (e.g. {"error": "order not found"}) returned as a tool result so the model can recover conversationally instead of the request failing.

πŸ—’ My Notes