Le streaming de réponses LLM via Server-Sent Events (SSE) est devenu un standard dans les applications modernes. Contrairement aux appels HTTP classiques où vous attendez la réponse complète avant de l'afficher, SSE permet au serveur d'envoyer des données de manière progressive au client, caractère par caractère. Pour l'utilisateur, cela crée une expérience fluide et immédiate : il voit la réponse s'écrire en temps réel, exactement comme ChatGPT ou Claude. Dans Angular, implémenter ce pattern demande une compréhension claire de la gestion des observables, de l'interception HTTP et des considérations de performance. Cet article vous montre comment construire une solution robuste et scalable.
Pourquoi SSE plutôt que WebSocket ou polling ?
SSE offre plusieurs avantages décisifs pour le streaming de réponses LLM. Contrairement au polling classique (requête toutes les 100ms), SSE établit une connexion persistent unilatérale : le serveur pousse les données sans que le client ne demande. Comparé aux WebSockets, SSE est plus simple à implémenter côté serveur (aucune negotiation), utilise HTTP standard (compatible avec tous les proxies), et se reconnecte automatiquement en cas de rupture réseau. Pour une application de chat ou de génération de contenu, SSE suffit amplement puisque vous n'avez besoin que d'une communication serveur→client. Le coût en ressources réseau et CPU est minimal : une seule connexion TCP par utilisateur, pas de heartbeat complexe. Angular dispose d'outils natifs pour consommer SSE via HttpClient, ce qui rend l'intégration naturelle et testable.
Implémenter l'observable SSE côté Angular
La clé réside dans l'utilisation de HttpClient avec l'option responseType: 'text' et le parsing manuel des événements. Voici une approche éprouvée : créez un service qui encapsule la logique de streaming et retourne un observable d'événements. Pour chaque événement reçu, vous décodez le format SSE (lignes commençant par data: ), puis émettez les données via un subject. Utilisez finalize() pour nettoyer la connexion et retry() avec backoff exponentiel pour gérer les interruptions réseau. Angular détecte automatiquement les changements quand vous souscrivez à cet observable dans votre composant via l'async pipe, ce qui met à jour le template en temps réel sans intervention manuelle. L'important est de ne jamais bloquer la zone Angular avec des calculs lourds dans le callback de souscription : utilisez runOutsideAngular() si nécessaire.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { retry, finalize, tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class LlmStreamService {
constructor(private http: HttpClient) {}
streamResponse(prompt: string): Observable<string> {
return this.http.post('/api/llm/stream', { prompt }, {
responseType: 'text',
reportProgress: true
}).pipe(
tap(event => {
if (typeof event === 'string') {
// Parser manuel SSE: 'data: contenu\n\n'
const lines = event.split('\n');
lines.forEach(line => {
if (line.startsWith('data: ')) {
const chunk = line.slice(6);
// Émettre le chunk via subject
}
});
}
}),
retry({ count: 3, delay: 1000 }),
finalize(() => console.log('Stream terminé'))
);
}
}