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éflexiontext: 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) 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)