How to Implement Function Calling with Azure OpenAI and External Tools: A Complete Guide
Function calling with Azure OpenAI allows models to request external code execution by defining JSON schemas, detecting function_call responses, and feeding results back into the chat history for natural language replies.
If you are building applications with the microsoft/generative-ai-for-beginners curriculum, learning how to implement function calling with Azure OpenAI and external tools unlocks the ability to connect LLMs to live APIs, databases, and custom business logic. This pattern transforms a static chatbot into a dynamic agent that can fetch real-time data and take action on behalf of users.
Understanding the Function Calling Architecture
The implementation follows a strict three-stage pattern that separates intent detection from execution.
The Three-Stage Pattern
According to the source code in 11-integrating-with-function-calling/README.md, the workflow consists of:
- Define the function schema – Provide the model with a JSON description of the function name, purpose, and required parameters. The schema is sent in the
functionsfield of theChatCompletionrequest. - Detect and execute the call – After the model responds, inspect
response_message.function_call. If it contains aname, map that name to a real Python callable, deserialize thearguments, invoke the function (e.g., an HTTP request to the Microsoft Learn Catalog API), and capture its output. - Feed the result back – Append two new messages to the chat history: one representing the model’s function-call response (role =
assistant,function_callpayload) and one containing the actual function output (role =function). Then issue a secondChatCompletioncall so the model can generate a natural-language reply that incorporates the fetched data.
Why Function Calling Works
This pattern succeeds because it combines structured output with external data retrieval. The model is forced to return arguments that match the JSON schema, eliminating ambiguous free-form text. By delegating to external APIs (such as the Learn Catalog in the reference implementation), you provide up-to-date information that the model itself does not possess, while maintaining a reusable framework for adding new tools.
Setting Up Your Azure OpenAI Client
Before implementing function calling, you must configure the client with your Azure-specific credentials. The lesson code in 11-integrating-with-function-calling/README.md demonstrates this setup:
import os, json
from openai import AzureOpenAI
from dotenv import load_dotenv
load_dotenv()
client = AzureOpenAI(
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
api_version="2023-07-01-preview",
)
deployment = os.getenv("AZURE_OPENAI_DEPLOYMENT")
Ensure your .env file (copied from the root .env.copy template) contains AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, and AZURE_OPENAI_DEPLOYMENT.
Defining Function Schemas for External Tools
The critical step in implementing function calling with Azure OpenAI is describing your external tools using a strict JSON schema. This example from the microsoft/generative-ai-for-beginners repository defines a function that queries the Microsoft Learn Catalog API:
functions = [
{
"name": "search_courses",
"description": "Retrieves courses from the search index based on the parameters provided",
"parameters": {
"type": "object",
"properties": {
"role": {"type": "string", "description": "Learner role (developer, student, …)"},
"product": {"type": "string", "description": "Product of interest (Azure, Power BI, …)"},
"level": {"type": "string", "description": "Experience level (beginner, intermediate, advanced)"}
},
"required": ["role"]
},
}
]
The name field must match the Python function you will map later. The description helps the model understand when to invoke the tool, while parameters enforces type safety using JSON Schema syntax.
Detecting and Executing Function Calls
Once you send the initial request with function_call="auto", you must inspect the response to determine if the model requested a tool execution:
messages = [{"role": "user", "content": "Find me a good course for a beginner student to learn Azure."}]
response = client.chat.completions.create(
model=deployment,
messages=messages,
functions=functions,
function_call="auto"
)
response_message = response.choices[0].message
if getattr(response_message, "function_call", None) and response_message.function_call.name:
# Map the name to a real Python function
def search_courses(role, product=None, level=None):
import requests
url = "https://learn.microsoft.com/api/catalog/"
params = {"role": role, "product": product, "level": level}
r = requests.get(url, params=params)
modules = r.json()["modules"]
return str([{"title": m["title"], "url": m["url"]} for m in modules[:5]])
available_functions = {"search_courses": search_courses}
fn_name = response_message.function_call.name
fn = available_functions[fn_name]
fn_args = json.loads(response_message.function_call.arguments)
fn_result = fn(**fn_args)
This pattern uses getattr to safely check for the function_call attribute, then maps the string name to an actual Python callable using a dictionary lookup. The arguments are parsed from JSON string to dictionary using json.loads.
Feeding Results Back to the Model
To complete the loop, you must append both the model's function request and the actual function output to the message history before requesting a final natural-language response:
# Add the model's function-call message
messages.append({
"role": response_message.role,
"function_call": {
"name": fn_name,
"arguments": response_message.function_call.arguments,
},
"content": None,
})
# Add the function's output
messages.append({
"role": "function",
"name": fn_name,
"content": fn_result,
})
second_response = client.chat.completions.create(
model=deployment,
messages=messages,
functions=functions,
function_call="auto",
temperature=0,
)
print(second_response.choices[0].message.content)
The message with role="function" contains the raw data returned by your external tool, while the assistant message with the function_call object preserves the context of what the model intended to do. Setting temperature=0 ensures deterministic output when synthesizing the final answer.
Summary
Implementing function calling with Azure OpenAI and external tools requires a structured three-stage pattern that separates intent detection from execution:
- Define schemas using JSON to describe available functions, their parameters, and descriptions in the
functionsarray passed tochat.completions.create. - Detect calls by inspecting
response_message.function_callafter settingfunction_call="auto", then map the function name to a real Python callable and deserialize arguments withjson.loads. - Feed results back by appending both the assistant's function request and a
role="function"message containing the tool output, then issue a second completion call to generate the natural language response.
This pattern, as demonstrated in the microsoft/generative-ai-for-beginners repository, enables LLMs to interact with live APIs, databases, and custom business logic while maintaining type safety and conversational context.
Frequently Asked Questions
What is the difference between function calling and fine-tuning in Azure OpenAI?
Function calling is a runtime capability that allows the model to output structured JSON matching a predefined schema, which your application then executes. Fine-tuning is a training process that modifies the model's weights to improve performance on specific tasks or domains. You use function calling when you need the model to interact with external APIs or tools; you use fine-tuning when you need to change how the model generates text or responds to specific prompts.
How do I handle errors when an external API call fails during function execution?
When implementing the function execution step, wrap your external API calls in try-except blocks. If the call fails, return a descriptive error message as the function result content. When you feed this back to the model via the role="function" message, the LLM can interpret the error and either retry with different parameters, ask the user for clarification, or explain the failure in natural language. Always validate the deserialized arguments with json.loads before passing them to your functions to prevent injection errors.
Can I force the model to call a specific function instead of letting it choose?
Yes, instead of passing function_call="auto", you can pass function_call={"name": "search_courses"} (or whatever your function name is) to force the model to generate arguments for that specific function. This is useful when you know the user intent requires a particular tool, or when you want to test the function schema without relying on the model's routing logic. After the forced call completes, you can return to function_call="auto" for subsequent turns to allow the model to decide whether additional function calls are needed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →