De texte brut à pipelines : analyseurs, LCEL, éléments exécutables et mémoire dans LangChain
Comment LangChain transforme le texte brut du modèle en données structurées, compose les étapes à l’aide de LCEL et de l’interface Runnable, ainsi qu’il gère la mémoire conversationnelle aujourd’hui.
Sources: LangChain Explained: Output Parsers, Chains, Runnables, and Memory
Appeler un modèle de langage à l’aide d’un modèle de prompt bien conçu n’est que la première étape d’une véritable application. Le modèle renvoie toujours une chaîne non structurée, chaque appel oublie tout ce qui a précédé, et le fait de connecter manuellement le prompt, le modèle et le traitement post-traitement se transforme rapidement en code générique. Ce guide aborde les quatre concepts de LangChain qui résolvent ces problèmes : les analyseurs de sortie, les chaînes construites avec LCEL, l’interface Runnable qui permet la composition, ainsi que la mémoire. Après cela, vous devriez être capable de lire n’importe quel pipeline LCEL, de choisir l’analyseur ou le Runnable approprié pour une tâche, et de savoir où se trouve l’état de la conversation dans LangChain actuel.
Ce guide part du principe que vous connaissez déjà ce qu’est un enveloppeur d’LLM et un modèle de prompt dans LangChain, car tout ce qui suit s’appuie directement sur eux.
Analysesurs de sortie : transformer du texte en données
Le problème que résolvent les analyseurs
Quelle que soit la question posée à un modèle, la réponse arrive sous forme de texte libre. Pour un petit chatbot qui affiche simplement la réponse à l’écran, cela suffit. Dès que votre code doit agir sur cette réponse, par exemple en l’enregistrant dans une base de données, en la transmettant à une autre fonction ou en la renvoyant via une API au format JSON, une simple chaîne de caractères devient un problème.
Pourquoi vous ne devriez pas parser manuellement
- La sortie du modèle n’a pas de format garanti ; la même requête peut produire des structures légèrement différentes à chaque exécution.
La famille d’analyseurs intégrés
LangChain fournit plusieurs analyseurs, chacun conçu pour un format cible différent :
- Analysateur de sortie en chaîne de caractères (
StrOutputParser) : l’option la plus simple, qui extrait le texte brut de la réponse du modèle. Utilisez-le lorsque vous souhaitez afficher une chaîne de caractères propre ou l’alimenter dans une autre étape. - Analysateur de liste : il divise la réponse en une liste Python, ce qui est pratique lorsque vous demandez plusieurs éléments tels que des mots-clés ou des suggestions.
- Analyste de sortie JSON : convertit la réponse en un objet JSON et constitue l’un des analystes les plus utilisés lorsqu’il est nécessaire de disposer de données structurées.
- Analyste de sortie Pydantic : associe directement la réponse à un modèle Pydantic, ce qui permet une validation de type en plus de la structure. C’est le choix idéal lorsque les données doivent respecter un schéma strict.
- Analyste de date et heure : génère un véritable objet
datetimede Python plutôt qu’une date écrite sous forme de texte. - Analyste Enum : restreint la sortie à une valeur prédefinie et renvoie cette valeur.
- Analyste XML : lit des données écrites sous forme de balises XML et les transforme en un dictionnaire.
Les analyseurs structurés disposent d’une méthode get_format_instructions(). Elle renvoie un bloc de texte que vous insérez dans votre prompt pour indiquer au modèle précisément comment organiser sa réponse afin que l’analyseur puisse la lire. L’analyseur de chaîne de caractères simple n’a rien à imposer, ce qui en fait le seul membre de cette famille pour lequel les instructions de formatage ne s’appliquent pas.
Analyseurs versus sortie structurée native
De nombreux modèles actuels peuvent retourner des données structurées de manière native, et LangChain met cela à disposition via with_structured_output(). Lorsque cette fonctionnalité est disponible, elle remplace souvent un analyseur, car c’est le fournisseur du modèle qui impose le schéma plutôt que votre prompt ne demandant poliment. Les analyseurs conservent leur utilité lorsque un modèle ne dispose pas de sortie structurée native, et lorsque vous souhaitez une validation ou une transformation supplémentaire par rapport à ce que le fournisseur garantit.
Chaînes et LCEL : relier les étapes
Qu’est-ce qu’une chaîne ?
Une chaîne est une série d’étapes dans lesquelles la sortie de chaque étape devient l’entrée de la suivante. Au lieu d’écrire trois blocs de code distincts pour formater une demande, appeler le modèle et analyser le résultat, on les relie en un seul pipeline réutilisable.
L’exemple canonique comprend trois étapes : un modèle de demande, puis le modèle lui-même, enfin un analyseur de sortie. Vous fournissez une entrée brute telle qu’un sujet ou une question, et la chaîne formate la demande, appelle le modèle et analyse ce qui est retourné dans un seul flux continu.
Quels avantages offrent les chaînes ?
- La connexion entre les étapes est écrite une seule fois au lieu d’être répétée à chaque appel.
- La demande, le modèle et l’analyseur sont des éléments indépendants, ce qui vous permet de remplacer l’un d’eux sans toucher aux autres.
Composition avec l’opérateur pipe
Le LangChain Expression Language, LCEL, est ce qui rend la création de chaînes simple. Il utilise l’opérateur | pour relier les composants, un peu comme on enchaîne des commandes dans une shell Unix, plutôt que d’empiler des appels de fonction les uns à l’intérieur des autres.
Les deux lignes ci-dessous construisent la chaîne en trois étapes décrite précédemment et l’exécutent une fois avec une entrée sous forme de dictionnaire. Notez que la clé du dictionnaire, topic, doit correspondre à une variable dans le modèle de prompt.
chain = prompt | model | output_parser
result = chain.invoke({"topic": "black holes"})
Les données d’entrée sont transmises à prompt, qui génère une demande formatée ; celle-ci est ensuite envoyée au modèle, et la réponse du modèle parvient au output_parser. Chaque composant transmet son résultat directement au suivant, et result contient ce que le parseur renvoie.
Chaque chaîne LCEL dispose également d’un ensemble standard de méthodes d’exécution :
.invoke()exécute la chaîne une fois pour une seule entrée..batch()l’exécute sur une liste d’entrées en une seule appel..stream()génère les résultats de manière progressive au fur et à mesure qu’ils sont créés, sans avoir à attendre la réponse complète. Le streaming ne vous transmet les données que token par token si chaque étape de la chaîne peut traiter un flux ; une étape qui a besoin de l’ensemble des données en premier fera un bufferage.
Ce style déclaratif est en grande partie responsable du fait que LCEL semble composable. Vous décrivez ce qui doit se produire et dans quel ordre, sans avoir à préciser les mécanismes selon lesquels chaque étape appelle la suivante.
Runnables : l’interface sous-jacente
Pourquoi tout peut être relié par des pipelines
Au sein de LCEL, chaque composant, qu’il s’agisse d’une instruction, d’un modèle, d’un analyseur ou d’une fonction que vous avez écrite vous-même, est un Runnable. Un Runnable représente une interface commune que tout composant peut implémenter, et c’est précisément ce contrat partagé qui permet au symbole | de fonctionner. Comme chaque élément expose les mêmes méthodes, ils peuvent être connectés indépendamment de leur fonctionnement interne.
C’est cette même interface qui donne l’impression que l’interaction avec une chaîne est uniforme. Que ce soit un composant qui appelle un modèle, qui restructure des données ou qui exécute plusieurs branches en même temps, on le fait fonctionner à l’aide des mêmes méthodes .invoke(), .batch() et .stream(). Une chaîne composée est elle-même un Runnable, ce qui permet de superposer des chaînes à l’intérieur d’autres chaînes plus grandes sans aucune manipulation particulière.
Les Runnables que vous utiliserez le plus
- RunnableSequence exécute les étapes strictement dans l’ordre, en passant chaque résultat à l’étape suivante. Le symbole
|pour relier des composants crée automatiquement l’un de ces objets en arrière-plan. Utilisation typique : premièrement un prompt, ensuite le modèle, puis le parseur.
RunnableParallel lorsque l’on a besoin que l’entrée originale soit transmise en même temps que les valeurs transformées. Utilisation typique : renvoyer la question originale de l’utilisateur accompagnée de la réponse générée.Lorsque vous constatez que les modèles, les analyseurs et les chaînes sont tous des éléments exécutables partageant une même interface, LangChain cesse de ressembler à un ensemble de cas particuliers. Cela devient alors une composition ordinaire : de petites unités ayant la même structure, combinées en unités plus grandes conservant cette même structure.
Précaution concernant la sur-composition
Puisque n’importe quoi peut devenir un Runnable, il est tentant d’exprimer toute la logique d’une application sous forme de chaînes imbriquées. Cela fonctionne bien pour des étapes simples, mais les branches et lambdas profondément imbriquées peuvent devenir plus difficiles à déboguer que du Python classique. Une règle raisonnable consiste à utiliser LCEL pour le pipeline orienté modèle et à conserver les flux de contrôle complexes dans du code normal, ou bien à les déplacer vers un framework graphique lorsqu’ils impliquent des boucles et de l’état.
Mémoire : conserver le contexte entre les tours
Que signifie la mémoire ici
Chaque appel à un modèle est par défaut sans état. Il n’a aucune connaissance de ce que vous avez demandé il y a un instant, à moins que votre application ne renvoie à nouveau l’échange précédent. Dans LangChain, la mémoire est l’ensemble des mécanismes permettant de stocker le contexte d’une conversation et de le récupérer ultérieurement, afin qu’une application puisse poursuivre une conversation plutôt que de traiter chaque message comme s’il était le premier.
Pourquoi les applications de conversation en ont besoin
- Sans mémoire, un chatbot perd tout dès l’arrivée d’un nouveau message : pas de continuité, pas de suivi, pas de contexte partagé.
- Une conversation naturelle dépend des échanges précédents, par exemple savoir que « il » dans un message suivant fait référence à quelqu’un mentionné deux messages plus tôt.
- Un contexte mémorisé permet à un assistant de paraître cohérent et conversationnel, au lieu de répondre à chaque message de manière isolée.
- C’est une exigence de base pour les bots d’assistance client, les assistants personnels et tout autre type d’application à plusieurs échanges.
Classes de mémoire classiques
La méthode traditionnelle utilisait des classes de mémoire intégrées au processus en cours d’exécution. La plus simple conservait un buffer contenant toute la conversation ; une autre condensait les messages anciens en un résumé continu afin d’économiser des tokens. À chaque nouvelle requête, le contexte stocké était réinjecté dans la prompt.
Où se trouve maintenant la mémoire
Ces classes de mémoire en cours d’exécution ont été dépréciées, car LangChain a transféré l’état de la conversation à la couche de persistance de LangGraph. Dans ce modèle, les outils de sauvegarde enregistrent l’état de la conversation pour chaque session (identifiée par un thread), tandis que des stockages distincts conservent la mémoire à plus long terme qui doit persister entre les sessions. Le concept reste inchangé : le contexte précédent doit être conservé quelque part afin de pouvoir être réutilisé. Ce qui a changé, c’est l’implémentation recommandée, qui est désormais plus fiable et mieux adaptée à l’environnement de production, car l’état peut être persisté dans une véritable base de données au lieu de disparaître lors du redémarrage du processus. Si vous souhaitez voir comment un outil de sauvegarde enregistre réellement cet état, l’article « Inside LangGraph’s InMemorySaver » décrit en détail son fonctionnement interne.
Une approche d’apprentissage judicieuse consiste d’abord à comprendre la notion classique de mémoire, car elle explique pourquoi la mémoire est importante et quels éléments doivent figurer dans une requête, puis à adopter l’approche basée sur la persistance pour tout ce qui est destiné à un environnement de production. Comme ce domaine évolue rapidement, consultez la documentation actuelle de LangChain et LangGraph pour connaître les API exactes avant de commencer à développer.
En résumé
Avec les enveloppeurs de LLM et les modèles de requête, ces concepts forment l’outil de base de LangChain. Chacun d’eux est simple en soi, mais leur valeur réside dans leur combinaison : un modèle de requête alimente un modèle, un analyseur transforme le texte généré en données, chaque élément constitue une tâche exécutable reliée les unes aux autres par |, et une couche de persistance conserve le historique des conversations entre les appels.
- Utilisez un analyseur de sortie, ou une sortie structurée native lorsque le modèle le prend en charge, chaque fois que du code doit traiter la réponse du modèle.
RunnableSequence ; optez pour RunnableParallel, RunnablePassthrough, RunnableLambda et RunnableBranch lorsque le flux n’est pas linéaire.Avec ces éléments de base en place, les sujets suivants naturels sont la génération augmentée par récupération d’informations et les agents, tous deux assemblés à partir de ces mêmes composants.