IA en production avec PHP : arrêtez le spaghetti code et adoptez le pattern Task
Vous intégrez votre premier appel à un LLM. Une ligne, une réponse, ça marche. Six mois plus tard, vous avez dix-sept de ces appels dispersés dans votre codebase, aucune visibilité sur les coûts, pas de gestion des erreurs, et un client qui vient de dépenser 300 € en une nuit à cause d'une boucle mal maîtrisée.
Ce scénario, quasi universel, illustre un problème architectural : un appel API synchrone ne suffit pas à faire tourner de l'IA en production. Que vous travailliez sous Laravel ou Symfony, le constat est le même. Cet article s'appuie sur une approche documentée par la communauté Laravel (source originale sur dev.to) pour en tirer les principes applicables à tout projet PHP sérieux.
Le piège de l'appel synchrone
L'intégration naïve ressemble à ceci :
$response = $openAiClient->chat()->create([
'model' => 'gpt-4o',
'messages' => [['role' => 'user', 'content' => $prompt]],
]);
Simple, lisible, fonctionnel en dev. Mais en production, ce pattern pose plusieurs problèmes critiques :
- Pas de retry : une erreur réseau ou un timeout OpenAI fait planter la requête utilisateur.
- Pas de traçabilité : impossible de savoir combien de tokens ont été consommés, par qui, pour quel usage.
- Pas de budget : un seul client ou une seule feature peut générer des coûts exponentiels sans aucune alerte.
- Pas de priorisation : une génération d'image en batch bloque la file au même niveau qu'une réponse temps réel.
- Tests impossibles : vos tests unitaires frappent l'API réelle ou nécessitent des mocks fragiles.
Ces problèmes n'apparaissent pas le premier jour. Ils s'accumulent, silencieusement, jusqu'à ce qu'ils deviennent des incidents de production.
Le pattern Task : encapsuler, orchestrer, observer
La solution architecturale consiste à encapsuler chaque interaction IA dans une classe dédiée, et à déléguer l'exécution, la persistance et le monitoring à une couche d'orchestration.
Concrètement, chaque "tâche IA" devient un objet avec une responsabilité claire :
// Exemple de structure en Symfony
class SummarizeDocumentTask
{
public function __construct(
private readonly string $content,
private readonly int $userId,
) {}
public function getPrompt(): string
{
return "Résume ce document en 3 points clés :\n\n{$this->content}";
}
public function getModel(): string
{
return 'gpt-4o-mini';
}
}
Cette approche offre plusieurs avantages immédiats :
1. Testabilité — La logique de construction du prompt est isolée et testable sans aucun appel réseau.
2. Réutilisabilité — La même tâche peut être exécutée en synchrone, en asynchrone via une queue Messenger, ou en streaming.
3. Lisibilité — Un développeur qui arrive sur le projet comprend immédiatement l'intention métier de chaque tâche.
Les couches indispensables en production
Le pattern Task seul ne suffit pas. Il doit s'accompagner de plusieurs mécanismes que toute architecture IA mature doit intégrer.
🗄️ Audit et traçabilité
Chaque exécution doit être persistée : prompt envoyé, réponse reçue, nombre de tokens, coût estimé, statut (succès, échec, retry). Une table dédiée (ai_runs dans l'approche Laravel documentée) permet de déboguer, d'auditer et d'analyser les usages.
CREATE TABLE ai_runs (
id BIGINT PRIMARY KEY,
task_class VARCHAR(255),
user_id BIGINT,
prompt TEXT,
response TEXT,
tokens_input INT,
tokens_output INT,
cost_usd DECIMAL(10,6),
status VARCHAR(50),
created_at TIMESTAMP
);
💸 Suivi des coûts par tenant
Dans un contexte multi-tenant, chaque organisation doit avoir un budget alloué. La couche d'orchestration doit vérifier avant chaque exécution si le budget est dépassé, et lever une exception métier explicite plutôt que de laisser les coûts s'envoler.
🔁 Retry et fallback
Les APIs LLM sont faillibles. Un mécanisme de retry avec backoff exponentiel est non négociable. Mieux encore : définissez des chaînes de fallback — si gpt-4o est indisponible, tentez gpt-4o-mini, puis loggez une alerte.
🧪 Faux providers pour les tests
Votre suite de tests ne doit jamais frapper une API réelle. Implémentez un FakeAiProvider qui retourne des réponses prévisibles et permet des assertions précises :
// Dans vos tests Symfony
$this->aiProvider->fake([
SummarizeDocumentTask::class => 'Réponse simulée pour les tests.',
]);
$this->aiProvider->assertTaskExecuted(SummarizeDocumentTask::class);
$this->aiProvider->assertTaskExecutedTimes(SummarizeDocumentTask::class, 1);
Ce que cela change concrètement
En adoptant cette architecture, vous passez d'une situation où l'IA est un détail d'implémentation éparpillé à une situation où elle est un composant de première classe, observable et gouvernable.
| Avant | Après |
|---|---|
| Appels dispersés dans les services | Tâches centralisées et nommées |
| Aucune visibilité sur les coûts | Dashboard avec coûts par tâche et par tenant |
| Tests qui frappent l'API réelle | Faux providers avec assertions |
| Pas de retry | Retry configurable avec fallback |
| Files de priorité unique | Queues différenciées par criticité |
L'approche décrite dans le package fomvasss/laravel-ai-tasks pour Laravel matérialise ces principes avec un outillage prêt à l'emploi. Pour les projets Symfony, les mêmes patterns s'appliquent en combinant Messenger pour les queues, Doctrine pour la persistance des runs, et une interface AiTaskInterface à définir selon vos besoins.
Conclusion
L'appel API synchrone est un excellent point de départ. C'est un très mauvais point d'arrivée.
Dès que votre application IA dépasse le stade du prototype, vous avez besoin d'une couche d'orchestration : traçabilité, gestion des coûts, retry, tests isolés, et priorisation des queues. Le pattern Task vous donne cette structure sans sacrifier la lisibilité du code.
Chez MulerTech, nous appliquons ces principes dans nos projets Symfony dès que l'IA sort du bac à sable. Si vous souhaitez échanger sur l'intégration de LLM dans votre stack PHP, n'hésitez pas à nous contacter.
📖 Article original : Stop Writing AI Spaghetti in Laravel — dev.to