Garde-fous LLM en streaming : un token envoyé ne se reprend pas

Garde-fous LLM en streaming : un token envoyé ne se reprend pas. Vitesse ou sécurité chez NeMo, Guardrails AI et Bedrock, et un sas qui vérifie chaque phrase.

Un token qu’un LLM a envoyé en streaming ne peut pas être rappelé. Une fois le texte sous les yeux du lecteur, un garde-fou (guardrail) qui y trouve à redire ne peut plus qu’arrêter la suite. Un garde-fou posé sur un flux ne prend donc qu’une vraie décision : vérifier avant de diffuser et faire attendre le lecteur, ou diffuser d’abord et accepter qu’une réponse bloquée ait déjà été lue. Trois schémas tiennent la route : contrôler après diffusion, retenir des blocs de taille fixe le temps de les vérifier, ou des phrases entières. L’assistant de ce site, que j’ai construit, fait passer chaque phrase par un sas de validation (sentence gate). Le coût : une phrase d’attente. La limite : il vérifie qu’une affirmation est étayée, pas qu’elle est vraie.

Cet article traite du dernier contrôle avant la diffusion d’une réponse ancrée dans les sources. Les couches qui le précèdent, du refus des questions hors périmètre au seuil minimal de score de récupération, sont décrites dans Garde-fous contre les hallucinations en RAG : une conception en cinq couches.

Pourquoi un token déjà envoyé en streaming ne se reprend pas

Un validateur a posteriori ne voit la réponse complète qu’une fois que le lecteur l’a lue. Si la troisième phrase invente une date, la supprimer ensuite modifie la trace enregistrée, pas ce qui a été lu. Une prépublication arXiv récente de Christopher M. Frost formule cette limite avec exactitude : « un contrôle qui détecte un motif interdit après avoir diffusé le bloc qui le complète ne peut pas le rappeler ».

Les fournisseurs documentent la même limite. Le filtre asynchrone d’Azure OpenAI, qu’il faut activer explicitement, renvoie le texte immédiatement et ne garantit son signal de violation qu’à moins d’environ 1 000 caractères du contenu en infraction. Sa rubrique de dépannage conseille de « mettre en place dans votre application un caviardage du contenu pour retirer le contenu déjà affiché ». La fiche LLM09:2025 de l’OWASP recommande des outils et des processus « pour valider automatiquement les sorties clés ». Reste à savoir à quel moment la validation s’exécute par rapport à la diffusion.

Trois façons de protéger un flux : a posteriori, par bloc, par phrase

Approche

Le lecteur voit le texte

Une réponse bloquée

Exemples

A posteriori (diffusion d’abord)

Avant sa vérification (NeMo : chaque bloc dès qu’il est constitué ; modes asynchrones d’Azure et de Bedrock : au fil de la génération)

En partie lue avant l’arrêt

NeMo avec stream_first activé (sa valeur par défaut) ; modes asynchrones d’Azure et de Bedrock

Validation par bloc

Après validation de chaque bloc

Le bloc rejeté n’est jamais affiché

Valeurs par défaut d’Azure et de Bedrock ; NeMo avec stream_first désactivé

Validation par phrase

Après validation de chaque phrase

La phrase rejetée n’est jamais affichée

L’assistant de ce site

Le compromis de stream_first dans NeMo Guardrails

Dans NeMo Guardrails, le streaming des rails de sortie est désactivé par défaut. Une fois activé, il exécute les rails de sortie, c’est-à-dire les contrôles que NeMo applique au texte généré, sur des blocs de 200 tokens par défaut, en reportant 50 tokens de contexte dans le bloc suivant. L’option stream_first vaut True par défaut : « Le client reçoit chaque bloc de tokens avant que les rails de sortie ne traitent ce bloc. Le délai jusqu’au premier token s’en trouve réduit, mais si un rail bloque le contenu, l’utilisateur a déjà reçu les tokens. » Le flux se termine alors par une erreur JSON.

Avec False, « l’utilisateur ne voit jamais le contenu bloqué, mais le délai jusqu’au premier token augmente du temps d’exécution du rail pour chaque bloc ». Tout le compromis est là : d’un côté le délai jusqu’au premier token (time to first token, TTFT), de l’autre du texte bloqué qui parvient au lecteur. Une fois le streaming des rails de sortie activé, NeMo diffuse d’abord par défaut ; les deux filtres cloud présentés plus bas vérifient d’abord. Si un rail existe pour arrêter quelque chose qui ne doit jamais être vu, changez la valeur par défaut.

Le compromis des validateurs par phrase chez Guardrails AI

Guardrails AI choisit la phrase : « Par défaut, les validateurs attendent d’avoir accumulé l’équivalent d’une phrase de contenu provenant du LLM avant d’exécuter la validation. » Valider des phrases isolées plutôt que la réponse entière « aide à réduire les coûts des validateurs qui nécessitent une inférence coûteuse ».

Les types on_fail ne sont pas tous pris en charge en streaming, et les corrections issues de plusieurs validateurs attendent « que tous les validateurs aient accumulé suffisamment de blocs » avant d’être fusionnées. La documentation ne dit pas clairement si les tokens bruts parviennent à l’utilisateur avant la validation : vérifiez-le dans votre propre intégration.

Mise en mémoire tampon par défaut : Azure OpenAI et Amazon Bedrock

Par défaut, Azure OpenAI met le texte de la complétion en mémoire tampon, filtre ce tampon et renvoie des « blocs de contenu » plutôt que des tokens isolés. Amazon Bedrock Guardrails analyse les blocs avant de les envoyer dans son mode synchrone par défaut, qui « introduit une certaine latence » mais « offre une meilleure précision ». En mode asynchrone, les blocs de Bedrock « peuvent contenir du contenu inapproprié tant que l’analyse des garde-fous n’est pas terminée », et les informations sensibles ne peuvent pas être masquées.

Comment un sas par phrase valide un flux

L’assistant de ce site répond uniquement à partir du contenu du site, et une question qui ne me concerne pas et qu’aucun passage n’étaye n’atteint jamais le modèle. Il parle de moi à la troisième personne. Sur un site qui porte mon nom, la pire défaillance est une qualification inventée, et une qualification est une affirmation. La phrase est la plus petite unité qui en porte une : le sas accumule donc le texte dans un tampon jusqu’à une fin de phrase, vérifie, et seulement ensuite diffuse. La démo RAG et sans RAG du Labo IA appelle le même endpoint de streaming.

Chronologie d’un sas par phrase : les tokens s’accumulent dans un tampon jusqu’à la fin d’une phrase ; la première phrase réussit les quatre vérifications et s’affiche sans son marqueur ; la seconde contient une valeur absente des sources, elle est donc retenue et le flux s’arrête.

Où finit une phrase quand les tokens arrivent encore

  • Signes de fin de phrase : . ! ? …, plus le point d’interrogation ؟ et le point final ۔ de l’écriture arabe et, pour le chinois et le hindi, 。 ! ?, le danda । et le double danda ॥. Une suite comme ?! compte pour une seule frontière. Le point-virgule de l’écriture arabe, ؛, relie des propositions : il est volontairement exclu.

  • Un espace doit suivre. Cette seule règle garde 3.14 et example.com intacts. Font exception le point, le point d’exclamation et le point d’interrogation chinois, qui ne sont suivis d’aucun espace. À l’intérieur d’une citation, ils terminent la phrase là où la citation se ferme, et pas du tout quand une virgule suit.

  • Tout point ne termine pas une phrase. Suivi d’une minuscule, il ne la termine pas (et al. et), pas plus que le point d’une abréviation d’une courte liste, comme e.g., p. ex. ou l’allemand bzw., ni celui qui suit le jour dans une date allemande (am 17. September).

  • Le caractère suivant tranche. Une frontière n’est définitive qu’à l’arrivée d’un caractère qui n’est ni un espace ni un marqueur de référence, car les marqueurs qui suivent un point appartiennent à la phrase qui les précède : tous, [S1][S2] comme [S1] [S2], et pas seulement le premier.

  • Un marqueur de liste après une frontière n’est pas une phrase. Sans cette règle, un 1. en tête de la réponse ou d’une nouvelle phrase serait jugé comme une affirmation sans référence et arrêterait une réponse ancrée dès sa première puce.

  • Un plafond. Au-delà de mille caractères sans frontière, le tampon est coupé au dernier espace et vérifié malgré tout.

  • Une ligne vide est conservée. Une phrase qui suivait une ligne vide est diffusée après une ligne vide, si bien qu’une réponse en deux paragraphes reste en deux paragraphes.

Attendre le caractère suivant coûte en général un caractère de retard, un peu plus quand des marqueurs suivent le point. La dernière phrase est vérifiée à la fin du flux. Les comparaisons normalisent les chiffres persans et leurs séparateurs (۳٫۱۴ correspond à 3.14) ainsi que les formes de lettres arabes, suppriment les signes de voyelles brèves de l’arabe et traitent l’antiliant sans chasse (ZWNJ) comme un espace : ce sont les mêmes variantes que la recherche hybride pour le RAG en persan traite côté récupération.

Les quatre vérifications déterministes : références, valeurs et personne grammaticale

  1. Plage des références. Un marqueur comme [S7] alors que six sources ont été fournies désigne une source inventée.

  2. Référence obligatoire. Une phrase de huit mots ou plus, ou contenant un nombre, une URL ou une adresse e-mail, doit porter un marqueur. Les phrases de liaison courtes passent.

  3. Présence littérale. Chaque nombre, chaque URL et chaque adresse e-mail doit figurer dans une source que la phrase cite. La correspondance respecte les frontières de chiffres : une année présente dans une source ne peut pas cautionner un pourcentage qui partage ses derniers chiffres, et des décimales qui ne diffèrent que par des zéros finaux sont une même valeur : 20.00 vaut 20. Une virgule qui ne sépare pas les milliers est une virgule décimale : 2,5 vaut 2.5, et non 25.

  4. Personne grammaticale. Un mot à la première personne arrête le flux. La liste anglaise est courte et fixe : les pronoms de première personne et leurs contractions. La liste persane est établie à la main : des pronoms et quelques formes verbales à risque, comme les équivalents persans de « je suis » et « j’ai ». Le persan marque la personne par des terminaisons verbales que des noms ordinaires peuvent partager : une règle assez large pour intercepter toutes ces terminaisons intercepterait aussi des noms. Les listes des autres langues du site contiennent les pronoms personnels et les possessifs, et celles de l’espagnol, du portugais, du russe, de l’arabe et du hindi y ajoutent quelques verbes, car ces langues disent le plus souvent « je » par le seul verbe ; en chinois, on cherche 我 n’importe où dans la phrase, sauf dans un composé de 自我, « auto- », comme 自我修正 (« autocorrection »). Les listes appliquées dépendent de la langue dans laquelle la réponse a été demandée. L’anglais, le persan, l’arabe, le russe, le hindi et le chinois s’appliquent à toutes les réponses. L’anglais est la langue des instructions du modèle, donc celle vers laquelle une réponse risque le plus de glisser ; les cinq autres s’écrivent dans des écritures où aucun nom ni sigle latin ne peut former l’un de leurs mots. L’exception est une réponse en arabe, qui n’est pas vérifiée pour le « je » et le « nous » persans, car en arabe ces mêmes mots signifient « de » et « ce que ». Les listes allemande, française, espagnole et portugaise ne s’appliquent qu’aux réponses demandées dans leur langue : leurs mots courts sont aussi des noms et des mots ordinaires d’autres langues, comme le « Ma » de « Ma et al. ». Les sigles en majuscules, un nom de famille devant « et al. » et le chiffre romain de « annexe I » sont écartés avant la lecture de toute liste.

Que se passe-t-il quand une phrase est rejetée ?

  • Toutes les phrases validées avant l’échec sont diffusées, y compris celles qui précèdent la phrase rejetée dans le même lot.

  • Le sas se ferme et reste fermé : une seule phrase non étayée discrédite la suite.

  • Un événement guard transporte les motifs de la violation ainsi qu’un texte fixe et localisé indiquant que la suite a été retenue, et l’événement final done consigne l’arrêt. La phrase bloquée n’est jamais envoyée ; le serveur n’en journalise que la longueur.

  • Le flux du fournisseur est fermé : c’est ainsi que la conception annule la requête en amont. Ce qui a été généré avant l’arrêt figure tout de même sur la facture, et il est donc imputé au plafond de dépenses mensuel. Les deux protocoles de fournisseur déclarent la consommation en sortie dans un événement final qu’un flux interrompu n’atteint jamais : le service conserve donc le texte reçu et comble ce que le fournisseur n’a pas déclaré à raison d’un token par octet UTF-8, un décompte qui, s’il se trompe, pèche par excès. Les tokens qu’un modèle de raisonnement consacre à réfléchir n’arrivent jamais sous forme de texte, et cette estimation les laisse donc de côté. L’article sur la conception en cinq couches traite de ce plafond.

Le panneau de chat marque ce tour de conversation comme retenu et conserve le texte déjà diffusé dans l’historique de la conversation, car ce texte est étayé jusqu’à l’arrêt.

La troncature est une autre défaillance, et une version antérieure de cet assistant confondait les deux. Quand le modèle manquait de place pour sa sortie, le sas jugeait le fragment coupé comme une phrase terminée et le retenait faute de référence. On annonçait alors au lecteur qu’une réponse qui avait seulement été interrompue ne pouvait pas être étayée par les sources. Quand aucun texte ne revenait du tout, le tour disparaissait sans le moindre message. Désormais, le fragment inachevé est écarté sans être jugé et un avis signale que la réponse a été coupée. Une phrase interrompue n’est pas une phrase non étayée. Si les deux surviennent, l’arrêt par le sas l’emporte.

Les marqueurs de référence restent internes

Le prompt demande un marqueur [Sn] sur chaque phrase qui énonce un fait, et le sas applique la règle de référence obligatoire à chaque phrase substantielle. Une fois la phrase validée, ses marqueurs sont retirés et l’espacement corrigé, y compris devant la ponctuation persane : le lecteur voit de la prose, pas des marqueurs. Ne plus demander de marqueurs dans le prompt ferait à l’inverse échouer chaque phrase substantielle sur la règle de référence obligatoire.

Quand le modèle n’a rien à dire

Quand les sources ne couvrent pas la question, le prompt demande au modèle un token fixe, pas une phrase. Un refus rédigé par le modèle serait une affirmation comme une autre, et sans référence : le sas l’arrêterait avec un avis qui met en cause les sources pour une question à laquelle elles n’ont jamais été censées répondre.

Le sas ne diffuse jamais ce token. Tant que le texte reçu peut encore devenir ce token, il est retenu ; le premier caractère qui l’exclut renvoie le texte sur le chemin habituel, si bien qu’une réponse ordinaire n’attend rien de plus. Si une réponse commence par le token, le service la remplace par le refus fixe du site, le même texte que pour tout autre refus. Si le token suit des phrases déjà diffusées, celles-ci restent et la réponse s’arrête là.

« Étayé » ne veut pas dire « vrai »

Dans une étude pilotée par Stanford, Magesh et ses collègues ont réalisé ce qu’ils décrivent comme la première évaluation préenregistrée d’outils de recherche juridique fondés sur l’IA. Les outils de recherche par IA de LexisNexis et de Thomson Reuters ont chacun halluciné entre 17 % et 33 % du temps. Une réponse était comptée comme hallucinée si elle était « soit incorrecte, soit mal ancrée » (misgrounded), et « mal ancrée » signifie que « des propositions factuelles clés sont citées, mais interprètent mal la source ou renvoient à une source inapplicable ». Les réponses mal ancrées comportent des références. Ce que cela implique pour les produits d’IA juridique fait l’objet d’un article distinct.

Un sas fondé sur des comparaisons de chaînes ne peut pas détecter une réponse mal ancrée, et celui-ci a des angles morts connus :

  • Une phrase qui cite deux sources peut tirer une valeur de l’une ou de l’autre. La vérification se fait par phrase, pas par proposition.

  • Un point décimal compte comme une frontière. 14.52 dans une source peut cautionner 52% ou 14, et un séparateur de milliers fait de même : 1,200 cautionne 200.

  • Les mots ne sont pas vérifiés. Une phrase sans nombre qui se trompe de rôle ou d’organisation passe avec n’importe quel marqueur compris dans la plage.

  • Les phrases courtes sans référence passent. Une phrase de moins de huit mots sans nombre, URL ni adresse n’a besoin d’aucun marqueur. Le chinois ne met pas d’espace entre les mots : on y compte donc les caractères, trois pour deux mots, et une phrase faite uniquement de caractères chinois n’a besoin d’aucun marqueur en dessous de douze.

  • Les listes de mots fixes ont des trous. En anglais, we'd et let's passent, de même qu’un pronom écrit tout en majuscules, comme WE ou US, qui est lu comme un sigle. En persan, la couverture de la première personne repose sur le prompt : un verbe à la première personne qui ne figure pas dans la liste établie à la main passe. Les listes des autres langues sont elles aussi établies à la main, et la première personne en allemand, en français, en espagnol ou en portugais dans une réponse demandée dans une autre langue est laissée au prompt.

  • La ponctuation fermante peut fusionner des phrases. Après un guillemet qui se ferme sur un signe de fin de phrase, comme dans ?» ou .", seule une majuscule ou un saut de ligne termine la phrase ; après une parenthèse, comme dans .), seul un saut de ligne ou un marqueur de référence. Deux phrases peuvent donc être jugées comme une seule : après .) devant une majuscule, après ؟» en arabe, en persan et en hindi, qui n’ont pas de majuscules, et après les guillemets allemands „…?“ et »…?«, que le segmenteur ne lit pas du tout comme fermants. Les signes chinois gardent leur propre règle, décrite plus haut.

L’erreur existe aussi dans l’autre sens. Une abréviation absente de la liste, suivie d’une majuscule, termine une « phrase » trop tôt, comme Mr. devant un nom, tout comme une question entre guillemets suivie d’un nom, ou, en allemand, de n’importe quel substantif, et si ce fragment compte huit mots ou plus sans marqueur, une réponse ancrée est retenue. Une liste introduite par une ligne terminée par un deux-points n’a pas de frontière avant son premier marqueur de liste. Le segmenteur coupe juste après 1., et ce fragment échoue à la règle de référence obligatoire. Dans les deux cas, le sas échoue en position fermée (fail closed) : il retient une réponse ancrée plutôt que de diffuser une réponse non vérifiée.

Le corpus évolue, lui aussi. Les articles publiés sont indexés pour l’assistant : leurs chiffres, y compris les 17 % et 33 % de cet article, deviennent des valeurs que le sas cautionnera. « Étayé » signifie « quelque part dans ce que la phrase cite », pas « à ce sujet ».

Le sas est le dernier contrôle avant diffusion, pas le seul. Joren et ses collègues ont constaté que les modèles plus grands « produisent souvent des réponses incorrectes au lieu de s’abstenir » lorsque le contexte n’est pas suffisant, et Google Research note que si le RAG améliore généralement les performances globales, il « réduit paradoxalement la capacité du modèle à s’abstenir de répondre lorsque cela s’impose ». C’est pourquoi le refus et un seuil de score passent en premier, comme l’expose la conception en cinq couches. La prépublication de Frost énonce la limite de son propre résultat en des termes qui valent aussi pour ce sas : « une conformité à la politique pour un filet de sécurité déterministe, et non une preuve de sûreté sémantique ni de complétude de la politique ».

SSE pour le streaming de LLM : fetch, proxys et annulation

Un sas par phrase n’est utile que si chaque phrase diffusée parvient aussitôt au lecteur. Quatre détails de transport en décident.

Pourquoi fetch et ReadableStream plutôt qu’EventSource

La spécification WHATWG donne au constructeur EventSource une URL et une seule option, withCredentials : ni méthode, ni corps. Une question accompagnée de sa langue et de son historique constitue un corps de requête ; l’endpoint est donc un POST qui renvoie des Server-Sent Events. Le navigateur le lit avec fetch, un lecteur de ReadableStream et un AbortController, dont la méthode abort(), pour reprendre les termes de MDN, « permet d’interrompre les requêtes fetch, la consommation de tout corps de réponse et les flux ».

Un parseur écrit à la main doit respecter ces règles :

  • Conserver un tampon d’une lecture à l’autre. Un bloc n’est pas un message.

  • Émettre l’événement sur une ligne vide. Tant qu’elle n’est pas arrivée, l’événement est incomplet.

  • Ignorer les lignes de commentaire, celles qui commencent par :.

  • Joindre les données multilignes : toutes les lignes data: d’un même événement sont reliées par un saut de ligne.

  • Accepter CRLF, LF et CR comme fins de ligne.

  • Retirer un espace après les deux-points, et un seul.

Décodez avec TextDecoder et {stream: true}, l’option à activer, selon MDN, « si les données sont traitées par blocs ». Sinon, un caractère persan coupé entre deux blocs est décodé en caractères de remplacement.

Mise en mémoire tampon par les proxys : nginx et Caddy

nginx met les réponses qu’il relaie en mémoire tampon par défaut, et un en-tête de réponse X-Accel-Buffering permet de désactiver ce comportement. Le flux de l’assistant envoie X-Accel-Buffering: no et Cache-Control: no-cache, no-store, no-transform ; no-transform est là parce qu’un proxy qui recompresse le corps peut regrouper les blocs. Caddy transmet immédiatement les réponses text/event-stream, et la configuration Caddy du site fixe en plus flush_interval -1 sur la route du chat. Le service worker n’enregistre aucune route pour le flux, car une stratégie de mise en cache retiendrait la réponse jusqu’à sa fin.

Keep-alive et erreurs dans un flux ouvert

La spécification SSE suggère une ligne de commentaire « toutes les 15 secondes environ » pour se prémunir contre les anciens proxys qui coupent les connexions au bout d’un court délai, et le serveur envoie un ping à cet intervalle. Le statut vaut déjà 200 quand le premier octet part : les échecs circulent donc sous forme d’événement error à l’intérieur du flux.

Une annulation qui arrête aussi la facture

Arrêter la réponse dans le navigateur interrompt le fetch. Le serveur vérifie à l’arrivée de chaque token si le client s’est déconnecté. Quand c’est le cas, il sort de la boucle et ferme le flux du fournisseur, ce qui, dans cette conception, annule la requête en amont. Deux points méritent un test plutôt qu’une supposition. D’abord, que chaque proxy et chaque CDN entre le navigateur et votre API transmette bien la déconnexion du client. Testez ce chemin ; ne vous fiez pas à la configuration. Ensuite, le moment où votre fournisseur déclare la consommation : si la consommation en sortie n’arrive qu’avec le dernier bloc, un flux que vous fermez plus tôt ne la reçoit jamais, et un plafond de dépenses qui compte la consommation déclarée sous-estimera ce qui a été généré.

Comment tester un garde-fou en streaming

Les tests couvrent le sas à trois niveaux :

  • Tests unitaires du sas sur des sources fixes. Valeurs inventées, marqueurs hors plage, affirmations sans référence et mots à la première personne (dans chacune des langues du site) arrêtent le flux ; les valeurs étayées et les phrases de liaison courtes passent ; les phrases correctes qui précèdent survivent à une phrase fautive qui suit.

  • Tests du segmenteur. Les nombres décimaux et les noms de domaine ne sont pas des frontières, les marqueurs en fin de phrase restent avec leur phrase, et ؟ termine une phrase. Le texte chinois se segmente en phrases sans espaces. Le test qui mérite d’être copié : un texte fourni par morceaux de trois caractères doit être segmenté exactement comme le texte entier.

  • Tests de flux avec un fournisseur scripté qui rejoue une réponse par petits morceaux : une valeur inventée n’atteint jamais le réseau, un arrêt par le sas ou une déconnexion ferme le flux en amont, et une troncature est signalée comme une troncature.

Voici une esquisse des deux tests centraux, écrite pour cet article et non reprise du site :

# Sketch only: SentenceSplitter and SentenceGate stand for your own classes.
import pytest

ANSWER = "The reading room opens early [S1]. Is the café open? Yes, on weekdays [S2]."

def pieces(text, size):
    return [text[i:i + size] for i in range(0, len(text), size)]

def split_streamed(text, size):
    splitter = SentenceSplitter()
    sentences = [s for piece in pieces(text, size) for s in splitter.feed(piece)]
    tail = splitter.flush()
    return sentences + ([tail] if tail else [])

@pytest.mark.parametrize("size", [1, 2, 3, 7])
def test_piece_size_does_not_change_sentences(size):
    assert split_streamed(ANSWER, size) == split_streamed(ANSWER, len(ANSWER))

def test_unsupported_value_never_reaches_the_reader():
    gate = SentenceGate(sources=["The library opens at nine."])
    released = ""
    for piece in pieces("It opens at nine [S1]. Late fees ended in 2019 [S1]. More", 7):
        outcome = gate.feed(piece)
        released += outcome.text
        if outcome.stopped:
            break
    assert "2019" not in released and gate.stopped

Aucun de ces tests ne soumet un modèle en service à du red teaming. Ce que j’ai mesuré, ce sont les erreurs du sas lui-même : des réponses enregistrées en production et rejouées hors ligne, pour trouver où il a arrêté une réponse ancrée. Cela compte de faux arrêts pour un modèle et un jeu de questions, pas la fréquence à laquelle un vrai modèle invente ; je ne le publie donc pas comme un taux.

Checklist : mettre en production un garde-fou sur une réponse en streaming

  • Décidez ce qui ne doit jamais être vu. C’est ce qui tranche entre vérifier d’abord et diffuser d’abord, quelle que soit la valeur par défaut de votre framework.

  • Choisissez l’unité et explicitez l’attente : un bloc de taille fixe ou une phrase.

  • Utilisez des vérifications déterministes là où une comparaison de chaînes suffit, après avoir normalisé chiffres et formes de lettres. Un rail fondé sur un modèle ajoute son temps d’exécution à chaque unité.

  • Ne tenez une frontière pour certaine qu’à l’arrivée du caractère suivant, et plafonnez le tampon.

  • En cas de violation, diffusez ce qui a été validé, arrêtez-vous, affichez un texte fixe, ne renvoyez jamais le texte bloqué et fermez le flux en amont.

  • Distinguez la troncature de la violation, et comptez ce qui a été généré, pas ce qui a été affiché.

  • Transport : POST avec fetch, un lecteur de flux et AbortController ; un décodeur en mode streaming ; des pings en commentaire ; les erreurs sous forme d’événements.

  • Proxys : X-Accel-Buffering: no, no-transform, transmission immédiate des réponses event-stream, un chemin de déconnexion testé, et aucun service worker sur le flux.

  • Tests : rejouez les réponses par petits morceaux et vérifiez qu’une valeur bloquée n’atteint jamais le réseau.

Si vous concevez un assistant ancré dans ses sources et souhaitez un regard extérieur sur son chemin de diffusion, c’est sur la page Diagnostic de projet que cette conversation commence.

Questions fréquentes

Peut-on valider la sortie d’un LLM en streaming sans renoncer au streaming ?

Oui. Mettez le texte en mémoire tampon jusqu’à une unité vérifiable, et diffusez chaque unité dès qu’elle est validée. Le lecteur voit le texte arriver phrase par phrase ou bloc par bloc, plutôt que token par token.

La mise en mémoire tampon par phrase dégrade-t-elle le délai jusqu’au premier token ?

Elle retarde d’une phrase la sortie visible : le texte apparaît une fois la première phrase complète et vérifiée. Les vérifications de chaînes coûtent peu à côté de la génération, mais je n’ai pas mesuré l’écart sur ce site ; je ne donne donc aucun chiffre.

Faut-il désactiver stream_first dans NeMo Guardrails ?

Désactivez-le si un bloc rejeté ne doit jamais être vu. NeMo documente le coût : le délai jusqu’au premier token augmente du temps d’exécution du rail pour chaque bloc. Ne gardez la valeur par défaut que si votre produit peut accepter qu’un lecteur voie des tokens qu’un rail bloque ensuite.

Pourquoi ne pas utiliser EventSource pour le streaming d’un LLM ?

EventSource accepte une URL et un indicateur d’identifiants (credentials), si bien qu’il ne peut pas envoyer de corps de requête. Or une requête de chat avec son historique est un POST : utilisez donc fetch avec un lecteur de flux, qui vous donne aussi l’annulation via AbortController.

Une vérification des références empêche-t-elle les hallucinations ?

Non. Elle arrête les numéros de source inventés, les phrases substantielles sans référence et les valeurs absentes des sources que cite une phrase. Elle ne peut pas dire si une source citée étaye l’affirmation, ce qui correspond à la défaillance « mal ancrée ». Considérez-la comme un filet de sécurité placé derrière le refus et un seuil de score de récupération.

Sources