Aetherio

Tester une application boostée à l'IA : stratégie pour des composants non-déterministes

12 minutes min de lecture

Partager l'article

Introduction

L'intégration de l'intelligence artificielle, et plus particulièrement des grands modèles de langage (LLM), transforme radicalement la manière dont nous concevons et développons nos applications. Au sein d'Aetherio, en tant que cabinet spécialisé dans le développement d'applications web sur-mesure pour les startups, PME et scale-ups à Lyon, nous observons une explosion des cas d'usage allant de l'automatisation de processus métiers à la génération de contenu. Cependant, cette révolution technologique introduit également un défi majeur : comment garantir la fiabilité et la qualité d'une application lorsque ses composants clés sont intrinsèquement non-déterministes ? C'est une question cruciale, notamment quand nous parlons de développement d'applications web sur-mesure où chaque détail compte.

Traditionnellement, les tests logiciels reposent sur l'hypothèse qu'une même entrée produira toujours la même sortie. Cette certitude est le fondement des tests unitaires, d'intégration et end-to-end classiques. Or, pour un LLM, cette règle ne s'applique plus. Demandez deux fois la même chose à ChatGPT ou Claude, et vous obtiendrez très probablement deux réponses différentes, bien que contextuellement similaires. Ce caractère "non-déterministe" rend les méthodes de test classiques obsolètes et exige l'adoption de stratégies innovantes. Cet article vise à vous guider à travers les techniques et approches pour efficacement tester application IA non déterministe et assurer la robustesse de vos solutions, même face à l'imprévisibilité de l'IA.

Développeur devant un tableau de bord de tests d'application IA avec des graphiques fluctuants

Le Problème du Non-Déterminisme : Pourquoi Tester l'IA est Différent

L'ère de l'IA générative a ouvert des horizons immenses pour la création de valeur, mais elle a aussi bousculé les principes fondamentaux de l'ingénierie logicielle, particulièrement en ce qui concerne la phase de test. Pour bien comprendre, il faut d'abord comprendre l'intelligence artificielle et ce qu'impliquent les LLM. Le cœur du problème réside dans un concept simple : l'aléatoire inhérent à ces systèmes. Un même prompt, envoyé deux fois au même modèle, peut générer des réponses qui, bien que sémantiquement similaires, ne seront jamais identiques mot pour mot. Par conséquent, un test d'égalité stricte (assert_equals) échouera systématiquement, même si la réponse générée est parfaitement pertinente et correcte d'un point de vue fonctionnel. C'est le défi central lorsqu'il s'agit de tester application IA non déterministe.

Les racines de l'imprévisibilité des LLM

Plusieurs facteurs contribuent à ce caractère non-déterministe des LLM, qui peuvent également varier selon le choix des LLM et leurs spécificités :

  • Facteur 'Temperature' : Un hyperparamètre clé qui contrôle le niveau de créativité ou de 'randomness' de la sortie du modèle. Une température élevée encourage des réponses plus diverses et imprévisibles.
  • Top-k et Top-p Sampling : Ces techniques déterminent l'ensemble des mots parmi lesquels le modèle choisit pour générer la suite du texte. Elles introduisent une part d'aléatoire pour éviter la répétition et favoriser la fluidité.
  • État Interne du Modèle : Bien que moins fréquent sur des APIs robustes, l'état interne du modèle ou la distribution des poids peut légèrement varier au fil du temps ou entre différentes instances utilisées pour servir les requêtes.
  • Prompts dynamiques : Souvent, les applications utilisent des prompts qui varient légèrement (ex: date, nom d'utilisateur) rendant chaque requête unique de facto.

Cette nature non-déterministe ne signifie pas que le LLM est 'cassé' ou défectueux. Au contraire, c'est souvent intentionnel pour permettre des interactions plus humaines et créatives. Mais cela signifie que les stratégies de test doivent évoluer. L'objectif n'est plus de vérifier une correspondance exacte, mais d'évaluer la qualité réponse IA test automatisé en termes de pertinence, de format, de sécurité et d'alignement avec les objectifs business. Pour vos projets d'intégration d'IA, comme une stratégie SEO automatisée par IA, la fiabilité des outputs générés devient cruciale.

Stratégies de Test pour Applications IA Non-Déterministes

Réussir à tester application IA non déterministe implique de repenser l'approche QA. Finis les tests d'égalité stricte, place à des méthodes qui évaluent la pertinence et le respect des contraintes plutôt que l'identité parfaite. Chez Aetherio, nous mettons en œuvre plusieurs stratégies complémentaires pour garantir la robustesse de nos intégrations IA.

1. Test de structure et de format (Schéma Validation)

Quand le contenu exact est imprévisible, le format l'est moins. Si votre application attend une réponse structurée (JSON, XML, Markdown formaté), vous pouvez valider cette structure indépendamment du contenu. C'est souvent le cas pour les agents IA qui doivent retourner des données pour une action spécifique dans l'application.

  • Validation JSON Schema : Utilisez des outils comme ajv en JavaScript pour vérifier que la réponse JSON respecte un schéma défini (présence des clés, types de données, valeurs dans des plages spécifiques).
  • Expressions régulières (Regex) : Pour les réponses textuelles, les regex peuvent vérifier la présence de mots-clés importants, le respect d'une syntaxe particulière (ex: un numéro de téléphone, une date), ou l'absence de certains termes.
  • Validation de la Longueur : Assurez-vous que la réponse ne dépasse pas une certaine limite de caractères ou qu'elle en atteint un minimum, pour éviter des réponses trop laconiques ou trop verbeuses.

Exemple concret : Si un LLM est censé extraire des entités (nom, prénom, adresse) d'un texte libre et les retourner en JSON, votre test ne vérifiera pas les valeurs exactes du nom, mais s'assurera que {"nom": "...", "prénom": "...", "adresse": "..."} est bien retourné, et que les valeurs sont des chaînes de caractères.

2. Le "LLM as Judge" : l'IA évalue l'IA

L'une des approches les plus prometteuses pour évaluer la qualité réponse IA test automatisé est d'utiliser un autre LLM (généralement plus performant ou spécifiquement fine-tuné) pour juger la qualité de la réponse générée. Ce concept de tests "LLM as judge" permet d'automatiser une évaluation qui, autrement, nécessiterait une revue humaine fastidieuse et subjective.

Le principe est simple : vous fournissez au LLM 'juge' le prompt original, la réponse générée par votre application, et une consigne d'évaluation claire. Le juge peut ensuite attribuer une note (sur 5 par exemple) à la réponse, ou même générer une explication des points forts et des points faibles. Chez Aetherio, nous avons mis en place des systèmes où le LLM juge évalue la pertinence, la concision, le ton, l'absence d'hallucinations, et le respect des instructions implicites ou explicites.

3. Jeux de questions de référence avec seuil de qualité

Pour des fonctionnalités critiques, il est impératif de disposer d'un répertoire de "golden questions" : des prompts de référence dont les réponses attendues sont connues et validées manuellement. Ces tests doivent être exécutés régulièrement pour monitorer la performance de l'IA.

  • Questionnement varié : Le jeu de questions doit couvrir différents cas d'usage, des requêtes simples aux plus complexes, incluant des cas limites ou des tentatives de 'jailbreak'.
  • Seuil de qualité dynamique : Pour chaque question, l'évaluation (manuelle ou via LLM as a judge) doit aboutir à un score. Ce score est comparé à un seuil défini. Par exemple, si 80% des réponses à ce jeu de questions obtiennent une note de 4/5 ou plus, le test est considéré comme "passant".
  • Suivi des métriques : Il est essentiel de suivre l'évolution de ce score au fil du temps. Une baisse significative pourrait indiquer une dégradation du modèle, un problème de prompt, ou un ajustement malheureux des hyperparamètres.

4. Tests de régression sur les prompts (Prompt Versioning)

Les prompts sont les nouveaux codes. Toute modification d'un prompt peut avoir un impact majeur sur la réponse de l'IA. Il est donc crucial de les versionner et de les soumettre à des tests de régression.

  • Versionnement : Intégrez vos prompts au contrôle de version (Git) comme n'importe quel autre code source.
  • Cas de test associés : Chaque prompt critique doit avoir un ensemble de cas de test spécifiques. Si vous modifiez un prompt, l'ensemble de ces tests doit être exécuté pour s'assurer que les changements n'introduisent pas de régressions non désirées.
  • Surveillance des métriques d'évaluation : Comparez les métriques d'évaluation pour une version de prompt donnée avant et après modification. Cela permet de quantifier l'impact du changement.

Intégration des Tests IA dans la CI/CD

La mise en place de pipelines CI/CD robustes est la pierre angulaire de tout développement logiciel moderne. Mais comment intégrer le test LLM non déterministe sans freiner le déploiement continu ? La clé est d'adopter une flexibilité et une vision pragmatique, reconnaissant la nature particulière des composants IA. Chez Aetherio, nous concevons des pipelines où les tests IA sont des éléments cruciaux mais adaptés au contexte.

Un pipeline CI/CD adapté à l'IA

Contrairement aux tests unitaires ou d'intégration classiques qui bloquent généralement le déploiement en cas d'échec, les tests d'IA doivent être traités différemment. Un échec ponctuel du test d'une application IA ne signifie pas forcément que l'application est cassée, mais plutôt que la performance de l'IA a fluctué.

  1. Exécution Asynchrone ou en Background : Les tests d'IA, surtout ceux impliquant des LLM as a judge, peuvent être longs et coûteux. Il est souvent préférable de les exécuter de manière asynchrone par rapport au pipeline principal de déploiement, ou sur une cadence moins fréquente (quotidienne plutôt qu'à chaque commit).
  2. Alerting plutôt que Blocage Dur : Si les tests échouent, le pipeline ne doit pas nécessairement s'arrêter. Au lieu de cela, un système d'alerte (Slack, email, dashboards) doit notifier les équipes concernées. L'objectif est de détecter les dégradations de performance et d'agir réactivement, plutôt que de bloquer un déploiement potentiellement stable sur les autres composants.
  3. Dashboards de Surveillance : Mettez en place des tableaux de bord clairs qui affichent l'évolution des scores des tests d'IA au fil du temps. Cela permet de visualiser les tendances, d'identifier les régressions et de comprendre l'impact des changements de modèle ou de prompt.
  4. Tests A/B pour les Prompts/Modèles : Lorsque vous envisagez de changer un prompt ou de mettre à jour un modèle, déployez d'abord la nouvelle version sur un petit segment d'utilisateurs ou dans un environnement de test dédié. Mesurez la performance et la qualité des réponses en comparaison avec la version précédente avant de généraliser le déploiement.

Exemple de pipeline CI/CD pour une application avec IA (simplifié)

Le pipeline ci-dessous sépare deux familles de tests. Les premiers bloquent : unitaires et d'intégration, rapides, gratuits, avec un LLM simulé, donc parfaitement déterministes. Les seconds mesurent : ils interrogent le vrai modèle, tournent chaque nuit et alertent l'équipe quand la qualité passe sous un seuil.

# .github/workflows/ci.yml
name: CI

on:
  pull_request:
  schedule:
    - cron: '0 3 * * *' # évaluation complète chaque nuit
  workflow_dispatch:

jobs:
  tests:
    # Le LLM est simulé : résultat déterministe, job bloquant pour le merge.
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run typecheck
      - run: npm test

  eval-ia:
    # Appels au vrai modèle : on mesure et on alerte, sans bloquer les pull requests.
    if: github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    env:
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - name: Tests de propriétés contre le vrai modèle
        run: npm run eval
      - name: Jeu de questions de référence, chaque cas exécuté 3 fois
        run: npx promptfoo eval -c evals/golden.yaml --repeat 3 -o results.json
        env:
          PROMPTFOO_FAILED_TEST_EXIT_CODE: 0 # le seuil global est vérifié à l'étape suivante
      - name: Vérifier le seuil de qualité
        run: |
          PASS_RATE=$(jq '.results.stats.successes / (.results.stats.successes + .results.stats.failures) * 100' results.json)
          echo "Taux de réussite : ${PASS_RATE} %"
          if (( $(echo "$PASS_RATE < 80" | bc -l) )); then
            curl -sS -X POST -H 'Content-Type: application/json' \
              -d "{\"text\": \"Évaluation IA : ${PASS_RATE} % de réussite, sous le seuil de 80 %\"}" \
              "$SLACK_WEBHOOK_URL"
            exit 1
          fi

Le seuil ne vaut pas pour un cas isolé : un cas qui échoue une fois sur trois exécutions ne fait pas échouer le job, une baisse générale oui. C'est exactement la logique du « seuil de qualité » décrite plus haut. Pour les pull requests qui modifient un prompt, vous pouvez ajouter une version réduite de cette évaluation, marquée continue-on-error: true, qui informe le relecteur sans bloquer.

Exemples de tests concrets en TypeScript

Les stratégies précédentes prennent forme avec des outils ordinaires : Vitest pour les tests, zod pour valider les structures, et une conception du code qui rend le modèle remplaçable.

Rendre le LLM injectable pour tester votre code

La partie non déterministe de l'application doit rester petite et isolée. Si la fonction qui appelle le modèle reçoit cet appel en paramètre, tout le reste (validation, gestion d'erreur, logique métier) se teste de façon déterministe.

// src/contacts/extract-contact.ts
import { z } from 'zod';

export const Contact = z.object({
  nom: z.string().min(1),
  prenom: z.string().min(1),
  email: z.string().email().nullable(),
});
export type Contact = z.infer<typeof Contact>;

// L'appel au modèle est injecté : en test, on le remplace par une fonction déterministe.
export type CompleteFn = (system: string, input: string) => Promise<string>;

const SYSTEM = 'Extrais le contact du texte. Réponds uniquement en JSON : {"nom", "prenom", "email"}.';

export async function extractContact(text: string, complete: CompleteFn): Promise<Contact | null> {
  const raw = await complete(SYSTEM, text);
  try {
    const parsed = Contact.safeParse(JSON.parse(raw));
    return parsed.success ? parsed.data : null;
  } catch {
    return null; // JSON invalide : l'appelant doit savoir gérer l'absence de résultat
  }
}

Les tests unitaires couvrent alors toutes les réponses possibles du modèle, y compris les mauvaises. C'est souvent là que se cachent les bugs : pas dans la réponse idéale, mais dans le texte libre renvoyé à la place du JSON attendu.

// src/contacts/extract-contact.test.ts
import { describe, expect, it } from 'vitest';
import { extractContact } from './extract-contact';

const reply = (text: string) => async () => text;

describe('extractContact', () => {
  it('retourne le contact quand le modèle répond un JSON valide', async () => {
    const contact = await extractContact('...', reply('{"nom":"Martin","prenom":"Léa","email":"lea@exemple.fr"}'));
    expect(contact).toEqual({ nom: 'Martin', prenom: 'Léa', email: 'lea@exemple.fr' });
  });

  it('retourne null quand le modèle répond du texte libre', async () => {
    expect(await extractContact('...', reply('Voici le contact : Léa Martin'))).toBeNull();
  });

  it('retourne null quand un champ obligatoire manque', async () => {
    expect(await extractContact('...', reply('{"nom":"Martin","email":null}'))).toBeNull();
  });
});

Les modes de sortie structurée proposés par les fournisseurs et par le Vercel AI SDK réduisent les erreurs de format. Ils ne dispensent pas de valider : un JSON bien formé peut contenir une valeur absurde.

Tester des propriétés contre le vrai modèle

Pour la suite d'évaluation nocturne (npm run eval, avec une configuration Vitest dédiée), on branche le vrai modèle et on vérifie des propriétés plutôt qu'une chaîne exacte : un format valide et les valeurs attendues, à chaque exécution.

// evals/extract-contact.eval.ts
import { describe, expect, it } from 'vitest';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
import { extractContact, type CompleteFn } from '../src/contacts/extract-contact';

const complete: CompleteFn = async (system, input) =>
  (await generateText({ model: openai('gpt-4o-mini'), system, prompt: input, temperature: 0 })).text;

const RUNS = 5;

describe('extractContact avec le vrai modèle', () => {
  it('extrait un contact valide à chaque exécution', async () => {
    const text = 'Bonjour, je suis Léa Martin, joignable à lea.martin@exemple.fr pour le devis.';
    const results = await Promise.all(Array.from({ length: RUNS }, () => extractContact(text, complete)));

    for (const contact of results) {
      expect(contact).not.toBeNull();
      expect(contact?.email).toBe('lea.martin@exemple.fr');
      expect(contact?.nom.toLowerCase()).toBe('martin');
    }
  }, 60_000);
});

Exécuter plusieurs fois le même cas rend visible l'instabilité : un test qui passe quatre fois sur cinq signale un prompt fragile, même si la plupart des exécutions sont correctes. Pour les critères qualitatifs (ton, pertinence, absence d'hallucination), le relais revient au LLM as judge, via des assertions llm-rubric dans promptfoo ou un juge maison.

Surveiller la qualité en production

Les tests avant déploiement ne voient que les entrées que vous avez imaginées. En production, les utilisateurs posent d'autres questions, les documents sont plus longs, et le fournisseur peut faire évoluer son modèle. La qualité se surveille donc aussi après la mise en ligne :

  • Des métriques techniques : taux de réponses rejetées par la validation de format, taux de refus ou de « je ne sais pas », latence et coût par requête. Une hausse soudaine de l'un d'eux est souvent le premier signal d'une régression.
  • Le retour des utilisateurs : un simple bouton « utile / pas utile » sous chaque réponse fournit un signal continu, et les réponses mal notées alimentent directement votre jeu de questions de référence.
  • Un échantillonnage évalué : chaque jour, une partie des conversations est soumise au juge, avec les mêmes critères que les tests. Attention aux données personnelles, qui doivent être pseudonymisées avant cette étape.
  • Des versions de modèle figées : quand le fournisseur propose des versions datées d'un modèle, utilisez-les plutôt qu'un alias qui peut changer sans prévenir, et relancez l'évaluation complète avant chaque changement de version.

Ces signaux s'intègrent dans votre dispositif de monitoring et d'observabilité existant. Le point clé est la boucle : chaque défaut observé en production devient un cas de test, et le jeu de référence s'enrichit de lui-même.

Conclusion

Tester une application boostée à l'IA ne consiste pas à obtenir deux fois la même réponse, mais à vérifier que chaque réponse respecte un contrat : un format, des faits, un ton, des limites. Le code autour du modèle se teste de façon classique, avec un LLM simulé et des tests bloquants. Le modèle lui-même s'évalue par propriétés, par jeux de référence exécutés plusieurs fois et par seuils de qualité, avec des alertes plutôt que des blocages.

Cette organisation garde un pipeline rapide et fiable pour les développeurs, tout en donnant une mesure continue de ce que les utilisateurs perçoivent vraiment. Si vous développez une fonctionnalité IA et voulez sécuriser sa qualité avant et après la mise en production, notre équipe peut vous accompagner dans la conception de votre application IA. Pour une vue plus large de la stratégie de tests d'une application web, consultez aussi notre guide des tests automatisés.

FAQ - Questions fréquentes