ITSkillsCenter
Intelligence Artificielle

Exploiter le raisonnement de DeepSeek et produire du JSON fiable

14 دقائق للقراءة

Un assistant qui répond en texte libre, c’est bien pour discuter. Mais dès qu’on veut brancher le résultat sur un autre système — une base de données, un outil de tickets, un tableau de bord — le texte libre devient un cauchemar à parser. Ce qu’il faut, ce sont des données structurées et fiables. Dans ce guide, vous allez apprendre à faire raisonner DeepSeek sur des cas ambigus, puis à lui faire produire du JSON propre et exploitable. Le fil conducteur : transformer un message de support écrit à la va-vite en un ticket structuré, avec catégorie, priorité et composant concerné.

📍 Guide principal de la série : DeepSeek : modèles, API et déploiement local. Si vous n’avez pas encore configuré l’API, lisez d’abord les premiers pas en Python.

🎯 Ce que vous allez apprendre

  • Distinguer le mode raisonnement (thinking) du mode direct, et savoir quand utiliser chacun.
  • Activer le raisonnement et lire la chaîne de pensée du modèle.
  • Éviter l’erreur la plus courante du mode raisonnement.
  • Forcer une sortie JSON valide avec le bon paramètre.
  • Valider et fiabiliser ce JSON côté code.
  • Combiner raisonnement et extraction pour traiter les cas ambigus.

🛠️ Ce que vous allez construire

Un extracteur de tickets : une fonction extraire_ticket(message) qui prend le texte brut d’une demande utilisateur et renvoie un dictionnaire Python prêt à insérer en base, avec les champs categorie, priorite, composant et resume. Le tout résistant aux formulations vagues et aux réponses malformées.

Prérequis

  • Une clé API DeepSeek fonctionnelle et le SDK openai installé.
  • Savoir manipuler des dictionnaires et le module json en Python.
  • Test express : si vous savez faire un json.loads() sur une chaîne et attraper l’exception, vous êtes prêt.
  • ⏱️ Temps estimé : environ 40 minutes.

Étape 1 — Raisonnement ou réponse directe ?

DeepSeek-V4 propose deux régimes de fonctionnement. En mode non-thinking, le modèle répond directement, vite et à moindre coût : parfait pour une extraction simple. En mode thinking, il déroule d’abord un raisonnement interne avant de conclure, ce qui améliore nettement la qualité sur les cas difficiles — au prix de plus de jetons et de latence. La règle pratique : réservez le raisonnement aux situations réellement ambiguës, et utilisez le mode direct pour le reste.

Pour notre extracteur, classer la priorité d’un ticket est typiquement le genre de jugement qui mérite un raisonnement, tandis que recopier un composant cité explicitement n’en a pas besoin. On va donc voir les deux.

Étape 2 — Activer le raisonnement et lire la chaîne de pensée

On active le mode thinking en passant un paramètre dédié, et on choisit l’intensité du raisonnement. La chaîne de pensée revient alors dans un champ séparé de la réponse finale, ce qui permet de l’inspecter sans la mélanger au résultat.

import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["DEEPSEEK_API_KEY"],
    base_url="https://api.deepseek.com",
)

r = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content": "Le site est totalement inaccessible depuis ce matin pour tous les clients. Quelle priorite ?"}],
    reasoning_effort="high",
    extra_body={"thinking": {"type": "enabled"}},
)

print("RAISONNEMENT:", r.choices[0].message.reasoning_content)
print("REPONSE:", r.choices[0].message.content)

Le paramètre extra_body active explicitement le mode thinking, et reasoning_effort en règle l’intensité (ici élevée). La réponse expose deux champs distincts : reasoning_content contient le cheminement — le modèle pèse l’impact, le nombre d’utilisateurs touchés, l’urgence — tandis que content porte la conclusion. Sur un cas « site totalement inaccessible pour tous les clients », le raisonnement doit logiquement conduire à une priorité critique.

Point d’étape — Vous obtenez deux champs séparés. Pour vérifier : reasoning_content est non vide et décrit une analyse, tandis que content donne une réponse courte. Si reasoning_content est vide, c’est que le mode thinking n’a pas été activé.

Étape 3 — La règle d’or à ne jamais oublier

Le raisonnement revient dans un champ séparé, et la manière de le gérer dépend du contexte. Dans une conversation classique à plusieurs tours, on n’ajoute à l’historique que le content de la réponse : le reasoning_content du tour précédent n’est pas nécessaire, et l’API l’ignore s’il est présent. Le modèle régénère sa réflexion à chaque appel.

historique = [{"role": "user", "content": "Le VPN tombe toutes les 5 minutes."}]

r = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=historique,
    reasoning_effort="high",
    extra_body={"thinking": {"type": "enabled"}},
)

# CORRECT : on n'ajoute que content, jamais reasoning_content
historique.append({"role": "assistant", "content": r.choices[0].message.content})

L’exception à connaître concerne les appels d’outils. Lorsque le modèle déclenche un outil en mode raisonnement, vous devez au contraire renvoyer le reasoning_content avec le message d’assistant correspondant ; si vous l’omettez, l’API répond par une erreur 400. La règle tient en une phrase : en conversation simple, on conserve uniquement la réponse ; dans un flux d’outils, on préserve aussi le raisonnement attendu par l’API.

Étape 4 — Forcer une sortie JSON valide

Passons à l’extraction structurée. Pour qu’un programme exploite la réponse, on ne peut pas se contenter d’espérer que le modèle « réponde en JSON » : il faut le lui imposer. DeepSeek offre un mode JSON dédié, activé par le paramètre response_format. Trois conditions doivent être réunies pour qu’il fonctionne de façon fiable.

import json

messages = [
    {"role": "system", "content": (
        "Tu extrais un ticket de support au format json avec les champs "
        "categorie, priorite, composant, resume. Exemple de sortie : "
        '{"categorie": "reseau", "priorite": "haute", "composant": "VPN", "resume": "..."}'
    )},
    {"role": "user", "content": "Impossible de me connecter au VPN depuis hier, c'est bloquant pour mon teletravail."},
]

r = client.chat.completions.create(
    model="deepseek-v4-flash",
    messages=messages,
    response_format={"type": "json_object"},
    extra_body={"thinking": {"type": "disabled"}},
    max_tokens=300,
)

ticket = json.loads(r.choices[0].message.content)
print(ticket["priorite"], "-", ticket["composant"])

Les trois conditions sont visibles dans ce code. Premièrement, response_format vaut {"type": "json_object"}. Deuxièmement, le mot « json » apparaît dans la consigne, accompagné d’un exemple de structure — sans cela, le mode peut échouer. Troisièmement, on fixe max_tokens pour éviter qu’une réponse longue soit tronquée et casse le JSON. On utilise ici deepseek-v4-flash en désactivant explicitement le raisonnement via extra_body : sur les modèles V4, le mode raisonnement est actif par défaut, or la sortie JSON s’obtient en mode direct. Sans cette désactivation, l’extraction structurée devient peu fiable.

Étape 5 — Valider plutôt que faire confiance

Un modèle de langage reste probabiliste : il peut, rarement, renvoyer un JSON incomplet, un champ manquant, ou une réponse vide. Un code de production ne fait jamais confiance aveuglément à la sortie. On valide donc systématiquement, et on réessaie en cas de pépin.

CONSIGNE_JSON = (
    "Tu extrais un ticket de support au format json avec les champs "
    "categorie, priorite, composant, resume. La priorite est l'une de : "
    "basse, moyenne, haute, critique. Exemple : "
    '{"categorie": "reseau", "priorite": "haute", "composant": "VPN", "resume": "acces VPN bloque"}'
)
CHAMPS = {"categorie", "priorite", "composant", "resume"}

def extraire_valide(message, tentatives=2):
    for _ in range(tentatives):
        r = client.chat.completions.create(
            model="deepseek-v4-flash",
            messages=[
                {"role": "system", "content": CONSIGNE_JSON},
                {"role": "user", "content": message},
            ],
            response_format={"type": "json_object"},
            extra_body={"thinking": {"type": "disabled"}},
            max_tokens=300,
        )
        brut = r.choices[0].message.content
        try:
            data = json.loads(brut)
        except json.JSONDecodeError:
            continue                      # JSON casse : on reessaie
        if CHAMPS.issubset(data):
            return data                   # tous les champs presents
    raise ValueError("extraction impossible apres plusieurs essais")

On vérifie deux choses : que la chaîne est bien un JSON parsable (sinon json.JSONDecodeError et nouvel essai), et que tous les champs attendus sont présents grâce à issubset. Cette double barrière transforme une sortie « généralement bonne » en sortie « garantie exploitable ou exception explicite » — la différence entre un prototype et un service fiable.

Point d’étape — Votre fonction renvoie soit un dictionnaire complet, soit une exception claire. Pour vérifier : passez-lui un message vide ; après les tentatives, elle doit lever ValueError plutôt que renvoyer un dictionnaire bancal.

Étape 6 — Combiner raisonnement et extraction

Les cas difficiles méritent le meilleur des deux mondes : un raisonnement pour trancher la priorité, puis une extraction structurée. On procède en deux temps — un premier appel en mode thinking pour évaluer la gravité, un second en mode JSON pour produire le ticket en intégrant cette évaluation.

def evaluer_priorite(message):
    r = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=[{"role": "user", "content":
            "Evalue la priorite (basse, moyenne, haute, critique) de cette demande "
            "en justifiant brievement : " + message}],
        reasoning_effort="high",
        extra_body={"thinking": {"type": "enabled"}},
    )
    return r.choices[0].message.content   # on ignore reasoning_content ici

def extraire_ticket(message):
    avis = evaluer_priorite(message)
    consigne = CONSIGNE_JSON + " Tiens compte de cette evaluation de priorite : " + avis
    r = client.chat.completions.create(
        model="deepseek-v4-flash",
        messages=[
            {"role": "system", "content": consigne},
            {"role": "user", "content": message},
        ],
        response_format={"type": "json_object"},
        extra_body={"thinking": {"type": "disabled"}},
        max_tokens=300,
    )
    return json.loads(r.choices[0].message.content)

Le premier appel raisonne et renvoie une priorité argumentée ; le second l’injecte dans sa consigne pour produire un ticket cohérent. Ce découpage coûte deux appels, mais sur les demandes ambiguës — celles où la gravité ne saute pas aux yeux — il rend l’extraction nettement plus juste. Pour les messages limpides, l’extraction directe de l’étape 5 suffit.

Étape 7 — L’extracteur complet et sa vérification

On assemble une version finale qui choisit la stratégie selon la longueur et la clarté du message, et qui retombe toujours sur une sortie validée.

messages_test = [
    "Le serveur de paie est en panne, la cloture est demain, personne ne peut travailler.",
    "Petite faute de frappe dans le pied de page du site interne.",
]

for m in messages_test:
    print(extraire_ticket(m))

En exécutant ce script, le premier message — panne bloquante avec échéance — doit ressortir en priorité critique ou haute, le second en priorité basse. Vous tenez là un composant directement branchable sur une file de tickets : entrée en texte libre, sortie en données structurées et validées.

Étape 8 — Afficher le raisonnement en direct

Sur les cas vraiment complexes, le raisonnement peut prendre plusieurs secondes. Plutôt que de laisser l’utilisateur devant un écran figé, on peut diffuser la chaîne de pensée au fil de l’eau, puis enchaîner sur la réponse. En mode streaming, DeepSeek envoie d’abord les fragments de raisonnement, puis les fragments de réponse, chacun dans son propre champ. Il suffit de les distinguer.

flux = client.chat.completions.create(
    model="deepseek-v4-pro",
    messages=[{"role": "user", "content":
        "Deux services tombent en meme temps : la messagerie et la sauvegarde. "
        "Lequel traiter en premier et pourquoi ?"}],
    reasoning_effort="high",
    extra_body={"thinking": {"type": "enabled"}},
    stream=True,
)

phase = None
for fragment in flux:
    delta = fragment.choices[0].delta
    pensee = getattr(delta, "reasoning_content", None)
    if pensee:
        if phase != "pensee":
            print("\n[reflexion] ", end="")
            phase = "pensee"
        print(pensee, end="", flush=True)
    elif delta.content:
        if phase != "reponse":
            print("\n[reponse] ", end="")
            phase = "reponse"
        print(delta.content, end="", flush=True)
print()

On lit chaque fragment et on regarde lequel des deux champs est rempli. Tant que reasoning_content arrive, on est en phase de réflexion ; dès que content prend le relais, on bascule en phase de réponse. La variable phase sert simplement à n’imprimer l’étiquette qu’une seule fois au changement. Côté interface, ce motif permet d’afficher une zone « réflexion en cours » repliable, suivie de la réponse définitive — l’utilisateur voit que le modèle travaille au lieu d’attendre dans le vide.

Attention toutefois : même diffusé, le raisonnement n’entre pas dans l’historique. Quand la conversation se poursuit, on accumule uniquement les fragments de content pour reconstituer la réponse à archiver, jamais ceux de reasoning_content. La règle d’or de l’étape 3 reste valable, streaming ou non.

Point d’étape — Le terminal affiche d’abord une réflexion qui se construit, puis une réponse. Pour vérifier : la transition entre les deux phases est nette, et la réponse finale reste cohérente avec le raisonnement diffusé juste avant.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
Erreur 400 lors d’un appel d’outils reasoning_content omis dans un flux avec outils Renvoyer le reasoning_content attendu ; en conversation simple, garder seulement content
Le mode JSON renvoie du texte libre Mot « json » absent de la consigne Inclure « json » et un exemple de structure
JSON tronqué en fin de réponse max_tokens trop bas ou non fixé Augmenter max_tokens
Réponse occasionnellement vide Comportement connu du mode JSON Valider et réessayer comme à l’étape 5
Raisonnement absent Mode thinking non activé Ajouter extra_body avec thinking enabled

Maîtriser le coût du raisonnement

Le mode thinking consomme bien plus de jetons que le mode direct, car la chaîne de pensée compte dans la facturation. Trois habitudes gardent la note sous contrôle. D’abord, réservez le raisonnement aux cas qui le justifient : un tri préalable sur la longueur ou des mots-clés d’urgence évite de mobiliser pro pour une coquille typographique. Ensuite, plafonnez max_tokens pour borner la longueur des réponses. Enfin, lorsque seule la conclusion vous intéresse, ne journalisez pas systématiquement le raisonnement — vous payez de toute façon sa génération, mais vous évitez d’alourdir vos journaux. Pour des volumes importants et sans contrainte de confidentialité, comparez le coût avec une exécution locale d’un modèle distillé.

✅ Récapitulatif

Vous savez désormais piloter les deux régimes de DeepSeek : le mode raisonnement pour les jugements délicats, avec sa chaîne de pensée dans reasoning_content et sa règle d’or — ne jamais la réinjecter — et le mode JSON pour des sorties structurées, avec ses trois conditions et sa validation côté code. En les combinant, vous avez bâti un extracteur de tickets robuste, qui transforme du texte humain désordonné en données propres prêtes à automatiser.

🧾 Aide-mémoire

Élément Rôle
extra_body={"thinking": {"type": "enabled"}} Activer le mode raisonnement
reasoning_effort Régler l’intensité du raisonnement
message.reasoning_content Lire la chaîne de pensée (ne jamais la réinjecter)
response_format={"type": "json_object"} Forcer une sortie JSON
Mot « json » + exemple dans la consigne Condition de fonctionnement du mode JSON
json.loads + vérification des champs Valider la sortie avant de l’utiliser

💪 À vous de jouer

Ajoutez un champ langue au ticket (la langue du message d’origine) et un champ action_suggeree. Adaptez la consigne et l’ensemble des champs attendus, puis vérifiez que la validation rejette toujours les sorties incomplètes.

Voir une solution
CHAMPS = {"categorie", "priorite", "composant", "resume", "langue", "action_suggeree"}
CONSIGNE_JSON = (
    "Tu extrais un ticket au format json avec les champs categorie, priorite, "
    "composant, resume, langue, action_suggeree. Exemple : "
    '{"categorie": "reseau", "priorite": "haute", "composant": "VPN", '
    '"resume": "...", "langue": "fr", "action_suggeree": "verifier la cle WireGuard"}'
)

La beauté de la validation par ensemble, c’est qu’il suffit d’ajouter les noms de champs à CHAMPS : la barrière s’adapte sans toucher au reste du code.

Dans la même série

Pour approfondir

FAQ

Pourquoi l’API renvoie-t-elle une erreur 400 ?
Dans un flux avec appels d’outils, parce que le reasoning_content attendu n’a pas été renvoyé. En conversation simple sans outils, contentez-vous d’ajouter le content à l’historique : le raisonnement précédent n’est pas requis.

Le mode JSON fonctionne-t-il aussi en mode raisonnement ?
L’extraction structurée est la plus fiable en mode direct. Pour les cas où le raisonnement est nécessaire, séparez les étapes : raisonner d’abord, extraire ensuite.

Comment garantir que la sortie est un JSON valide ?
En la validant côté code : tenter json.loads, vérifier la présence des champs attendus, et réessayer en cas d’échec.

Le raisonnement coûte-t-il plus cher ?
Oui, la chaîne de pensée est facturée comme des jetons de sortie. Réservez le mode thinking aux décisions qui le méritent vraiment.

مشاركة
Service ITSkillsCenter

Application mobile Android et iOS

Création d'application mobile Android et iOS. À partir de 350 000 FCFA.

Démarrer mon projet
Publicité