D’un appel de modèle à un agent
Un appel de modèle classique est une fonction : un texte entre, un texte sort. Le modèle ne connaît que ce qui figure dans sa requête et dans ses données d’entraînement. Il ne peut ni consulter votre base clients, ni calculer de façon fiable, ni envoyer un courriel.
Un agent ajoute une boucle autour du modèle. À chaque tour, le modèle reçoit l’historique de la conversation et choisit entre deux actions :
- appeler un outil (une fonction de votre programme) avec des arguments ;
- donner la réponse finale.
Quand le modèle demande un outil, votre programme l’exécute, ajoute le résultat à l’historique, puis rappelle le modèle. La boucle s’arrête quand le modèle répond, ou quand une limite est atteinte.
Point essentiel : le modèle n’exécute rien lui-même. Il produit une demande structurée, par exemple {"tool": "meteo", "args": {"ville": "Lyon"}}. C’est votre code qui décide de l’exécuter ou non. Cette séparation est la base de tous les garde-fous vus plus loin.
Décrire les outils
Les API de modèles qui gèrent l’appel d’outils (tool calling ou function calling) attendent pour chaque outil un nom, une description en langage naturel et un schéma des arguments, souvent au format JSON Schema. La description compte autant que le code : c’est elle qui permet au modèle de choisir le bon outil.
TOOLS_SPEC = [
{"name": "meteo",
"description": "Température actuelle en °C pour une ville française.",
"parameters": {"type": "object",
"properties": {"ville": {"type": "string"}},
"required": ["ville"]}},
]
Le modèle peut se tromper : inventer un outil qui n’existe pas, oublier un argument, passer un texte à la place d’un nombre. Votre code doit donc valider chaque appel avant de l’exécuter.
La boucle, pas à pas
def run_agent(question, model, tools, max_steps=5):
messages = [{"role": "user", "content": question}]
for step in range(max_steps):
decision = model(messages) # le modèle propose
if "answer" in decision:
return decision["answer"] # réponse finale
name, args = decision["tool"], decision.get("args", {})
if name not in tools:
result = f"ERREUR: outil inconnu {name}"
else:
try:
result = tools[name](**args) # votre code exécute
except Exception as e:
result = f"ERREUR: {e}"
messages.append({"role": "tool", "name": name, "content": str(result)})
return None # limite atteinte
Trois remarques :
- l’historique (
messages) est la seule mémoire du modèle d’un tour à l’autre : tout ce qu’il doit savoir doit y figurer ; - les erreurs sont transformées en messages lisibles : le modèle peut alors corriger son appel au tour suivant ;
- la boucle est bornée par
max_steps. Sans limite, un modèle qui rappelle sans fin le même outil consommerait du temps et de l’argent indéfiniment.
Simuler le modèle pour tester
Pour mettre au point la boucle, inutile d’appeler un vrai modèle : une fonction locale qui renvoie des décisions prévisibles suffit. Elle rend les tests reproductibles, gratuits et exécutables sans réseau.
def fake_model(messages):
resultats = [m for m in messages if m["role"] == "tool"]
if not resultats:
return {"tool": "meteo", "args": {"ville": "Lyon"}}
return {"answer": f"Il fait {resultats[-1]['content']} °C."}
Vous testez ainsi le chemin nominal, mais aussi les cas difficiles : un modèle qui boucle, qui invente un outil ou qui passe de mauvais arguments. Le vrai modèle ne sera branché qu’une fois ces comportements maîtrisés.
Quand ne pas utiliser d’agent
Un agent est utile quand l’enchaînement des étapes dépend des résultats intermédiaires. Si les étapes sont toujours les mêmes (extraire, classer, enregistrer), un workflow codé en dur est plus simple, moins cher et plus facile à tester. Commencez par la solution la plus déterministe qui répond au besoin, et réservez l’autonomie aux parties qui en ont réellement besoin.
À retenir
- Agent = boucle « le modèle propose, le code exécute ».
- Chaque appel d’outil est validé avant exécution ; les erreurs reviennent au modèle sous forme de texte.
- La boucle est toujours bornée.
- Un modèle simulé permet de tester la boucle de façon déterministe.
Exercices
Les solutions et les tests restent sur le serveur. Dans l'espace étudiant, ton code est exécuté et corrigé, et le tuteur donne des indices avant la solution.
Écrire la boucle d'agent
Complétez
run_agent(question, model, tools, max_steps=5).- L’historique commence par
{"role": "user", "content": question}. - À chaque tour, appelez
model(messages). S’il renvoie une cléanswer, retournez{"answer": ..., "steps": n}oùnest le nombre d’outils exécutés. - Sinon, exécutez l’outil
decision["tool"]avecdecision["args"]et ajoutez{"role": "tool", "name": ..., "content": str(resultat)}à l’historique. - Outil inconnu : le contenu ajouté est
"ERREUR: outil inconnu <nom>"(cela compte comme une étape). - Après
max_stepsappels du modèle sans réponse, retournez{"answer": None, "steps": max_steps, "error": "max_steps"}.
def meteo(ville): donnees = {"Lyon": 18, "Lille": 12} return donnees.get(ville, "inconnue") def convertir_f(celsius): return round(celsius * 9 / 5 + 32, 1) TOOLS = {"meteo": meteo, "convertir_f": convertir_f} def fake_model(messages): """Modèle simulé : il décide d'après les résultats d'outils déjà présents.""" resultats = [m for m in messages if m["role"] == "tool"] if not resultats: return {"tool": "meteo", "args": {"ville": "Lyon"}} if len(resultats) == 1: return {"tool": "convertir_f", "args": {"celsius": int(resultats[0]["content"])}} return {"answer": f"Il fait {resultats[-1]['content']} °F à Lyon."} def run_agent(question, model, tools, max_steps=5): messages = [{"role": "user", "content": question}] # à compléter ...- L’historique commence par
Valider un appel d'outil
Écrivez
validate_call(call, schemas)qui renvoie la liste des erreurs d’un appel{"tool": ..., "args": {...}}(liste vide si l’appel est valide).schemasassocie à chaque outil un dictionnaireargument -> type attendu(un type ou un tuple de types).- Outil absent de
schemas: renvoyer uniquement["outil inconnu: <nom>"]. - Pour chaque argument attendu absent :
"argument manquant: <arg>". - Pour chaque argument non prévu :
"argument inattendu: <arg>". - Pour chaque argument présent du mauvais type :
"type invalide: <arg>".
L’ordre des erreurs n’a pas d’importance.
SCHEMAS = { "meteo": {"ville": str}, "convertir_f": {"celsius": (int, float)}, } def validate_call(call, schemas): errors = [] # à compléter return errors- Outil absent de
Qui exécute l'outil ?
Dans une boucle agent-outils, que se passe-t-il quand le modèle « appelle » l’outil
rembourser?- Le modèle exécute lui-même le remboursement sur le serveur du fournisseur
- Le modèle renvoie une demande structurée ; votre programme décide de l'exécuter après vérification
- L'API du fournisseur exécute la fonction Python à distance
Quiz de fin de leçon
Pourquoi borner la boucle d’un agent ?
- Pour accélérer le modèle
- Pour éviter les boucles infinies et maîtriser coût et durée
- Parce que les API refusent plus de 5 appels
Que faire quand un outil lève une exception ?
- Arrêter l'agent avec une trace d'erreur
- Renvoyer au modèle un message d'erreur lisible comme résultat
- Ignorer l'appel et recommencer la même requête
Quel est l’intérêt d’un modèle simulé (fonction locale) ?
- Il est plus intelligent que le vrai modèle
- Il rend les tests de la boucle déterministes, gratuits et sans réseau
- Il remplace définitivement le modèle en production
La suite du cours
- Planification et mémoire20 min
- Garde-fous et validation humaine20 min
- Orchestration de tâches20 min
- Échecs et reprise20 min