Aller au contenu principal

Les Thinking Chunks : Anatomie d'une Réponse

Mis à jour le 29 juillet 2026

⚠️ Modèle déprécié (mise à jour du 28 juillet 2026) : les modèles Magistral sont dépréciés par Mistral, avec des retraits échelonnés jusqu’à mi-2026. Le raisonnement est désormais intégré aux modèles généralistes (Mistral Small 4, Medium 3.5). Les concepts de ce cours restent instructifs, mais ne construisez plus de nouveau projet sur Magistral — consultez le cours « Mistral en 2026 » pour la migration.

Comprendre la structure de la réponse

Quand le raisonnement est actif avec reasoning_effort="high", la réponse du modèle n’est plus une simple chaîne de caractères. Elle devient un tableau de chunks (blocs), chacun porteur d’un type spécifique. C’est le premier point de rupture pour un code existant : une application qui faisait print(message.content) affichera désormais une liste d’objets. Maîtriser cette structure conditionne toute intégration sérieuse du raisonnement.

Anatomie d’un thinking chunk

La réponse JSON renvoyée par l’API contient un champ content qui est un tableau d’objets :

{
  "choices": [
    {
      "message": {
        "content": [
          {
            "type": "thinking",
            "text": "Analysons ce problème étape par étape.\n\n1. D'abord, je dois factoriser le polynôme x^3 - 6x^2 + 11x - 6.\n2. Je cherche les racines rationnelles possibles parmi les diviseurs de 6 : ±1, ±2, ±3, ±6.\n3. Testons x=1 : 1 - 6 + 11 - 6 = 0. Donc x=1 est une racine.\n4. Division par (x-1) : x^2 - 5x + 6 = (x-2)(x-3).\n5. Les trois racines sont donc x=1, x=2, x=3."
          },
          {
            "type": "text",
            "text": "Les solutions de l'équation sont x = 1, x = 2 et x = 3."
          }
        ]
      }
    }
  ]
}

Deux types de chunks coexistent. Le type thinking contient le raisonnement interne du modèle, ce « brouillon mental » où il explore, vérifie et structure sa réflexion — vous voyez ici les cinq étapes de factorisation. Le type text porte la réponse finale, claire et synthétique, celle qu’attend l’utilisateur : une seule phrase pour trois racines.

Parser les thinking chunks en Python

L’extraction consiste à parcourir le tableau et à aiguiller selon le type. Le SDK expose les chunks comme des objets, ce qui rend le test sur chunk.type direct :

from mistralai import Mistral

client = Mistral(api_key="VOTRE_CLE_API")

response = client.chat.complete(
    model="mistral-small-latest",
    messages=[
        {"role": "user", "content": "Quel est le plus court chemin entre 5 villes ?"}
    ],
    reasoning_effort="high"
)

# Extraire les chunks
message = response.choices[0].message
thinking_text = ""
final_text = ""

for chunk in message.content:
    if chunk.type == "thinking":
        thinking_text = chunk.text
    elif chunk.type == "text":
        final_text = chunk.text

print("=== Raisonnement ===")
print(thinking_text)
print("\n=== Réponse finale ===")
print(final_text)

Affichage dans une interface utilisateur

Une fois les deux blocs séparés, il vous reste à décider ce que l’utilisateur voit. Trois stratégies couvrent l’essentiel des situations.

La plus sobre consiste à n’exposer que la réponse finale. Un chatbot grand public gagne à masquer entièrement le brouillon : montrer au client d’une banque que le modèle a hésité entre deux interprétations n’inspire pas confiance.

def get_answer_only(response):
    """Extrait uniquement la réponse finale."""
    for chunk in response.choices[0].message.content:
        if chunk.type == "text":
            return chunk.text
    return ""

La deuxième approche conserve les deux blocs et laisse le choix à l’utilisateur. Elle convient aux outils internes et aux applications pédagogiques, où voir le cheminement fait partie de la valeur :

def format_with_toggle(response):
    """Formate la réponse avec un toggle pour le raisonnement."""
    parts = {"thinking": "", "text": ""}
    for chunk in response.choices[0].message.content:
        parts[chunk.type] = chunk.text

    return {
        "answer": parts["text"],
        "reasoning": parts["thinking"],
        "show_reasoning": bool(parts["thinking"])
    }

Le dictionnaire retourné se branche directement sur un composant dépliable — accordéon ou balise details/summary — que l’utilisateur ouvre s’il le souhaite.

La troisième stratégie ne montre rien mais garde tout. Envoyer le raisonnement dans les logs en debug et la réponse en info vous donne, le jour où un utilisateur signale une réponse aberrante, la trace exacte du chemin suivi par le modèle :

import logging

logger = logging.getLogger("reasoning")

def query_with_logging(client, question):
    """Exécute la requête et log le raisonnement."""
    response = client.chat.complete(
        model="mistral-small-latest",
        messages=[{"role": "user", "content": question}],
        reasoning_effort="high"
    )

    for chunk in response.choices[0].message.content:
        if chunk.type == "thinking":
            logger.debug(f"Raisonnement: {chunk.text}")
        elif chunk.type == "text":
            logger.info(f"Réponse: {chunk.text}")
            return chunk.text

Gestion du streaming

En mode streaming, les chunks arrivent progressivement et vous ne pouvez plus attendre la réponse complète pour trancher. La difficulté est de repérer le moment où le flux passe de la réflexion au texte final, afin d’annoncer la transition à l’interface :

response_stream = client.chat.stream(
    model="mistral-small-latest",
    messages=[{"role": "user", "content": "Analyse cette situation..."}],
    reasoning_effort="high"
)

current_type = None
for event in response_stream:
    delta = event.data.choices[0].delta
    if hasattr(delta, "content") and delta.content:
        for chunk in delta.content:
            if chunk.type != current_type:
                current_type = chunk.type
                print(f"\n--- {current_type.upper()} ---")
            if hasattr(chunk, "text"):
                print(chunk.text, end="", flush=True)

Cas sans raisonnement

Reste le piège le plus courant en production. Quand reasoning_effort="none", le champ content ne contient qu’un seul chunk de type text — et selon les cas, vous pouvez recevoir une chaîne de caractères plutôt qu’un tableau. Un extracteur robuste teste donc le type Python avant de boucler :

def safe_extract(response):
    """Extrait la réponse, avec ou sans raisonnement."""
    content = response.choices[0].message.content

    # Si content est une string (pas de chunks)
    if isinstance(content, str):
        return {"answer": content, "reasoning": None}

    # Si content est un tableau de chunks
    result = {"answer": "", "reasoning": None}
    for chunk in content:
        if chunk.type == "thinking":
            result["reasoning"] = chunk.text
        elif chunk.type == "text":
            result["answer"] = chunk.text
    return result

Adoptez cette fonction dès le prototype : elle vous évitera un TypeError le jour où un routeur basculera une requête en mode "none" sans prévenir le reste du code.

Points clés à retenir

  • La réponse avec raisonnement contient un tableau de chunks, pas une simple string
  • Deux types : thinking (réflexion interne) et text (réponse finale)
  • Adaptez l’affichage à votre contexte : masquer, déplier, ou logger le raisonnement
  • En streaming, gérez la transition entre les types de chunks
  • Prévoyez toujours le cas où le raisonnement est absent (mode “none” ou erreur)