Appels parallèles et séquentiels
Appels parallèles et séquentiels
Le modèle peut émettre plusieurs appels de fonction en une seule réponse, et il le fait de lui-même dès qu’il identifie des besoins indépendants. Ce n’est pas une optimisation cosmétique : trois appels de deux secondes exécutés ensemble coûtent deux secondes, enchaînés ils en coûtent six.
Le vrai sujet de cette leçon n’est donc pas comment paralléliser — le modèle s’en charge — mais comment reconnaître les cas où il ne faut pas. Deux appels sont indépendants quand l’ordre d’exécution ne change rien au résultat. Dès qu’une opération lit ce qu’une autre écrit, ou qu’un paramètre doit venir d’un appel précédent, le parallélisme devient un bug difficile à reproduire.
Appels parallèles
Rien n’est à activer : le comportement est celui par défaut. Une question portant sur trois villes produit trois function_call dans la même réponse, à vous de les exécuter. Notez que le modèle raisonne sur le sens des fonctions, tel que vos descriptions le lui donnent — c’est encore la description qui décide, comme dans presque tout ce cours.
from openai import OpenAI
import json
import asyncio
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_meteo",
"description": "Obtenir la météo actuelle d'une ville",
"parameters": {
"type": "object",
"properties": {
"ville": {"type": "string"}
},
"required": ["ville"],
"additionalProperties": False
},
"strict": True
},
{
"type": "function",
"name": "get_taux_change",
"description": "Obtenir le taux de change entre deux devises",
"parameters": {
"type": "object",
"properties": {
"source": {"type": "string"},
"cible": {"type": "string"}
},
"required": ["source", "cible"],
"additionalProperties": False
},
"strict": True
}
]
response = client.responses.create(
model="gpt-5.6-terra",
input="Quel temps fait-il a Paris et Berlin, "
"et quel est le taux EUR/USD ?",
tools=tools
)
# Le modèle émet 3 appels en parallèle
appels = [item for item in response.output if item.type == "function_call"]
print(f"Nombre d'appels : {len(appels)}")
# => Nombre d'appels : 3
Exécuter les appels en parallèle côté client
Recevoir trois appels d’un coup ne sert à rien si votre code les exécute l’un après l’autre : tout le gain se perd côté client. asyncio.gather est le minimum ; ajoutez-y deux précautions pour la production — un plafond de concurrence, afin de ne pas ouvrir cinquante connexions simultanées vers un service qui n’en supporte pas dix, et une gestion d’erreur par appel, pour qu’un échec isolé ne fasse pas tomber les autres résultats.
async def executer_fonction(name: str, arguments: str) -> dict:
args = json.loads(arguments)
if name == "get_meteo":
return await api_meteo(args["ville"])
elif name == "get_taux_change":
return await api_change(args["source"], args["cible"])
async def traiter_appels_paralleles(appels):
taches = [
executer_fonction(appel.name, appel.arguments)
for appel in appels
]
resultats = await asyncio.gather(*taches)
return [
{
"type": "function_call_output",
"call_id": appel.call_id,
"output": json.dumps(resultat)
}
for appel, resultat in zip(appels, resultats)
]
# Executer
resultats = asyncio.run(traiter_appels_paralleles(appels))
# Renvoyer au modele
response_finale = client.responses.create(
model="gpt-5.6-terra",
input=response.output + resultats,
tools=tools
)
print(response_finale.output_text)
Désactiver les appels parallèles
parallel_tool_calls=False force le modèle à n’émettre qu’un appel par réponse. C’est un réglage à utiliser sciemment, car il coûte en latence ce qu’il apporte en sûreté — réservez-le aux fonctions qui écrivent, modifient ou déclenchent quelque chose d’externe.
response = client.responses.create(
model="gpt-5.6-terra",
input="Traite ces trois commandes",
tools=tools,
parallel_tool_calls=False # Un seul appel par reponse
)
Cas d’usage pour désactiver le parallèle :
- Opérations qui doivent s’exécuter dans un ordre précis
- Fonctions avec des effets de bord qui interfèrent entre elles
- Quand le résultat d’un appel conditionne le suivant
Appels séquentiels avec boucle
Le séquentiel ne se règle pas par un paramètre : il découle de la boucle. Vous exécutez l’appel, vous renvoyez le résultat, et le modèle décide de la suite en fonction de ce qu’il vient d’apprendre. C’est précisément ce qui distingue un enchaînement d’un plan figé — si l’étape 2 révèle que l’étape 3 est inutile, le modèle ne l’appellera pas.
def boucle_function_calling(prompt: str, tools: list, max_tours: int = 10):
"""Boucle complete de function calling séquentiel."""
messages_input = prompt
tour = 0
while tour < max_tours:
response = client.responses.create(
model="gpt-5.6-terra",
input=messages_input,
tools=tools,
parallel_tool_calls=False
)
# Vérifier s'il y a des appels de fonction
appels = [i for i in response.output if i.type == "function_call"]
if not appels:
# Le modèle a fini, il répond en texte
return response.output_text
# Executer chaque appel
resultats = []
for appel in appels:
resultat = executer_fonction_sync(appel.name, appel.arguments)
resultats.append({
"type": "function_call_output",
"call_id": appel.call_id,
"output": json.dumps(resultat)
})
# Préparer le prochain tour
messages_input = response.output + resultats
tour += 1
return "Limite de tours atteinte"
Exemple concret : pipeline de traitement
Un cas typique où le séquentiel s’impose, parce que chaque étape consomme ce que la précédente a produit :
tools_pipeline = [
{
"type": "function",
"name": "valider_commande",
"description": "Vérifier la validité d'une commande (stock, prix, client)",
"parameters": {
"type": "object",
"properties": {
"commande_id": {"type": "string"}
},
"required": ["commande_id"],
"additionalProperties": False
},
"strict": True
},
{
"type": "function",
"name": "calculer_livraison",
"description": "Calculer les frais et délais de livraison. "
"Nécessite une commande validée.",
"parameters": {
"type": "object",
"properties": {
"commande_id": {"type": "string"},
"mode": {"type": "string", "enum": ["standard", "express"]}
},
"required": ["commande_id", "mode"],
"additionalProperties": False
},
"strict": True
},
{
"type": "function",
"name": "confirmer_commande",
"description": "Confirmer la commande et déclencher le paiement. "
"Necessite validation et calcul livraison prealables.",
"parameters": {
"type": "object",
"properties": {
"commande_id": {"type": "string"},
"livraison_id": {"type": "string"}
},
"required": ["commande_id", "livraison_id"],
"additionalProperties": False
},
"strict": True
}
]
# Le modèle appellera naturellement dans l'ordre :
# 1. valider_commande
# 2. calculer_livraison (avec le résultat de l'etape 1)
# 3. confirmer_commande (avec les résultats des etapes 1 et 2)
resultat = boucle_function_calling(
"Traite la commande CMD-42567 en livraison express",
tools_pipeline
)
Pattern hybride : parallèle puis séquentiel
En pratique, la plupart des workflows réels ont cette forme mixte : une phase de collecte où tout peut partir en même temps, puis une phase de traitement qui suit l’ordre des dépendances. Vous n’avez rien de particulier à coder pour cela — c’est la conséquence naturelle de la boucle, à condition que vos descriptions rendent les dépendances lisibles.
# Tour 1 : collecte parallèle de données
# Le modele appelle get_client ET get_stock en parallele
# Tour 2 : décision séquentielle basée sur les résultats
# Le modele appelle valider_commande avec les infos collectees
# Tour 3 : action finale
# Le modele appelle confirmer_commande
Ce pattern est naturel : le modèle parallélise automatiquement quand les appels sont indépendants, puis enchaîne séquentiellement quand une dépendance apparaît.
Points clés à retenir
- Le modèle parallélise automatiquement les appels indépendants
- Utilisez
asyncio.gatherpour exécuter les appels parallèles côté client - Désactivez avec
parallel_tool_calls=Falsequand l’ordre compte - Implémentez une boucle pour les workflows multi-étapes
- Les descriptions de fonctions guident le modèle sur les dépendances entre étapes