Les thinking chunks : lire la réflexion du modèle
Sur cette page
Leçon réécrite le 2 novembre 2026 d’après la section Handling thinking chunks de la documentation Mistral. La version précédente lisait la réflexion dans un champ
textdu fragment de réflexion : ce champ n’existe pas, la réflexion est une liste de fragments texte.
Deux formes de réponse
Avec reasoning_effort="high", le champ message.content d’une réponse n’est plus une chaîne mais une liste de fragments. Avec reasoning_effort="none", c’est de nouveau une simple chaîne, sans trace de réflexion. Un code robuste accepte toujours les deux formes : c’est la règle de base de cette leçon.
Anatomie d’un fragment de réflexion
Deux types de fragments apparaissent dans la liste :
ThinkChunk(type: "thinking") porte la trace de réflexion. Son champthinkingest lui-même une liste de fragmentsTextChunk;TextChunk(type: "text") porte la réponse finale, dans son champtext.
Voici, tirée de l’exemple officiel, la forme d’une réponse de Mistral Small 4 (textes abrégés) :
{
"model": "mistral-small-2603",
"usage": {"prompt_tokens": 77, "completion_tokens": 1114, "total_tokens": 1191},
"choices": [{
"finish_reason": "stop",
"message": {
"role": "assistant",
"content": [
{"type": "thinking", "thinking": [
{"type": "text", "text": "Okay, let's try to figure out John's age..."}
]},
{"type": "text", "text": "Let's denote the ages of the sisters as follows..."}
]
}
}]
}
Le modèle d’objets du SDK Python suit exactement cette structure : ThinkChunk.thinking contient les fragments de réflexion, et chaque TextChunk expose son contenu dans .text.
Parser la réponse en Python
Le code officiel distingue les fragments avec isinstance, après avoir écarté le cas de la chaîne simple :
from mistralai.client.models import TextChunk, ThinkChunk
content = chat_response.choices[0].message.content
if isinstance(content, str): # reasoning_effort="none"
print(content)
else:
for chunk in content:
if isinstance(chunk, ThinkChunk):
print("--- réflexion ---")
for inner in chunk.thinking:
if isinstance(inner, TextChunk):
print(inner.text)
print("--- fin de la réflexion ---")
elif isinstance(chunk, TextChunk):
print(chunk.text)
Dans une application, on préfère une fonction qui renvoie séparément la réflexion et la réponse. Nous l’appellerons separer et la réutiliserons dans la suite du cours :
from mistralai.client.models import TextChunk, ThinkChunk
def separer(content):
"""Renvoie (réflexion, réponse) pour un contenu en chaîne ou en liste de fragments."""
if isinstance(content, str):
return "", content
reflexion, reponse = [], []
for chunk in content or []:
if isinstance(chunk, ThinkChunk):
reflexion.extend(i.text for i in chunk.thinking if isinstance(i, TextChunk))
elif isinstance(chunk, TextChunk):
reponse.append(chunk.text)
return "".join(reflexion), "".join(reponse)
reflexion, reponse = separer(chat_response.choices[0].message.content)
Afficher la réflexion dans une interface
Séparer les deux parties vous laisse décider de ce que voit l’utilisateur. Trois partis pris se rencontrent :
- masquer la réflexion et n’afficher que la réponse, pour un assistant grand public ;
- replier la réflexion dans un bloc que l’utilisateur ouvre s’il le souhaite, pour un outil d’expert ;
- montrer la réflexion en entier, pour un tuteur pédagogique où la démarche compte autant que le résultat.
def rendu_html(content, montrer_reflexion=False):
reflexion, reponse = separer(content)
html = f"<div class='reponse'>{reponse}</div>"
if montrer_reflexion and reflexion:
html = (
"<details><summary>Voir la réflexion du modèle</summary>"
f"<pre>{reflexion}</pre></details>" + html
)
return html
Dans un vrai projet, échappez le texte avant de l’insérer dans du HTML (par exemple avec html.escape) : la réflexion comme la réponse sont du texte généré, pas du balisage de confiance.
Le streaming en trois phases
En streaming, delta.content change de forme au fil de la réponse :
- phase de réflexion :
delta.contentest une liste qui contient unThinkChunk; - transition : une seule liste qui contient à la fois le
ThinkChunkde clôture et le premierTextChunk; - phase de réponse :
delta.contentest une simple chaîne.
Le code suivant, adapté de l’exemple officiel, affiche les deux phases en suivant un indicateur d’état :
import os
from mistralai.client import Mistral
from mistralai.client.models import TextChunk, ThinkChunk
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
en_reflexion = False
for event in client.chat.stream(
model="mistral-large-4-0",
messages=[{"role": "user", "content": "Combien font 17 × 23 ?"}],
reasoning_effort="high",
):
delta = event.data.choices[0].delta.content
if not delta:
continue
if isinstance(delta, str): # phase de réponse
if en_reflexion:
print("\n--- fin de la réflexion ---")
en_reflexion = False
print(delta, end="", flush=True)
continue
for chunk in delta: # phase de réflexion et transition
if isinstance(chunk, ThinkChunk):
if not en_reflexion:
print("--- réflexion ---")
en_reflexion = True
for inner in chunk.thinking:
if isinstance(inner, TextChunk):
print(inner.text, end="", flush=True)
elif isinstance(chunk, TextChunk):
if en_reflexion:
print("\n--- fin de la réflexion ---")
en_reflexion = False
print(chunk.text, end="", flush=True)
Le streaming change l’expérience : au lieu d’attendre la fin d’une longue réflexion devant un écran figé, l’utilisateur voit le modèle travailler, ou au moins un indicateur d’activité si vous choisissez de masquer la réflexion.
Points clés à retenir
contentest une chaîne avecnone, une liste de fragments avechigh: acceptez toujours les deuxThinkChunk.thinkingest une liste deTextChunk; la réponse finale est dans unTextChunkde premier niveau- Une fonction
separerqui renvoie (réflexion, réponse) simplifie tout le reste du code - En streaming : liste de réflexion, transition mixte, puis chaînes de réponse
- Échappez la réflexion comme la réponse avant de les afficher