LOÏC.SAPONE

Symfony Developer

Retour

symfony/ai : retour d'expérience sur la boîte à outils IA de Symfony

8 min de lecture

Intégrer un LLM dans une application PHP, jusqu'ici, ça voulait dire écrire un client HTTP maison au-dessus de l'API d'OpenAI ou de Mistral, puis le réécrire trois mois plus tard quand l'API change. Symfony s'attaque au sujet avec symfony/ai, un ensemble de composants officiels pour construire des agents, du RAG ou des chatbots directement dans vos applications.

J'ai passé plusieurs semaines à tester le projet, jusqu'à y contribuer un bridge pour OVHcloud. Voici ce qui fonctionne bien, et ce qui coince encore.

Des briques à la carte

symfony/ai n'est pas un gros framework monolithique mais une collection de composants qu'on assemble selon ses besoins. Platform fournit une interface unifiée vers les fournisseurs de modèles (OpenAI, Anthropic, Gemini, Mistral, Ollama, OVHcloud et une trentaine d'autres). Agent orchestre les appels au modèle, les tools et les workflows. Store abstrait le stockage vectoriel pour le RAG, avec des bridges vers Postgres (pgvector), MariaDB, Meilisearch, Qdrant, ChromaDB, entre autres. Chat gère l'historique de conversation, et deux bundles (AI Bundle et MCP Bundle) branchent tout ça dans le conteneur de services.

Schéma des composants symfony/ai : l'AI Bundle au-dessus d'Agent, reposant sur la couche fondation Platform, Store et Tools, elle-même branchée sur les bridges tiers

Ce découpage est pour moi le vrai point fort du projet. Besoin d'un simple appel à un modèle ? Platform seul suffit. Besoin d'un agent avec des tools ? On ajoute Agent. Le RAG vient plus tard ? Store s'ajoute sans rien casser. On ne subit pas l'architecture d'un framework tout-en-un pensé pour un cas d'usage qui n'est pas le nôtre.

Tous les composants sont encore marqués comme expérimentaux : ils ne sont pas couverts par la promesse de rétrocompatibilité de Symfony. À garder en tête avant de partir en production.

Un agent en quelques lignes

Si vous connaissez Symfony, la prise en main est immédiate. On configure une plateforme et un agent en YAML :

# config/packages/ai.yaml
ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'
    agent:
        product_assistant:
            model: 'gpt-4o-mini'
            prompt: 'Tu es un assistant produit pour une boutique en ligne. Réponds de façon concise.'

Et on injecte l'agent comme n'importe quel service :

namespace App\Assistant;

use Symfony\AI\Agent\AgentInterface;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

final readonly class ProductAssistant
{
    public function __construct(
        private AgentInterface $productAssistantAgent,
    ) {
    }

    public function ask(string $question): string
    {
        $messages = new MessageBag(
            Message::ofUser($question),
        );

        return $this->productAssistantAgent->call($messages)->getContent();
    }
}

Pour donner des capacités concrètes à l'agent, on déclare des tools : de simples services annotés avec l'attribut #[AsTool]. Le composant génère le schéma JSON attendu par le modèle à partir des types PHP et des docblocks. C'est du Symfony pur jus.

namespace App\Assistant\Tool;

use App\Repository\ProductRepository;
use Symfony\AI\Agent\Toolbox\Attribute\AsTool;

#[AsTool('product_stock', 'Retourne le stock disponible pour une référence produit')]
final readonly class ProductStockTool
{
    public function __construct(
        private ProductRepository $productRepository,
    ) {
    }

    /**
     * @param string $sku La référence du produit
     */
    public function __invoke(string $sku): array
    {
        $product = $this->productRepository->findOneBySku($sku);

        return [
            'sku' => $sku,
            'quantity' => $product?->getStock() ?? 0,
            'available' => null !== $product && $product->getStock() > 0,
        ];
    }
}

L'agent décide seul quand appeler le tool, enchaîne les appels si nécessaire, et un garde-fou limite le nombre d'itérations pour éviter les boucles infinies (et la facture qui va avec). Sur un projet e-commerce, brancher un assistant sur le stock, les commandes ou le catalogue prend une après-midi. Il y a un an, le même sujet représentait des semaines de développement.

Le RAG sans quitter le framework

Le composant Store couvre le pipeline RAG classique : découper des documents, les vectoriser, les stocker, puis retrouver les plus pertinents au moment de la requête. L'indexation tient en quelques lignes :

use Symfony\AI\Store\Document\TextDocument;
use Symfony\AI\Store\Document\Vectorizer;
use Symfony\AI\Store\Indexer\DocumentIndexer;
use Symfony\AI\Store\Indexer\DocumentProcessor;

$vectorizer = new Vectorizer($platform, $model);
$indexer = new DocumentIndexer(new DocumentProcessor($vectorizer, $store));

$indexer->index(new TextDocument(
    'product-4021',
    'Veste imperméable Alpine : membrane 20 000 mm, coutures étanchées, capuche ajustable.',
));

Et la recherche par similarité passe par le Retriever, qui vectorise la question et interroge le store :

use Symfony\AI\Store\Retriever;

$retriever = new Retriever($store, $vectorizer);
$documents = $retriever->retrieve('Quelle veste pour randonner sous la pluie ?');

Le bundle fournit aussi des commandes ai:store:setup, ai:store:clear et ai:store:drop pour gérer le cycle de vie du store. Des loaders sont fournis pour charger du texte, du Markdown, du CSV ou des flux RSS, et l'interface LoaderInterface permet d'écrire le sien, par exemple pour indexer des entités Doctrine.

Ce qui coince encore

Le projet est jeune, et ça se voit sur certaines briques. Le cas des stores vectoriels m'a le plus gêné : ils sont pensés pour vivre seuls. Le store Postgres, par exemple, veut sa table gérée par ai:store:setup, avec son schéma à lui. Faire cohabiter proprement des vecteurs à côté de vos données métier existantes, dans une base déjà en place avec ses migrations Doctrine, demande de réécrire une bonne partie du fonctionnement par-dessus. En pratique, soit vous dédiez une base au store, soit vous recodez la couche de persistance. Pour de l'e-commerce où les embeddings ont du sens collés aux fiches produits, c'est frustrant.

Autre irritant : les bridges de Platform ne sont pas tous au même niveau. Certains exposent finement les options et capacités de leur plateforme, d'autres restent minimalistes, alors même que les plateformes proposent des options identiques (structured output, streaming, appels de tools). Selon le fournisseur choisi, on n'a pas accès aux mêmes possibilités, ce qui affaiblit la promesse d'interface unifiée.

Enfin, le rythme des APIs d'IA pose un problème structurel. Les modèles, leurs capacités et les options changent toutes les semaines, et le code n'est pas conçu pour évoluer aussi vite. Les catalogues de modèles de certaines plateformes sont mis à jour par une tâche GitHub, mais il faut attendre une release pour en profiter, et toutes les plateformes ne sont pas couvertes. Résultat : un modèle sorti hier peut rester inutilisable un moment, sauf à le déclarer soi-même. Ce n'est pas un défaut d'implémentation, c'est une tension de fond entre le cycle de release d'une librairie et celui des APIs d'IA, et je n'ai pas de solution miracle à proposer.

Lire le code pour comprendre l'IA

Un bénéfice inattendu de symfony/ai : il démystifie le fonctionnement des applications IA. Tout ce que les frameworks propriétaires cachent derrière de la magie est ici du PHP lisible. Comment un appel de tool fonctionne ? Le composant Agent montre la boucle complète : le schéma JSON généré depuis les types PHP, la réponse du modèle qui demande un appel, l'exécution du service, le résultat renvoyé dans la conversation. Comment un RAG est monté ? Le pipeline filter, transform, vectorize, store est écrit noir sur blanc dans le DocumentProcessor. Comment optimiser la mémoire d'une conversation ? Le composant Chat expose ses stratégies.

Un LLM appelé avec les bons messages, des tools décrits en JSON, une recherche par similarité : une fois qu'on a lu ce code, l'IA générative redevient de l'ingénierie logicielle classique. Pour se former, c'est une meilleure porte d'entrée que n'importe quel article de vulgarisation.

Et l'équipe derrière le projet joue le jeu. Quand j'ai proposé un bridge pour les AI Endpoints d'OVHcloud, la contribution a été accueillie et intégrée rapidement, il est aujourd'hui disponible via symfony/ai-ovh-platform. Le projet avance vite quand il s'agit de combler des manques, et c'est plutôt bon signe pour la suite : les défauts listés plus haut sont connus des mainteneurs et ont une chance raisonnable d'être gommés.

Give it a try!

symfony/ai n'est pas encore mûr pour tous les usages, mais il couvre déjà assez de besoins pour livrer des fonctionnalités qui auraient demandé des semaines de développement il y a peu. Si vous êtes sur du Symfony, c'est le point de départ naturel pour vos sujets IA, et le code source est en bonus une excellente école.

La documentation officielle couvre chaque composant avec des exemples, et le dépôt GitHub contient un répertoire d'exemples exécutables pour à peu près chaque cas d'usage. Les contributions sont les bienvenues, surtout sur les bridges.

Vous avez aimé cet article ?

Je publie régulièrement des articles sur Symfony, PHP et l'architecture logicielle. N'hésitez pas à me suivre sur Bluesky ou GitHub pour ne rien manquer.