Imaginez un assistant qui répond à « Quelle heure est-il à Tokyo et combien font 234 × 17 ? » : pour la première moitié il consulte une API horloge, pour la seconde il fait du calcul. Un LLM seul ne peut faire ni l'un ni l'autre de manière fiable. Le tool calling (ou function calling) lui donne le droit de demander l'appel de fonctions Python que vous exécutez, puis lui réinjectez le résultat. C'est le mécanisme qui transforme un chatbot en agent autonome, et qui marche entièrement en local avec Ollama en 2026.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Ce qu'est le tool calling et comment il diffère des sorties structurées
- Connecter un premier outil Python à un LLM Ollama
- Gérer plusieurs outils et laisser le LLM choisir
- Construire une boucle agentique qui chaîne plusieurs appels d'outils dans une même requête utilisateur
Tool calling vs Structured Outputs : ne pas confondre
Section intitulée « Tool calling vs Structured Outputs : ne pas confondre »| Structured Outputs | Tool Calling | |
|---|---|---|
| But | Forcer le format de la sortie | Demander au LLM d'invoquer une fonction |
| Le modèle produit… | Du JSON conforme à un schéma | Une intention d'appel : nom de fonction + arguments |
| Qui exécute ? | Personne — c'est la réponse finale | Vous (le code Python) exécutez la fonction |
| Cas d'usage | Extraction d'entités, classification | Agents, recherche, intégrations API |
→ Si vous n'avez pas lu, commencez par Sorties structurées avec Ollama pour le contraste.
Le flow complet en 5 étapes
Section intitulée « Le flow complet en 5 étapes »Quand on dit que « le LLM appelle un outil », c'est un raccourci. En réalité, le code Python orchestre tout. Voici le vrai cycle :
- Vous envoyez au LLM la question utilisateur + la liste des outils disponibles (sous forme de schéma JSON).
- Le LLM décide s'il a besoin d'un outil. Si oui, il renvoie un objet
tool_callsavec le nom et les arguments. Il n'exécute rien. - Votre code exécute la fonction Python correspondante.
- Vous renvoyez au LLM la même conversation + un nouveau message de rôle
toolcontenant le résultat. - Le LLM formule une réponse finale en langage naturel pour l'utilisateur, en s'appuyant sur ce résultat.
C'est vous qui orchestrez. Le LLM n'a aucun accès au système.
Prérequis et installation
Section intitulée « Prérequis et installation »# Modèle recommandé en 2026 (le plus stable sur tool calling)ollama pull qwen3
# Dépendances Pythonuv add "ollama==0.6.2"Premier outil : la météo
Section intitulée « Premier outil : la météo »On va donner au LLM un seul outil : une fonction get_weather(city) qui retourne une météo fictive. Le but est de voir le flow complet bout en bout.
from ollama import Client
MODEL = "qwen3"HOST = "http://localhost:11434"
# 1. La fonction Python qui sera appeléedef get_weather(city: str) -> dict: """Retourne une météo factice pour une ville française.""" db = { "Paris": {"temp_c": 14, "condition": "Nuageux", "humidite": 78}, "Marseille": {"temp_c": 22, "condition": "Ensoleillé", "humidite": 45}, } return db.get(city, {"error": f"Ville inconnue : {city}"})
# 2. Le schéma de l'outil (JSON Schema standard)TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "Obtenir la météo actuelle d'une ville française.", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "Nom de la ville."}, }, "required": ["city"], }, }, }]
client = Client(host=HOST)messages = [ {"role": "user", "content": "Quel temps fait-il à Marseille ?"},]
# Tour 1 : on envoie la question + les outilsresponse = client.chat(model=MODEL, messages=messages, tools=TOOLS, options={"temperature": 0})msg = response["message"]print("Le LLM a-t-il décidé d'appeler un outil ?", bool(msg.get("tool_calls")))
# Tour 2 : on exécute l'outil et on lui renvoie le résultatmessages.append(msg)for call in msg["tool_calls"]: if call["function"]["name"] == "get_weather": args = call["function"]["arguments"] result = get_weather(args["city"]) messages.append({"role": "tool", "content": str(result), "name": "get_weather"})
# Tour 3 : le LLM formule la réponse finale pour l'utilisateurfinal = client.chat(model=MODEL, messages=messages, options={"temperature": 0})print(final["message"]["content"])Sortie réelle testée sur H100 avec qwen3 :
Le LLM a-t-il décidé d'appeler un outil ? Trueget_weather({'city': 'Marseille'}) → {'temp_c': 22, 'condition': 'Ensoleillé', 'humidite': 45}Il fait actuellement 22°C à Marseille avec un ciel ensoleillé. L'humidité est à 45%. 🌞Plusieurs outils : le LLM choisit
Section intitulée « Plusieurs outils : le LLM choisit »Dans la vraie vie, vous donnez plusieurs outils au LLM et il choisit le bon selon la question. La logique est la même, on enrichit juste la liste TOOLS et on ajoute un dispatcher.
def calculer(expression: str) -> str: """Évalue une expression mathématique simple.""" allowed = set("0123456789+-*/. ()") if not all(c in allowed for c in expression): return "Expression non autorisée." return str(eval(expression, {"__builtins__": {}}, {}))
def recherche_doc(query: str) -> str: """Retourne un extrait fictif de doc interne.""" db = { "ollama": "Ollama est un runtime local pour LLM open-source.", "h100": "NVIDIA H100 : 80 Go HBM3, idéal pour LLM jusqu'à 70B paramètres.", } for key, val in db.items(): if key in query.lower(): return val return "Aucun résultat."
TOOLS = [ {"type": "function", "function": { "name": "calculer", "description": "Calculer une expression mathématique.", "parameters": {"type": "object", "properties": {"expression": {"type": "string"}}, "required": ["expression"]}, }}, {"type": "function", "function": { "name": "recherche_doc", "description": "Rechercher dans la doc interne (Ollama, H100).", "parameters": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}, }},]
DISPATCH = {"calculer": calculer, "recherche_doc": recherche_doc}
# Le LLM choisit lequel utiliserclient = Client(host="http://localhost:11434")def ask(question: str) -> None: messages = [{"role": "user", "content": question}] response = client.chat(model="qwen3", messages=messages, tools=TOOLS, options={"temperature": 0}) msg = response["message"] if not msg.get("tool_calls"): print("→", msg.get("content", "")) return messages.append(msg) for call in msg["tool_calls"]: name = call["function"]["name"] result = DISPATCH[name](**call["function"]["arguments"]) print(f" Tool: {name} → {result}") messages.append({"role": "tool", "content": str(result), "name": name}) final = client.chat(model="qwen3", messages=messages, options={"temperature": 0}) print("→", final["message"]["content"])
ask("Combien font 234 * 17 ?")ask("C'est quoi le H100 ?")Sortie réelle :
Tool: calculer → 3978→ 234 multiplié par 17 égale **3 978**.
Tool: recherche_doc → NVIDIA H100 : 80 Go HBM3, idéal pour servir des LLM jusqu'à 70B paramètres.→ Le NVIDIA H100 est une carte graphique de la série Hopper, dotée de 80 Go de mémoire HBM3...Boucle agentique : chaîner plusieurs outils
Section intitulée « Boucle agentique : chaîner plusieurs outils »Le vrai pouvoir du tool calling, c'est de chaîner plusieurs outils dans une même requête utilisateur. Exemple : « Quel est le réalisateur d'Inception et combien de temps dure le film en heures et minutes ? ». Le LLM doit :
- Appeler
chercher_film(titre="Inception")→ durée en minutes - Appeler
convertir_minutes(minutes=148)→ format « 2h28 » - Formuler la réponse finale en combinant les deux résultats
Il faut une boucle qui continue tant que le LLM appelle des outils. Avec un garde-fou MAX_TURNS pour éviter qu'il tourne en rond.
from ollama import Client
MAX_TURNS = 5 # Garde-fou : interdit plus de 5 tours pour 1 question utilisateur
def chercher_film(titre: str) -> str: db = {"Inception": {"realisateur": "Christopher Nolan", "annee": 2010, "duree_min": 148}} return str(db.get(titre, {"error": f"Film inconnu : {titre}"}))
def convertir_minutes(minutes: int) -> str: h, m = divmod(minutes, 60) return f"{h}h{m:02d}"
TOOLS = [ {"type": "function", "function": { "name": "chercher_film", "description": "Métadonnées d'un film par titre.", "parameters": {"type": "object", "properties": {"titre": {"type": "string"}}, "required": ["titre"]}, }}, {"type": "function", "function": { "name": "convertir_minutes", "description": "Convertir des minutes en heures+minutes.", "parameters": {"type": "object", "properties": {"minutes": {"type": "integer"}}, "required": ["minutes"]}, }},]DISPATCH = {"chercher_film": chercher_film, "convertir_minutes": convertir_minutes}
client = Client(host="http://localhost:11434")messages = [{"role": "user", "content": "Quel est le réalisateur d'Inception et combien dure le film en heures ?"}]
for turn in range(MAX_TURNS): response = client.chat(model="qwen3", messages=messages, tools=TOOLS, options={"temperature": 0}) msg = response["message"] if not msg.get("tool_calls"): print(f"[Tour {turn+1}] Réponse finale : {msg['content']}") break messages.append(msg) for call in msg["tool_calls"]: name = call["function"]["name"] result = DISPATCH[name](**call["function"]["arguments"]) print(f"[Tour {turn+1}] {name} → {result}") messages.append({"role": "tool", "content": str(result), "name": name})else: print(f"⚠️ Boucle interrompue après {MAX_TURNS} tours.")Sortie réelle sur H100 avec qwen3 :
[Tour 1] chercher_film → {'realisateur': 'Christopher Nolan', 'annee': 2010, 'duree_min': 148}[Tour 2] convertir_minutes → 2h28[Tour 3] Réponse finale : Le film Inception a été réalisé par Christopher Nolan et dure 2h28.Le LLM a bien chaîné les deux outils, sans intervention, et formulé une réponse synthétique.
Pièges à connaître
Section intitulée « Pièges à connaître »Modèles 2026 testés sur tool calling
Section intitulée « Modèles 2026 testés sur tool calling »Mesures réelles sur NVIDIA H100 avec Ollama 0.24 :
| Modèle | VRAM | Fiabilité tool calling | Hallucinations d'arguments |
|---|---|---|---|
qwen3 | 5.2 Go | Excellente | Très rares |
gemma4 | 9.6 Go | Excellente (function calling natif entraîné) | Rares |
llama3.1 | 4.7 Go | Bonne | Occasionnelles |
mistral | 4.4 Go | Moyenne | Fréquentes |
Mon choix par défaut :
qwen3. C'est le modèle qui hallucine le moins les arguments sur les tests intensifs 2026.
Aller plus loin : frameworks d'agents
Section intitulée « Aller plus loin : frameworks d'agents »Le tool calling natif d'Ollama est parfait pour commencer. Pour des agents complexes (mémoire, multi-étapes, fallback, observabilité), passer à un framework :
- PydanticAI : agents typés avec validation Pydantic à chaque étape
- LangGraph : graphe d'états, agents avec mémoire persistante
- smolagents : agents qui écrivent du code Python pour résoudre
Q : Mon modèle ne renvoie jamais de tool_calls, il répond en texte libre.
R : Trois causes possibles : (1) le modèle ne supporte pas le tool calling — testez qwen3 ; (2) Ollama serveur trop ancien — passez en 0.5+ ; (3) la description des outils est trop vague pour le modèle.
Q : Les arguments sont mal typés (ex: "148" au lieu de 148).
R : Vérifiez le type dans le schéma JSON. Si vous mettez "integer", Ollama force le type côté serveur. Sinon le modèle peut renvoyer une string.
Q : Comment gérer les erreurs d'exécution des outils ?
R : Toujours encapsuler DISPATCH[name](**args) dans un try/except. En cas d'erreur, renvoyer au LLM un message du type {"error": "...", "details": "..."} — il saura formuler une réponse polie à l'utilisateur.
Q : Peut-on faire du tool calling en streaming ?
R : Ollama supporte le streaming sur la réponse finale, mais pas pendant l'appel d'outil. Le tool_call arrive en un seul bloc.
À retenir
Section intitulée « À retenir »- Tool calling = LLM décide quoi appeler, vous exécutez, vous renvoyez le résultat, LLM répond. Cinq étapes claires.
- Le LLM n'a aucun accès au système — il propose, vous disposez.
- Boucle agentique = répéter le cycle jusqu'à ce que le LLM ne propose plus d'outil, avec un
MAX_TURNS. qwen3est le modèle 2026 le plus fiable sur ce cas d'usage.- Validez en aval : un résultat d'outil correct ne garantit pas une réponse finale correcte du LLM.