Introduction : Le défi du test des agents IA
Construire des agents intelligents avec des modèles de langage de grande taille (LLM) est passionnant, mais cela introduit des défis majeurs en génie logiciel. Contrairement au code déterministe traditionnel, les agents LLM reposent sur des API externes, des sorties stochastiques et une gestion d'état complexe. Cela rend le test difficile. Comment vérifier la logique lorsque le « cerveau » de votre application est un appel d'API non déterministe ?
La solution réside dans des motifs de conception logicielle stricts, en particulier l'injection de dépendances (DI). PydanticAI, un framework Python moderne pour la construction d'applications LLM, excelle en la matière en exploitant les annotations de types et les modèles pydantic pour gérer proprement les dépendances. En isolant le client LLM de votre logique métier, vous pouvez écrire des tests unitaires rapides et fiables qui ne vous coûtent pas d'argent en jetons d'API et qui évitent les problèmes de réseau instables.
Pourquoi l'injection de dépendances est importante pour les agents
Dans un flux de travail agentique typique, vous pourriez avoir un agent qui recherche sur le web, traite les résultats, puis génère un résumé. Si l'outil de recherche web est codé en dur dans l'agent, vous ne pouvez pas tester la logique de résumé sans réellement interroger un moteur de recherche.
L'injection de dépendances vous permet de remplacer ces dépendances. En production, vous injectez un `WebSearchTool` réel. Dans les tests, vous injectez un `MockWebSearchTool` qui renvoie des données prédéfinies. Ce motif garantit que vos tests unitaires se concentrent strictement sur la logique de votre agent, et non sur la fiabilité des services externes.
Mise en place d'un agent testable
D'abord, définissons un agent simple qui utilise une dépendance. Nous utiliserons le système de PydanticAI pour gérer les dépendances d'outils.
from pydantic_ai import Agent
from pydantic_ai.models import Model
from pydantic_ai.toolsets import Toolset
from pydantic import BaseModel
class Fact(BaseModel):
fact: str
source: str
# Définir un Toolset personnalisé pour gérer les dépendances
class FactToolset(Toolset):
model: Model
facts: list[Fact]
def __init__(self, model: Model, facts: list[Fact]):
self.model = model
self.facts = facts
@property
def get_facts(self):
"""Retourne une liste de faits connus."""
return self.facts
# Définir l'Agent
agent = Agent[FactToolset](
model='openai:gpt-4o',
system_prompt='Vous êtes un vérificateur de faits. Utilisez les faits fournis pour répondre aux questions.',
)
async def main():
# En production, vous injecteriez des faits réels
toolset = FactToolset(model=agent.model, facts=[Fact(fact="L'eau est mouillée", source="Physique")])
try:
result = await agent.run("L'eau est-elle mouillée ?", toolset=toolset)
print(result.output)
finally:
await toolset.aclose()
Tests unitaires avec des mocks
Maintenant, nous pouvons écrire un test unitaire. La clé est de mocker le `Model` lui-même afin de ne pas effectuer d'appels d'API réels. PydanticAI fournit un `MockModel` à cet effet.
import pytest
from pydantic_ai import Agent
from pydantic_ai.models import MockModel
from pydantic_ai.toolsets import Toolset
from pydantic import BaseModel
from pydantic_ai.messages import ModelResponse, TextPart, ToolCallPart
class Fact(BaseModel):
fact: str
source: str
class FactToolset(Toolset):
def __init__(self, facts: list[Fact]):
self.facts = facts
def get_facts(self):
return self.facts
# Créer l'agent avec un modèle placeholder. Le modèle réel sera injecté plus tard.
agent = Agent[FactToolset](
model='mock',
system_prompt='Vous êtes un vérificateur de faits.'
)
@pytest.mark.anyio
async def test_agent_facts():
# 1. Définir la réponse attendue du LLM
# Le LLM devrait appeler l'outil 'get_facts'
mock_response = ModelResponse(
parts=[
ToolCallPart(tool_name='get_facts', args={}),
TextPart(content='D\'après les faits, l\'eau est mouillée.')
]
)
# 2. Créer un MockModel qui renvoie notre réponse prédéfinie
mock_model = MockModel(messages=[mock_response])
# 3. Créer le toolset avec des données de test
test_facts = [Fact(fact="L'eau est mouillée", source="Source de test")]
toolset = FactToolset(facts=test_facts)
# 4. Exécuter l'agent avec le modèle mock et le toolset
# Note : Nous injectons le modèle mock dans le contexte de l'agent
async with agent.run_with_tools("L'eau est-elle mouillée ?", toolset=toolset, model=mock_model) as result:
assert result.output == 'D\'après les faits, l\'eau est mouillée.'
Meilleures pratiques pour des tests robustes
1.
Isoler l'état : Assurez-vous que chaque test crée une nouvelle instance du toolset. Évitez l'état partagé entre les tests pour prévenir la contamination.
2.
Valider les outils : Dans vos tests, vérifiez que les bons outils ont été appelés. Vous pouvez inspecter `result.all_messages` pour affirmer que des objets `ToolCallPart` avec des noms spécifiques existent.
3.
Utiliser des sorties structurées : PydanticAI prend en charge les sorties structurées (modèles pydantic). Testez que la réponse du LLM respecte votre schéma. C'est crucial pour le traitement des données en aval.
Conclusion
La mise en œuvre d'agents LLM nécessite un changement de mentalité, passant du « scripting » à l'« ingénierie ». En exploitant les capacités d'injection de dépendances de PydanticAI, vous pouvez construire des agents qui sont non seulement fonctionnels, mais aussi testables et maintenables.
La combinaison de `MockModel` pour les réponses LLM et de `Toolsets` personnalisés pour les dépendances de données vous permet de découpler votre logique de la nature non déterministe de l'IA. Commencez par isoler vos outils, mockez vos modèles, et vous poserez les fondations d'applications IA fiables qui peuvent évoluer avec confiance.