Comment intégrer l'IA dans une application Symfony ?

On intègre l'IA dans une application Symfony comme n'importe quel service externe : un client HTTP configuré, une classe de service à l'interface claire, un traitement en arrière-plan pour le travail lent, et la sécurité, la journalisation et les tests habituels. Symfony fournit déjà toutes les briques. HttpClient dialogue avec l'API OpenAI ou Claude, Messenger exécute les appels longs dans des workers, StreamedResponse diffuse les réponses, Doctrine DBAL interroge pgvector et le composant RateLimiter plafonne l'usage par utilisateur.

Symfony est notre stack back end principale, et ce guide reflète la façon dont nous intégrons des fonctionnalités IA dans des produits Symfony. Si vous voulez qu'une équipe le fasse dans votre base de code, découvrez nos services de développement Symfony ou, pour du code plus ancien ou sans framework, nos services de développement PHP.

Le plan en bref :

  1. Un HttpClient scopé par fournisseur, avec les clés issues des variables d'environnement.
  2. Une seule interface de service pour « interroger le modèle », afin de pouvoir changer de fournisseur.
  3. Des messages et handlers Messenger pour chaque appel non interactif.
  4. Un endpoint en streaming pour le chat.
  5. pgvector pour la recherche dans vos données, filtrée par tenant et par permissions.
  6. Des limites de débit par utilisateur et par tenant, et une table de journalisation des tokens et des coûts.

Quelles fonctionnalités IA conviennent à un produit Symfony ?

Les fonctionnalités IA qui conviennent le mieux à un produit Symfony sont celles qui s'appuient sur des règles métier que vous avez déjà : elles lisent vos entités, respectent vos voters et réécrivent les résultats via vos services. Les produits Symfony sont souvent des back-offices, des ERP, des marketplaces et des plateformes SaaS avec de nombreux rôles, précisément là où ces fonctionnalités font gagner le plus de temps.

Fonctionnalité Fonctionnement dans Symfony Première version typique
Résumés et brouillons Handler Messenger, résultat stocké sur l'entité Résumés de tickets, de commandes ou de rapports relus par l'équipe
Classification et routage Handler Messenger à la création ou à l'import Demandes entrantes étiquetées et affectées à une file
Extraction de documents Upload, puis un handler qui valide la sortie structurée Champs de factures ou de formulaires extraits vers un écran de validation
Questions sur vos données Endpoint en streaming avec recherche dans pgvector Un assistant sur les articles d'aide ou les procédures internes
Agent pour un processus Des outils qui appellent vos services existants, avec étapes de validation Préparer un rapport hebdomadaire ou trier une boîte mail partagée

Commencez par l'une d'elles, mesurée sur des exemples réels, avant de construire un assistant généraliste. Chaque ligne réutilise la même plomberie décrite ci-dessous : la deuxième fonctionnalité coûte donc bien moins cher que la première.

Comment appeler l'API OpenAI ou Claude avec Symfony HttpClient ?

Définissez un client scopé par fournisseur dans la configuration, pour que l'URL de base, les en-têtes, le timeout et la politique de nouvelles tentatives soient au même endroit, puis injectez-le par son nom.

# config/packages/framework.yaml
framework:
  http_client:
    scoped_clients:
      anthropic.client:
        base_uri: 'https://api.anthropic.com'
        headers:
          x-api-key: '%env(ANTHROPIC_API_KEY)%'
          anthropic-version: '2023-06-01'
        timeout: 120
        retry_failed:
          max_retries: 2
      openai.client:
        base_uri: 'https://api.openai.com'
        auth_bearer: '%env(OPENAI_API_KEY)%'
        timeout: 120
        retry_failed:
          max_retries: 2

retry_failed relance les limites de débit et les erreurs serveur avec un délai croissant. Symfony autowire un client scopé par son nom en camelCase : HttpClientInterface $anthropicClient reçoit donc le premier.

namespace App\Ai;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class ClaudeModel implements LanguageModel
{
    public function __construct(
        private HttpClientInterface $anthropicClient,
        #[Autowire(env: 'ANTHROPIC_MODEL')] private string $model,
    ) {}

    public function complete(string $system, string $user, int $maxTokens = 4096): ModelResult
    {
        $data = $this->anthropicClient->request('POST', '/v1/messages', [
            'json' => [
                'model' => $this->model,
                'max_tokens' => $maxTokens,
                'system' => $system,
                'messages' => [['role' => 'user', 'content' => $user]],
            ],
        ])->toArray(); // lève une exception sur les 4xx et 5xx

        $text = implode('', array_column(
            array_filter($data['content'], fn (array $b) => $b['type'] === 'text'),
            'text'
        ));

        return new ModelResult($text, $data['usage']['input_tokens'], $data['usage']['output_tokens']);
    }
}

Une classe OpenAiModel implémente la même interface LanguageModel avec un POST vers /v1/responses. Liez l'interface à une implémentation dans services.yaml, ou choisissez par fonctionnalité avec un petit routeur. C'est cette interface qui fait d'un futur changement de fournisseur, ou d'un test A/B entre modèles, une simple modification de configuration. Notre comparatif Claude ou OpenAI : quelle API choisir explique quand chaque fournisseur convient.

Anthropic publie un SDK PHP officiel, et des clients PHP communautaires existent pour OpenAI. Ce sont de bons choix ; mais avec le HttpClient de Symfony, vous n'en avez souvent pas besoin pour une ou deux fonctionnalités, et vous gardez le profiler, les nouvelles tentatives et les outils de mock de Symfony.

Comment exécuter les appels LLM en asynchrone avec Symfony Messenger ?

Envoyez un message pour chaque tâche IA que l'utilisateur ne suit pas en direct, et laissez un worker dédié la traiter avec une stratégie de nouvelles tentatives. Les requêtes web restent rapides, et les limites de débit du fournisseur deviennent des délais plutôt que des erreurs.

// src/Message/SummarizeDocument.php
final class SummarizeDocument
{
    public function __construct(public readonly int $documentId) {}
}

// src/MessageHandler/SummarizeDocumentHandler.php
#[AsMessageHandler]
final class SummarizeDocumentHandler
{
    public function __construct(
        private LanguageModel $model,
        private DocumentRepository $documents,
        private PromptLibrary $prompts,
        private EntityManagerInterface $em,
    ) {}

    public function __invoke(SummarizeDocument $message): void
    {
        $document = $this->documents->find($message->documentId)
            ?? throw new UnrecoverableMessageHandlingException('Document deleted');

        $result = $this->model->complete(
            $this->prompts->get('document-summary', version: 4),
            $document->getText(),
            maxTokens: 1024,
        );

        $document->setSummary($result->text);
        $this->em->flush();
    }
}
# config/packages/messenger.yaml
framework:
  messenger:
    transports:
      ai:
        dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
        options: { queue_name: ai }
        retry_strategy:
          max_retries: 3
          delay: 5000
          multiplier: 4
    routing:
      App\Message\SummarizeDocument: ai

Faites tourner le transport ai avec ses propres workers (messenger:consume ai --time-limit=3600) pour qu'un retard de travail IA ne bloque jamais les e-mails ou les paiements. Levez UnrecoverableMessageHandlingException pour les erreurs qu'une nouvelle tentative ne corrigera pas, comme un enregistrement supprimé ou une réponse 400 : le message part alors dans le transport d'échec au lieu d'être relancé pour rien.

Comment diffuser les réponses IA en streaming dans Symfony ?

Utilisez EventSourceHttpClient pour lire les Server-Sent Events du fournisseur et une StreamedResponse pour ne transmettre que le texte au navigateur. Les premiers mots s'affichent en une seconde environ.

use Symfony\Component\HttpClient\Chunk\ServerSentEvent;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Component\HttpFoundation\StreamedResponse;

#[Route('/assistant/stream', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function stream(Request $request, HttpClientInterface $anthropicClient): StreamedResponse
{
    $question = (string) $request->getPayload()->get('q');
    $model = $this->getParameter('app.anthropic_model');
    $client = new EventSourceHttpClient($anthropicClient);

    return new StreamedResponse(function () use ($client, $question, $model) {
        $source = $client->request('POST', '/v1/messages', ['json' => [
            'model' => $model,
            'max_tokens' => 4096,
            'stream' => true,
            'messages' => [['role' => 'user', 'content' => $question]],
        ]]);

        foreach ($client->stream($source) as $chunk) {
            if (!$chunk instanceof ServerSentEvent) {
                continue;
            }
            $event = json_decode($chunk->getData(), true);
            if (($event['type'] ?? null) === 'content_block_delta'
                && ($event['delta']['type'] ?? null) === 'text_delta') {
                echo 'data: '.json_encode(['text' => $event['delta']['text']])."\n\n";
                flush();
            }
        }
        echo "event: done\ndata: {}\n\n";
        flush();
    }, 200, [
        'Content-Type' => 'text/event-stream',
        'Cache-Control' => 'no-cache',
        'X-Accel-Buffering' => 'no',
    ]);
}

Les versions récentes de Symfony ajoutent une classe de réponse dédiée aux flux d'événements : consultez la documentation de votre version. Sous PHP-FPM, chaque flux ouvert occupe un worker pendant toute sa durée : dimensionnez le pool pour les chats simultanés, ou faites tourner l'application sur FrankenPHP avec le composant Runtime. Si vous utilisez déjà Mercure pour le temps réel, comme dans l'ERP sur mesure pour une société américaine de maintenance aéronautique, un worker peut aussi publier les réponses partielles sur un topic Mercure au lieu de garder une connexion HTTP ouverte.

Comment stocker les embeddings avec Doctrine et pgvector ?

Ajoutez l'extension pgvector à PostgreSQL, créez la colonne vectorielle et son index dans une migration Doctrine en SQL brut, et interrogez via la connexion DBAL avec les filtres de tenant dans la même requête.

// dans une migration Doctrine
$this->addSql('CREATE EXTENSION IF NOT EXISTS vector');
$this->addSql('ALTER TABLE chunk ADD embedding vector(1536)'); // à aligner sur votre modèle d'embeddings
$this->addSql('CREATE INDEX chunk_embedding_idx ON chunk USING hnsw (embedding vector_cosine_ops)');
final class ChunkSearch
{
    public function __construct(private Connection $connection) {}

    /** @param float[] $embedding */
    public function nearest(int $tenantId, array $embedding, int $limit = 8): array
    {
        return $this->connection->fetchAllAssociative(
            'SELECT id, document_id, content
               FROM chunk
              WHERE tenant_id = :tenant
              ORDER BY embedding <=> CAST(:q AS vector)
              LIMIT '.(int) $limit,
            ['tenant' => $tenantId, 'q' => '['.implode(',', $embedding).']']
        );
    }
}

Les embeddings proviennent d'un modèle d'embeddings, comme l'endpoint embeddings d'OpenAI ; Anthropic ne propose pas ses propres modèles d'embeddings, les projets basés sur Claude l'associent donc à OpenAI ou à un autre fournisseur. Générez les embeddings dans un handler Messenger quand les documents changent, et conservez un hash du contenu pour ne pas recalculer les textes inchangés. Les entités Doctrine n'ont même pas besoin de connaître la colonne vectorielle. Pour le découpage, la recherche hybride, le reranking et l'évaluation, lisez notre guide de mise en œuvre du RAG.

Comment limiter le débit des fonctionnalités IA dans Symfony ?

Utilisez le composant RateLimiter avec un limiteur par utilisateur et par tenant, et vérifiez-le avant chaque appel au modèle. Un modèle qui tourne en boucle, ou un utilisateur qui automatise votre chat, doit buter sur une limite, pas sur votre facture.

# config/packages/rate_limiter.yaml
framework:
  rate_limiter:
    ai_per_user:
      policy: 'token_bucket'
      limit: 30
      rate: { interval: '10 minutes', amount: 30 }
public function __construct(private RateLimiterFactory $aiPerUserLimiter) {}

public function ask(User $user, string $question): ModelResult
{
    $limit = $this->aiPerUserLimiter->create((string) $user->getId())->consume(1);
    if (!$limit->isAccepted()) {
        throw new TooManyRequestsHttpException($limit->getRetryAfter()->getTimestamp() - time());
    }
    // ... appel au modèle
}

Combinez les limites avec les leviers de coût valables pour toute stack : cache de prompt pour les instructions longues et stables, modèle plus petit pour les étapes simples, API batch pour les traitements nocturnes, et une table de journalisation avec feature, prompt_version, model, les compteurs de tokens, la latence et le coût de chaque appel. Un budget mensuel par tenant peut être vérifié dans cette table avant chaque appel.

Comment tester et sécuriser les fonctionnalités IA dans Symfony ?

Testez votre code avec MockHttpClient et des réponses enregistrées du fournisseur, et testez la qualité séparément avec un jeu d'évaluation. Considérez tout ce que le modèle lit comme une entrée non fiable.

$client = new MockHttpClient([
    new JsonMockResponse([
        'content' => [['type' => 'text', 'text' => 'Invoice 1042 is overdue by 12 days.']],
        'usage' => ['input_tokens' => 300, 'output_tokens' => 14],
    ]),
]);

Déclarez le mock comme client scopé dans l'environnement de test, puis vérifiez ce que votre handler a enregistré, ce qu'il a journalisé et comment il gère une réponse 429 ou un timeout. Pour la qualité, gardez 50 à 200 entrées réelles avec les sorties attendues et lancez-les à chaque changement de prompt ou de modèle.

Les règles de sécurité que nous appliquons à chaque fonctionnalité IA sous Symfony :

  • Clés uniquement côté serveur, injectées depuis les variables d'environnement ou les secrets Symfony.
  • Voters avant la recherche. Seuls les documents que l'utilisateur peut lire sont interrogés.
  • Le texte non fiable reste une donnée. Les saisies utilisateur et les documents récupérés sont clairement délimités dans le prompt et jamais traités comme des instructions.
  • Validation de la sortie structurée avec le composant Validator avant qu'elle n'atteigne la base.
  • Validation humaine des actions. Tout ce qui envoie, paie ou supprime exige une confirmation humaine.

Que recommandons-nous aux équipes Symfony ?

Notre verdict pour la plupart des produits Symfony : des HttpClient scopés derrière votre propre interface LanguageModel, Messenger pour chaque appel non interactif, un endpoint en streaming pour le chat, pgvector via DBAL et le rate limiter dès le premier jour. Suivez l'initiative IA de Symfony et adoptez ses composants lorsqu'ils seront stables et résoudront un problème que vous avez réellement. Cette configuration n'utilise que des composants que votre équipe connaît déjà, et c'est la principale raison pour laquelle elle tient en production.

Notre propre SaaS IA, AI Resume Master, tourne sur PHP et Symfony avec React, Python et l'API OpenAI ; il a été construit en trois mois environ et a atteint 50 000 utilisateurs actifs mensuels. Si votre application est sous Laravel, vous retrouverez les mêmes principes dans notre guide pour intégrer OpenAI ou Claude dans une application Laravel.

Prochaine étape

Si vous voulez qu'une équipe Symfony ajoute une fonctionnalité IA à votre produit, découvrez nos services de développement Symfony. Chez nous, les fonctionnalités IA dans un produit existant démarrent à 10 000 USD, avec un devis forfaitaire après un court appel de cadrage. Contactez-nous en décrivant la fonctionnalité envisagée et les données dont elle a besoin.

Études de cas

Questions fréquemment posées

Le projet Symfony a lancé sa propre initiative IA, avec des composants pour appeler les plateformes de modèles, construire des agents, stocker des vecteurs et exposer du MCP. Elle est jeune et évolue vite : vérifiez son état actuel et sa stabilité avant d'en dépendre en production. Tout ce qui est décrit dans ce guide fonctionne avec des composants Symfony stables et éprouvés : HttpClient, Messenger, le rate limiter et Doctrine.

Oui, pour tout ce que l'utilisateur ne suit pas en direct : résumés, classification, extraction, enrichissement et traitement de documents. Un appel LLM peut prendre de quelques secondes à plus d'une minute et se heurte souvent aux limites de débit. Un transport Messenger avec une stratégie de nouvelles tentatives et ses propres workers garde les requêtes web rapides et relance les appels en échec avec un délai croissant, au lieu de faire échouer la requête de l'utilisateur.

Doctrine n'a pas de type vectoriel natif : la plupart des équipes ajoutent la colonne et l'index dans une migration en SQL brut et lancent les requêtes de similarité via la connexion DBAL. Des packages communautaires ajoutent un type Doctrine personnalisé si vous voulez des vecteurs sur vos entités. Gardez les filtres de tenant et de permissions dans la même requête SQL, pour que le modèle ne voie jamais que les données que l'utilisateur courant a le droit de lire.

Souvent oui, car l'intégration repose surtout sur des appels HTTP et des tâches de fond. Mais les très anciennes versions de Symfony et de PHP n'ont ni les attributs, ni les fonctions modernes de HttpClient, ni les correctifs de sécurité, et les fonctionnalités IA augmentent la quantité d'entrées non fiables que votre application traite. Nous recommandons généralement de passer d'abord, ou en parallèle, à une version supportée ; une mise à niveau typique prend 3 à 12 semaines.

Chez nous, une fonctionnalité IA ajoutée à un produit existant démarre à 10 000 USD et prend généralement 4 à 8 semaines, avec un devis forfaitaire après un court appel de cadrage. Une première fonctionnalité bien ciblée peut tenir dans un AI Sprint : 10 000 USD au forfait pour 4 semaines. L'usage de l'API du modèle est facturé directement par le fournisseur et va de quelques dizaines de dollars par mois pour un outil interne à plusieurs milliers pour une fonctionnalité client très sollicitée.

Commençons votre projet
Réservez un appel