Appels d’outils DeepSeek : tutoriel Python et sécurité

Les appels d’outils DeepSeek, également appelés Tool Calls ou Function Calling, permettent au modèle de demander l’utilisation de fonctions définies par votre application. Il peut ainsi proposer une consultation de catalogue, une recherche dans une base de connaissances ou une opération métier, avec des arguments structurés.

Pour une fonction personnalisée, votre application reste responsable de l’exécution. DeepSeek propose un appel ; votre backend vérifie son nom, ses arguments et les autorisations, exécute la fonction autorisée, puis transmet le résultat au modèle. La documentation officielle des Tool Calls confirme cette séparation. Certains outils intégrés au fournisseur, abordés plus loin, suivent un fonctionnement différent.

Ce tutoriel met en œuvre une boucle complète en Python avec un catalogue fictif en lecture seule. Il explique ensuite comment gérer le mode Thinking, le choix des outils, les schémas stricts et les erreurs sans donner au modèle un accès incontrôlé à votre système.

Vérification documentaire : 31 octobre 2026.

Comment fonctionne un appel d’outil avec DeepSeek ?

Le cycle relie trois éléments : le modèle, votre application et une fonction autorisée. L’exemple officiel de conversation avec outils illustre l’alternance entre les demandes du modèle et les résultats fournis par le programme.

  1. Déclarer : l’application décrit les fonctions disponibles dans tools, avec leurs paramètres.
  2. Recevoir : le modèle répond directement ou renvoie des demandes dans tool_calls.
  3. Contrôler et exécuter : le backend valide chaque demande avant d’appeler une fonction.
  4. Restituer : chaque résultat revient dans un message de rôle tool, associé au bon tool_call_id.
  5. Poursuivre : l’historique enrichi est renvoyé au modèle, qui peut répondre ou demander un autre outil.
Cycle d'appel d'outil DeepSeek avec tool_calls et tool_call_id

Une demande d’appel ne prouve donc pas qu’une opération a réussi. Seul le code qui exécute réellement la fonction peut produire son résultat.

Tool Calling ou JSON Output ?

JSON Output sert à obtenir une réponse structurée, par exemple une catégorie et un résumé. Le Function Calling sert à demander une opération externe. Un objet contenant {"stock": 7} ne démontre pas qu’un catalogue a été consulté ; une fonction exécutée par votre application peut, elle, lire cette donnée dans une source définie.

Un agent ajoute l’orchestration de ces étapes. Il peut choisir plusieurs outils successifs, mais leur déclaration ne lui accorde pas automatiquement des droits sur vos fichiers, vos comptes ou vos bases de données.

Quels modèles et quelle API utiliser ?

La liste officielle des modèles consultée le 31 octobre 2026 indique que deepseek-flash et deepseek-v4-pro prennent en charge les Tool Calls. L’URL de base compatible OpenAI est https://api.deepseek.com.

Le tutoriel utilise Chat Completions, avec deepseek-flash et le mode Thinking explicitement désactivé. Ce choix simplifie la première lecture du protocole ; il ne constitue pas une affirmation selon laquelle ce mode serait toujours plus performant.

Les exemples anciens, les passerelles tierces et les serveurs locaux peuvent employer d’autres identifiants ou paramètres. Vérifiez le contrat de l’endpoint réellement appelé plutôt que de supposer une compatibilité intégrale parce qu’un client accepte le format OpenAI.

Exemple Python complet : consulter un catalogue avec un outil

L’exemple expose une seule fonction, consulter_produit, qui lit un dictionnaire local. Les produits et stocks sont fictifs. Aucun service externe supplémentaire, paiement, compte client ou modification de données n’est nécessaire.

Préparer l’environnement

Utilisez Python 3.11 ou une version ultérieure et installez le client HTTP :

python -m pip install httpx

Créez votre clé depuis la plateforme officielle DeepSeek, puis rendez-la disponible dans la variable d’environnement DEEPSEEK_API_KEY à l’aide de votre configuration locale ou de votre gestionnaire de secrets. Ne l’inscrivez pas dans le programme, un dépôt public ou du JavaScript exécuté dans le navigateur.

Le code utilise des requêtes HTTP directes pour rendre les messages visibles sans dépendre d’un framework d’agents. HTTPX fournit l’envoi du corps JSON et l’accès à la réponse. Les appels réels au modèle nécessitent un compte API utilisable et peuvent entraîner des frais.

Créer le programme

Enregistrez le programme suivant dans deepseek_tools.py. Il inclut la fonction, son schéma, la validation des arguments, la boucle de conversation et des conditions d’arrêt explicites.

import json
import os
import re

import httpx

URL = "https://api.deepseek.com/chat/completions"
MODELE = "deepseek-flash"
MAX_REQUETES = 4
MAX_APPELS = 6

# Donnees fictives, publiques et exclusivement en lecture.
CATALOGUE = {
    "CLAV-01": {"nom": "Clavier compact", "stock": 7},
    "SOUR-02": {"nom": "Souris filaire", "stock": 0},
}


def consulter_produit(reference: str) -> dict:
    produit = CATALOGUE.get(reference)
    if produit is None:
        return {"ok": False, "erreur": "reference_introuvable"}
    return {"ok": True, "reference": reference, **produit}


FONCTIONS = {"consulter_produit": consulter_produit}
OUTILS = [{
    "type": "function",
    "function": {
        "name": "consulter_produit",
        "description": (
            "Consulter le nom et le stock d'un produit fictif. "
            "La reference doit etre fournie par l'utilisateur."
        ),
        "parameters": {
            "type": "object",
            "properties": {
                "reference": {
                    "type": "string",
                    "pattern": "^[A-Z]{4}-[0-9]{2}$",
                    "description": "Reference du catalogue, exemple CLAV-01.",
                }
            },
            "required": ["reference"],
            "additionalProperties": False,
        },
    },
}]


def objet_sans_doublons(paires: list) -> dict:
    objet = {}
    for cle, valeur in paires:
        if cle in objet:
            raise ValueError("Cle JSON dupliquee.")
        objet[cle] = valeur
    return objet


def executer(nom: object, arguments: object) -> dict:
    if not isinstance(nom, str) or nom not in FONCTIONS:
        return {"ok": False, "erreur": "outil_non_autorise"}
    if not isinstance(arguments, str) or len(arguments) > 2000:
        return {"ok": False, "erreur": "arguments_invalides"}
    try:
        donnees = json.loads(arguments, object_pairs_hook=objet_sans_doublons)
    except (ValueError, RecursionError):
        return {"ok": False, "erreur": "json_invalide"}

    if not isinstance(donnees, dict) or set(donnees) != {"reference"}:
        return {"ok": False, "erreur": "champs_invalides"}
    reference = donnees["reference"]
    if not isinstance(reference, str) or not re.fullmatch(r"[A-Z]{4}-[0-9]{2}", reference):
        return {"ok": False, "erreur": "format_reference_invalide"}
    try:
        return FONCTIONS[nom](reference)
    except Exception:
        # Ne pas exposer de details internes dans le resultat d'outil.
        return {"ok": False, "erreur": "execution_impossible"}


def dialoguer(client: httpx.Client, cle: str, question: str,
              thinking: bool = False) -> str:
    messages = [
        {"role": "system", "content": (
            "Tu consultes un catalogue fictif. Utilise consulter_produit "
            "pour toute information de catalogue. Ne devine ni reference "
            "ni stock. Signale les erreurs d'outil. Reponds en francais."
        )},
        {"role": "user", "content": question},
    ]
    total_appels = 0
    ids_utilises = set()

    for tour in range(MAX_REQUETES):
        try:
            reponse = client.post(
                URL,
                headers={"Authorization": f"Bearer {cle}"},
                json={
                    "model": MODELE,
                    "messages": messages,
                    "tools": OUTILS,
                    "tool_choice": "auto",
                    "thinking": {"type": "enabled" if thinking else "disabled"},
                    "max_tokens": 2048,
                    "stream": False,
                },
            )
        except httpx.TimeoutException:
            raise RuntimeError("Delai reseau depasse, sans relance automatique.") from None
        except httpx.RequestError:
            raise RuntimeError("Connexion API impossible.") from None

        if reponse.status_code != 200:
            raise RuntimeError(f"Echec API HTTP {reponse.status_code}.")
        try:
            corps = reponse.json()
        except ValueError:
            raise RuntimeError("Reponse HTTP non JSON.") from None
        choix = corps.get("choices") if isinstance(corps, dict) else None
        if not isinstance(choix, list) or not choix or not isinstance(choix[0], dict):
            raise RuntimeError("Enveloppe API inattendue.")
        raison = choix[0].get("finish_reason")
        message = choix[0].get("message")
        if not isinstance(message, dict) or message.get("role") != "assistant":
            raise RuntimeError("Message assistant absent ou invalide.")
        appels = message.get("tool_calls")
        if appels is None:
            appels = []
        if not isinstance(appels, list):
            raise RuntimeError("Liste d'appels invalide.")

        if not appels:
            texte = message.get("content")
            if raison != "stop" or not isinstance(texte, str) or not texte.strip():
                raise RuntimeError("Pas de reponse finale complete.")
            return texte

        if raison != "tool_calls":
            raise RuntimeError("Appels incomplets : aucune execution.")
        if tour == MAX_REQUETES - 1 or total_appels + len(appels) > MAX_APPELS:
            raise RuntimeError("Budget atteint : aucune nouvelle execution.")

        # Verifier tout le lot avant la premiere execution.
        ids_lot = set()
        for appel in appels:
            if not isinstance(appel, dict) or appel.get("type") != "function":
                raise RuntimeError("Type d'outil inattendu.")
            identifiant = appel.get("id")
            if (not isinstance(identifiant, str) or not identifiant.strip()
                    or identifiant in ids_utilises or identifiant in ids_lot):
                raise RuntimeError("Identifiant d'appel absent ou duplique.")
            if not isinstance(appel.get("function"), dict):
                raise RuntimeError("Description de fonction absente.")
            fonction = appel["function"]
            if (not isinstance(fonction.get("name"), str)
                    or not isinstance(fonction.get("arguments"), str)):
                raise RuntimeError("Appel de fonction mal forme.")
            ids_lot.add(identifiant)

        # Conserver aussi reasoning_content lorsqu'il est present.
        messages.append(message)
        for appel in appels:
            fonction = appel["function"]
            resultat = executer(fonction.get("name"), fonction.get("arguments"))
            messages.append({
                "role": "tool",
                "tool_call_id": appel["id"],
                "content": json.dumps(resultat, ensure_ascii=False),
            })
        total_appels += len(appels)
        ids_utilises.update(ids_lot)

    raise RuntimeError("Aucune reponse finale dans le budget autorise.")


if __name__ == "__main__":
    cle = os.environ.get("DEEPSEEK_API_KEY", "").strip()
    if not cle:
        raise SystemExit("Definissez DEEPSEEK_API_KEY dans l'environnement.")
    try:
        with httpx.Client(timeout=httpx.Timeout(45.0, connect=5.0)) as client:
            print(dialoguer(client, cle, "Quel est le stock de CLAV-01 et SOUR-02 ?"))
    except RuntimeError as erreur:
        raise SystemExit(str(erreur)) from None

Exécuter et comprendre le résultat

Lorsque la variable d’environnement est configurée, lancez :

python deepseek_tools.py

Le modèle peut demander les deux consultations dans une même réponse ou dans des tours successifs. Le programme traite chaque demande et lui associe un résultat distinct. Dans le catalogue fictif, CLAV-01 possède sept unités et SOUR-02 aucune : ces nombres proviennent des données locales, pas d’une réponse observée sur l’API réelle.

Les limites de quatre requêtes modèle, six appels d’outils et 2 000 caractères d’arguments sont des choix de démonstration, non des quotas imposés par DeepSeek. Le programme refuse une nouvelle exécution lorsqu’il ne reste plus de requête disponible pour en transmettre le résultat au modèle.

Vérification locale : la syntaxe et 54 tests ont été exécutés avec Python 3.13.5 et HTTPX 0.28.1. Ils couvrent notamment la validation, plusieurs appels, la conservation de l’historique, les identifiants dupliqués, les réponses incomplètes, les budgets et les erreurs simulées. Ces tests ne mesurent ni la qualité du modèle ni son comportement réel sur le réseau.

Pourquoi valider les arguments avant l’exécution ?

Le champ function.arguments contient une chaîne JSON. La référence Chat Completions avertit que les arguments peuvent être invalides ou contenir des paramètres non définis. Leur présence ne suffit donc pas à autoriser l’appel.

Dans l’exemple, la validation comporte trois niveaux : le nom doit appartenir à FONCTIONS, les données doivent former un objet possédant uniquement reference, et cette référence doit respecter le format prévu. Une référence correctement formée mais absente du catalogue reçoit une erreur distincte.

Le schéma annonce les attentes avec required et additionalProperties: false, dont le rôle est expliqué dans la référence JSON Schema sur les objets. Le programme applique ses propres vérifications avant l’exécution ; il ne considère pas la seule déclaration du schéma comme une protection suffisante.

La limite de taille et le rejet des clés JSON dupliquées rendent également le comportement explicite. La documentation Python recommande de limiter les données JSON non fiables à analyser et expose object_pairs_hook pour personnaliser leur décodage.

Enfin, une erreur d’outil n’est jamais remplacée par une valeur plausible. Elle est renvoyée sous forme contrôlée avec ok: false. Cela permet au modèle de demander une précision ou de signaler l’échec, sans inventer un stock.

Les identifiants et la fin de génération comptent aussi

Chaque message de résultat doit reprendre l’identifiant de l’appel concerné dans tool_call_id. Deux demandes adressées à la même fonction restent deux appels distincts. Le code vérifie les identifiants du lot avant de commencer les exécutions.

Dans la référence de réponse Chat Completions, finish_reason="tool_calls" indique une demande d’outils, tandis que length signale une limite atteinte. Le programme n’exécute pas les arguments d’une génération interrompue, même si un fragment semble analysable.

Ces contrôles sécurisent le chemin d’exécution, mais ne prouvent pas que la reformulation finale du modèle est exacte. Avant d’afficher une donnée importante à un client, vérifiez aussi sa cohérence avec le résultat de la fonction.

Thinking : conserver le message assistant complet

La documentation Thinking Mode indique que le raisonnement est activé par défaut. Le tutoriel choisit explicitement le mode sans raisonnement ; pour comparer avec Thinking, appelez dialoguer avec thinking=True et adaptez le budget de sortie à la tâche.

Lorsqu’une requête porte le paramètre tools, DeepSeek demande de retransmettre intégralement les champs reasoning_content des tours précédents, y compris ceux sans appel d’outil. Leur omission peut provoquer une erreur 400.

Schéma officiel des appels d'outils DeepSeek en mode Thinking

La ligne messages.append(message) conserve le message retourné au lieu de reconstruire uniquement son texte. Ne supprimez donc pas reasoning_content pendant une conversion de l’historique.

Le programme traite une question avec un historique en mémoire. Pour une conversation réutilisée entre plusieurs messages utilisateur, conservez aussi les réponses finales précédentes et leurs champs requis. Protégez cet historique ; ne publiez pas le raisonnement brut dans la réponse utilisateur ou les journaux ordinaires.

Avec le SDK OpenAI, le guide DeepSeek précise que le réglage thinking se transmet via extra_body. Avec HTTP direct, comme ici, il figure dans le corps JSON.

Comment utiliser tool_choice ?

La référence Chat Completions décrit quatre possibilités :

ValeurComportement
autoLe modèle peut répondre ou demander des outils.
noneAucun outil n’est demandé.
requiredLe modèle doit demander au moins un outil.
{"type":"function","function":{"name":"consulter_produit"}}La fonction nommée est imposée.
Options tool_choice auto, none et required et restrictions du mode Thinking dans DeepSeek

Dans Chat Completions, required et le choix d’une fonction nommée ne sont actuellement pas compatibles avec Thinking : l’API renvoie 400. Désactivez ce mode avant de les utiliser. N’étendez pas cette règle à toutes les autres interfaces.

Pour notre démonstration, auto laisse notamment le modèle demander une référence manquante. Si vous forcez un outil au premier tour, revenez à un choix permettant une réponse finale après son résultat. Sinon, vous risquez d’entretenir une succession d’appels.

Imposer une fonction ne remplace jamais la vérification de ses arguments ou des permissions.

Strict mode : contraindre le schéma sans supprimer les contrôles

Le mode strict des Tool Calls reste documenté comme une fonctionnalité Beta. Il nécessite la base https://api.deepseek.com/beta, strict: true pour chaque fonction et un schéma compatible.

Dans le programme HTTP précédent, les modifications correspondantes sont :

URL = "https://api.deepseek.com/beta/chat/completions"
OUTILS[0]["function"]["strict"] = True

Chaque objet du schéma doit déclarer toutes ses propriétés dans required et définir additionalProperties: false. Le sous-ensemble documenté ne prend notamment pas en charge minLength, maxLength, minItems et maxItems.

Configuration du mode Strict Beta pour les appels d'outils DeepSeek

Cette contrainte de format ne confirme ni l’existence d’une référence ni le droit d’un utilisateur à consulter une ressource. Conservez la validation applicative et les contrôles métier. Ne confondez pas non plus le schéma des arguments d’outil avec celui d’une réponse finale structurée.

Appels multiples, streaming et Responses API

Plusieurs demandes ne signifient pas exécution parallèle obligatoire

Notre boucle parcourt tous les appels reçus et les exécute séquentiellement. C’est un choix simple pour un catalogue local. Avant de paralléliser des opérations, vérifiez leur indépendance : deux lectures peuvent convenir, mais une consultation dépendant d’une création préalable doit attendre son résultat.

Le contrôle de concurrence appartient à votre application. Gardez une limite sur les appels et la durée des opérations, même si le modèle continue à proposer de nouvelles actions.

En streaming, attendre les arguments complets

Le format streaming de Chat Completions peut transmettre les arguments par fragments dans les deltas d’outils. Regroupez-les par appel, attendez la fin appropriée, puis validez le JSON complet. N’exécutez pas chaque fragment dès sa réception.

Une interface peut afficher la progression sans déclencher l’action. Séparez cette présentation du moment où le backend accepte effectivement l’exécution.

Responses API utilise d’autres messages

Dans la Responses API, les demandes utilisent notamment function_call et leurs résultats function_call_output, reliés par call_id. Ce ne sont pas les messages role: "tool" du tutoriel Chat Completions.

Cette API est documentée comme sans état : l’application doit renvoyer l’historique nécessaire dans input. Ne supposez pas que previous_response_id restaure une conversation stockée.

Le guide de compatibilité indique également que parallel_tool_calls et max_tool_calls sont ignorés. Une limite d’exécution doit donc être appliquée dans votre programme, pas seulement déclarée dans ces paramètres.

Ce même guide décrit un cas distinct des fonctions personnalisées : web_search est exécuté côté serveur avec deepseek-v4-pro, mais ignoré avec deepseek-flash. Les outils intégrés tels que file_search, code_interpreter ou mcp ne doivent pas être considérés comme disponibles simplement parce que les fonctions personnalisées sont prises en charge.

Erreurs fréquentes : quoi vérifier en premier ?

Distinguez une erreur de requête API d’un refus de validation ou d’un échec de votre fonction. Le tableau suivant associe chaque symptôme à une vérification concrète.

SymptômeVérification et correction
Aucun outil n’est demandé.Vérifiez la présence de tools, le choix configuré et la description. Une réponse directe ou une question de clarification peut être normale avec auto.
Les arguments sont invalides.Rejetez l’exécution. Renvoyez une erreur contrôlée ou arrêtez le traitement ; ne devinez pas les valeurs manquantes.
HTTP 400 après activation de Thinking.Examinez l’historique et les restrictions de tool_choice. L’absence de raisonnement n’est pas la seule cause possible.
Un résultat d’outil semble manquer.Vérifiez qu’un résultat correspond à chaque appel du lot et que tous les identifiants sont conservés.
Le schéma strict est refusé.Contrôlez les propriétés obligatoires, additionalProperties et les mots-clés effectivement acceptés.
Le même outil est demandé en boucle.Vérifiez le retour de son résultat, un éventuel choix forcé et le budget d’appels. Arrêtez proprement au plafond.
La génération est coupée.N’exécutez pas les arguments incomplets. Réexaminez la limite de sortie et la taille de la tâche.
Un framework fonctionne autrement que l’appel HTTP.Comparez le corps réellement envoyé et les champs d’historique conservés. Un adaptateur peut masquer une incompatibilité.

Pour une erreur HTTP de compte ou de service, consultez aussi les codes d’erreur officiels. Une erreur 401 relève de l’authentification et une erreur 402 du solde : modifier le schéma de votre fonction ne les résoudra pas.

Sécuriser les outils avant de connecter des données réelles

L’autorisation doit rester dans le backend

Le catalogue de démonstration est fictif et public. Pour une commande client ou un document privé, récupérez l’identité depuis la session authentifiée, puis vérifiez l’accès à la ressource dans le service métier. Un identifiant proposé par le modèle ne doit pas permettre de changer de client ou d’organisation.

Les recommandations OWASP sur l’autonomie excessive des agents préconisent notamment de limiter les fonctions, les permissions et l’autonomie. Une fonction de consultation ne devrait pas utiliser un compte capable de supprimer toute une base.

Exposez des opérations étroites et imposez une confirmation appropriée avant une action sensible. Évitez les outils génériques acceptant une commande shell, une requête SQL arbitraire ou une destination réseau sans contrôle.

Un résultat d’outil peut contenir des instructions malveillantes

Une page récupérée, un document ou un ticket peut contenir une injection de prompt. OWASP distingue notamment les injections indirectes, où des instructions proviennent d’une source externe traitée par le modèle.

Ces contenus doivent rester des données, pas devenir des autorisations. Une instruction trouvée dans un document ne doit jamais suffire à divulguer un secret ou à déclencher une suppression. Le message système aide à cadrer le comportement, mais les protections effectives doivent aussi être appliquées dans les outils et les services.

Éviter les doublons lors d’une nouvelle tentative

Le programme ne relance pas automatiquement une requête échouée. Pour une opération en écriture, un timeout peut laisser le résultat incertain : l’action a pu réussir sans que sa confirmation soit arrivée.

La stratégie d’idempotence décrite dans l’Amazon Builders’ Library consiste à organiser les opérations pour éviter de reproduire un effet lorsqu’une demande est répétée. Appliquez ce principe dans votre service métier : identifiant d’opération durable, résultat mémorisé et contrôle des répétitions.

tool_call_id relie un appel à son résultat ; ce n’est pas une garantie générale d’exécution unique. Le modèle peut proposer une nouvelle demande identique avec un autre identifiant. Vérifiez l’état de l’opération avant de renvoyer un e-mail, créer un ticket ou déclencher un remboursement.

Quels tests et quelles limites prévoir en production ?

Les simulations permettent de vérifier votre orchestration sans facturer chaque essai au fournisseur. Elles ne remplacent pas une évaluation ultérieure sur les modèles réellement utilisés.

  • Protocole : testez une réponse sans outil, un appel, plusieurs appels et plusieurs tours, avec les identifiants et champs d’historique attendus.
  • Validation : testez un nom inconnu, un JSON mal formé, des champs supplémentaires, une référence absente et des données incompatibles.
  • Arrêt sûr : testez les générations tronquées, les erreurs réseau, les plafonds d’appels et les demandes répétées.
  • Accès métier : vérifiez qu’un utilisateur ne peut pas consulter une ressource appartenant à un autre compte et qu’une action sensible ne s’exécute pas sans les validations prévues.

Ajoutez une limite de durée globale et des délais propres aux outils distants. Les timeouts HTTPX contrôlent différentes phases réseau ; ils ne constituent pas à eux seuls un plafond de durée pour l’ensemble d’un workflow.

La facturation des modèles dépend des jetons selon la grille DeepSeek. Une boucle peut comporter plusieurs requêtes, avec définitions d’outils, historique et résultats retransmis. Les services externes peuvent ajouter leurs propres frais. Mesurez donc le coût d’une tâche terminée, pas seulement celui de sa première requête.

Si vous remplacez HTTPX par le SDK OpenAI, vérifiez sa politique de reprises : sa documentation Python décrit des retries automatiques configurables avec max_retries. Évitez de superposer plusieurs mécanismes qui multiplieraient les appels sans le rendre visible.

La méthode à retenir

Pour commencer avec les appels d’outils DeepSeek, exposez une fonction simple en lecture seule, définissez ses paramètres et implémentez le cycle jusqu’à la réponse finale. Vérifiez les arguments avant l’exécution, conservez les messages nécessaires et associez chaque résultat au bon identifiant.

Ajoutez ensuite Thinking, strict mode, streaming ou opérations métier uniquement lorsque leurs contraintes sont comprises et testées. Le modèle choisit une opération ; votre application décide si elle peut être exécutée et contrôle ce qui s’est réellement passé.