Skip to main content

IA

Intégration Ollama et Drupal 11 : IA locale souveraine avec PHP 8.4

EN BREF

  • Les API d'IA en cloud public exposent les contenus non publiés de votre organisation à des risques juridiques et à des fuites de propriété intellectuelle.
  • Héberger un modèle de langage compact (Mistral 7B, Llama 3) via Ollama sur une instance Debian 12 dédiée garantit l'étanchéité absolue de vos données d'entreprise.
  • L'exploitation des fonctionnalités modernes de PHP 8.4 permet d'interfacer Drupal 11 avec le runtime local par des flux HTTP optimisés et typés.
  • L'utilisation conjointe du mode JSON natif et des validateurs de taxonomie Drupal supprime les hallucinations et protège l'intégrité de la base de données éditoriale.
  • Une architecture IA locale bien calibrée réduit les coûts récurrents à l'abonnement du serveur tout en augmentant la productivité des équipes de rédaction.

Les équipes éditoriales réclament des fonctionnalités d'intelligence artificielle pour accélérer la rédaction, résumer les documents volumineux et automatiser le balisage taxonomique. Face à cette demande, le réflexe dominant consiste à brancher un module communautaire sur les interfaces d'OpenAI ou d'Anthropic. Pour un directeur technique ou un architecte logiciel, cette facilité apparente constitue un angle mort stratégique majeur.

L'intégration d'un runtime d'inférence local sous Debian 12, couplé à Drupal 11 et orchestré par PHP 8.4, offre une alternative performante. Cette architecture assure une automatisation éditoriale avancée tout en maintenant les flux de traitement à l'intérieur de votre périmètre d'infrastructure.

Schéma d'architecture réseau Drupal Ollama

Les enjeux de souveraineté et de confidentialité : pourquoi l'API tierce pose problème

Le recours systématique aux plateformes d'IA hébergées en mode SaaS transforme le CMS d'entreprise en passerelle de fuite d'informations. Dans les environnements réglementés ou hautement stratégiques, cette approche soulève des objections de premier ordre.

Les risques juridiques et stratégiques du transfert de données non publiées

Lorsqu'un contributeur sollicite une aide à la rédaction pour un communiqué confidentiel, une analyse financière préliminaire ou un dossier médical, le contenu non publié transite en clair sur les serveurs d'un tiers. Même lorsque les contrats stipulent que les données ne servent pas au réentraînement des modèles, l'entreprise s'expose à :

  • Une violation manifeste des obligations de conformité RGPD en cas de présence de données à caractère personnel dans les brouillons.
  • Une perte de contrôle sur la localisation géographique réelle des traitements automatisés.
  • Un risque d'interception ou de journalisation non consentie lors des incidents techniques chez le prestataire tiers.
  • Une exposition des secrets de fabrication ou des annonces stratégiques avant leur diffusion officielle.

La maîtrise budgétaire et l'indépendance technologique face aux géants du cloud

L'analyse économique des projets d'automatisation met en lumière les limites des modèles de facturation au jeton (token) :

  • Les coûts deviennent imprévisibles dès que le volume de nœuds à traiter augmente ou que des processus de traitement par lots sont exécutés.
  • Les débits et temps de réponse subissent les aléas de saturation des infrastructures mutualisées distantes.
  • Les politiques de tarification et les conditions générales d'utilisation peuvent être modifiées unilatéralement par le fournisseur d'API.
  • L'hébergement dédié sur infrastructure souveraine (telle qu'une instance OVHcloud en France ou en Europe) transforme un coût opérationnel variable en une charge d'infrastructure fixe et budgétable.

Déploiement et sécurisation d'Ollama sous Debian 12 : socle d'inférence souverain

Ollama s'est imposé comme l'environnement d'exécution de référence pour faire tourner des modèles ouverts tels que Mistral Nemo, Llama 3.1 ou Gemma 2. Son packaging sous Linux permet un déploiement fluide et une intégration étroite avec les mécanismes de sécurité du système d'exploitation.

Dimensionnement matériel : CPU intensif contre accélération GPU

Le choix de l'infrastructure d'hébergement dépend du volume de requêtes concurrentes et de la latence tolérée par les contributeurs :

  • Configuration CPU intensif : un serveur doté de 16 cœurs récents et de 32 gigaoctets de mémoire vive DDR5 peut exécuter un modèle quantifié au format 4-bit (Mistral 7B) avec une vitesse d'environ 5 à 9 jetons par seconde, ce qui suffit pour des résumés asynchrones en arrière-plan.
  • Configuration accélération GPU : une instance équipée d'une carte graphique dédiée pourvue d'au moins 16 gigaoctets de VRAM (de type NVIDIA A10 ou L4) atteint 40 à 70 jetons par seconde, autorisant une assistance en temps réel pendant la saisie des articles.
  • Débit mémoire : la vitesse d'inférence d'un modèle de langage est principalement limitée par la bande passante de la mémoire vive, ce qui impose de privilégier des architectures matérielles récentes.

Installation d'Ollama et configuration sous systemd

Sous Debian 12 (Bookworm), l'installation d'Ollama s'effectue simplement via les dépôts officiels ou le script d'installation supervisé. L'essentiel réside dans sa déclaration en tant que démon d'arrière-plan géré par systemd.

# Téléchargement et installation d'Ollama
curl -fsSL https://ollama.com/install.sh | sh

# Création d'un utilisateur système dédié sans privilèges
sudo useradd -r -s /bin/false -m -d /usr/share/ollama ollama

Le fichier de configuration systemd doit être ajusté pour limiter l'exposition de l'API à l'adresse de bouclage local ou à l'adresse de l'interface réseau privée :

[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=/usr/bin/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MODELS=/var/lib/ollama/models"
Environment="OLLAMA_NUM_PARALLEL=2"

[Install]
WantedBy=multi-user.target

Une fois le service démarré, le téléchargement du modèle ciblé s'effectue en ligne de commande :

sudo systemctl daemon-reload
sudo systemctl enable --now ollama
sudo -u ollama ollama pull mistral

Sécurisation du point d'accès : isolation réseau et reverse proxy Unix

Par défaut, Ollama ne dispose d'aucun mécanisme d'authentification native. Il est impératif d'interdire tout accès direct depuis les interfaces réseau publiques :

  • Liaison stricte sur l'adresse locale 127.0.0.1 si Drupal réside sur le même serveur physique.
  • Utilisation d'un tunnel chiffré WireGuard ou d'un réseau privé isolé (vRack) si Drupal et Ollama sont séparés.
  • Mise en place d'un reverse proxy Nginx configuré avec des règles de pare-feu et une authentification par certificat client mTLS pour tout trafic inter-serveurs.

Architecture d'intégration Drupal 11 et PHP 8.4 : conception du service

Drupal 11 s'appuie sur les composants Symfony et tire pleinement avantage des gains de performance de PHP 8.4. Pour interagir proprement avec le runtime Ollama, la bonne pratique consiste à concevoir un service applicatif sur mesure au lieu de greffer des bibliothèques externes lourdes.

Déclaration du service dans l'architecture modulaire de Drupal

Le module sur mesure, nommé ici custom_llm, déclare un service dédié au sein du fichier custom_llm.services.yml. Cette approche respecte le principe d'injection de dépendances de Drupal.

services:
  custom_llm.ollama_client:
    class: Drupal\custom_llm\Service\OllamaClientService
    arguments:
      - '@http_client'
      - '@logger.factory'
      - '%custom_llm.ollama_endpoint%'

Les paramètres d'environnement ou le fichier settings.php alimentent la variable d'endpoint :

$settings['custom_llm.ollama_endpoint'] = 'http://127.0.0.1:11434';

Implémentation du client d'inférence : exploitation des atouts de PHP 8.4

La version 8.4 de PHP introduit des améliorations ergonomiques et fonctionnelles de premier ordre, notamment la promotion des propriétés de constructeur avec typage strict et une gestion native optimisée des requêtes HTTP.

Voici l'implémentation du service d'inférence exploitant le client Guzzle intégré à Drupal :

<?php

declare(strict_types=1);

namespace Drupal\custom_llm\Service;

use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
use Psr\Log\LoggerInterface;

final class OllamaClientService {

  public function __construct(
    private readonly ClientInterface $httpClient,
    private readonly LoggerInterface $logger,
    private readonly string $endpoint = 'http://127.0.0.1:11434',
  ) {}

  /**
   * Envoie une requête de génération au modèle local.
   *
   * @param string $prompt
   *   Le prompt d'instruction destiné au modèle.
   * @param string $model
   *   Le nom du modèle installé localement.
   * @param array<string, mixed> $options
   *   Options additionnelles (température, contexte).
   *
   * @return string
   *   Le contenu textuel généré par le modèle.
   */
  public function generateText(string $prompt, string $model = 'mistral', array $options = []): string {
    $url = sprintf('%s/api/generate', rtrim($this->endpoint, '/'));

    $payload = [
      'model' => $model,
      'prompt' => $prompt,
      'stream' => FALSE,
      'options' => [
        'temperature' => $options['temperature'] ?? 0.2,
        'top_p' => 0.9,
      ],
    ];

    try {
      $response = $this->httpClient->request('POST', $url, [
        'json' => $payload,
        'headers' => [
          'Content-Type' => 'application/json',
          'Accept' => 'application/json',
        ],
        'timeout' => 60.0,
      ]);

      if ($response->getStatusCode() !== 200) {
        $this->logger->error('Erreur API Ollama, code HTTP : @code', [
          '@code' => $response->getStatusCode(),
        ]);
        return '';
      }

      $data = json_decode($response->getBody()->getContents(), TRUE, 512, JSON_THROW_ON_ERROR);

      return trim((string) ($data['response'] ?? ''));
    }
    catch (GuzzleException $e) {
      $this->logger->error('Échec de la communication réseau avec Ollama : @message', [
        '@message' => $e->getMessage(),
      ]);
      return '';
    }
    catch (\JsonException $e) {
      $this->logger->error('Réponse JSON invalide reçue d Ollama : @message', [
        '@message' => $e->getMessage(),
      ]);
      return '';
    }
  }

}
Diagramme de séquence UML HTTP synchrone  Drupal

Branchement du service aux formulaires de contribution

L'objectif opérationnel consiste à aider le contributeur directement lors de la création ou de la révision d'un contenu. Pour y parvenir sans rechargement de page, nous raccordons un bouton AJAX au formulaire de nœud via un hook_form_BASE_FORM_ID_alter.

<?php

declare(strict_types=1);

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;

/**
 * Implémente hook_form_BASE_FORM_ID_alter() pour node_form.
 */
function custom_llm_form_node_form_alter(array &$form, FormStateInterface $form_state, string $form_id): void {
  // Ajout du bouton d'assistance IA uniquement pour le type de contenu article
  $node = $form_state->getFormObject()->getEntity();
  if ($node->bundle() !== 'article') {
    return;
  }

  $form['ai_tools'] = [
    '#type' => 'details',
    '#title' => t('Assistance IA locale'),
    '#open' => TRUE,
    '#weight' => 10,
  ];

  $form['ai_tools']['generate_summary_btn'] = [
    '#type' => 'button',
    '#value' => t('Générer le résumé automatiquement'),
    '#ajax' => [
      'callback' => 'custom_llm_ajax_generate_summary_callback',
      'event' => 'click',
      'progress' => [
        'type' => 'throbber',
        'message' => t('Analyse locale en cours...'),
      ],
    ],
  ];
}

/**
 * Callback AJAX pour la synthèse du texte saisi.
 */
function custom_llm_ajax_generate_summary_callback(array &$form, FormStateInterface $form_state): AjaxResponse {
  $response = new AjaxResponse();
  
  $body_values = $form_state->getValue('body');
  $body_text = $body_values[0]['value'] ?? '';

  if (empty($body_text)) {
    return $response;
  }

  // Récupération du service d'inférence
  /** @var \Drupal\custom_llm\Service\OllamaClientService $ollama_client */
  $ollama_client = \Drupal::service('custom_llm.ollama_client');

  $prompt = "Tu es un assistant éditorial professionnel. Rédige un résumé clair et factuel en deux phrases maximum du texte suivant, sans inventer d'information :\n\n" . strip_tags($body_text);

  $summary = $ollama_client->generateText($prompt, 'mistral');

  if (!empty($summary)) {
    // Injection de la réponse dans le champ résumé de l'interface Drupal
    $response->addCommand(new InvokeCommand('textarea[name="body[0][summary]"]', 'val', [$summary]));
  }

  return $response;
}

Gouvernance des prompts et garde-fous : fiabiliser les générations

Les modèles de langage peuvent générer des hallucinations ou produire des formulations non conformes si les instructions ne sont pas strictement balisées. Dans un contexte de production, la rigueur logicielle impose de contraindre l'IA locale par des structures de données rigides.

Contraintes de format via le mode JSON natif

Ollama supporte l'argument format: json, qui force le moteur d'inférence à contraindre sa matrice de décodage pour ne retourner qu'un document JSON valide. Cette fonction est essentielle pour les opérations de métadonnées ou d'auto-étiquetage.

Voici un exemple de payload transmis à Ollama pour classifier automatiquement un article éditorial :

{
  "model": "mistral",
  "prompt": "Analyse le texte suivant et identifie le niveau de lecture (junior, intermediaire, expert) ainsi que trois mots-cles techniques. Texte : Introduction approfondie aux hooks Drupal et aux services Symfony.",
  "format": "json",
  "stream": false
}

La sortie du modèle respecte scrupuleusement la syntaxe d'un dictionnaire JSON :

{
  "niveau": "intermediaire",
  "mots_cles": [
    "Drupal",
    "Symfony",
    "Services"
  ]
}

Validation et mapping strict des taxonomies

Une erreur classique consiste à insérer aveuglément les termes retournés par l'IA dans les vocabulaires Drupal. Cette pratique pollue les bases de données avec des synonymes superflus ou des fautes de frappe.

Pour maintenir l'intégrité du référentiel taxonomique, le service Drupal doit appliquer un filtre de réconciliation :

  • Extraction des termes candidats générés au format JSON.
  • Comparaison insensible à la casse avec les entités de taxonomie existantes dans le vocabulaire cible via un EntityQuery.
  • Affectation de l'identifiant numérique de l'entité (TID) uniquement en cas de correspondance exacte ou supérieure à un seuil de similarité défini (via l'algorithme de Levenshtein).
  • Rejet pur et simple ou mise en file de modération humaine pour tout nouveau terme proposé par le modèle.
  • Journalisation systématique des suggestions refusées afin d'affiner le jeu de consignes du prompt système.

Foire aux questions sur l'IA locale avec Drupal

Pourquoi préférer Ollama plutôt que l'API officielle de Mistral ou OpenAI ?

La motivation première repose sur la conformité et la souveraineté des données d'entreprise. Avec Ollama, aucun octet de vos contenus ne franchit les frontières de votre réseau privé. De plus, les coûts d'infrastructure restent fixes quel que soit le volume de requêtes, ce qui évite les mauvaises surprises de facturation à l'usage.

Quels modèles de langage offrent le meilleur compromis sous Debian ?

Pour une machine dotée de ressources raisonnables, le modèle Mistral 7B (ou sa déclinaison Nemo) et le modèle Llama 3.1 8B représentent la référence absolue. Ils fournissent un niveau de compréhension du français et de respect des instructions remarquable tout en s'exécutant sur des instances pourvues de 16 à 32 gigaoctets de mémoire vive.

Un processeur standard suffit-il ou faut-il impérativement un GPU ?

Un processeur moderne multicoeur suffit largement pour des tâches asynchrones exécutées par les workers cron de Drupal, telles que l'indexation de nuit ou le résumé par lots de documents archivés. En revanche, si vous souhaitez offrir aux rédacteurs une assistance interactive en direct sur les formulaires avec un temps de réponse inférieur à deux secondes, la présence d'une carte graphique dédiée reste fortement recommandée.

Comment éviter le blocage de PHP pendant les inférences longues ?

Il ne faut jamais laisser une inférence de longue durée s'exécuter dans le fil synchrone de traitement d'une page publique. Pour les tâches lourdes, confiez le travail au système de files d'attente de Drupal (Queue API) couplé à des exécuteurs asynchrones tournant en arrière-plan via Drush ou des processus PHP autonomes supervisés par systemd.

Cyprien Prouvot

Cyprien Prouvot

Associé & Directeur Technique

Associé de l'agence, Cyprien pilote la vision technique et garantit la qualité des développements web. Il encadre les équipes internes et conseille les clients sur les choix d'architecture ou d'outils digitaux les plus pertinents. Il intervient également sur ce blog pour décrypter l'écosystème web, les tendances tech et les bonnes pratiques de conception.


Prêt ? Partez.

Que ce soit pour vous aider à faire le point sur vos besoins ou vous présenter les avantages et fonctionnalités de nos solutions, nous sommes là.
 

Back to top