Pour commencer, je vous invite à aller lire notre article C'est quoi un agent IA ?. Dans ce dernier, nous avons vu qu'un agent décide lui-même du chemin à suivre : les outils, les arguments, quand s'arrêter, etc., ce qui peut devenir compliqué à déboguer.
Dans cet article, nous allons nous attaquer à Logfire, un outil qui permet de voir étape par étape ce que fait votre agent.
Pourquoi un agent IA est-il difficile à déboguer ?
De manière classique, dans un programme, une entrée suit le même chemin dans le code. Avec un agent, ce n'est plus le cas, car, pour une question identique, le modèle peut appeler différents outils.
Voici deux termes que vous croiserez souvent :
-
span : une unité de travail avec un début et une durée, par exemple un appel au modèle ou l'exécution d'un outil
-
trace : l'ensemble des spans d'une exécution
C'est ce que Logfire va suivre pour nous.
Qu'est-ce que Pydantic Logfire ?
Logfire est une plateforme développée par l'équipe de Pydantic et PydanticAI. Vous connaissez peut-être OpenTelemetry, technologie sur laquelle s'appuie Logfire. Il s'agit d'un standard ouvert qui permet de collecter des traces, logs et métriques.
Logfire est composée de deux parties :
-
un SDK que l'on installe dans son projet Python
-
une plateforme web pour consulter les données
Le SDK est open source (licence MIT). Par contre, la plateforme est fermée, bien qu'il soit possible de l'héberger soi-même avec une licence entreprise.
Logfire propose un plan Personal qui est gratuit, incluant 10 millions de logs, ce qui est largement suffisant pour tester et pour de petits projets.
Concernant le stockage des données, deux régions sont proposées : USA et Europe.
Attention
Le choix de la région se fait à l'inscription !
Choix de la région
Vous pouvez vous créer un compte : vous en aurez besoin pour la suite.
Comment installer Logfire ?
Pas de surprise pour les habitués, créons un projet avec uv :
uv init --no-package note-de-frais cd note-de-frais uv add logfire "pydantic-ai-slim[mistral]" httpx python-dotenv
Attention
Pourquoi installer httpx à la main ? Actuellement, le modèle Mistral de PydanticAI importe httpx, alors que le SDK de mistralai est passé à httpx2. Sans ça, vous aurez une erreur ModuleNotFoundError: No module named 'httpx'.
J'ai signalé le problème dans une issue, une pull request est ouverte, alors peut-être qu'un correctif est sorti au moment où vous lisez ces lignes.
PydanticAI a besoin de la clé d'API Mistral dans la variable MISTRAL_API_KEY. Nous allons la stocker dans un fichier .env à la racine du projet.
# .env MISTRAL_API_KEY=votre-clé
Nous allons maintenant nous connecter à Logfire depuis le projet :
uv run logfire --region eu auth
Votre navigateur s'ouvre, vous n'avez plus qu'à autoriser, et les identifiants sont sauvegardés dans ~/.logfire/default.toml.
Ensuite, depuis la racine du dossier, vous pouvez créer un projet :
uv run logfire projects new note-de-frais
Il y a maintenant un dossier .logfire qui contient le jeton d'écriture du projet.
Vérifions que tout fonctionne. Dans le fichier main.py, remplacez le contenu par :
import logfire # Connexion au projet Logfire (identifiants de `logfire auth`) logfire.configure() # {nom} est remplacé par la valeur de l'attribut nom logfire.info("Bonjour {nom} !", nom="Patrick")
Puis lancez :
uv run main.py
Le message apparaît dans le Live view de votre projet.
Live view
Comment tracer un agent PydanticAI avec Logfire ?
Maintenant, on passe aux choses sérieuses : Patrick. Il a besoin d'un assistant comptable, alors on va lui en coder un dans main.py :
import logfire from dotenv import load_dotenv from pydantic_ai import Agent # On charge la clé d'API Mistral depuis le fichier .env load_dotenv() # 1. On connecte le SDK à notre projet Logfire logfire.configure() # 2. On enregistre chaque exécution de nos agents Pydantic AI logfire.instrument_pydantic_ai() NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20} PLAFOND = 150.0 agent = Agent( "mistral:mistral-small-latest", name="assistant_comptable", instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.", ) @agent.tool_plain def consulter_note_de_frais(prenom: str) -> float: """Renvoie le montant de la note de frais d'un employé. Args: prenom: Prénom de l'employé. """ return NOTES_DE_FRAIS.get(prenom.lower(), 0.0) @agent.tool_plain def verifier_plafond(montant: float) -> bool: """Indique si un montant dépasse le plafond de remboursement. Args: montant: Montant en euros. """ return montant > PLAFOND resultat = agent.run_sync("La note de frais de Sébastien dépasse-t-elle le plafond ?") print(resultat.output)
-
load_dotenv()charge la clé d'API se trouvant dans le.env -
logfire.configure()se connecte au projet -
logfire.instrument_pydantic_ai()enregistre les exécutions de nos agents -
pour la partie PydanticAI, n'hésitez pas à aller voir notre article sur le sujet. Nous n'appelons jamais nous-mêmes les outils décorés de
@agent.tool_plain, c'est l'agent qui décide quand s'en servir
Les deux lignes de Logfire suffisent ! Maintenant, lançons main.py :
uv run main.py # Résultat : Logfire project URL: https://logfire-eu.pydantic.dev/gabrieltrouve5/note-de-frais 15:44:59.663 assistant_comptable run 15:44:59.665 chat mistral-small-latest 15:45:00.833 running tool: consulter_note_de_frais 15:45:00.833 running tool: verifier_plafond 15:45:00.837 chat mistral-small-latest La note de frais de Sébastien est de **0,00 €**, elle ne dépasse donc pas le plafond.
Problème : Sébastien a une note de 230,50 €, au-dessus des 150 €. L'agent se trompe et aucune erreur n'est levée.
Comment trouver une erreur dans la trace d'un agent IA ?
Regardez le Live view de Logfire et cliquez sur la trace assistant_comptable run. Dépliez consulter_note_de_frais pour identifier le coupable :
consulter_note_de_frais
Trouvé ! Le modèle a envoyé le prénom avec l'accent : Sébastien. Et comme "Sébastien".lower() donne sébastien, une clé absente du dictionnaire, la méthode get() renvoie la valeur par défaut 0.0.
Si je déplie l'appel à verifier_plafond :
verifier_plafond
verifier_plafond reçoit 0 et répond false, puis l'agent annonce une note à 0,00 €. On va corriger ça !
Corriger l'outil avec ModelRetry
Nous allons signaler le problème au modèle avec ModelRetry :
from pydantic_ai import Agent, ModelRetry @agent.tool_plain def consulter_note_de_frais(prenom: str) -> float: """Renvoie le montant de la note de frais d'un employé. Args: prenom: Prénom de l'employé, sans accent. """ cle = prenom.lower() if cle not in NOTES_DE_FRAIS: # On renvoie l'erreur au modèle pour qu'il corrige son appel raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.") return NOTES_DE_FRAIS[cle]
Nous avons modifié consulter_note_de_frais en utilisant ModelRetry. Regardez les logs :
consulter_note_de_frais avec ModelRetry
Après le premier appel, Pydantic ajoute Fix the errors and try again., puis retente sans l'accent… avec succès !
Ajouter ses spans
Le décorateur @logfire.instrument() permet de tracer ses propres fonctions. En effet, Logfire ne se contente pas de ce que fait l'agent !
import logfire from dotenv import load_dotenv from pydantic_ai import Agent, ModelRetry load_dotenv() logfire.configure() logfire.instrument_pydantic_ai() NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20} PLAFOND = 150.0 agent = Agent( "mistral:mistral-small-latest", name="assistant_comptable", instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.", ) @agent.tool_plain def consulter_note_de_frais(prenom: str) -> float: """Renvoie le montant de la note de frais d'un employé. Args: prenom: Prénom de l'employé, sans accent. """ cle = prenom.lower() if cle not in NOTES_DE_FRAIS: # On renvoie l'erreur au modèle pour qu'il corrige son appel raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.") return NOTES_DE_FRAIS[cle] @agent.tool_plain def verifier_plafond(montant: float) -> bool: """Indique si un montant dépasse le plafond de remboursement. Args: montant: Montant en euros. """ return montant > PLAFOND # Notre propre span, qui englobera l'exécution de l'agent @logfire.instrument("Contrôle de la note de {prenom}") def controler_note(prenom: str) -> str: resultat = agent.run_sync( f"La note de frais de {prenom} dépasse-t-elle le plafond ?" ) return resultat.output print(controler_note("Sébastien"))
Dès qu'il y a un appel à controler_note(), le décorateur crée un span, et l'exécution de l'agent est imbriquée dans notre span :
11:37:27.609 Contrôle de la note de Sébastien 11:37:27.624 assistant_comptable run 11:37:27.626 chat mistral-small-latest 11:37:28.340 running tool: consulter_note_de_frais 11:37:28.341 chat mistral-small-latest 11:37:28.691 running tool: verifier_plafond 11:37:28.694 chat mistral-small-latest
Créer vos propres spans permet de donner des noms « métier » à la trace, ce qui permet d'englober tout ce que la fonction fait autour de l'agent.
Comment éviter d'envoyer des données sensibles à Logfire ?
Vous vous en doutez, tout ce que Logfire enregistre quitte votre ordinateur.
Le masquage automatique
Le SDK masque automatiquement les attributs dont la clé ressemble à une donnée sensible : secret, api_key, password, etc.
Faisons un test ! Je crée un fichier masquage.py :
import logfire logfire.configure() with logfire.span( "Connexion de {utilisateur}", utilisateur="patrick", password="azerty", secret="azerty", api_key="azerty" ): pass
Résultat :
Arguments: { 'api_key': "[Scrubbed due to 'api_key']", 'password': "[Scrubbed due to 'password']", 'secret': "[Scrubbed due to 'secret']", 'utilisateur': 'patrick' }
On remarque bien que les valeurs sensibles sont masquées automatiquement. Si vous le souhaitez, vous pouvez ajouter vos propres motifs, car mot_de_passe partirait en clair.
import logfire # On ajoute nos propres motifs, en plus de ceux par défaut logfire.configure( scrubbing=logfire.ScrubbingOptions(extra_patterns=["mot[._ -]?de[._ -]?passe"]) ) with logfire.span( "Connexion de {utilisateur}", utilisateur="patrick", mot_de_passe="azerty", ): pass
Résultat :
Arguments: { 'mot_de_passe': "[Scrubbed due to 'mot_de_passe']", 'utilisateur': 'patrick' }
Et pour les prompts ?
Vos échanges avec le modèle ne sont pas masqués : un modèle peut alors écrire une donnée sensible. Vous pouvez couper l'envoi de ce contenu :
logfire.instrument_pydantic_ai(include_content=False)
Le problème ici est que nous ne voyons plus les prompts, réponses, arguments et résultats.
Peut-on tracer toute une application avec Logfire ?
Le SDK est capable de gérer plus de 40 bibliothèques comme Django ou encore FastAPI. Et tout ça en une ligne.
Faisons le test avec FastAPI. Mais il faut installer fastapi, uvicorn et l'extra fastapi de logfire.
uv add fastapi uvicorn "logfire[fastapi]"
Maintenant, notre fichier main.py :
import logfire from dotenv import load_dotenv from fastapi import FastAPI from pydantic_ai import Agent, ModelRetry load_dotenv() logfire.configure() logfire.instrument_pydantic_ai() NOTES_DE_FRAIS = {"sebastien": 230.50, "patrick": 87.20} PLAFOND = 150.0 agent = Agent( "mistral:mistral-small-latest", name="assistant_comptable", instructions="Tu es l'assistant de la comptabilité. Réponds en une phrase.", ) @agent.tool_plain def consulter_note_de_frais(prenom: str) -> float: """Renvoie le montant de la note de frais d'un employé. Args: prenom: Prénom de l'employé, sans accent. """ cle = prenom.lower() if cle not in NOTES_DE_FRAIS: # On renvoie l'erreur au modèle pour qu'il corrige son appel raise ModelRetry(f"Aucun employé nommé {prenom}. Réessaie sans accent.") return NOTES_DE_FRAIS[cle] @agent.tool_plain def verifier_plafond(montant: float) -> bool: """Indique si un montant dépasse le plafond de remboursement. Args: montant: Montant en euros. """ return montant > PLAFOND # Notre propre span, qui englobera l'exécution de l'agent @logfire.instrument("Contrôle de la note de {prenom}") def controler_note(prenom: str) -> str: resultat = agent.run_sync( f"La note de frais de {prenom} dépasse-t-elle le plafond ?" ) return resultat.output app = FastAPI() # Chaque requête HTTP devient une trace logfire.instrument_fastapi(app) @app.get("/notes/{prenom}") def verifier_note(prenom: str) -> dict[str, str]: return {"reponse": controler_note(prenom)}
Lancez le serveur, puis, depuis un autre terminal, appelez la route :
# Lancement du serveur uv run uvicorn main:app --reload # Appel de la route curl http://127.0.0.1:8000/notes/Sebastien
Résultat :
INFO: Started server process [18035] INFO: Waiting for application startup. INFO: Application startup complete. 14:51:47.398 GET /notes/Sebastien 14:51:47.406 Contrôle de la note de Sebastien 14:51:47.409 assistant_comptable run 14:51:47.410 chat mistral-small-latest 14:51:48.495 running tool: consulter_note_de_frais 14:51:48.496 chat mistral-small-latest 14:51:49.064 running tool: verifier_plafond 14:51:49.065 chat mistral-small-latest INFO: 127.0.0.1:51398 - "GET /notes/Sebastien HTTP/1.1" 200 OK
Vous remarquez que la requête, la partie métier et l'agent se retrouvent dans une même trace.