TL;DR — Pydantic AI v1.89.0
- Agent loop = machine à états finis implémentée via
pydantic-graph— nœuds UserPromptNode → ModelRequestNode → CallToolsNode - Type safety bout-en-bout :
Agent[DepsT, OutputT]paramétré — mypy/pyright valident à la compilation - Structured output — 3 modes : Tool Output (défaut), Native Output (structured outputs natifs), Prompted Output (fallback texte)
- Injection de dépendances explicite via dataclass
Deps— DB pool, HTTP client, user context injectés proprement - 4 packages séparés :
pydantic-ai,pydantic-ai-slim,pydantic-graph,pydantic-evals— chacun indépendant - CUDA / GPU : aucun — Pydantic AI appelle des APIs LLM. Pas de serving local (voir vLLM pour ça)
Sommaire
- Le problème que Pydantic AI résout
- Philosophie et décisions de design
- Installation — 4 packages distincts
- L'Agent : anatomie complète
- Injection de dépendances — le pattern Deps
- Tools — du décorateur au schéma JSON
- Structured Output — 3 modes
- Historique de messages et multi-turn
- Multi-agents — 5 niveaux de complexité
- pydantic-graph — la machine à états de l'agent loop
- MCP — Pydantic AI comme client MCP
- Testing — TestModel et FunctionModel
- Observabilité — Logfire et OpenTelemetry
- pydantic-evals — évaluer ses agents
- Capabilities — comportements composables (v1)
- Pydantic AI vs LangChain vs LangGraph
- FAQ
1. Le problème que Pydantic AI résout
En 2024, construire des agents IA en Python revenait à choisir entre LangChain (écosystème massif, qualité d'ingénierie hétérogène), AutoGen (Microsoft, verbeux), ou du code LLM artisanal. Aucune option ne satisfaisait simultanément les trois critères d'un framework de production sérieux :
- Type safety réelle — vérifiable statiquement avec mypy/pyright, pas juste documentée
- Testabilité déterministe — unit tests sans mocker chaque appel API manuellement
- Injection de dépendances explicite — DB pools, HTTP clients, sessions transmis proprement aux tools
Samuel Colvin, l'auteur de Pydantic (la bibliothèque de validation de données la plus téléchargée de Python avec ~300M de downloads/mois en 2025), a déclaré :
"We built Pydantic AI because no existing agent framework met our bar for developer ergonomics, engineering quality, and production readiness. Building GenAI applications is still just engineering. Engineering best practices still apply, developer experience matters."
— Samuel Colvin, Latent Space podcast (2025)
OpenAI a commenté à Colvin que Pydantic AI ressemblait à ce que "swarms aurait été s'il était production-ready" — ce qui illustre exactement le positionnement : la puissance des frameworks multi-agents, avec les standards d'ingénierie d'un outil de prod.
2. Philosophie et décisions de design
Pydantic AI s'inspire explicitement de FastAPI : "l'ergonomie FastAPI pour les agents IA". Les deux frameworks partagent :
- Déclaration via décorateurs (
@agent.tool≈@app.get) - Génération automatique de schémas depuis les type hints Python
- Validation runtime via Pydantic v2
- Injection de dépendances explicite
Les décisions architecturales qui distinguent Pydantic AI :
| Décision | Justification |
|---|---|
Generics Python pour Agent[DepsT, OutputT] | Maintenabilité sur les grands codebases, même si la courbe d'apprentissage est plus raide |
| Agent loop = graphe pydantic-graph | Colvin résistait initialement ; convaincu par les retours utilisateurs — les diagrammes Mermaid se génèrent automatiquement depuis les type hints |
| OpenTelemetry, pas Logfire exclusif | "Embrace open standards rather than locking developers into half-baked proprietary standards" |
| 100% test coverage + docs testées | Éviter les exemples invalides dans la documentation officielle |
| DI explicite via dataclass | "Tools are virtually useless without access to dependencies" — pas d'état global implicite |
Audience cible
Pydantic AI cible les développeurs maîtrisant les generics Python. La doc officielle indique explicitement que pydantic-graph est "not designed to be as beginner-friendly". Si vous débutez avec les agents IA, LangChain a une meilleure courbe d'entrée.
3. Installation — 4 packages distincts
Le dépôt est un monorepo avec 4 packages indépendants sur PyPI :
| Package | Contenu | Dépend de |
|---|---|---|
pydantic-ai | Package complet (core + tous les extras) | pydantic-ai-slim |
pydantic-ai-slim | Core framework seul, sans SDK vendeurs | pydantic v2, anyio |
pydantic-graph | Machine à états génériques, indépendant de pydantic-ai | pydantic v2 |
pydantic-evals | Framework d'évaluation, indépendant de pydantic-ai | pydantic v2, logfire (optionnel) |
# Package complet (recommandé pour débuter)
pip install pydantic-ai
# Slim + provider spécifique (prod, image Docker plus légère)
pip install pydantic-ai-slim
pip install 'pydantic-ai-slim[openai]' # + openai SDK
pip install 'pydantic-ai-slim[anthropic]' # + anthropic SDK
pip install 'pydantic-ai-slim[google]' # + google-genai SDK
pip install 'pydantic-ai-slim[groq,mistral]' # multi-providers
# Python requis : >= 3.10 (union types X | Y natifs)
L'exigence Python ≥ 3.10 est délibérée : elle permet d'utiliser la syntaxe X | Y pour les types union nativement, ce qui simplifie les signatures des tools et les output_type unions.
4. L'Agent : anatomie complète
La classe Agent[AgentDepsT, OutputDataT] est le point d'entrée principal. Elle est paramétrée sur deux TypeVars : le type des dépendances et le type de sortie.
from pydantic import BaseModel
from pydantic_ai import Agent
from typing import Literal
# Structured output défini comme modèle Pydantic
class SentimentAnalysis(BaseModel):
sentiment: Literal['positive', 'negative', 'neutral']
confidence: float # 0.0 – 1.0
key_points: list[str]
# Agent avec output structuré, sans dépendances (NoneType par défaut)
agent = Agent(
'anthropic:claude-opus-4-6',
output_type=SentimentAnalysis,
system_prompt="Tu es un analyste de texte expert. Analyse le sentiment du texte fourni.",
retries=2,
)
# Exécution synchrone
result = agent.run_sync("Ce framework est incroyablement bien conçu !")
print(result.output)
# SentimentAnalysis(sentiment='positive', confidence=0.97, key_points=['framework', 'bien conçu'])
# Métriques d'usage
usage = result.usage()
print(f"Tokens: {usage.input_tokens} → {usage.output_tokens}")
Les trois méthodes d'exécution
| Méthode | Cas d'usage | Type retourné |
|---|---|---|
agent.run_sync(...) | Scripts, tests, CLI | AgentRunResult |
await agent.run(...) | FastAPI, applications async | AgentRunResult |
async with agent.run_stream(...) as s: | Streaming token par token | StreamedRunResult |
async for event in agent.run_stream_events(...): | Streaming avec events (tool calls, etc.) | AsyncIterator[AgentStreamEvent] |
async with agent.iter(...) as run: | Introspection nœud par nœud du graphe | AgentRun |
AgentRunResult — accès aux données
result = await agent.run("Analyse ce texte")
result.output # SentimentAnalysis — résultat final validé par Pydantic
result.usage() # Usage(requests=1, input_tokens=245, output_tokens=42, tool_calls=1)
result.all_messages() # list[ModelMessage] — historique complet incluant tool calls
result.new_messages() # list[ModelMessage] — messages de CE run seulement (pour multi-turn)
result.run_id # UUID unique par appel agent.run()
result.conversation_id # UUID partagé entre runs liés (nouveau en v1.89.0)
run_id vs conversation_id — nouveau en v1.89.0
Chaque agent.run() génère un run_id unique. Le conversation_id est propagé automatiquement entre les runs liés via message_history — il persiste sur toute la durée d'une conversation multi-tour. Indispensable pour le tracing et la facturation cross-runs.
5. Injection de dépendances — le pattern Deps
C'est l'une des décisions architecturales les plus importantes de Pydantic AI. Dans LangChain, les ressources externes (DB, HTTP client, user session) sont souvent transmises via des variables d'instance ou de l'état global. Pydantic AI impose une injection explicite, typée et testable.
from dataclasses import dataclass
import asyncpg
import httpx
from pydantic_ai import Agent, RunContext
# 1. Déclarer les dépendances comme dataclass
@dataclass
class AppDeps:
db: asyncpg.Pool # Pool de connexions PostgreSQL
http: httpx.AsyncClient # Client HTTP async réutilisable
user_id: str # Contexte utilisateur courant
# 2. Agent paramétré sur AppDeps
agent = Agent('openai:gpt-4o', deps_type=AppDeps)
# 3. System prompt dynamique avec accès aux deps
@agent.system_prompt
async def get_context(ctx: RunContext[AppDeps]) -> str:
username = await ctx.deps.db.fetchval(
"SELECT name FROM users WHERE id = $1", ctx.deps.user_id
)
return f"Tu assistes {username}. Date : {datetime.now().strftime('%Y-%m-%d')}"
# 4. Tool avec accès aux deux deps
@agent.tool
async def fetch_article(ctx: RunContext[AppDeps], url: str) -> str:
"""Récupère le contenu d'un article depuis son URL.
Args:
url: L'URL complète de l'article à analyser.
"""
resp = await ctx.deps.http.get(url)
return resp.text[:5000]
# 5. Exécution avec injection des deps
async def main():
async with asyncpg.create_pool("postgresql://...") as pool:
async with httpx.AsyncClient() as client:
deps = AppDeps(db=pool, http=client, user_id="usr_42")
result = await agent.run("Analyse l'article https://...", deps=deps)
Ce qui se passe sous le capot : deps_type=AppDeps ne crée pas d'instance — c'est un paramètre de type checking qui indique aux outils statiques (mypy, pyright) que ctx.deps est de type AppDeps. Les erreurs de type sont levées à la compilation, pas au runtime.
Fonctions sync vs async dans les tools
Les deux sont supportées. Les fonctions synchrones sont exécutées via run_in_executor dans un thread pool — aucun risque de bloquer la boucle asyncio. L'async def est préférable pour les opérations I/O.
6. Tools — du décorateur au schéma JSON
Pydantic AI génère automatiquement le schéma JSON des tools depuis les type hints Python et les docstrings. Le LLM reçoit exactement ce schéma comme spécification du tool call.
# Tool AVEC contexte (accès aux deps)
@agent.tool
async def search_documents(
ctx: RunContext[AppDeps],
query: str,
limit: int = 5,
min_score: float = 0.7
) -> list[dict]:
"""Recherche des documents dans la base de connaissances.
Args:
query: La requête de recherche en langage naturel.
limit: Nombre maximum de résultats (1-20).
min_score: Score de similarité minimum (0.0-1.0).
"""
return await ctx.deps.db.fetch(
"SELECT * FROM docs WHERE similarity(embedding, $1) > $2 LIMIT $3",
embed(query), min_score, limit
)
# Tool SANS contexte (@tool_plain)
@agent.tool_plain
def format_currency(amount: float, currency: str = "EUR") -> str:
"""Formate un montant monétaire.
Args:
amount: Le montant à formater.
currency: Code ISO 4217 de la devise (EUR, USD, GBP…).
"""
return f"{amount:,.2f} {currency}"
# Tool déclaré via constructeur (réutilisable entre agents)
from pydantic_ai import Tool
agent = Agent('openai:gpt-4o', tools=[Tool(search_documents), Tool(format_currency)])
Génération de schéma — les règles
RunContextest exclu du schéma JSON (paramètre interne, invisible pour le LLM)- Les docstrings au format Google, NumPy ou Sphinx sont détectés automatiquement — les sections
Args:alimentent les descriptions des paramètres - Si un tool a un seul paramètre complexe (dataclass, TypedDict, BaseModel), le schéma se simplifie à la définition de l'objet — le LLM passe directement l'objet, pas un wrapper
require_parameter_descriptions=TruedansAgent(...)force la documentation de tous les paramètres
Human-in-the-loop avec requires_approval
@agent.tool(requires_approval=True)
async def delete_record(ctx: RunContext[AppDeps], record_id: str) -> str:
"""Supprime définitivement un enregistrement. Nécessite approbation humaine."""
await ctx.deps.db.execute("DELETE FROM records WHERE id = $1", record_id)
return "Supprimé"
# Le run se met en pause et retourne DeferredToolRequests
result = await agent.run("Supprime l'enregistrement rec_123", deps=deps)
if isinstance(result, DeferredToolRequests):
for approval_req in result.approvals:
print(f"Approuver : {approval_req.tool_name}({approval_req.args})")
user_ok = input("Confirmer ? [y/n] ") == 'y'
# Construire DeferredToolResults avec ToolApproved / ToolDenied
7. Structured Output — 3 modes
Obtenir une sortie structurée fiable est le problème numéro un en production LLM. Pydantic AI propose trois modes distincts, avec des trade-offs différents :
Mode 1 — Tool Output (défaut)
Le schéma JSON du modèle Pydantic devient les paramètres d'un "output tool" spécial que le LLM doit appeler. Pour les unions de types, chaque type est un tool séparé.
from pydantic import BaseModel, Field
from pydantic_ai import Agent
class ExtractedEntity(BaseModel):
name: str
type: Literal['person', 'org', 'location', 'date']
confidence: float = Field(ge=0.0, le=1.0)
context: str
class ExtractionResult(BaseModel):
entities: list[ExtractedEntity]
language: str
ambiguities: list[str]
# Mode Tool Output — comportement par défaut
ner_agent = Agent(
'anthropic:claude-opus-4-6',
output_type=ExtractionResult,
output_retries=3, # Réessaie jusqu'à 3 fois si la validation Pydantic échoue
)
# Validation post-parsing : peut lever ModelRetry
@ner_agent.output_validator
async def validate_confidence(ctx, result: ExtractionResult) -> ExtractionResult:
low = [e.name for e in result.entities if e.confidence < 0.5]
if low:
from pydantic_ai import ModelRetry
raise ModelRetry(f"Confidence trop faible pour : {low}. Reconsidère ces entités.")
return result
Mode 2 — Native Output
Exploite les "Structured Outputs" natifs du provider (JSON Schema contraint côté API). Plus fiable pour les providers qui le supportent nativement (OpenAI, Anthropic), mais incompatible avec les tools simultanés sur Gemini. Si votre provider ne garantit pas un JSON strict, préférer explicitement ToolOutput (défaut) ou PromptedOutput — il n'y a pas de fallback automatique entre modes.
from pydantic_ai.output import NativeOutput
agent = Agent(
'openai:gpt-4o',
output_type=NativeOutput(ExtractionResult),
# Certains providers/modèles exposent un mode structuré natif
# — vérifier la compatibilité exacte côté provider avant d'utiliser ce mode
)
Mode 3 — Prompted Output
Le schéma JSON est injecté dans les instructions système. Fallback universel pour les modèles sans structured outputs natifs, mais moins fiable.
from pydantic_ai import Agent
from pydantic_ai.models.ollama import OllamaModel
from pydantic_ai.providers.ollama import OllamaProvider
from pydantic_ai.output import PromptedOutput
# Via OllamaModel + OllamaProvider (approche recommandée)
model = OllamaModel(
'qwen2.5:14b',
provider=OllamaProvider(base_url='http://localhost:11434/v1'),
)
agent = Agent(
model,
output_type=PromptedOutput(ExtractionResult),
)
Union de types comme output
class Success(BaseModel):
data: dict
execution_time_ms: int
class Failure(BaseModel):
error_code: str
message: str
recoverable: bool
# Chaque type devient un tool séparé pour le LLM
# → réduit la complexité du schéma JSON unique
agent = Agent('openai:gpt-4o', output_type=Success | Failure)
result = await agent.run("Execute this task...")
match result.output:
case Success(data=d): print(f"OK: {d}")
case Failure(message=m): print(f"Erreur: {m}")
8. Historique de messages et conversations multi-turn
Pydantic AI modélise chaque échange LLM comme une séquence typée de ModelMessage. La distinction entre requêtes (agent → LLM) et réponses (LLM → agent) est structurelle, pas juste documentaire.
# Hiérarchie des types
# ModelMessage = ModelRequest | ModelResponse
# ModelRequest → parts: SystemPromptPart | UserPromptPart | ToolReturnPart | RetryPromptPart
# ModelResponse → parts: TextPart | ToolCallPart | ThinkingPart (extended thinking)
agent = Agent('anthropic:claude-opus-4-6')
# Tour 1
r1 = await agent.run("Qu'est-ce que le PagedAttention ?")
print(r1.output)
# Tour 2 — passer new_messages() pour continuer la conversation
r2 = await agent.run(
"Comment ça se compare à Flash Attention ?",
message_history=r1.new_messages(), # Messages du tour 1
)
# Persistance : sérialisation/désérialisation JSON
from pydantic_ai import ModelMessagesTypeAdapter
from pydantic_core import to_json
messages = r2.all_messages()
json_bytes = to_json(messages) # → stocker en Redis, PostgreSQL, etc.
restored = ModelMessagesTypeAdapter.validate_json(json_bytes) # → restaurer
Instructions vs System Prompt — distinction critique
Quand message_history est fourni, les instructions des messages précédents sont exclues — seules les instructions de l'agent courant s'appliquent. Les system_prompt sont persistants ; les instructions sont scoped au run. Confondre les deux cause des bugs subtils en multi-agents.
History processors — contrôler la fenêtre de contexte
# Limiter l'historique envoyé au LLM à 10 derniers messages
def trim_history(messages, ctx):
return messages[-10:]
agent = Agent(
'openai:gpt-4o',
history_processors=[trim_history],
# Ou : résumer les anciens messages via un sous-agent
)
9. Multi-agents — 5 niveaux de complexité
Pydantic AI définit 5 niveaux d'architecture multi-agents, du plus simple au plus autonome :
| Niveau | Pattern | Cas d'usage |
|---|---|---|
| 1 | Agent unique | Tâches simples, un seul LLM |
| 2 | Agent delegation — agent as tool | Spécialisation par domaine |
| 3 | Programmatic hand-off | Séquençage contrôlé par le code |
| 4 | Graph-based control flow | Workflows avec cycles et conditions |
| 5 | Deep Agents | Planification autonome, exécution sandboxée |
Pattern "agent as tool" (niveau 2)
# Agents spécialisés
code_agent = Agent('openai:gpt-4o', system_prompt="Tu es un expert Python senior.")
review_agent = Agent('anthropic:claude-opus-4-6', system_prompt="Tu es un code reviewer.")
# Agent orchestrateur
orchestrator = Agent('openai:gpt-4o', deps_type=AppDeps)
@orchestrator.tool
async def generate_code(ctx: RunContext[AppDeps], spec: str) -> str:
"""Génère du code Python depuis une spécification."""
result = await code_agent.run(
spec,
deps=ctx.deps,
usage=ctx.usage, # CRITIQUE : agrège les tokens dans le run parent
)
return result.output
@orchestrator.tool
async def review_code(ctx: RunContext[AppDeps], code: str) -> str:
"""Effectue une revue de code et retourne les suggestions."""
result = await review_agent.run(
f"Revue ce code Python :\n{code}",
usage=ctx.usage,
)
return result.output
# Différents agents peuvent utiliser différents providers LLM
result = await orchestrator.run(
"Crée une fonction de tri avec complexité O(n log n) et fais-la réviser",
deps=deps
)
Passer usage=ctx.usage — règle d'or
Sans ce paramètre, les tokens utilisés par les sous-agents ne sont pas agrégés dans le RunUsage du run parent. Le tracking de coût et les usage_limits ne fonctionnent pas correctement. C'est un oubli courant qui cause des surprises de facturation.
10. pydantic-graph — la machine à états de l'agent loop
Le fait peu connu : l'agent loop de Pydantic AI est implémentée comme un graphe via pydantic-graph — une bibliothèque de machines à états génériques indépendante du framework agent. Comprendre cette architecture permet d'inspecter chaque étape de l'exécution.
# Flux interne :
# UserPromptNode → ModelRequestNode → CallToolsNode → (boucle ou End)
# Itération manuelle nœud par nœud via agent.iter() (API publique)
async with agent.iter("Quelle est la capitale de la France ?") as run:
async for node in run:
print(f"Nœud actif : {type(node).__name__}")
print(f"Résultat : {run.result.output}")
Définir ses propres graphes pydantic-graph
La signature architecturale unique de pydantic-graph : les arêtes sortantes d'un nœud sont déclarées par les annotations de type de retour. Le graphe est vérifiable statiquement.
from dataclasses import dataclass
from pydantic_graph import BaseNode, Graph, End, GraphRunContext
@dataclass
class State:
attempts: int = 0
result: str = ""
# Les arêtes sortantes sont déclarées dans les annotations de retour
@dataclass
class GenerateNode(BaseNode[State, None, str]):
prompt: str
async def run(self, ctx: GraphRunContext[State, None]) -> 'ValidateNode | End[str]':
ctx.state.attempts += 1
response = await llm_call(self.prompt)
ctx.state.result = response
if ctx.state.attempts > 3:
return End(response) # Forcer la terminaison
return ValidateNode(response)
@dataclass
class ValidateNode(BaseNode[State, None, str]):
content: str
async def run(self, ctx: GraphRunContext) -> 'GenerateNode | End[str]':
if is_valid(self.content):
return End(self.content)
return GenerateNode(f"Améliore cette réponse : {self.content}")
graph = Graph(nodes=[GenerateNode, ValidateNode])
result, final_state = await graph.run(GenerateNode("Explique la rétropropagation"), state=State())
11. MCP — Pydantic AI comme client MCP
Depuis mars 2025, Pydantic AI supporte le Model Context Protocol comme client. Un agent Pydantic AI peut utiliser n'importe quel serveur MCP comme source de tools.
from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStdio, MCPServerStreamableHTTP
# Transport 1 : subprocess via stdin/stdout
python_server = MCPServerStdio(
'uv',
args=['run', 'mcp-run-python', 'stdio'],
timeout=10,
tool_prefix='python', # Évite les conflits de noms de tools
)
# Transport 2 : HTTP Streamable (production)
db_server = MCPServerStreamableHTTP(
'http://mcp-db-service:8080/mcp',
read_timeout=300, # 5 min — défaut pour les tools longs
cache_tools=True, # Cache la liste des tools entre appels
)
# Passer les serveurs MCP comme toolsets
agent = Agent('openai:gpt-4o', toolsets=[python_server, db_server])
async def main():
async with python_server, db_server:
result = await agent.run(
"Calcule la moyenne des ventes du mois dernier depuis la DB et affiche un graphique"
)
# Chargement depuis fichier de config (style Claude Desktop)
from pydantic_ai.mcp import load_mcp_servers
servers = load_mcp_servers('mcp_config.json') # Supporte ${VAR:-default}
12. Testing — TestModel et FunctionModel
La testabilité est un first-class citizen dans Pydantic AI. Deux outils permettent de tester les agents sans appel LLM réel.
from pydantic_ai.models.test import TestModel
from pydantic_ai import capture_run_messages
import pytest
# Bloquer les appels réels en test — à placer dans conftest.py, pas dans les tests
# import pydantic_ai.models as _m; _m.ALLOW_MODEL_REQUESTS = False
# Préférer agent.override(model=TestModel()) qui est scoped au contexte
# TestModel : appelle TOUS les tools et génère des données valides
# C'est du Python procédural pur — aucun ML impliqué
@pytest.mark.asyncio
async def test_agent_calls_tools():
with my_agent.override(model=TestModel()):
with capture_run_messages() as messages:
result = await application_code("Analyse les données")
# Vérifier que les tools ont été appelés
tool_calls = [m for m in messages if hasattr(m, 'parts')]
assert any(p.tool_name == 'fetch_data' for m in tool_calls for p in m.parts)
# FunctionModel : logique de réponse entièrement contrôlée
from pydantic_ai.models.function import FunctionModel, AgentInfo, ModelFuncContext
def mock_model(messages, info: AgentInfo):
# Décider des tool calls et réponses en Python pur
if "capital" in str(messages[-1]).lower():
return "Paris est la capitale de la France."
return "Je ne sais pas."
async def test_specific_behavior():
with my_agent.override(model=FunctionModel(mock_model)):
result = await my_agent.run("Quelle est la capitale de la France ?")
assert "Paris" in result.output
TestModel — aucun ML, 100% déterministe
TestModel génère des données qui satisfont le schéma JSON des tools via du code Python procédural pur. Les valeurs générées passent la validation Pydantic mais ne sont pas sémantiquement significatives. C'est conçu pour tester la structure de l'agent (quels tools sont appelés, dans quel ordre) — pas la qualité des réponses LLM.
13. Observabilité — Logfire et OpenTelemetry
Pydantic AI est instrumenté nativement avec OpenTelemetry. Logfire est le backend SaaS recommandé, mais tout backend OTEL compatible fonctionne (Jaeger, Tempo, Grafana, Datadog…).
import logfire
# Option 1 : Logfire SaaS (une ligne)
logfire.configure()
logfire.instrument_pydantic_ai()
# Option 2 : OpenTelemetry générique (Jaeger, Tempo, Datadog...)
from pydantic_ai import Agent
Agent.instrument_all() # Instrumente tous les agents du process
# Option 3 : par agent
agent = Agent('openai:gpt-4o', instrument=True)
# Ce qui est tracé automatiquement :
# - Span par run d'agent (avec run_id, conversation_id)
# - Span par requête modèle (latence, model_name)
# - Span par tool call (nom, arguments, résultat, durée)
# - Token usage par provider
# - Cost tracking agrégé cross-agents
L'avantage de Logfire sur les outils d'observabilité AI-only (LangSmith, Phoenix) : il trace l'ensemble du stack applicatif — requêtes PostgreSQL, appels HTTP, queues — pas seulement la couche LLM. En production, la majorité des bugs viennent de l'infrastructure, pas du LLM.
14. pydantic-evals — évaluer ses agents
pydantic-evals est un framework d'évaluation indépendant de pydantic-ai, conçu pour évaluer n'importe quelle "fonction stochastique" — agents, pipelines RAG, fonctions de preprocessing.
from pydantic_evals import Dataset, Case
from pydantic_evals.evaluators import EqualsExpected, IsInstance, LLMJudge
# Définir les cas de test
dataset = Dataset(
cases=[
Case(
name="sentiment_positif",
inputs={"text": "Ce produit est fantastique !"},
expected_output=SentimentAnalysis(sentiment="positive", confidence=0.9, key_points=[]),
),
Case(
name="sentiment_negatif",
inputs={"text": "Service client déplorable."},
expected_output=SentimentAnalysis(sentiment="negative", confidence=0.85, key_points=[]),
),
],
evaluators=[
EqualsExpected(fields=["sentiment"]), # Vérifie le champ sentiment uniquement
IsInstance(SentimentAnalysis), # Vérifie le type de retour
LLMJudge( # Évaluateur LLM pour qualité subjective
prompt="Les key_points sont-ils pertinents ? Score 0-1.",
model='openai:gpt-4o-mini',
),
]
)
# Fonction à évaluer
async def run_sentiment(inputs: dict) -> SentimentAnalysis:
result = await ner_agent.run(inputs["text"])
return result.output
# Lancer l'évaluation
report = await dataset.evaluate(run_sentiment)
print(report.summary())
# → accuracy: 2/2, avg_llm_score: 0.87, duration_ms: 1240
Les évaluateurs peuvent retourner : bool (pass/fail), float (score 0–1), str (label catégoriel comme "hallucination"), ou EvaluationReason (résultat annoté). Cette flexibilité permet de mixer assertions déterministes et jugements LLM dans le même dataset.
15. Capabilities — comportements composables (v1)
Les Capabilities sont le concept central de la v1 : des unités de comportement réutilisables qui encapsulent tools + lifecycle hooks + instructions + model settings. Elles permettent de composer des comportements sans sous-classer Agent.
from pydantic_ai.capabilities import Thinking, WebSearch, WebFetch
# Built-in capabilities — stables
agent = Agent(
'anthropic:claude-opus-4-6',
capabilities=[
Thinking(effort='high'), # Raisonnement étendu cross-provider
WebSearch(), # Recherche web native ou DuckDuckGo fallback
WebFetch(), # Lecture de pages web
]
)
# Les capabilities custom (AbstractCapability) permettent d'encapsuler
# tools + lifecycle hooks + instructions. L'API des hooks custom évolue
# entre les mineures — consulter la doc de votre version exacte.
5 phases de lifecycle hooks
- Run-level — avant/après l'ensemble du run
- Model requests —
before_model_request,after_model_request - Tool validation — avant la validation des arguments du tool
- Tool execution —
wrap_tool_execute(middleware style) - Output processing — avant/après la validation du résultat final
La sémantique de composition : les hooks before_* s'exécutent dans l'ordre de déclaration (cap1 → cap2 → cap3), les after_* en ordre inversé, les wrap_* comme des middlewares imbriqués (cap1 = couche externe).
API Capabilities encore évolutive
Les noms de hooks (before_model_request, wrap_tool_execute…) ont évolué entre les mineures. Avant d'implémenter des capabilities custom en production, vérifier la doc correspondant à votre version exacte via pydantic-ai --version.
16. Pydantic AI vs LangChain vs LangGraph : comparaison honnête
| Critère | Pydantic AI | LangChain | LangGraph |
|---|---|---|---|
| Type safety | Native, compilateur | Optionnelle | Partielle |
| Structured output | 3 modes, auto-retry | Responsabilité dev | Via Pydantic |
| Injection de dépendances | Explicite, type-safe | Absent | Via state dict |
| Testing intégré | TestModel, FunctionModel | Mocks manuels | Mocks manuels |
| Observabilité | OpenTelemetry standard | LangSmith (propriétaire) | LangSmith |
| Écosystème intégrations | OpenAI, Anthropic, Gemini, Mistral, Groq, Ollama, OpenRouter, LiteLLM, Bedrock… | 1000+ intégrations | — |
| Adoption | En forte croissance | Très large | — |
| Stable API v1 | Sept. 2025 | Mature | GA mai 2025 |
| Courbe d'apprentissage | Raide (generics) | Accessible | Intermédiaire |
Recommandation terrain (2026)
Une combinaison émerge sur les projets production : Pydantic AI pour les behaviors d'agents individuels (type safety, DI, testing) + LangGraph pour l'orchestration globale (state management, routing complexe). Les deux sont interopérables — un LangGraph node peut appeler un agent Pydantic AI.
Sur les performances, les gains observés en production viennent principalement de l'élimination des retry loops silencieux de LangChain et de la réduction des tokens consommés (pas d'orchestration interne verbose). Les chiffres varient fortement selon le workload — benchmarker sur votre propre cas d'usage reste la seule mesure fiable.
17. FAQ
Conclusion
Pydantic AI v1.89.0 est aujourd'hui l'un des frameworks Python les plus solides pour construire des agents IA maintenables : type safety à la compilation, testabilité déterministe, injection de dépendances explicite et ergonomie proche de FastAPI.
La décision architecturale clé — l'agent loop comme graphe pydantic-graph vérifiable statiquement — est ce qui le distingue structurellement des frameworks où la boucle d'exécution est opaque. Pour les équipes qui investissent sur la durée, c'est un avantage décisif.