Aller au contenu principal

Function calling avancé : schémas et validation

Mis à jour le 29 juillet 2026

Function calling avancé : schémas et validation

Le function calling est le mécanisme central qui permet à un agent de décider quand et comment appeler vos outils. Déclarer un tool suffit pour un prototype ; le mettre en production suppose de contraindre les appels, de valider les valeurs métier et de gérer les échecs sans faire tomber l’agent. C’est l’objet de cette leçon.

Schémas JSON et strict mode

La Responses API utilise des schémas JSON pour décrire les paramètres de chaque tool, et le mode strict garantit que le modèle respecte exactement le schéma. Concrètement, un Enum ou un Literal ferme la porte aux valeurs inventées.

from agents import function_tool
from pydantic import BaseModel, Field
from typing import Literal
from enum import Enum

class Priorite(str, Enum):
    BASSE = "basse"
    MOYENNE = "moyenne"
    HAUTE = "haute"
    CRITIQUE = "critique"

class CreerTicket(BaseModel):
    titre: str = Field(description="Titre court du ticket (max 100 caractères)")
    description: str = Field(description="Description détaillée du problème")
    priorite: Priorite = Field(description="Niveau de priorité du ticket")
    composant: Literal["frontend", "backend", "infra", "data"] = Field(
        description="Composant technique affecté"
    )
    tags: list[str] = Field(
        default_factory=list,
        description="Tags optionnels pour catégoriser le ticket"
    )

@function_tool
def creer_ticket_support(ticket: CreerTicket) -> str:
    """Crée un nouveau ticket de support technique."""
    # En production : appel à Jira, Linear, GitHub Issues, etc.
    return f"Ticket créé : [{ticket.priorite.value}] {ticket.titre} ({ticket.composant})"

Avec le strict mode activé par défaut, le modèle ne pourra jamais envoyer une priorité invalide ou un composant hors de la liste. Vous n’aurez donc pas à écrire, côté backend, le code défensif qui traduit « urgent » ou « P1 » en une valeur connue de votre système de tickets.

Validation personnalisée avec Pydantic

Le typage arrête les valeurs impossibles, pas les valeurs absurdes. Un IBAN de quinze caractères est une chaîne parfaitement valide ; c’est votre métier qui le rejette. Pydantic permet d’inscrire ces règles directement dans le schéma.

from pydantic import BaseModel, Field, field_validator

class TransfertArgent(BaseModel):
    compte_source: str = Field(description="IBAN du compte source")
    compte_destination: str = Field(description="IBAN du compte destination")
    montant: float = Field(gt=0, le=50000, description="Montant en euros (max 50 000)")
    motif: str = Field(min_length=5, max_length=200, description="Motif du transfert")

    @field_validator("compte_source", "compte_destination")
    @classmethod
    def valider_iban(cls, v):
        if not v.startswith("FR"):
            raise ValueError("Seuls les IBAN français sont acceptés")
        if len(v) != 27:
            raise ValueError("L'IBAN doit contenir 27 caractères")
        return v

@function_tool
def effectuer_transfert(transfert: TransfertArgent) -> str:
    """Effectue un transfert bancaire entre deux comptes."""
    return f"Transfert de {transfert.montant}€ initié de {transfert.compte_source[:8]}... vers {transfert.compte_destination[:8]}..."

Les contraintes gt=0 et le=50000 sur le montant jouent le même rôle sur un plan différent : elles rendent structurellement impossible un virement négatif ou hors plafond, quelle que soit la formulation de la demande utilisateur.

Gérer les erreurs dans les tools

Reste le cas où le tool s’exécute mais échoue. La tentation est de laisser remonter l’exception ; c’est une erreur, car elle interrompt la boucle de l’agent. Retournez plutôt un message d’erreur clair, que le modèle pourra lire et interpréter.

import json
from agents import function_tool

@function_tool
def interroger_api_externe(endpoint: str, parametres: dict) -> str:
    """Interroge une API externe avec les paramètres spécifiés."""
    try:
        # En production : appel HTTP réel
        import httpx
        response = httpx.get(f"https://api.example.com/{endpoint}", params=parametres, timeout=10)
        response.raise_for_status()
        return json.dumps(response.json(), ensure_ascii=False)
    except httpx.TimeoutException:
        return json.dumps({
            "erreur": True,
            "message": f"Timeout lors de l'appel à {endpoint}. Réessayez dans quelques instants."
        })
    except httpx.HTTPStatusError as e:
        return json.dumps({
            "erreur": True,
            "message": f"Erreur HTTP {e.response.status_code} sur {endpoint}.",
            "details": str(e)
        })
    except Exception as e:
        return json.dumps({
            "erreur": True,
            "message": f"Erreur inattendue : {str(e)}"
        })

Chaque branche formule le message dans une langue que le modèle comprend. Sur un timeout, l’agent tentera un nouvel appel ; sur une erreur HTTP 404, il informera l’utilisateur ou changera d’approche plutôt que de s’obstiner.

Structured output pour les tools

La contrainte vaut aussi pour la sortie finale. Avec output_type, final_output n’est plus un texte à parser mais un objet typé, directement exploitable par le code appelant.

from pydantic import BaseModel
from agents import Agent, Runner

class ReponseAnalyse(BaseModel):
    resume: str
    points_cles: list[str]
    recommandation: str
    score_confiance: float

agent = Agent(
    name="Analyste",
    instructions="Analysez les données fournies et produisez un rapport structuré.",
    model="gpt-5.6-terra",
    output_type=ReponseAnalyse,
)

result = Runner.run_sync(agent, "Analysez les tendances de vente du Q1 2026.")
# result.final_output est de type ReponseAnalyse
print(f"Résumé : {result.final_output.resume}")
print(f"Score : {result.final_output.score_confiance}")

Pattern : tools avec contexte partagé

Un dernier problème récurrent : vos tools ont souvent besoin d’un état partagé — session utilisateur, connexion à la base, clé d’API — qu’il serait absurde de faire transiter par le modèle. Le contexte règle la question en le passant à côté de la conversation.

from dataclasses import dataclass
from agents import Agent, Runner, RunContextWrapper, function_tool

@dataclass
class ContexteApp:
    user_id: str
    db_connection: object  # Votre connexion DB
    api_key: str

@function_tool
def obtenir_profil_utilisateur(ctx: RunContextWrapper[ContexteApp]) -> str:
    """Récupère le profil de l'utilisateur courant."""
    user_id = ctx.context.user_id
    # Utiliser ctx.context.db_connection pour la requête
    return f"Profil utilisateur {user_id} : Premium, actif depuis 2024"

agent = Agent(
    name="Agent personnalisé",
    instructions="Vous assistez l'utilisateur connecté.",
    tools=[obtenir_profil_utilisateur],
)

contexte = ContexteApp(user_id="usr_123", db_connection=None, api_key="xxx")
result = Runner.run_sync(agent, "Quel est mon profil ?", context=contexte)

Le tool n’a aucun paramètre métier : l’identité de l’utilisateur vient du contexte, pas du modèle. C’est aussi une mesure de sécurité, puisque l’agent ne peut pas consulter le profil de quelqu’un d’autre en changeant un argument.

Points clés à retenir

  • Les schémas Pydantic avec enums et Literal garantissent la validité des appels
  • Les field validators ajoutent une validation métier en plus du typage
  • Retournez des messages d’erreur JSON plutôt que de lever des exceptions dans les tools
  • output_type force l’agent à structurer sa réponse finale
  • Le contexte partagé (RunContextWrapper) permet aux tools d’accéder à l’état de l’application