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) ettext(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)