Aller au contenu
Développement medium

PydanticAI : des agents IA typés en Python

11 min de lecture

logo python

Les deux guides précédents ont écrit la boucle d'un agent à la main — instructif, mais on ne la réécrit pas à chaque projet. PydanticAI fournit cette boucle, et lui ajoute ce qui sépare un script d'un composant de production : une sortie typée garantie — un modèle Pydantic validé, jamais du texte à parser —, l'injection de dépendances, et des retries automatiques quand le modèle se trompe. Vous construirez un agent release-notes qui résume les notes de version d'un dépôt GitHub en une structure typée, sur un modèle servi par Ollama. Au passage, vous verrez les trois modes de sortie structurée de PydanticAI et lequel choisir selon le modèle. Public visé : développeur ayant lu les deux guides sur les agents et le function calling.

  • Pourquoi une sortie typée change tout par rapport au texte libre.
  • L'anatomie d'un agent PydanticAI : modèle, dépendances, sortie, outils.
  • Les trois modes de sortie structurée — Tool, Native, Prompted — et lequel choisir.
  • Injecter du contexte avec les dépendances (deps).
  • Gérer les erreurs d'outil avec ModelRetry.

Ce guide prolonge Comprendre les agents IA et Function calling. Il vous faut :

  • Une instance Ollama avec le modèle qwen2.5.
  • Les bases de Pydantic et un accès internet pour l'API GitHub.
  • Python 3.10+.

Les agents des guides précédents renvoyaient une chaîne de caractères. Pour un affichage, c'est suffisant. Pour un composant qui s'insère dans une chaîne de traitement — alimenter une base, déclencher une action, être testé — c'est ingérable : il faut parser ce texte, espérer qu'il a la bonne forme, gérer le cas où il ne l'a pas.

PydanticAI renverse le problème. On déclare la sortie attendue comme un modèle Pydantic, et l'agent garantit de renvoyer une instance validée de ce modèle — ou d'échouer franchement. Plus de parsing, plus d'espoir : le type est une certitude. C'est le même principe que le function calling du guide précédent — un schéma, une validation — appliqué cette fois à la réponse finale de l'agent.

Un agent PydanticAI se déclare en un objet Agent, paramétré par quatre éléments. Le modèle d'abord. PydanticAI fournit une classe dédiée à Ollama auto-hébergé — OllamaModel, branchée sur un OllamaProvider.

from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from pydantic_ai.providers.ollama import OllamaProvider
MODELE = OllamaModel(
"qwen2.5", provider=OllamaProvider(base_url="http://localhost:11434/v1")
)

Les trois autres éléments sont le type des dépendances (deps_type), le type de sortie (output_type) et les outils. Les deux types sont déclarés à la création de l'agent ; les outils, par décoration. On les détaille un par un.

La sortie de l'agent release-notes est une structure, pas une phrase :

from pydantic import BaseModel, Field
class SyntheseRelease(BaseModel):
"""Synthèse structurée des dernières releases d'un dépôt."""
derniere_version: str = Field(description="Tag de la release la plus récente")
nombre_de_releases: int = Field(description="Nombre de releases examinées")
resume: str = Field(description="Résumé factuel des changements, 2 à 3 phrases")

Ce modèle devient le output_type de l'agent. Dès lors, agent.run_sync(...) renvoie un résultat dont le .output est une instance de SyntheseRelease — ou une exception UnexpectedModelBehavior si le modèle n'y est pas parvenu, même après ses retries. Il n'y a pas d'entre-deux : un appel qui réussit a produit une sortie valide et typée.

Comment PydanticAI obtient-il cette structure du modèle ? Il existe trois modes, et les confondre coûte des heures de débogage.

Le Tool Output est le mode par défaut. PydanticAI décrit le schéma de sortie comme un outil que le modèle doit appeler pour livrer son résultat. C'est l'approche la plus compatible — « supportée par pratiquement tous les modèles » — et celle que la documentation recommande pour les modèles locaux.

Le Native Output s'appuie sur la fonction de structured output native du modèle, qui le contraint à respecter le schéma. Très fiable, mais tous les modèles ne le supportent pas — à éviter par défaut sur un modèle local.

Le Prompted Output injecte le schéma JSON dans les instructions et parse la réponse texte. Compatible avec tout modèle, mais « souvent l'approche la moins fiable » : rien ne contraint le modèle.

ModeSupportFiabilitéPour un modèle local
Tool Output (défaut)Tous les modèlesTrès hauteRecommandé
Native OutputModèles sélectionnésTrès hauteRarement supporté
Prompted OutputTous les modèlesModéréeSolution de repli

L'agent de ce guide utilise le Tool Output — il suffit de passer la classe Pydantic à output_type, sans rien envelopper.

Un agent a besoin de ressources — un client HTTP, une connexion, une configuration. Les coder en variables globales rend l'agent impossible à tester et à réutiliser. PydanticAI propose l'injection de dépendances : un objet, déclaré par deps_type, passé à chaque exécution et transmis aux outils.

from dataclasses import dataclass
import httpx
@dataclass
class Deps:
"""Dépendances de l'agent — ici, le client HTTP partagé."""
client: httpx.Client

L'agent se déclare alors avec ses quatre paramètres, et le budget de retries — retries=3 couvre les outils comme la sortie :

agent = Agent(
MODELE,
deps_type=Deps,
output_type=SyntheseRelease,
retries=3,
system_prompt=(
"Tu résumes les notes de version d'un dépôt GitHub. Appelle l'outil "
"lister_releases, puis produis une synthèse structurée et factuelle."
),
)

Un outil PydanticAI qui a besoin des dépendances se décore avec @agent.tool et reçoit un RunContext typé — par lequel il accède à ctx.deps.

from pydantic_ai import ModelRetry, RunContext
@agent.tool
def lister_releases(ctx: RunContext[Deps], depot: str) -> list[dict]:
"""Liste les dernières releases d'un dépôt GitHub.
depot : identifiant au format owner/repo, par exemple astral-sh/uv.
"""
if "/" not in depot:
raise ModelRetry("Le dépôt doit être au format owner/repo.")
return recuperer_releases(ctx.deps.client, depot)

Deux mécanismes se rencontrent ici. Le RunContext[Deps] donne à l'outil le client HTTP injecté — ctx.deps.client — sans aucun global. Et le ModelRetry : si l'outil lève cette exception, PydanticAI ne plante pas — il renvoie le message d'erreur au modèle, qui corrige son appel et réessaie, dans la limite du budget retries. Un argument mal formé devient ainsi une boucle de correction automatique, pas un échec.

L'agent s'exécute avec run_sync, en lui passant l'instance des dépendances :

def resumer(depot: str) -> SyntheseRelease:
with httpx.Client(timeout=15) as client:
resultat = agent.run_sync(
f"Résume les dernières releases du dépôt {depot}.",
deps=Deps(client=client),
)
return resultat.output

Sur le dépôt astral-sh/uv, la sortie est une structure, pas un paragraphe :

Type de sortie : SyntheseRelease
Dernière version : 0.11.16
Releases examinées : 5
Résumé : Les dernières mises à jour du dépôt astral-sh/uv ont
apporté plusieurs corrections de bugs et améliorations fonctionnelles...

La suite de tests du lab tient en trois contrôles. La logique métier : la fonction qui interroge l'API GitHub renvoie des releases exploitables — un test d'intégration, déterministe sur un dépôt public stable. Le retry : un dépôt mal formé lève bien ModelRetry — un test unitaire, sans modèle ni réseau. Et la sortie typée : l'agent complet renvoie une instance de SyntheseRelease — ce que PydanticAI garantit dès lors que run_sync n'a pas levé d'exception.

SymptômeCause probableSolution
UnexpectedModelBehavior: Exceeded maximum output retriesLe modèle ne produit pas une sortie valideAugmenter retries ; si besoin, passer en Prompted Output
Agent(output_retries=...) dépréciéAncienne APIUtiliser retries={'output': N} ou retries=N
L'agent ne voit pas les dépendancesdeps non passé à run_sync, ou deps_type absentDéclarer deps_type et passer deps= à chaque run
Le modèle boucle sur le même outilModèle faible en function callingChoisir un modèle solide (qwen2.5) ; relire sa doc
ModelRetry non prise en compteLevée hors d'un outil ou d'un validateurModelRetry ne vaut que dans un outil ou une fonction de sortie
  • PydanticAI fournit la boucle d'un agent ; on ne la réécrit pas à chaque projet.
  • La sortie typée est garantie : run_sync réussi renvoie une instance Pydantic validée, jamais du texte à parser.
  • Trois modes de sortie : Tool Output (défaut, recommandé pour les modèles locaux), Native Output (réservé aux modèles compatibles), Prompted Output (repli).
  • Les dépendances s'injectent via deps_type et RunContext — pas de variables globales.
  • ModelRetry transforme une erreur d'outil en boucle de correction automatique.
  • Le mode de sortie et le modèle se choisissent ensemble : un modèle faible échoue même en Tool Output.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn