DEV-AI
Pydantic AI v1.89.0 Publié le 1 mai 2026 Python ≥ 3.10

Pydantic AI en Python 2026 :
agents type-safe et structured output

Samuel Colvin (auteur de Pydantic) a construit ce framework parce qu'aucun agent framework existant ne répondait à ses standards d'ergonomie et de solidité en production. Voici pourquoi c'est différent — et comment l'utiliser vraiment.

MIT License · Stable depuis v1.0.0 — sept. 2025 · 4 packages indépendants

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)

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 :

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 :

Les décisions architecturales qui distinguent Pydantic AI :

DécisionJustification
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-graphColvin 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 :

PackageContenuDépend de
pydantic-aiPackage complet (core + tous les extras)pydantic-ai-slim
pydantic-ai-slimCore framework seul, sans SDK vendeurspydantic v2, anyio
pydantic-graphMachine à états génériques, indépendant de pydantic-aipydantic v2
pydantic-evalsFramework d'évaluation, indépendant de pydantic-aipydantic v2, logfire (optionnel)
bash
# 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.

python
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éthodeCas d'usageType retourné
agent.run_sync(...)Scripts, tests, CLIAgentRunResult
await agent.run(...)FastAPI, applications asyncAgentRunResult
async with agent.run_stream(...) as s:Streaming token par tokenStreamedRunResult
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 grapheAgentRun

AgentRunResult — accès aux données

python
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.

python
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.

python
# 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

Human-in-the-loop avec requires_approval

python
@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é.

python
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.

python
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.

python
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

python
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.

python
# 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

python
# 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 :

NiveauPatternCas d'usage
1Agent uniqueTâches simples, un seul LLM
2Agent delegation — agent as toolSpécialisation par domaine
3Programmatic hand-offSéquençage contrôlé par le code
4Graph-based control flowWorkflows avec cycles et conditions
5Deep AgentsPlanification autonome, exécution sandboxée

Pattern "agent as tool" (niveau 2)

python
# 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.

python
# 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.

python
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.

python
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.

python
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…).

python
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.

python
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.

python
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

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èrePydantic AILangChainLangGraph
Type safetyNative, compilateurOptionnellePartielle
Structured output3 modes, auto-retryResponsabilité devVia Pydantic
Injection de dépendancesExplicite, type-safeAbsentVia state dict
Testing intégréTestModel, FunctionModelMocks manuelsMocks manuels
ObservabilitéOpenTelemetry standardLangSmith (propriétaire)LangSmith
Écosystème intégrationsOpenAI, Anthropic, Gemini, Mistral, Groq, Ollama, OpenRouter, LiteLLM, Bedrock…1000+ intégrations
AdoptionEn forte croissanceTrès large
Stable API v1Sept. 2025MatureGA mai 2025
Courbe d'apprentissageRaide (generics)AccessibleIntermé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

Pydantic AI vs LangChain : quelle différence fondamentale ?

La différence fondamentale est l'architecture de type safety. Dans LangChain, les types sont souvent perdus au profit de dict et Any à travers les chains. Dans Pydantic AI, Agent[DepsT, OutputT] est un contrat vérifiable : mypy/pyright valident que vos tools reçoivent les bons types de dépendances et que votre code gère correctement le type de sortie. Sur un codebase de 50k+ lignes, cette différence est critique — le type checker détecte des bugs que les tests manuels manquent.

Pydantic AI fonctionne-t-il avec Ollama ?

Oui — via OllamaModel + OllamaProvider (approche recommandée par la doc officielle) :

from pydantic_ai.models.ollama import OllamaModel
from pydantic_ai.providers.ollama import OllamaProvider

model = OllamaModel('llama3.2:3b', provider=OllamaProvider(base_url='http://localhost:11434/v1'))
agent = Agent(model)

Pour les structured outputs locaux, préférer PromptedOutput — les garanties JSON strict varient selon le modèle et sa version Ollama.

Comment intégrer Pydantic AI avec FastAPI ?
@app.post("/analyze")
async def analyze(request: AnalysisRequest, db: asyncpg.Pool = Depends(get_db)):
    deps = AppDeps(db=db, user_id=request.user_id)
    result = await agent.run(request.text, deps=deps)
    return result.output

Passer le pool DB via FastAPI Depends() et le réinjecter dans les deps Pydantic AI. Pour le streaming, utiliser agent.run_stream() avec une StreamingResponse FastAPI.

Peut-on utiliser Pydantic AI sans Logfire ?

Oui. Logfire est optionnel — Pydantic AI utilise OpenTelemetry standard. Agent.instrument_all() configure l'instrumentation. N'importe quel backend OTEL fonctionne : Jaeger, Grafana Tempo, Honeycomb, Datadog, etc. Logfire est recommandé pour sa DX, pas obligatoire.

Pydantic AI supporte-t-il le streaming des structured outputs ?

Oui via agent.run_stream(). Le StreamedRunResult expose stream_text() (stream brut), stream_structured() (structured output partiellement construit au fur et à mesure) et get_output() (attendre la complétion). Pour les événements de tool calls pendant le stream, utiliser agent.run_stream_events().

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.

Articles liés

Serving LLM

vLLM Python 2026 : PagedAttention et V1 Engine

Orchestration

LangGraph 2026 : agents stateful avec Ollama

Protocole

MCP : le standard Anthropic qui connecte les LLMs

Production

FastAPI streaming LLM : SSE et StreamingResponse