Avancé 20 minAPI

Maîtriser l'Appel d'Outils (Tool Calling) avec Ollama et Python

L'appel d'outil (tool calling) transforme un modèle qui se contente de générer du texte en un agent capable de déclencher du vrai code : appeler une API météo, interroger une base, lancer un calcul. Ce guide montre comment maîtriser l'appel d'outil Ollama en Python de bout en bout — format JSON des outils, boucle d'exécution, streaming des tool calls (série 0.17) et structured outputs contraints par un JSON Schema appliqué directement au décodage. Tout tourne en local sur http://localhost:11434, sans clé API ni fuite de données.

Par Mohamed Meguedmi·Màj 2026-08-18·Testé sur Windows, macOS, Linux

#Pourquoi l'appel d'outil (tool calling) ?

Un LLM seul ne sait rien du monde réel après son entraînement : il ne connaît ni la météo d'aujourd'hui, ni le solde d'un compte, ni le contenu de votre base de données. L'appel d'outil comble ce vide. Vous décrivez au modèle une liste de fonctions disponibles, il décide lesquelles appeler et avec quels arguments, votre code les exécute, puis renvoie le résultat au modèle pour qu'il rédige une réponse informée.

Le point crucial à comprendre : le modèle n'exécute jamais rien lui-même. Il se contente de produire une demande structurée — « appelle get_meteo avec ville='Lyon' ». C'est votre programme Python qui exécute la fonction et garde le contrôle total. Cette séparation est ce qui rend le tool calling sûr et prévisible.

Données fraîches
Le modèle interroge une API en temps réel plutôt que de deviner à partir de ses souvenirs d'entraînement.
Actions concrètes
Créer un ticket, envoyer un e-mail, écrire dans une base — le LLM orchestre, votre code agit.
Fiabilité
Les calculs et les recherches exactes sont délégués à du code déterministe, pas hallucinés par le modèle.
100 % local
Avec Ollama, toute la chaîne reste sur votre machine : ni clé API, ni requête sortante, ni facture au token.

#Comment fonctionne l'appel d'outil Ollama

Le cycle complet tient en cinq temps. Bien le visualiser évite la confusion la plus fréquente : croire qu'un seul appel suffit. Il en faut au moins deux — un pour obtenir la demande d'outil, un pour obtenir la réponse finale.

  1. 01
    Vous envoyez la question + les outils
    La requête chat contient le message de l'utilisateur et la liste des outils disponibles (paramètre tools).
  2. 02
    Le modèle renvoie une demande d'outil
    Au lieu de répondre en texte, il retourne un ou plusieurs tool_calls avec le nom de la fonction et les arguments.
  3. 03
    Votre code exécute la fonction
    Vous récupérez name et arguments, appelez la vraie fonction Python correspondante, obtenez un résultat.
  4. 04
    Vous renvoyez le résultat
    Le résultat est ajouté à l'historique sous forme d'un message de rôle « tool », puis vous relancez chat.
  5. 05
    Le modèle rédige la réponse finale
    Fort du résultat, il produit cette fois une réponse en langage naturel pour l'utilisateur.
i
Deux appels minimum
Un cycle de tool calling complet = deux passages par le modèle au minimum. Si le modèle enchaîne plusieurs outils, on boucle jusqu'à ce qu'il n'y ait plus de tool_calls dans sa réponse.

#Prérequis

Trois briques : le daemon Ollama qui tourne, un modèle qui supporte réellement les outils, et la librairie Python officielle. Attention au deuxième point — tous les modèles ne savent pas faire du tool calling. Visez les familles récentes conçues pour ça.

Ollama à jour
Série 0.17 ou plus récente pour profiter du streaming des tool calls. Le daemon écoute sur http://localhost:11434.
Un modèle compatible outils
Qwen 3, Llama 3.1 / 3.3, Mistral, Firefunction, Command-R. Les modèles marqués « tools » sur ollama.com/library.
Assez de VRAM
Un 7B (Q4_K_M ≈ 5 Go) suffit pour tester ; un 14B (≈ 9 Go) suit mieux les consignes multi-outils. RTX 3060 12 Go comme entrée de gamme.
La librairie ollama
pip install -U ollama. Elle sait construire le schéma d'un outil directement depuis une fonction Python typée.
Préparer l'environnement
# Le daemon Ollama doit tourner (souvent déjà lancé en service)
ollama serve

# Un modèle qui supporte les outils
ollama pull qwen3

# La librairie Python officielle
pip install -U ollama
!
Modèle sans support d'outils
Passer un paramètre tools à un modèle qui ne le gère pas ne lève pas toujours d'erreur claire : le modèle ignore les outils et répond en texte, ou renvoie du faux JSON dans le contenu. Vérifiez toujours l'étiquette « Tools » du modèle avant de coder.

#Le format JSON des outils

Un outil se décrit avec un schéma JSON strictement aligné sur celui d'OpenAI : un objet type: "function" contenant un nom, une description et un objet parameters au format JSON Schema. La description compte énormément — c'est elle que le modèle lit pour décider quand et comment appeler l'outil. Soyez explicite.

Définition d'un outil (format OpenAI)
{
  "type": "function",
  "function": {
    "name": "get_meteo",
    "description": "Renvoie la météo actuelle pour une ville donnée",
    "parameters": {
      "type": "object",
      "properties": {
        "ville": {
          "type": "string",
          "description": "Nom de la ville, ex : Lyon"
        },
        "unite": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "Unité de température souhaitée"
        }
      },
      "required": ["ville"]
    }
  }
}
Laissez la librairie écrire le schéma
En Python, vous n'êtes pas obligé d'écrire ce JSON à la main. Si vous passez directement une fonction typée avec une docstring, la librairie ollama en déduit automatiquement le schéma (noms, types, description). C'est le moyen le plus sûr d'éviter les fautes de frappe dans le JSON.

#Premier appel d'outil en Python

Commençons par le cas le plus simple : une fonction, une question, et on observe ce que le modèle décide. Les annotations de type et la docstring servent à générer le schéma envoyé au modèle.

Un premier tool call
import ollama

def get_meteo(ville: str, unite: str = "celsius") -> str:
    """Renvoie la météo actuelle pour une ville donnée.

    Args:
        ville: Nom de la ville (ex : Lyon).
        unite: Unité de température, celsius ou fahrenheit.
    """
    # Ici, un vrai appel à une API météo. On simule le retour.
    return f"21 degrés, ciel dégagé à {ville} ({unite})."

reponse = ollama.chat(
    model="qwen3",
    messages=[{"role": "user", "content": "Quel temps fait-il à Lyon ?"}],
    tools=[get_meteo],  # la lib introspecte signature + docstring
)

# Le modèle n'a pas répondu en texte : il demande un outil
for appel in reponse.message.tool_calls or []:
    print(appel.function.name)       # -> get_meteo
    print(appel.function.arguments)  # -> {'ville': 'Lyon'}

À ce stade, message.content est généralement vide : le modèle a renvoyé sa demande dans message.tool_calls. Chaque tool_call expose function.name (une chaîne) et function.arguments (déjà désérialisé en dictionnaire Python par la librairie). Il ne reste plus qu'à exécuter et à renvoyer.

#La boucle d'exécution complète

Voici le squelette réutilisable d'un agent tool calling : un dictionnaire qui associe chaque nom d'outil à sa fonction, l'exécution des appels demandés, l'ajout des résultats à l'historique, puis un second appel pour la réponse finale. On enveloppe le tout dans une boucle pour gérer le cas où le modèle enchaîne plusieurs outils.

Boucle agent complète
import ollama

def get_meteo(ville: str, unite: str = "celsius") -> str:
    """Météo actuelle d'une ville."""
    return f"21 degrés, ciel dégagé à {ville}."

# Registre nom -> fonction réelle
OUTILS = {"get_meteo": get_meteo}

messages = [{"role": "user", "content": "Météo à Lyon puis à Marseille ?"}]

while True:
    reponse = ollama.chat(model="qwen3", messages=messages, tools=[get_meteo])
    messages.append(reponse.message)  # on garde la demande dans l'historique

    if not reponse.message.tool_calls:
        # Plus d'outil demandé : c'est la réponse finale
        print(reponse.message.content)
        break

    for appel in reponse.message.tool_calls:
        fonction = OUTILS.get(appel.function.name)
        if fonction is None:
            resultat = f"Erreur : outil inconnu '{appel.function.name}'"
        else:
            resultat = fonction(**appel.function.arguments)
        messages.append({
            "role": "tool",
            "tool_name": appel.function.name,
            "content": str(resultat),
        })
!
Ne jamais faire confiance aux arguments
Les arguments viennent du modèle : ils peuvent être incomplets, mal typés ou hors bornes. Validez-les avant d'exécuter la fonction, surtout si elle touche un système de fichiers, une base ou une commande shell. Un tool calling non validé est une porte ouverte à l'injection.

Le message de résultat porte le rôle tool et un champ tool_name qui indique à quel appel il répond. Le content doit être une chaîne : sérialisez vos objets (json.dumps) avant de les renvoyer. Le modèle relit ce contenu comme s'il s'agissait d'une observation du monde.

#Parité OpenAI : le même code avec le client openai

Ollama expose un endpoint compatible OpenAI sur /v1. Si votre code utilise déjà le client openai, vous n'avez presque rien à changer : pointez base_url vers Ollama et mettez une clé API factice. Le format des outils et des tool_calls est identique — c'est la « parité OpenAI » qui rend la migration triviale.

Tool calling via le client openai
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

reponse = client.chat.completions.create(
    model="qwen3",
    messages=[{"role": "user", "content": "Météo à Lyon ?"}],
    tools=[{
        "type": "function",
        "function": {
            "name": "get_meteo",
            "description": "Météo actuelle d'une ville",
            "parameters": {
                "type": "object",
                "properties": {"ville": {"type": "string"}},
                "required": ["ville"],
            },
        },
    }],
)

print(reponse.choices[0].message.tool_calls)
i
Une différence à connaître
Via le client openai, function.arguments arrive sous forme de chaîne JSON (à parser avec json.loads), alors que la librairie ollama native vous donne déjà un dictionnaire. Pensez-y en migrant d'un client à l'autre.

#Streaming des tool calls (série 0.17)

Historiquement, activer le streaming désactivait le tool calling : il fallait choisir. Depuis la série 0.17, Ollama sait streamer les appels d'outils au fil de la génération. Concrètement, vous recevez les tool_calls dans les morceaux (chunks) du flux, en même temps que le texte éventuel, ce qui permet d'afficher une réponse fluide tout en déclenchant des outils.

Tool calls en streaming
import ollama

flux = ollama.chat(
    model="qwen3",
    messages=[{"role": "user", "content": "Météo à Nice ?"}],
    tools=[get_meteo],
    stream=True,
)

for morceau in flux:
    # Le texte arrive token par token
    if morceau.message.content:
        print(morceau.message.content, end="", flush=True)
    # Les appels d'outils arrivent aussi dans le flux
    for appel in morceau.message.tool_calls or []:
        print("\n[outil]", appel.function.name, appel.function.arguments)
Quand streamer
Le streaming brille pour les interfaces conversationnelles où l'utilisateur voit la réponse se construire. Pour un traitement batch ou une extraction de données, restez en mode non-streamé : c'est plus simple à gérer et vous récupérez la réponse complète d'un coup.

#Structured outputs : forcer un JSON Schema

Le tool calling sert à agir ; les structured outputs servent à garantir la forme de la réponse. Avec le paramètre format, vous passez un JSON Schema qu'Ollama applique au décodage : le modèle est contraint, token par token, à ne produire qu'une sortie valide au regard du schéma. Fini le parsing fragile de JSON approximatif — la structure est garantie par construction.

Le plus pratique en Python est de décrire la structure avec un modèle Pydantic, puis d'en tirer le schéma via model_json_schema(). Vous récupérez ensuite un objet typé et validé.

Sortie structurée validée par Pydantic
from pydantic import BaseModel
import ollama

class Facture(BaseModel):
    numero: str
    montant_ttc: float
    devise: str
    lignes: list[str]

reponse = ollama.chat(
    model="qwen3",
    messages=[{
        "role": "user",
        "content": "Extrais numéro, montant TTC, devise et lignes de : "
                   "Facture F-2026-0042, total 149,90 EUR, "
                   "prestations : audit, rédaction.",
    }],
    # Le schéma est appliqué au décodage : sortie garantie conforme
    format=Facture.model_json_schema(),
)

facture = Facture.model_validate_json(reponse.message.content)
print(facture.montant_ttc)  # -> 149.9
i
format="json" vs schéma complet
format="json" force seulement du JSON valide, sans imposer de structure. Passer un JSON Schema complet va bien plus loin : il contraint les champs, les types et les valeurs autorisées au moment du décodage. Préférez toujours le schéma explicite quand vous connaissez la forme attendue.
Le combo gagnant
Tool calling pour aller chercher la donnée, structured output pour la restituer proprement. Un agent qui appelle une API puis renvoie un objet Pydantic validé est bien plus robuste qu'un modèle à qui l'on demande « réponds en JSON » en espérant que ça marche.

#Gestion des erreurs et pièges courants

Le tool calling échoue rarement bruyamment : le plus souvent, le modèle « déraille » silencieusement. Voici les pannes fréquentes et comment les traiter.

Aucun tool_call renvoyé
Le modèle a répondu en texte alors qu'un outil s'imposait. Améliorez la description de l'outil, ou changez de modèle : les petits modèles ratent souvent la décision d'appeler.
Arguments manquants ou faux
function.arguments peut omettre un champ required ou mal le typer. Validez avec Pydantic ou un try/except avant d'appeler la vraie fonction, et renvoyez l'erreur au modèle comme résultat d'outil.
Outil halluciné
Le modèle invente un nom de fonction inexistant. D'où le OUTILS.get(name) qui renvoie un message d'erreur au lieu de planter — le modèle peut alors se corriger au tour suivant.
Boucle infinie d'outils
Un modèle peut redemander sans fin le même outil. Ajoutez un compteur d'itérations maximal (ex : 5) pour couper la boucle et éviter de tourner à vide.
Contexte tronqué
Ollama limite parfois le contexte à 2048 tokens par défaut, ce qui écrase l'historique des outils sur les longues sessions. Augmentez num_ctx via les options du modèle.
Résultat non sérialisé
Renvoyer un objet Python brut comme content casse la requête. Sérialisez toujours en chaîne (json.dumps ou str) avant de l'ajouter aux messages.
Exécution défensive d'un outil
import json

def executer_outil(appel, registre, garde_fou=5):
    nom = appel.function.name
    fonction = registre.get(nom)
    if fonction is None:
        return f"Erreur : outil inconnu '{nom}'."
    try:
        resultat = fonction(**appel.function.arguments)
    except TypeError as e:
        return f"Erreur d'arguments pour {nom} : {e}"
    except Exception as e:
        return f"Échec de {nom} : {e}"
    return json.dumps(resultat, ensure_ascii=False, default=str)

L'idée clé : ne jamais laisser une erreur d'outil faire crasher l'agent. Renvoyez le message d'erreur au modèle comme s'il s'agissait d'un résultat. Un bon modèle lit « outil inconnu » ou « argument manquant » et ajuste son prochain appel de lui-même.


#Pour aller plus loin

Vous savez désormais faire du tool calling Ollama en Python : format JSON des outils, boucle d'exécution, parité OpenAI, streaming et structured outputs contraints par schéma. Ces guides prolongent naturellement le sujet.

L'API REST d'Ollama
« Intégrer Ollama dans une application Python via l'API REST » — les bases de l'endpoint :11434, du streaming et du JSON mode, socle de tout ce guide.
Agents avec LangChain
« Créer un agent IA local en Python avec LangChain et Ollama » — orchestrer plusieurs outils et de la mémoire au-dessus du tool calling brut.
Choisir sa quantification
« Choisir sa quantification (Q4, Q5, Q8, FP16) » — pour équilibrer VRAM et qualité du modèle qui pilotera vos outils.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.