Introduction to Function Calling with Mistral Models
Function calling is one of the most powerful capabilities in modern large language models, allowing developers to bridge the gap between natural language understanding and structured programmatic execution. Mistral AI has built robust function calling support into its models, enabling applications to parse user intent, select appropriate tools, and execute structured actions with reliable, typed outputs. In this tutorial, we will explore what function calling is, why it matters, how to implement it with Mistral models, and the best practices that will help you build production-grade applications.
What Is Function Calling?
Function calling is a mechanism where a language model is given a set of tool definitions — typically described as JSON schemas — and is prompted to decide whether and how to invoke those tools based on a user's input. Instead of generating free-form text, the model emits a structured response containing the name of the function it wants to call and the arguments it wants to pass. The host application then executes the actual function and optionally feeds the result back to the model for a follow-up response.
This transforms the model from a passive text generator into an active agent that can interact with external systems, query databases, call APIs, perform calculations, and orchestrate multi-step workflows. Mistral models such as Mistral-Large, Mistral-Small, and the open-weight Mistral-Nemo all support function calling natively through their chat completion API.
Why Function Calling Matters
Before function calling existed, developers relied on brittle prompt engineering to coax models into producing parseable output. You would ask the model to respond in JSON, hope it followed the format, and write fragile regex or JSON parsers to extract the relevant fields. This approach broke frequently, especially when users asked unexpected questions.
Function calling solves several critical problems:
- Reliability: The model is constrained to produce arguments that match a defined schema, dramatically reducing parsing failures.
- Safety: The application retains control over execution. The model never runs code directly; it only suggests what should be run.
- Composability: Multiple tools can be offered simultaneously, and the model selects the right one based on context.
- Grounding: By feeding tool results back into the conversation, responses become grounded in real data rather than model hallucinations.
- Multi-step reasoning: Complex tasks can be decomposed into sequences of tool calls, enabling agentic workflows.
How Mistral Function Calling Works
Mistral's function calling follows a straightforward request-response loop. You send a chat completion request that includes a tools array describing available functions. The model responds either with a normal text message or with a tool_calls object containing one or more function invocations. Your application executes those functions, appends the results as tool role messages, and sends the conversation back to the model for a final answer.
The typical flow looks like this:
- Define your tools as JSON schemas.
- Send the user message plus tools to the Mistral API.
- Check whether the response contains
tool_calls. - If it does, execute each function locally and collect results.
- Append the assistant message and tool result messages to the conversation.
- Send the updated conversation back to the model.
- Receive a final natural-language response grounded in the tool outputs.
Setting Up Your Environment
To follow along, you will need a Mistral API key. Install the official Python SDK and set your API key as an environment variable.
pip install mistralai
import os
os.environ["MISTRAL_API_KEY"] = "your_api_key_here"
Alternatively, export the key in your shell before running your script:
export MISTRAL_API_KEY="your_api_key_here"
Defining Your First Tool
Let us start with a simple example. We will define a tool that retrieves the current weather for a given city. Tools are described using a JSON schema format that tells the model the function name, its purpose, and the parameters it accepts.
from mistralai import Mistral
import json
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
weather_tool = {
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a specified city.",
"parameters": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. Paris or Tokyo."
},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit to use."
}
},
"required": ["city"]
}
}
}
Notice how each property has a description. These descriptions are critical — the model uses them to understand when and how to use each parameter. Clear, specific descriptions directly improve tool selection accuracy.
Implementing the Actual Function
The tool definition only tells the model about the function. You still need to implement the function in your application code. In a real project, this might call a weather API. For this tutorial, we will use a mock implementation.
def get_weather(city: str, unit: str = "celsius") -> dict:
"""Mock weather function. Replace with a real API call."""
mock_data = {
"Paris": {"celsius": 18, "fahrenheit": 64, "condition": "Partly cloudy"},
"Tokyo": {"celsius": 26, "fahrenheit": 79, "condition": "Sunny"},
"New York": {"celsius": 15, "fahrenheit": 59, "condition": "Rainy"},
}
city_key = city.strip().title()
if city_key not in mock_data:
return {"error": f"Weather data not available for {city}."}
data = mock_data[city_key]
return {
"city": city_key,
"temperature": data[unit],
"unit": unit,
"condition": data["condition"]
}
It is good practice to keep a mapping from function names to their Python implementations so you can dispatch dynamically.
available_functions = {
"get_weather": get_weather,
}
Sending the Request to Mistral
Now we will send a user message along with our tool definition and let the model decide whether to call the function.
messages = [
{"role": "user", "content": "What is the weather like in Tokyo right now?"}
]
response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
tools=[weather_tool],
tool_choice="auto"
)
message = response.choices[0].message
print(message)
When the model decides a tool call is appropriate, the response message will contain a tool_calls list. Each entry includes the function name and a JSON string of arguments.
Handling the Tool Call Response
The core of any function calling implementation is the dispatch loop. You inspect the response, execute any requested functions, and feed the results back.
messages.append(message)
if message.tool_calls:
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
print(f"Model requested: {function_name}({function_args})")
func = available_functions.get(function_name)
if func is None:
result = {"error": f"Unknown function: {function_name}"}
else:
result = func(**function_args)
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Send the conversation back with tool results
final_response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
tools=[weather_tool],
tool_choice="auto"
)
print(final_response.choices[0].message.content)
else:
print(message.content)
If the user asked about Tokyo's weather, the model would first emit a tool call for get_weather with {"city": "Tokyo"}. After receiving the mock data, it would produce a natural response such as: "The current weather in Tokyo is 26°C and sunny."
Working with Multiple Tools
Real applications rarely have just one tool. Let us add a second function that converts currencies, then offer both to the model simultaneously.
currency_tool = {
"type": "function",
"function": {
"name": "convert_currency",
"description": "Convert an amount from one currency to another using current exchange rates.",
"parameters": {
"type": "object",
"properties": {
"amount": {"type": "number", "description": "The amount to convert."},
"from_currency": {"type": "string", "description": "Source currency code, e.g. USD."},
"to_currency": {"type": "string", "description": "Target currency code, e.g. EUR."}
},
"required": ["amount", "from_currency", "to_currency"]
}
}
}
def convert_currency(amount: float, from_currency: str, to_currency: str) -> dict:
rates = {"USD": 1.0, "EUR": 0.92, "JPY": 151.0, "GBP": 0.79}
from_currency = from_currency.upper()
to_currency = to_currency.upper()
if from_currency not in rates or to_currency not in rates:
return {"error": "Unsupported currency."}
usd_amount = amount / rates[from_currency]
converted = usd_amount * rates[to_currency]
return {
"amount": amount,
"from_currency": from_currency,
"to_currency": to_currency,
"converted_amount": round(converted, 2)
}
available_functions["convert_currency"] = convert_currency
Now send a request that could trigger either tool:
messages = [
{"role": "user", "content": "I have 500 USD. How much is that in EUR? Also, what is the weather in Paris?"}
]
response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
tools=[weather_tool, currency_tool],
tool_choice="auto"
)
Mistral models can emit multiple tool calls in a single response. Your dispatch loop should handle each one, append all results, and then send the conversation back in a single follow-up request.
Controlling Tool Selection with tool_choice
The tool_choice parameter gives you fine-grained control over how the model uses tools. There are three main options:
"auto"— The model decides whether to call a tool based on the user's message. This is the default and most flexible option."none"— The model is instructed not to call any tools and must respond with text only.{"type": "function", "function": {"name": "get_weather"}}— Forces the model to call a specific function. Useful when you know exactly which tool should be used.
# Force a specific function call
response = client.chat.complete(
model="mistral-large-latest",
messages=messages,
tools=[weather_tool, currency_tool],
tool_choice={"type": "function", "function": {"name": "get_weather"}}
)
Building a Reusable Agent Loop
For production applications, you will want a reusable loop that can handle multiple rounds of tool calls. Some queries require the model to call a tool, inspect the result, then call another tool based on that result. Here is a generalized agent loop:
def run_agent(client, model, messages, tools, available_functions, max_iterations=5):
for iteration in range(max_iterations):
response = client.chat.complete(
model=model,
messages=messages,
tools=tools,
tool_choice="auto"
)
message = response.choices[0].message
messages.append(message)
if not message.tool_calls:
return message.content
for tool_call in message.tool_calls:
function_name = tool_call.function.name
function_args = json.loads(tool_call.function.arguments)
func = available_functions.get(function_name)
if func is None:
result = {"error": f"Unknown function: {function_name}"}
else:
try:
result = func(**function_args)
except Exception as e:
result = {"error": str(e)}
messages.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
return "Maximum iterations reached without a final answer."
You can now run complex multi-step queries with a single call:
messages = [{"role": "user", "content": "What is the weather in Tokyo, and if it is sunny, convert 100 USD to JPY for my trip."}]
answer = run_agent(
client=client,
model="mistral-large-latest",
messages=messages,
tools=[weather_tool, currency_tool],
available_functions=available_functions
)
print(answer)
Using Mistral Open-Weight Models Locally
If you are running Mistral open-weight models such as Mistral-Nemo or Mistral-7B-Instruct locally through vLLM or Ollama, function calling is supported through a special prompt format. The Mistral team provides a chat template that wraps tool definitions and tool call outputs in specific XML-like tokens.
For example, with vLLM you can use the OpenAI-compatible endpoint:
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
response = client.chat.completions.create(
model="mistralai/Mistral-7B-Instruct-v0.3",
messages=messages,
tools=[weather_tool, currency_tool],
tool_choice="auto"
)
The response structure mirrors the Mistral API, so your dispatch loop code remains the same. Just be aware that smaller models may be less reliable at selecting the correct tool and producing valid arguments, so always validate inputs before executing functions.
Best Practices for Function Calling with Mistral
Write Detailed Descriptions
The model relies entirely on your descriptions to understand what each tool does and when to use it. Vague descriptions lead to poor tool selection. Instead of "Get data", write "Retrieve the current stock price for a given ticker symbol from the market data API." Include edge cases and constraints in the description when relevant.
Validate All Arguments
Never trust model-generated arguments blindly. Even with strong schemas, models can produce unexpected values. Validate types, ranges, and allowed values before passing arguments to your functions. Return clear error messages as tool results so the model can self-correct.
def get_weather(city: str, unit: str = "celsius") -> dict:
if not isinstance(city, str) or len(city.strip()) == 0:
return {"error": "City must be a non-empty string."}
if unit not in ("celsius", "fahrenheit"):
return {"error": "Unit must be 'celsius' or 'fahrenheit'."}
# ... rest of implementation
Keep Schemas Simple
Avoid deeply nested schemas with many optional fields when possible. Flatter schemas with clear required parameters produce more reliable results. If a function needs complex input, consider splitting it into multiple simpler tools.
Handle Errors Gracefully
When a function fails, return a structured error as the tool result rather than raising an exception. This lets the model understand what went wrong and potentially retry with corrected arguments or inform the user.
Limit the Number of Tools
While Mistral models can handle multiple tools, offering too many at once degrades selection accuracy. A practical limit is around 10 to 15 tools per request. If you have more, consider a retrieval step that selects relevant tools before calling the model.
Use Parallel Tool Calls Wisely
Mistral models can return multiple tool calls in one response. If your tools are independent, execute them in parallel to reduce latency. If they are dependent, force sequential execution by returning only the first result and letting the model decide on the next call.
Log Everything
During development, log the full conversation including tool calls, arguments, and results. This makes it much easier to debug unexpected behavior and improve your tool descriptions over time.
import logging
logging.basicConfig(level=logging.INFO)
# Inside your dispatch loop:
logging.info(f"Tool call: {function_name} with args {function_args}")
logging.info(f"Tool result: {result}")
Choose the Right Model
For complex function calling tasks with many tools or intricate schemas, use Mistral-Large for the best accuracy. For simpler, cost-sensitive workloads, Mistral-Small offers a good balance. Always benchmark on your specific tool set before committing to a model for production.
Conclusion
Function calling with Mistral models opens the door to building intelligent applications that can reason about user intent, select appropriate tools, and execute real actions with structured, reliable outputs. By defining clear tool schemas, implementing a robust dispatch loop, validating arguments, and following best practices around error handling and model selection, you can create agentic systems that are both powerful and dependable. Start with a single tool, iterate on your descriptions based on observed behavior, and gradually expand your tool set as your application grows. With Mistral's native function calling support, the gap between natural language and programmatic action has never been easier to bridge.