Streaming et outils
Streaming avec function calling
Le streaming permet de recevoir la réponse du modèle progressivement, token par token. C’est essentiel pour les interfaces utilisateur réactives. Mais comment le streaming interagit-il avec le function calling ?
La règle fondamentale
Les appels de fonctions sont retournés en un seul chunk, pas streamés progressivement. C’est une différence majeure avec le texte normal, qui arrive token par token.
Concrètement :
- Le texte de la réponse est streamé progressivement
- Les tool_calls arrivent d’un bloc dans un seul événement
Cette contrainte existe pour une bonne raison : un tool_call doit être complet (nom + arguments JSON valides) pour que vous puissiez l’exécuter. Un JSON partiel serait inutilisable.
Implémenter le streaming avec outils
stream = client.chat.completions.create(
model="grok-3",
messages=messages,
tools=tools,
stream=True
)
collected_tool_calls = []
collected_content = ""
for chunk in stream:
delta = chunk.choices[0].delta
# Texte streamé
if delta.content:
collected_content += delta.content
print(delta.content, end="", flush=True)
# Tool calls (arrivent en un bloc)
if delta.tool_calls:
for tc in delta.tool_calls:
collected_tool_calls.append(tc)
# Vérifier la fin du stream
if chunk.choices[0].finish_reason == "tool_calls":
# Le modèle attend des résultats d'outils
break
elif chunk.choices[0].finish_reason == "stop":
# Réponse terminée, pas d'outil appelé
break
Reconstruction des tool_calls en streaming
Dans certains cas, les arguments d’un tool_call peuvent arriver en plusieurs morceaux dans le stream. Vous devez les reconstruire :
tool_calls_buffer = {}
for chunk in stream:
delta = chunk.choices[0].delta
if delta.tool_calls:
for tc_chunk in delta.tool_calls:
idx = tc_chunk.index
if idx not in tool_calls_buffer:
tool_calls_buffer[idx] = {
"id": tc_chunk.id,
"function": {
"name": tc_chunk.function.name or "",
"arguments": ""
}
}
if tc_chunk.function.name:
tool_calls_buffer[idx]["function"]["name"] = tc_chunk.function.name
if tc_chunk.function.arguments:
tool_calls_buffer[idx]["function"]["arguments"] += tc_chunk.function.arguments
Cette approche de buffer garantit que vous reconstruisez correctement chaque tool_call, même si les données arrivent fragmentées.
Architecture pour le streaming avec outils
Dans une application réelle, vous devez gérer deux flux simultanés :
- Affichage en temps réel : montrer le texte au fur et à mesure
- Accumulation des tool_calls : collecter les appels pour exécution
async def handle_streaming_response(stream):
content_parts = []
tool_calls = []
current_tool = None
async for chunk in stream:
delta = chunk.choices[0].delta
if delta.content:
content_parts.append(delta.content)
yield {"type": "text", "data": delta.content}
if delta.tool_calls:
for tc in delta.tool_calls:
tool_calls.append(tc)
if chunk.choices[0].finish_reason == "tool_calls":
# Exécuter les outils puis relancer
results = await execute_tools(tool_calls)
yield {"type": "tool_results", "data": results}
Latence et expérience utilisateur
Le streaming avec outils implique des pauses naturelles dans le flux :
- L’utilisateur pose sa question
- Le modèle commence à réfléchir (pas de token visible)
- Le modèle décide d’appeler un outil (vous recevez le tool_call)
- Pause — vous exécutez la fonction
- Vous renvoyez le résultat
- Le modèle génère sa réponse (streamée progressivement)
Pour l’expérience utilisateur, affichez un indicateur pendant l’étape 4 :
🔍 Recherche de la météo à Paris...
Cela évite que l’utilisateur pense que l’application est bloquée.
Server-Sent Events (SSE) pour les applications web
Si vous construisez une API web, utilisez les SSE pour relayer le stream :
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
@app.get("/chat")
async def chat(message: str):
async def event_stream():
stream = client.chat.completions.create(
model="grok-3",
messages=[{"role": "user", "content": message}],
tools=tools,
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
yield f"data: {chunk.choices[0].delta.content}\n\n"
return StreamingResponse(event_stream(), media_type="text/event-stream")
Points clés à retenir
- Les tool_calls arrivent en un seul chunk, pas streamés progressivement
- Le texte normal continue d’être streamé token par token
- Reconstruisez les tool_calls fragmentés avec un buffer indexé par
index - Gérez deux flux en parallèle : affichage texte + collecte des tool_calls
- Affichez un indicateur pendant l’exécution des outils pour l’UX
- Utilisez les SSE pour les applications web