Tutoriel Pydantic AI 2026 : Construire des agents LLM type-safe en Python

Si vous avez déjà mis en production une fonctionnalité LLM en Python, vous connaissez le scénario d'échec : le modèle renvoie une chaîne au format JSON, vous la parsez, un champ manque, et vous écrivez encore un try/except défensif qui camoufle le problème. Pydantic AI renverse la logique. Il considère vos schémas Pydantic comme le contrat que le modèle doit honorer et vous offre par-dessus un runtime d'agents avec injection de dépendances à la FastAPI — streaming, outils, retries et observabilité déjà câblés.
Dans ce tutoriel, vous installerez Pydantic AI à partir de zéro, construirez un agent de support client avec sorties structurées et outils de base de données, basculerez les modèles entre OpenAI et Anthropic, streamerez les tokens vers un endpoint FastAPI, et instrumenterez le tout avec Logfire pour voir exactement ce que votre agent a fait et pourquoi.
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Python 3.10 ou plus récent installé
- Une clé API pour au moins un fournisseur (OpenAI, Anthropic, Google, Mistral, ou une instance Ollama locale)
- Une connaissance de base des modèles Pydantic et de
async/await - Un terminal et un éditeur de code (VS Code avec l'extension Python recommandé)
- Optionnel : un compte Logfire pour l'observabilité (le palier gratuit est largement suffisant)
Ce que vous allez construire
À la fin de ce tutoriel, vous aurez :
- Un projet Pydantic AI avec des dépendances isolées dans un environnement
- Un agent de support typé qui renvoie des réponses structurées validées par Pydantic
- Des outils adossés à une base SQLite pour la consultation de tickets et le statut des commandes
- Un endpoint de chat en streaming exposé via FastAPI
- Une configuration agnostique du fournisseur qui bascule OpenAI, Anthropic et Gemini à l'exécution
- Une observabilité de bout en bout avec Logfire, incluant l'usage des tokens et les spans des outils
- Un harnais Pytest qui exécute l'agent contre un faux LLM pour des tests rapides et déterministes
Étape 1 : Installer Pydantic AI
Pydantic AI est livré sous forme d'un paquet unique avec des extras optionnels pour chaque fournisseur. Créez d'abord un environnement isolé — uv est l'option la plus rapide en 2026, mais venv fonctionne tout aussi bien.
mkdir support-agent && cd support-agent
uv venv
source .venv/bin/activate
uv pip install "pydantic-ai[openai,anthropic,logfire]" fastapi uvicorn aiosqliteSi vous préférez pip classique :
python -m venv .venv
source .venv/bin/activate
pip install "pydantic-ai[openai,anthropic,logfire]" fastapi uvicorn aiosqliteVérifiez l'installation :
python -c "import pydantic_ai; print(pydantic_ai.__version__)"Vous devriez voir une version commençant par 0.x. Pydantic AI est encore avant la 1.0 au moment de la rédaction, mais l'API publique est stable depuis plusieurs versions et l'équipe s'est engagée sur le semver pour la suite.
Étape 2 : Configurer vos fournisseurs
Exportez les clés API des fournisseurs que vous souhaitez utiliser. Pydantic AI les lit à l'exécution via les SDK sous-jacents, donc ne les commitez jamais.
export OPENAI_API_KEY="sk-..."
export ANTHROPIC_API_KEY="sk-ant-..."
export GEMINI_API_KEY="..."Pour de vrais projets, mettez-les dans un fichier .env et chargez-les avec python-dotenv ou direnv. Ajoutez .env à .gitignore immédiatement.
Étape 3 : Votre premier agent typé
Créez agent.py avec un agent minimal qui renvoie une SupportResponse structurée :
# agent.py
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class SupportResponse(BaseModel):
"""Structured response from the support agent."""
answer: str = Field(description="Plain-language answer for the user")
needs_human: bool = Field(
description="True when the issue cannot be resolved by the bot"
)
confidence: float = Field(ge=0, le=1, description="Self-rated confidence")
support_agent = Agent(
"openai:gpt-4o-mini",
output_type=SupportResponse,
system_prompt=(
"You are a calm, concise customer-support agent for an e-commerce store. "
"Always return a SupportResponse. Set needs_human=true if the user is angry, "
"asks for a refund over 200 USD, or mentions legal action."
),
)
if __name__ == "__main__":
result = support_agent.run_sync(
"My order #4421 has been stuck on 'shipped' for 14 days. What now?"
)
print(result.output)
print("Tokens used:", result.usage())Exécutez-le :
python agent.pyPydantic AI envoie votre system prompt et le message utilisateur à GPT-4o-mini, lui demande de renvoyer du JSON correspondant à SupportResponse, valide la réponse avec Pydantic, et vous donne un objet entièrement typé. Si le modèle renvoie un JSON invalide ou un champ manquant, l'agent réessaie automatiquement avec un message correctif — aucun parsing manuel requis.
Étape 4 : Ajouter l'injection de dépendances
Les vrais agents ont besoin d'accéder à des bases de données, des clients HTTP et de la configuration. Pydantic AI utilise un système de dépendances à la FastAPI : vous déclarez un deps_type et y accédez depuis les outils et les prompts via le RunContext.
Créez deps.py :
# deps.py
from dataclasses import dataclass
import aiosqlite
@dataclass
class SupportDeps:
"""Resources the agent needs at runtime."""
db: aiosqlite.Connection
customer_id: intMettez à jour agent.py pour l'utiliser :
from pydantic_ai import Agent, RunContext
from deps import SupportDeps
support_agent = Agent(
"openai:gpt-4o-mini",
deps_type=SupportDeps,
output_type=SupportResponse,
system_prompt=(
"You are a customer-support agent. Use the provided tools to look up orders "
"before answering. Never invent order details."
),
)
@support_agent.system_prompt
async def add_customer_context(ctx: RunContext[SupportDeps]) -> str:
"""Inject customer-specific context into every system prompt."""
async with ctx.deps.db.execute(
"SELECT name, tier FROM customers WHERE id = ?", (ctx.deps.customer_id,)
) as cursor:
row = await cursor.fetchone()
if not row:
return "Customer record not found."
name, tier = row
return f"You are speaking with {name} (tier: {tier}). Address them by first name."Remarquez comment le system prompt est une fonction async ordinaire avec un accès complet à deps. Vous obtenez la même ergonomie qu'avec une dépendance FastAPI, sauf que le consommateur est le LLM.
Étape 5 : Définir des outils que le modèle peut appeler
Les outils sont des fonctions async décorées avec @support_agent.tool. Pydantic AI inspecte leurs annotations de type et génère des schémas JSON que le modèle peut appeler. La valeur de retour est renvoyée dans la conversation comme un message d'outil.
from datetime import datetime
from typing import Literal
@support_agent.tool
async def get_order_status(
ctx: RunContext[SupportDeps],
order_id: int,
) -> dict:
"""Look up the latest status for a customer order.
Args:
order_id: The numeric order identifier shown on receipts.
"""
async with ctx.deps.db.execute(
"SELECT status, last_update FROM orders WHERE id = ? AND customer_id = ?",
(order_id, ctx.deps.customer_id),
) as cursor:
row = await cursor.fetchone()
if not row:
return {"error": "order_not_found", "order_id": order_id}
status, last_update = row
return {
"order_id": order_id,
"status": status,
"last_update": last_update,
"stale": (datetime.utcnow().isoformat() > last_update),
}
@support_agent.tool
async def issue_refund(
ctx: RunContext[SupportDeps],
order_id: int,
amount_cents: int,
reason: Literal["damaged", "late", "wrong_item", "other"],
) -> dict:
"""Issue a refund for a specific order. Amounts over 20000 cents require human approval."""
if amount_cents > 20_000:
return {"approved": False, "reason": "amount_exceeds_bot_limit"}
await ctx.deps.db.execute(
"INSERT INTO refunds (order_id, amount_cents, reason) VALUES (?, ?, ?)",
(order_id, amount_cents, reason),
)
await ctx.deps.db.commit()
return {"approved": True, "order_id": order_id, "amount_cents": amount_cents}Deux choses à remarquer :
- Les annotations de type deviennent le schéma. Le
Literalpourreasondevient un enum parmi lequel le modèle doit choisir. Pydantic valide chaque appel d'outil avant que votre fonction ne s'exécute. - Les outils peuvent se protéger eux-mêmes. L'outil de remboursement refuse tout montant supérieur à 200 USD et laisse l'agent escalader. Vous n'avez pas à enseigner la limite au modèle en prose — l'outil l'impose.
Étape 6 : Exécuter l'agent contre une vraie base de données
Reliez le tout dans main.py :
# main.py
import asyncio
import aiosqlite
from agent import support_agent
from deps import SupportDeps
async def setup_db() -> aiosqlite.Connection:
db = await aiosqlite.connect(":memory:")
await db.executescript(
"""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT, tier TEXT);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
status TEXT,
last_update TEXT
);
CREATE TABLE refunds (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER,
amount_cents INTEGER,
reason TEXT
);
INSERT INTO customers VALUES (1, 'Aya', 'gold');
INSERT INTO orders VALUES
(4421, 1, 'shipped', '2026-04-13T08:00:00'),
(4422, 1, 'delivered', '2026-04-22T14:30:00');
"""
)
await db.commit()
return db
async def main() -> None:
db = await setup_db()
deps = SupportDeps(db=db, customer_id=1)
result = await support_agent.run(
"Order #4421 still says shipped after two weeks. Can you refund 35 USD as a goodwill credit?",
deps=deps,
)
print("Answer:", result.output.answer)
print("Needs human:", result.output.needs_human)
print("Confidence:", result.output.confidence)
print("Tool calls:", [m.kind for m in result.all_messages() if m.kind == "tool-call"])
if __name__ == "__main__":
asyncio.run(main())Exécutez :
python main.pyL'agent appellera get_order_status, verra que la commande est obsolète, appellera issue_refund pour 3500 cents, et renverra une SupportResponse que vous pouvez transmettre directement à votre couche UI.
Étape 7 : Streamer l'agent via FastAPI
Pydantic AI expose agent.run_stream qui produit une sortie incrémentale. Combinez-le avec StreamingResponse de FastAPI pour un endpoint de chat qui stream les tokens vers le navigateur.
# api.py
from contextlib import asynccontextmanager
import aiosqlite
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from agent import support_agent
from deps import SupportDeps
class ChatBody(BaseModel):
customer_id: int
message: str
db_holder: dict = {}
@asynccontextmanager
async def lifespan(app: FastAPI):
db_holder["db"] = await aiosqlite.connect("support.db")
yield
await db_holder["db"].close()
app = FastAPI(lifespan=lifespan)
@app.post("/chat")
async def chat(body: ChatBody) -> StreamingResponse:
deps = SupportDeps(db=db_holder["db"], customer_id=body.customer_id)
async def token_stream():
async with support_agent.run_stream(body.message, deps=deps) as result:
async for chunk in result.stream_text(delta=True):
yield chunk
return StreamingResponse(token_stream(), media_type="text/plain")Démarrez le serveur :
uvicorn api:app --reloadEt depuis un autre terminal :
curl -N -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{"customer_id": 1, "message": "Where is order 4422?"}'Vous verrez les tokens arriver en temps réel. Comme output_type est défini, la valeur agrégée finale reste une SupportResponse validée — vous obtenez l'UX du streaming sans renoncer à la structure.
Étape 8 : Changer de fournisseur sans réécrire le code
Le premier argument d'Agent est un identifiant de modèle. Changez une seule chaîne et l'agent s'exécute contre un autre fournisseur. Pour les environnements où le choix est dynamique, utilisez directement pydantic_ai.models :
import os
from pydantic_ai.models.anthropic import AnthropicModel
from pydantic_ai.models.openai import OpenAIModel
def pick_model():
name = os.getenv("AGENT_MODEL", "openai:gpt-4o-mini")
if name.startswith("openai:"):
return OpenAIModel(name.split(":", 1)[1])
if name.startswith("anthropic:"):
return AnthropicModel(name.split(":", 1)[1])
raise ValueError(f"Unknown model: {name}")
support_agent = Agent(
pick_model(),
deps_type=SupportDeps,
output_type=SupportResponse,
system_prompt="...",
)Maintenant AGENT_MODEL=anthropic:claude-sonnet-4-6 python main.py exécute le même agent sur Claude Sonnet 4.6 sans aucun autre changement de code. Outils, sorties structurées et injection de dépendances fonctionnent tous à l'identique car Pydantic AI normalise pour vous les différences entre fournisseurs.
Étape 9 : Ajouter l'observabilité avec Logfire
Pydantic AI est conçu par l'équipe Pydantic, donc il s'intègre nativement avec Logfire — leur produit d'observabilité basé sur OpenTelemetry. Chaque appel de modèle, invocation d'outil, retry et erreur de validation devient un span que vous pouvez chercher.
Inscrivez-vous sur logfire.pydantic.dev et récupérez un write token. Ensuite :
# observability.py
import logfire
logfire.configure(token="your-write-token", service_name="support-agent")
logfire.instrument_pydantic_ai()Importez ce module une seule fois en haut d'api.py. Redémarrez le serveur et envoyez quelques requêtes de chat. Dans l'interface Logfire vous verrez :
- Un span racine par exécution d'agent
- Des spans enfants pour chaque appel de modèle, avec prompt, réponse et compte de tokens
- Des spans d'outils montrant arguments, valeurs de retour et durée
- Des spans de validation quand Pydantic AI réessaie sur une mauvaise sortie
Pour de l'observabilité auto-hébergée, remplacez l'appel par logfire.configure(send_to_logfire=False) et pointez OTLP standard vers votre propre collecteur. L'instrumentation reste la même.
Étape 10 : Tester l'agent sans brûler de tokens
Le pydantic_ai.models.test.TestModel vous permet d'exécuter des tests d'agent de bout en bout avec zéro appel réseau. Il renvoie une réponse structurée déterministe qui correspond à votre output_type, et vous pouvez asserter sur les appels d'outils que l'agent a effectués.
# test_agent.py
import pytest
from pydantic_ai.models.test import TestModel
from agent import support_agent
from deps import SupportDeps
@pytest.mark.asyncio
async def test_refund_flow(tmp_db):
deps = SupportDeps(db=tmp_db, customer_id=1)
with support_agent.override(model=TestModel()):
result = await support_agent.run(
"Refund 50 USD for order 4421",
deps=deps,
)
tool_calls = [m for m in result.all_messages() if m.kind == "tool-call"]
assert any(t.tool_name == "issue_refund" for t in tool_calls)
assert isinstance(result.output.answer, str)
assert 0 <= result.output.confidence <= 1Ajoutez pytest et pytest-asyncio à vos dépendances de développement et exécutez :
pytest -vLa suite complète se termine en quelques millisecondes car aucun vrai LLM n'est impliqué. Utilisez TestModel pour les tests unitaires, puis ajoutez un petit ensemble de tests d'intégration qui appellent un vrai fournisseur sur chaque release candidate.
Tester votre implémentation
Reparcourez le chemin nominal complet une fois de plus :
python main.pyrenvoie uneSupportResponseavecneeds_human=Falseet un remboursement enregistré dans SQLitecurlcontre/chatstream les tokens et termine avec une sortie structuréeAGENT_MODEL=anthropic:claude-sonnet-4-6 python main.pyproduit la même forme sur Claude- Logfire affiche un arbre de spans avec appels d'outils et usage de tokens
pytestpasse en moins d'une seconde avecTestModel
Si l'un de ces points échoue, les coupables les plus fréquents sont des clés API manquantes, un extra fournisseur dépassé (lancez uv pip install --upgrade "pydantic-ai[openai,anthropic]"), ou une fonction outil qui ne déclare pas de types que Pydantic peut introspecter.
Dépannage
Le modèle continue à renvoyer du texte libre au lieu de sortie structurée. Assurez-vous que output_type est défini sur l'Agent et que vous ne demandez pas aussi du texte libre dans le system prompt. Pydantic AI utilise le tool calling sous le capot ; certains modèles plus anciens doivent être épinglés sur une variante capable de function calling.
Les erreurs de validation bouclent sans fin. Pydantic AI réessaie jusqu'à retries=1 par défaut. Augmentez avec Agent(..., retries=3) pour les modèles instables, mais si un champ est impossible à satisfaire, vous brûlerez des tokens. Lisez l'erreur de validation attentivement — elle pointe généralement vers une contrainte Field trop stricte.
Les outils ne sont jamais appelés. Vérifiez que vous les avez décorés avec @agent.tool (et non @agent.tool_plain sauf si vous voulez sauter RunContext) et que leurs docstrings décrivent quand les appeler. Les modèles s'appuient massivement sur les descriptions d'outils pour décider.
L'endpoint de streaming renvoie tout le message d'un coup. C'est du buffering FastAPI. Assurez-vous que vous renvoyez une StreamingResponse et que vous n'awaitez pas le générateur avant de yield.
Prochaines étapes
- Combinez Pydantic AI avec notre guide FastAPI Docker en production pour livrer l'agent derrière un reverse proxy
- Associez-le à la recherche full-text Postgres côté outils pour un retrieval plus riche
- Comparez l'expérience développeur au pattern d'agent Vercel AI SDK côté TypeScript
- Lisez la doc officielle Pydantic AI pour les patterns avancés : workflows en graphe, handoff multi-agents et streaming structuré avec validation delta
Conclusion
Pydantic AI prend les parties du développement web Python qui fonctionnent déjà — schémas typés, injection de dépendances et API async-first — et les applique aux agents LLM. Vous arrêtez de penser au parsing JSON et aux chaînes en forme de prompt et commencez à penser en contrats : que renvoie mon agent, quels outils peut-il appeler, et de quoi a-t-il besoin pour faire son travail. Le résultat est du code d'agent qui ressemble au reste de votre codebase Python, qui se teste comme le reste de votre codebase Python, et qui se livre avec la même confiance que le reste de votre codebase Python.
Construisez le bot de support ci-dessus, instrumentez-le avec Logfire, et la prochaine fois qu'on vous demandera comment vous gérez les sorties LLM structurées, vous pourrez pointer vers une suite de tests qui passe au lieu d'une regex pleine d'espoir.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.

Construire un Agent IA Autonome avec Agentic RAG et Next.js
Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

Guide d'Integration de Chatbot IA : Construire des Interfaces Conversationnelles Intelligentes
Un guide complet pour integrer des chatbots IA dans vos applications en utilisant OpenAI, Anthropic Claude et ElevenLabs. Apprenez a construire des chatbots textuels et vocaux avec Next.js.