Aller au contenu principal
Retour au blog

Symfony AI Platform : une seule API pour piloter tous tes LLM

Symfony AI Spotlight dévoile Platform : une abstraction unique pour appeler OpenAI, Claude, Mistral ou Gemini sans réécrire ton code métier à chaque provider.

Flavien Métivier · 8 min de lecture
Sommaire · 8 parties

En juin 2026, l'équipe Symfony a lancé sur son blog officiel une nouvelle série baptisée « Symfony AI Spotlight », dont le premier volet — « One API to Rule Them All » — s'attaque à un problème que toute équipe PHP ayant expérimenté l'intégration d'un LLM connaît bien : chaque fournisseur (OpenAI, Anthropic, Mistral, Google) expose son propre SDK, ses propres conventions de message, sa propre gestion des erreurs et sa propre facturation. On a déjà présenté l'initiative Symfony AI dans son ensemble sur ce blog ; cet article se concentre sur un angle plus précis et tout aussi structurant pour l'architecture d'une application : le composant Platform, sa promesse d'une API unique pour piloter n'importe quel LLM, et ce que ça change concrètement le jour où il faut changer de fournisseur.

Le problème que Platform vient résoudre : la fragmentation des SDK

Construire une intégration LLM directement sur le SDK d'un fournisseur, c'est accepter un couplage fort dès la première ligne de code. Le format des messages, la structure des réponses, la gestion du streaming et même le vocabulaire des paramètres (temperature, max_tokens, top_p) diffèrent d'un provider à l'autre. Changer de fournisseur — parce que les tarifs évoluent, qu'un modèle est déprécié, ou qu'une exigence de conformité impose un hébergement européen — revient alors à réécrire une part non négligeable de la couche d'accès, et souvent les tests qui vont avec. Le composant Platform de Symfony AI répond à ce problème avec une approche familière à quiconque connaît Doctrine DBAL ou le composant Mailer : une interface commune devant, des implémentations spécifiques par fournisseur derrière, appelées des bridges.

PlatformInterface et invoke() : une seule méthode pour tout

Toute la surface d'API du composant tient dans une interface, PlatformInterface, et une méthode unique : invoke(). Elle prend un identifiant de modèle, une entrée — une chaîne, un MessageBag construit avec Message::forSystem() et Message::ofUser(), ou un contenu multimodal comme une image — et un tableau d'options, puis retourne un objet Result paresseux. C'est cet objet qui expose asText(), asObject() ou asVectors() selon ce que tu veux en extraire. La signature ne change jamais, quel que soit le fournisseur invoqué derrière — c'est précisément ce contrat stable qui rend la bascule de provider possible sans douleur.

bash
# Installation du composant
composer require symfony/ai-platform
php
<?php
declare(strict_types=1);

// Appel minimal via le bridge OpenAI
use Symfony\AI\Platform\Bridge\OpenAi\Factory;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;

$platform = Factory::createPlatform($_ENV['OPENAI_API_KEY']);

$messages = new MessageBag(
    Message::forSystem('Tu es un assistant technique Symfony.'),
    Message::ofUser('Explique le pattern Bridge en une phrase.'),
);

$result = $platform->invoke('gpt-5.5', $messages);

echo $result->asText();

Basculer de fournisseur sans toucher au code métier

Le vrai test d'une abstraction, c'est de remplacer un fournisseur par un autre sans modifier la logique applicative qui l'entoure. Avec Platform, ça se limite à changer le bridge utilisé pour instancier la plateforme — le reste du code, l'appel à invoke() et le traitement du Result, reste identique à la ligne près. Le seul élément qui change vraiment, c'est l'identifiant du modèle passé en premier argument, puisque chaque bridge sait reconnaître les modèles qui lui appartiennent.

php
<?php
// Avec le bridge OpenAI
use Symfony\AI\Platform\Bridge\OpenAi\Factory as OpenAiFactory;

$platform = OpenAiFactory::createPlatform($_ENV['OPENAI_API_KEY']);
$result = $platform->invoke('gpt-5.5', $messages);

// Avec le bridge Anthropic — code métier strictement identique
use Symfony\AI\Platform\Bridge\Anthropic\Factory as AnthropicFactory;

$platform = AnthropicFactory::createPlatform($_ENV['ANTHROPIC_API_KEY']);
$result = $platform->invoke('claude-opus-4-8', $messages);

Piloter plusieurs fournisseurs en simultané : le routage par modèle

Basculer entre deux configurations mono-fournisseur n'est qu'un premier niveau. Platform va plus loin : il accepte un tableau de providers à l'instanciation et route chaque appel invoke() vers le bon fournisseur en fonction du seul identifiant de modèle demandé. Une application peut ainsi interroger GPT et Claude dans le même run PHP, sans jongler avec deux clients HTTP ni deux services applicatifs distincts. C'est ce qui permet, par exemple, de router vers un modèle capable d'un raisonnement long les tâches complexes, et vers un modèle plus léger et moins cher les tâches de classification simple, en gardant un seul point d'entrée dans le code.

php
<?php
use Symfony\AI\Platform\Platform;
use Symfony\AI\Platform\Bridge\OpenAi\Factory as OpenAiFactory;
use Symfony\AI\Platform\Bridge\Anthropic\Factory as AnthropicFactory;

$platform = new Platform([
    OpenAiFactory::createProvider($_ENV['OPENAI_API_KEY']),
    AnthropicFactory::createProvider($_ENV['ANTHROPIC_API_KEY']),
]);

// Chaque appel est routé vers le bon provider selon le nom du modèle
$classification = $platform->invoke('gpt-5.5', $simpleQuery)->asText();
$reasoning = $platform->invoke('claude-opus-4-8', $complexQuery)->asText();

Le même mécanisme permet d'enregistrer plusieurs comptes d'un même fournisseur sous des noms distincts — un provider OpenAI pour l'Europe, un autre pour les États-Unis, par exemple. C'est un détail qui compte dès qu'une clause contractuelle ou réglementaire impose que certains flux ne quittent jamais une région donnée : la contrainte se résout en configuration, pas en code conditionnel dispersé dans l'application.

Votre équipe utilise Claude Code ?

Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.

Voir les formats →

Configuration déclarative avec l'AI Bundle : les clés d'API ne se codent jamais en dur

Pour une application Symfony complète, l'usage recommandé passe par symfony/ai-bundle, qui expose la configuration du Platform en YAML et injecte automatiquement une instance de PlatformInterface dans le conteneur de services. Les identifiants d'API transitent systématiquement par des variables d'environnement via %env()% — jamais en clair dans le YAML versionné, ni a fortiori dans le code PHP. C'est une discipline de sécurité élémentaire, mais qui prend tout son sens avec des credentials multi-fournisseurs : sans elle, chaque nouveau provider ajouté est une nouvelle occasion de faire fuiter une clé dans un commit.

yaml
# config/packages/ai.yaml
ai:
    platform:
        openai:
            api_key: '%env(OPENAI_API_KEY)%'
        anthropic:
            api_key: '%env(ANTHROPIC_API_KEY)%'

Injecter PlatformInterface, pas un SDK fournisseur

La portabilité réelle du code métier suppose une dernière discipline : ne jamais injecter un bridge concret dans un service applicatif, mais toujours l'interface PlatformInterface, autowirée par le bundle. Le nom du modèle lui-même mérite le même traitement — le sortir du code et le porter en paramètre de configuration ou en variable d'environnement, pour qu'un changement de fournisseur ou de version de modèle ne nécessite ni modification de code ni nouveau déploiement, juste une variable à mettre à jour.

php
<?php
declare(strict_types=1);

namespace App\Service;

use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\PlatformInterface;

final class AssistantService
{
    public function __construct(
        private readonly PlatformInterface $platform,
        private readonly string $defaultModel, // bindé depuis %env(DEFAULT_LLM_MODEL)%
    ) {
    }

    public function ask(string $question): string
    {
        $messages = new MessageBag(
            Message::forSystem('Tu es un assistant technique Symfony.'),
            Message::ofUser($question),
        );

        return $this->platform->invoke($this->defaultModel, $messages)->asText();
    }
}

Le cas du streaming : un contrat identique, un fournisseur différent

La bascule de fournisseur ne casse pas non plus les fonctionnalités transverses comme le streaming. L'option 'stream' => true passée à invoke() fait retourner un Result dont la méthode asTextStream() s'itère token par token, quel que soit le bridge choisi. La mécanique SSE (Server-Sent Events) qu'on avait détaillée pour l'intégration directe de Claude 3.5 dans un service Symfony injectable reste valable telle quelle : le même contrôleur qui relaie les tokens en SSE au frontend fonctionne à l'identique, que le flux vienne de GPT ou de Claude, sans branche conditionnelle par provider.

Pourquoi cette abstraction compte plus qu'il n'y paraît

Une API unique pour plusieurs LLM n'est pas qu'un confort de développeur. C'est un choix d'architecture qui a des conséquences directes sur la résilience et le coût d'un projet dans la durée :

  • Portabilité réelle — changer de fournisseur pour raison tarifaire ou parce qu'un modèle est déprécié ne touche ni le code métier, ni les tests qui l'entourent.
  • Continuité de service — un FailoverPlatform dédié peut basculer automatiquement vers un second provider si le premier répond en erreur ou en timeout, sans logique de retry à écrire côté application.
  • Conformité et souveraineté — les providers nommés permettent de router certains flux vers une région ou un fournisseur précis pour respecter une clause contractuelle ou réglementaire.
  • Comparaison coût/qualité — deux providers configurés en parallèle permettent un A/B test réel en production, sur le même code, avant de trancher durablement.

Pour une équipe qui a suivi le pipeline RAG PgVector et Claude Sonnet 4 qu'on avait construit pas à pas dans cet article dédié, la bonne nouvelle est que rien de cette architecture n'est perdu : le bridge Anthropic utilisé dans ce tutoriel s'enregistre exactement de la même façon dans un Platform multi-provider, aux côtés d'un second fournisseur choisi pour d'autres tâches. L'abstraction ne remplace pas le travail déjà fait, elle le rend simplement moins fragile face aux évolutions du marché des LLM.

Cette discipline d'abstraction — une interface stable, des implémentations interchangeables, des secrets qui ne fuient jamais dans le code — n'est pas propre à l'IA : c'est la même hygiène architecturale que Symfony prône depuis toujours sur ses composants historiques. Ce qui change avec les LLM, c'est le rythme : les modèles se périment plus vite que les frameworks, et une équipe qui découvre ce genre de pattern en pleine urgence de migration paie cash le temps perdu à le faire dans la précipitation. Si la tienne veut prendre l'habitude d'architecturer ces intégrations correctement avant d'en avoir besoin dans l'urgence — plutôt que de la découvrir le jour où changer de fournisseur devient vital — c'est exactement le terrain que couvre notre Workshop Claude Code, pensé pour outiller une équipe entière sur ces pratiques d'intégration IA en amont, pas en réparation.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Votre équipe utilise Claude Code ?

Formation intra sur vos projets réels : de la demi-journée à 2 jours, 6 devs max.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.