Aller au contenu principal

Les Thinking Chunks : Anatomie d'une Réponse

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 avec un type spécifique. Maîtriser cette structure est essentiel pour intégrer correctement le raisonnement dans vos applications.

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."
          }
        ]
      }
    }
  ]
}

Les deux types de chunks

  • thinking : contient le raisonnement interne du modèle. C’est le “brouillon mental” où il explore, vérifie et structure sa réflexion
  • text : contient la réponse finale, claire et synthétique, destinée à l’utilisateur final

Parser les thinking chunks en Python

Voici comment extraire et traiter correctement les deux types de chunks :

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

Selon votre application, vous pouvez choisir différentes stratégies d’affichage :

Stratégie 1 : Afficher uniquement la réponse finale

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 ""

Cette approche est idéale pour les chatbots grand public où la simplicité prime.

Stratégie 2 : Afficher le raisonnement dans un accordéon

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"])
    }

Vous pouvez ensuite afficher le raisonnement dans un composant dépliable (accordéon, details/summary) que l’utilisateur peut consulter s’il le souhaite.

Stratégie 3 : Logger le raisonnement pour le debugging

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. Vous devez gérer la transition entre les types :

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

Quand reasoning_effort="none", le champ content ne contient qu’un seul chunk de type text. Votre code doit gérer les deux cas :

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

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)