Aller au contenu principal

Combiner les outils dans un même appel

Combiner les outils dans un même appel

Jusqu’ici chaque outil a été vu isolément. En production, les questions intéressantes n’ont presque jamais cette forme : « nos ventes tiennent-elles la comparaison avec le marché ? » suppose d’aller chercher un chiffre interne, un chiffre externe, puis de les mettre en regard. Trois outils, trois sources, un seul appel.

Ce qui change alors, c’est qui décide de l’ordre. Vous ne programmez pas la séquence : vous déclarez les outils disponibles et vous décrivez le résultat attendu, et le modèle établit son plan. C’est confortable et c’est le point à surveiller — un plan implicite est un plan que vous ne pouvez pas relire. La dernière partie de cette leçon montre comment reprendre la main quand l’ordre compte vraiment.

Principe de la combinaison

Tous les outils se déclarent dans le même tableau tools, et c’est là toute la mécanique : il n’y a pas d’API de composition, seulement une liste de capacités disponibles. Le modèle lit votre demande, en déduit les étapes nécessaires et appelle ce dont il a besoin, dans l’ordre qu’il juge utile. Un outil déclaré mais inutile pour la tâche ne sera simplement pas appelé — il ne coûte rien d’autre que les quelques tokens de sa description.

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Compare nos prévisions de ventes avec les tendances du marche, "
          "puis cree un graphique de la comparaison.",
    tools=[
        {"type": "file_search", "vector_store_ids": ["vs_previsions"]},
        {"type": "web_search_preview"},
        {"type": "code_interpreter"}
    ]
)

Le modèle va :

  1. Chercher les prévisions internes via File Search
  2. Chercher les tendances du marché via Web Search
  3. Générer un graphique comparatif via Code Interpreter

Exemple complet : assistant analytique

with open("donnees_ventes.csv", "rb") as f:
    fichier = client.files.create(file=f, purpose="assistants")

tools = [
    # Recherche dans les documents internes
    {
        "type": "file_search",
        "vector_store_ids": ["vs_rapports", "vs_objectifs"],
        "max_num_results": 10
    },
    # Recherche web pour le contexte marche
    {
        "type": "web_search_preview",
        "search_context_size": "medium"
    },
    # Execution de code pour l'analyse
    {"type": "code_interpreter"},
    # Fonction pour sauvegarder le rapport
    {
        "type": "function",
        "name": "sauvegarder_rapport",
        "description": "Sauvegarder un rapport d'analyse dans le systeme documentaire",
        "parameters": {
            "type": "object",
            "properties": {
                "titre": {"type": "string"},
                "contenu": {"type": "string"},
                "categorie": {
                    "type": "string",
                    "enum": ["analyse", "prevision", "audit", "comparaison"]
                },
                "tags": {
                    "type": "array",
                    "items": {"type": "string"}
                }
            },
            "required": ["titre", "contenu", "categorie", "tags"],
            "additionalProperties": False
        },
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-5.6-terra",
    input=[{
        "role": "user",
        "content": [
            {"type": "text", "text": (
                "A partir de ce fichier de ventes : "
                "1. Analyse les tendances des 12 derniers mois "
                "2. Compare avec nos objectifs annuels (cherche dans les docs) "
                "3. Cherche les tendances du marche sur le web "
                "4. Génère un graphique comparatif "
                "5. Redige un rapport de synthese "
                "6. Sauvegarde le rapport"
            )},
            {"type": "input_file", "file_id": fichier.id}
        ]
    }],
    tools=tools,
    instructions=(
        "Tu es un analyste business senior. "
        "Utilise tous les outils a ta disposition pour produire "
        "une analyse complete et sourcée."
    )
)

Gérer la boucle multi-outils

Il y a ici une asymétrie qu’il faut avoir en tête, parce qu’elle détermine la structure de votre code. Les outils intégrés — Web Search, File Search, Code Interpreter — s’exécutent chez OpenAI : le modèle les appelle, obtient le résultat et poursuit, le tout dans un unique aller-retour. Vos fonctions personnalisées, elles, s’exécutent chez vous : le modèle ne peut que demander leur exécution, puis il rend la main et attend.

D’où la boucle ci-dessous. Tant que la réponse contient des appels de fonction, vous les exécutez, vous renvoyez les résultats, et vous relancez. Prévoyez un nombre maximal d’itérations : un modèle qui rappelle indéfiniment le même outil parce que le résultat ne le satisfait pas est un cas rare mais réel, et sans garde-fou il consomme votre budget jusqu’à épuisement.

import json

def boucle_multi_outils(prompt, tools, fichiers=None, max_tours=15):
    """Boucle de function calling avec support multi-outils."""
    content = [{"type": "text", "text": prompt}]
    if fichiers:
        for fid in fichiers:
            content.append({"type": "input_file", "file_id": fid})

    messages = [{"role": "user", "content": content}]

    for tour in range(max_tours):
        response = client.responses.create(
            model="gpt-5.6-terra",
            input=messages,
            tools=tools,
            instructions="Utilise les outils nécessaires pour répondre complètement."
        )

        # Les outils built-in (web_search, file_search, code_interpreter)
        # sont exécutés automatiquement côté serveur.
        # On ne traite que les function_call personnalises.
        appels_custom = [
            item for item in response.output
            if item.type == "function_call"
        ]

        if not appels_custom:
            # Pas d'appel personnalise : le modèle a fini
            return {
                "reponse": response.output_text,
                "tours": tour + 1,
                "tokens": response.usage.total_tokens
            }

        # Exécuter les fonctions personnalisées
        resultats = []
        for appel in appels_custom:
            resultat = dispatcher_fonctions(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 = response.output + resultats

    return {"reponse": "Limite de tours atteinte", "tours": max_tours}

Stratégies d’orchestration

Guider l’ordre des outils

L’orchestration automatique convient tant que l’ordre est indifférent. Il cesse de l’être dès qu’une étape doit informer la suivante : chercher dans vos documents avant le web, par exemple, parce que le vocabulaire interne conditionne la pertinence de la requête externe. Le prompt système est le levier — non pas comme une contrainte technique, mais comme une consigne de méthode que le modèle suit d’autant mieux qu’elle est justifiée.

# Strategy 1 : recherche d'abord, action ensuite
instructions_recherche_dabord = (
    "Avant toute action, cherche d'abord les informations nécessaires "
    "dans les documents internes et sur le web. "
    "Ne propose des actions qu'apres avoir rassemble toutes les données."
)

# Strategy 2 : analyse de données puis enrichissement
instructions_analyse_dabord = (
    "Commence par analyser les données avec Code Interpreter. "
    "Puis enrichis tes conclusions avec des recherches web. "
    "Enfin, compare avec les documents internes."
)

# Strategy 3 : verification croisee
instructions_verification = (
    "Pour chaque information importante, verifie-la dans au moins deux sources : "
    "documents internes ET recherche web. Signale les contradictions."
)

Limiter les outils par étape

L’approche inverse consiste à ne pas donner le choix. Plutôt qu’un appel unique disposant de tout l’outillage, vous découpez en plusieurs appels dont chacun ne déclare que l’outil de son étape. Vous perdez l’orchestration automatique et gagnez trois choses qui comptent en production : un ordre garanti, un coût prévisible et des erreurs localisables. Pour un flux critique, c’est presque toujours le bon arbitrage.

# Étape 1 : collecte de données (pas de code)
response_collecte = client.responses.create(
    model="gpt-5.6-terra",
    input="Rassemble les informations sur le projet Alpha",
    tools=[
        {"type": "file_search", "vector_store_ids": ["vs_projets"]},
        {"type": "web_search_preview"}
    ]
)

# Étape 2 : analyse avec les données collectées
response_analyse = client.responses.create(
    model="gpt-5.6-terra",
    input=f"Analyse ces informations et généré des graphiques :\n"
          f"{response_collecte.output_text}",
    tools=[{"type": "code_interpreter"}]
)

Pattern : agent multi-compétences

Ce dernier exemple assemble les deux approches : un routeur choisit la configuration d’outils selon la nature de la demande, puis laisse le modèle orchestrer à l’intérieur de ce périmètre restreint. C’est le compromis qui tient à l’usage — assez de liberté pour absorber la variété des demandes, assez de cadre pour que la facture et le comportement restent prévisibles.

class AgentMultiCompetences:
    def __init__(self, vector_stores: list[str], fonctions: list[dict]):
        self.client = OpenAI()
        self.tools = [
            {"type": "file_search", "vector_store_ids": vector_stores},
            {"type": "web_search_preview", "search_context_size": "medium"},
            {"type": "code_interpreter"},
        ] + fonctions

    def traiter(self, requete: str, fichiers: list[str] = None) -> dict:
        return boucle_multi_outils(requete, self.tools, fichiers)

# Instancier l'agent
agent = AgentMultiCompetences(
    vector_stores=["vs_docs", "vs_contrats"],
    fonctions=[
        {
            "type": "function",
            "name": "envoyer_email",
            "description": "Envoyer un email de notification",
            "parameters": {
                "type": "object",
                "properties": {
                    "destinataire": {"type": "string"},
                    "sujet": {"type": "string"},
                    "corps": {"type": "string"}
                },
                "required": ["destinataire", "sujet", "corps"],
                "additionalProperties": False
            },
            "strict": True
        },
        {
            "type": "function",
            "name": "creer_tache",
            "description": "Créer une tâche dans le gestionnaire de projet",
            "parameters": {
                "type": "object",
                "properties": {
                    "titre": {"type": "string"},
                    "assignee": {"type": "string"},
                    "priorite": {"type": "string", "enum": ["basse", "moyenne", "haute"]},
                    "echeance": {"type": "string"}
                },
                "required": ["titre", "assignee", "priorite"],
                "additionalProperties": False
            },
            "strict": True
        }
    ]
)

# L'agent peut maintenant :
# - Chercher dans les documents
# - Chercher sur le web
# - Analyser des données
# - Envoyer des emails
# - Créer des taches
resultat = agent.traiter(
    "Analyse les ventes du Q1, compare avec les objectifs, "
    "et cree une tache pour le directeur commercial si on est en retard."
)

Points clés à retenir

  • Tous les outils coexistent dans le même tableau tools
  • Les outils built-in s’exécutent côté serveur, les fonctions personnalisées côté client
  • Le modèle orchestre automatiquement l’ordre des appels
  • Guidez la stratégie d’orchestration via le prompt système
  • Implémentez une boucle pour gérer les appels de fonctions personnalisées