Composition de pipelines LangChain avec LCEL : linéaires, parallèles et à branches.
Apprenez à connecter des prompts, des modèles et des analyseurs dans des pipelines LangChain linéaires, à plusieurs étapes, parallèles et conditionnels en utilisant l’opérateur pipe, RunnableParallel et RunnableBranch.
Sources: Chains in LangChain: Simple, Sequential, Parallel, and Conditional — Part 7
Les fonctionnalités réelles des LLM ne s’arrêtent presque jamais à une seule appelation de modèle. Vous formatez un prompt, appelez un modèle, traitez les résultats obtenus, utilisez ces résultats dans un autre prompt, exécutez parfois plusieurs requêtes en même temps, et choisissez occasionnellement une autre approche en fonction de l’entrée. Associer manuellement chaque étape se transforme rapidement en du code verbeux difficile à lire et encore plus difficile à modifier. Ce guide présente quatre modèles de composition dans LangChain, conçus avec le LangChain Expression Language (LCEL) : des pipelines simples, des pipelines à plusieurs étapes, une distribution via RunnableParallel, et un routage grâce à RunnableBranch. Chaque modèle est présenté deux fois, une fois avec Google Gemini et une fois avec OpenAI, afin que vous puissiez voir exactement quels éléments changent lors du passage d’un fournisseur à un autre et lesquels restent inchangés.
Qu’est-ce qu’une chaîne en réalité ?
Une chaîne est un pipeline de composants LangChain dans lequel chaque étape reçoit ce que l’étape précédente a produit. Les éléments de base typiques sont des modèles de prompts, des modèles de chat ou de complétion, ainsi que des analyseurs de sortie, mais tout élément implémentant l’interface Runnable de LangChain peut y participer. Le pipeline le plus simple et utile ressemble à ceci :
Prompt → LLM → Output Parser → Output
Chaque étape a une fonction précise. Le modèle de prompt prend vos variables d’entrée et les transforme en instructions destinées au modèle. Le modèle lit ces instructions et génère une réponse. L’analyseur de sortie reformate cette réponse selon le format souhaité par votre application, par exemple une chaîne de caractères simple ou un objet validé. Ce qui sort de l’analyseur constitue le résultat de tout le pipeline.
L’avantage de l’abstraction est que vous n’avez plus besoin de transférer manuellement les données entre ces différentes étapes. Vous déclarez l’ordre une seule fois, et LangChain s’occupe d’appeler chaque composant avec les bons données. Conceptuellement, vous assemblez de petits blocs pour former une unité plus grande :
Input
↓
Component 1
↓
Component 2
↓
Component 3
↓
Output
Cela a peu d’importance pour un script à deux étapes, mais cela devient essentiel lorsque l’application compte une dizaine d’étapes. Un pipeline déclaré permet de voir le flux des données en un seul endroit, au lieu qu’il soit dispersé dans diverses fonctions d’aide. Le modèle mental correspond à un rythme répétitif : entrée, traitement, sortie, puis traitement à nouveau de cette sortie, jusqu’à ce que la valeur finale soit obtenue.
Pourquoi la composition est préférable au câblage manuel
Lorsque chaque valeur intermédiaire est transmise explicitement, le code devient bruyant et fragile. Les chaînes résolvent trois problèmes pratiques.
Flux de données sans code de liaison
La promesse fondamentale est que le résultat d’une étape devient automatiquement l’argument de la suivante. Prenons à nouveau cette séquence familière :
Prompt → LLM → Output Parser → Output
Manuellement, on formaterait la requête, on appellerait le modèle avec cette valeur formatée, on extrairait le texte de l’objet de réponse et on le transmettrait au analyseur, en conservant une variable pour chaque étape. Avec LCEL, ce même flux se résume en une seule expression construite à l’aide de l’opérateur pipe :
chain = prompt | llm | parser
L’opérateur | fonctionne parce que les requêtes, les modèles et les analyseurs implémentent tous l’interface Runnable identique, de sorte que chacun sait comment accepter la sortie du composant précédent. Moins de variables temporaires signifie moins d’endroits où un mauvais valeur pourrait être transmise, et le pipeline s’exécute de gauche à droite dans le même ordre.
La logique multi-étapes reste lisible
Les fonctionnalités de production nécessitent souvent plusieurs appels au modèle avec des prompts différents. Une approche courante consiste à générer quelque chose à partir d’un sujet, puis à traiter ce résultat intermédiaire avec un deuxième prompt :
Topic
↓
Prompt 1
↓
LLM
↓
Output Parser
↓
Prompt 2
↓
LLM
↓
Output Parser
↓
Final Output
Géré manuellement, ce type de flux s’étend à mesure que davantage d’étapes sont ajoutées. Expressé sous forme de chaîne, les étapes restent dans une seule déclaration, et la règle selon laquelle chaque étape alimente la suivante est appliquée par le framework plutôt que par une discipline stricte.
Les composants peuvent être remplacés
Puisque chaque étape est un composant indépendant, vous pouvez en remplacer un sans toucher aux autres. Supposons qu’un pipeline fonctionne actuellement avec Gemini :
chain = prompt | gemini | parser
Passer à OpenAI signifie modifier uniquement le composant du modèle :
chain = prompt | openai | parser
La requête et le analyseur restent tels quels. Cela est utile lorsque les exigences changent, lorsqu’une équipe compare des fournisseurs en termes de coût ou de qualité, ou lorsque l’on souhaite un modèle moins cher en phase de développement et un modèle plus performant en production.
Les avantages en bref
- les résultats sont transmis automatiquement à l’étape suivante.
- plusieurs requêtes, modèles et analyseurs coexistent au sein d’un pipeline bien défini.
- les composants peuvent être réutilisés ou remplacés, y compris le fournisseur de modèle, sans avoir à redessiner l’ensemble du flux.
Quatre modèles de composition
C’est utile de regrouper les pipelines LangChain en quatre types. Stricto sensu, il ne s’agit pas de classes distinctes dans LangChain moderne ; ce sont des schémas que l’on construit à partir des mêmes primitives Runnable. Ces noms restent un vocabulaire pratique :
- Chaîne simple : une seule ligne droite allant du prompt au analyseur.
- Chaîne séquentielle : plusieurs de ces lignes reliées bout à bout.
- Chaîne parallèle : plusieurs sous-chaînes indépendantes qui traitent en même temps la même entrée.
- Chaîne conditionnelle : un point de décision qui choisit quelle sous-chaîne va traiter l’entrée.
Tous ces types couvrent tout, des prompts uniques aux workflows qui se développent et prennent des décisions. Chacun est expliqué ci-dessous à l’aide d’un exemple fonctionnel utilisant deux fournisseurs.
Chaînes simples : un seul traitement
Le pipeline le plus simple relie ses composants en ligne droite. La sortie de chaque composant sert d’entrée au suivant, sans aucun branchement ou décision en cours de route :
Input
↓
Prompt Template
↓
LLM
↓
Output Parser
↓
Output
L’exemple fonctionnel sert d’aide à l’étude : donné un sujet, le modèle doit produire quelques faits accessibles sans entrer dans des détails techniques complexes. La chaîne pour cela est aussi courte que possible :
prompt → llm → parser
En termes simples, une valeur topic entre dans le modèle de prompt, le prompt formaté est envoyé au modèle, et la réponse du modèle passe par un analyseur de sortie avant d’être renvoyée. Cet analyseur est techniquement optionnel, mais son inclusion permet d’obtenir un type de retour prévisible.
Concrètement, l’exemple utilise cricket comme sujet et demande cinq faits intéressants. Les données se déplacent de la manière suivante :
{'topic': 'cricket'}
↓
PromptTemplate
↓
LLM
↓
StrOutputParser
↓
Five interesting facts about cricket
La version Gemini
Voici le programme complet utilisant Gemini de Google grâce à l’intégration langchain_google_genai :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
prompt = PromptTemplate(
template='Generate 5 interesting facts about {topic}',
input_variables=['topic']
)
parser = StrOutputParser()
chain = prompt | llm | parser
result = chain.invoke({'topic':'cricket'})
# print(result)
chain.get_graph().print_ascii()
Il crée un modèle GoogleGenerativeAI basé sur gemini-2.5-flash, une requête à une seule variable, ainsi qu’un StrOutputParser, puis les assemble avec la séquence prompt | llm | parser. La dernière ligne affiche un diagramme ASCII de la chaîne, dont nous parlerons plus en détail. Notez que la ligne print(result) est commentée dans cette version, de sorte que le script ne montre que le graphique à moins de la réactiver.
Décortiquage du code Gemini
Les importations permettent d’inclure les quatre éléments nécessaires au script :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
GoogleGenerativeAI est l’encapsulage autour du modèle Gemini. PromptTemplate définit un prompt contenant une case {topic} à remplir au moment de l’exécution. StrOutputParser transforme la sortie du modèle en une chaîne de caractères Python standard. load_dotenv lit un fichier local .env pour y extraire les variables d’environnement.
L’appel à load_dotenv rend la clé API ainsi que toutes les autres configurations provenant de .env disponibles pour le processus avant même que tout client ne soit créé :
load_dotenv()
L’intégration Gemini lit sa clé depuis l’environnement, ce qui exige que cet appel soit effectué avant l’instanciation du modèle. Il est d’usage de conserver les clés dans un fichier .env (et hors contrôle de version).
Ensuite, le modèle lui-même est créé, spécifiquement pour la version gemini-2.5-flash :
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
Le modèle de prompt indique à la fois le texte et les variables attendues :
prompt = PromptTemplate(
template='Generate 5 interesting facts about {topic}',
input_variables=['topic']
)
Lors de l’appel, le placeholder {topic} est rempli à partir du dictionnaire des entrées. En passant ces données :
{'topic': 'cricket'}
on obtient le prompt concret suivant :
Generate 5 interesting facts about cricket
Le parseur est créé sans arguments :
parser = StrOutputParser()
Sa fonction consiste à retourner la réponse du modèle sous forme de chaîne de caractères. Avec un modèle de complétion comme GoogleGenerativeAI, qui renvoie déjà du texte, son rôle est presque nul. Avec un modèle de chat, qui renvoie un objet message, il extrait le contenu textuel. Dans tous les cas, l’utilisateur final reçoit une simple str.
Puis vient la ligne qui définit l’ensemble du pipeline :
chain = prompt | llm | parser
L’opérateur pipe relie les trois composants de manière que la demande formatée alimente le modèle et la réponse du modèle alimente l’analyseur :
PromptTemplate → Gemini 2.5 Flash → StrOutputParser
Finalement, la chaîne est exécutée à l’aide d’un dictionnaire contenant le sujet :
result = chain.invoke({'topic':'cricket'})
Cette seule appelée déclenche chaque étape dans l’ordre, sans variables intermédiaires dans votre code :
cricket
↓
PromptTemplate
↓
Gemini 2.5 Flash
↓
StrOutputParser
↓
Final Result
Ce qui est retourné
La valeur de retour est une chaîne contenant cinq faits sur le cricket, quelque chose comme ceci :
1. Cricket originated in England and has been played for several centuries.
2. A cricket team consists of 11 players.
3. Test cricket matches can last up to five days.
4. The Cricket World Cup is one of the major international cricket tournaments.
5. Cricket is especially popular in countries such as India, Australia, England, and New Zeakand.
Puisque le texte est généré, la formulation et les faits spécifiques diffèrent à chaque exécution, et l’exemple de sortie contient même une faute d’orthographe (“Zeakand”). Considérez les exemples de sortie comme des illustrations de la structure, et non comme des résultats fixes. Si vous avez besoin d’un formatage déterministe, c’est le rôle d’un analyseur plus strict ou d’une sortie structurée, et non de compter sur le fait que le modèle se reproduise.
Même pipeline sur OpenAI
Désormais, le même outil d’aide à l’étude fonctionne sur OpenAI. Ce qui est frappant, c’est la faible quantité de changements :
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
prompt = PromptTemplate(
template='Generate 5 interesting facts about {topic}',
input_variables=['topic']
)
parser = StrOutputParser()
chain = prompt | llm | parser
result = chain.invoke({'topic':'cricket'})
print(result)
chain.get_graph().print_ascii()
Le modèle est maintenant ChatOpenAI avec gpt-4o-mini. La requête, le analyseur et la composition sont exactement les mêmes qu’auparavant, et cette version affiche à la fois le résultat et le graphe.
La seule nouveauté est l’objet modèle :
llm = ChatOpenAI(model="gpt-4o-mini")
ChatOpenAI de langchain_openai remplace GoogleGenerativeAI. Il s’agit d’un modèle de chat, il renvoie donc un objet message plutôt qu’une chaîne brute, ce qui explique précisément pourquoi il est utile de conserver l’analyseur dans la chaîne.
Le modèle de requête n’a pas changé :
prompt = PromptTemplate(
template='Generate 5 interesting facts about {topic}',
input_variables=['topic']
)
De même pour l’analyseur :
parser = StrOutputParser()
Et de même pour la composition :
chain = prompt | llm | parser
La structure est identique à celle de la ligne en trois étapes, avec OpenAI au milieu :
PromptTemplate → OpenAI → StrOutputParser
L’appel de la chaîne et l’affichage du résultat se font comme ceci :
result = chain.invoke({'topic':'cricket'})
print(result)
Le traitement via OpenAI renvoie également une chaîne contenant cinq faits sur le cricket, par exemple :
1. Cricket is believed to have originated in England.
2. Each cricket team has 11 players.
3. A Test match can be played over a maximum of five days.
4. The Cricket World Cup is one of the most important international cricket tournaments.
5. Cricket has a particularly large following in India and several other countries.
Encore une fois, la formulation exacte variera selon les exécutions et les modèles.
Affichage de la structure de la chaîne
Les deux scripts se terminent par une fonction qui affiche le pipeline sous forme de diagramme texte :
chain.get_graph().print_ascii()
get_graph() renvoie une représentation graphique du Runnable, et print_ascii() la dessine dans le terminal. Pour la chaîne Gemini, on obtient quelque chose comme ceci :
PromptInput
*
*
PromptTemplate
*
*
GoogleGenerativeAI
*
*
StrOutputParser
*
*
StrOutputParserOutput
Les nœuds PromptInput et StrOutputParserOutput représentent les points d’entrée et de sortie dérivés du schéma ; les trois nœuds qui se trouvent entre eux correspondent à vos composants. Il s’agit d’une vérification rapide pour s’assurer que la chaîne est connectée comme vous l’imaginez. Dans la plupart des environnements, print_ascii() dépend du package optionnel grandalf ; installez-le si l’appel génère une erreur d’importation.
Seul le modèle a changé
Placez les deux pipelines côte à côte : la différence réside en un seul nœud. Avec Gemini :
Input
↓
PromptTemplate
↓
GoogleGenerativeAI
↓
StrOutputParser
↓
Output
Avec OpenAI :
Input
↓
PromptTemplate
↓
ChatOpenAI
↓
StrOutputParser
↓
Output
Dans le code, toute la différence réside dans l’objet de modèle que vous créez :
# Google Gemini
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
# OpenAI
llm = ChatOpenAI(model="gpt-4o-mini")
Tout ce qui l’entoure peut rester inchangé. C’est là l’avantage pratique de s’appuyer sur des composants standardisés : le choix du fournisseur devient une décision de configuration plutôt qu’une réécriture. En pratique, il existe encore de petites différences à prendre en compte, comme les modèles de chat par rapport aux modèles de complétion, les noms des paramètres et la manière dont chaque fournisseur gère les instructions système, mais le pipeline lui-même est portable.
Le parcours complet des données
En termes généraux, une chaîne simple se présente ainsi :
Input
↓
PromptTemplate
↓
LLM
↓
StrOutputParser
↓
Output
Avec une entrée de type cricket, chaque valeur intermédiaire devient visible, depuis le dictionnaire jusqu’à l’invite affichée, en passant par la réponse brute du modèle et la chaîne finale :
{'topic': 'cricket'}
↓
PromptTemplate
↓
"Generate 5 interesting facts about cricket"
↓
LLM
↓
Raw LLM Response
↓
StrOutputParser
↓
Final String Output
C’est là toute l’idée. Chaque composant effectue une seule tâche, et son résultat devient l’entrée du composant suivant, dans un seul passage continu.
Chaînes séquentielles : passer une appel de modèle à l’autre
Une chaîne séquentielle étend la même idée à plusieurs étapes. Une chaîne simple contient généralement une seule séquence de cette forme :
Prompt → LLM → Output Parser
Une chaîne séquentielle peut contenir plusieurs modèles de prompts, plusieurs appels de modèle et plusieurs analyseurs, tous reliés entre eux. La règle fondamentale reste inchangée : ce que produit l’étape actuelle est ce que consomme l’étape suivante. Il n’y a toujours pas de branches ; les étapes s’exécutent toujours dans le même ordre fixe. Une version à deux étapes ressemble à ceci :
Input
↓
Prompt Template 1
↓
LLM 1
↓
Output Parser
↓
Prompt Template 2
↓
LLM 2
↓
Output Parser
↓
Output
Exemple : un rapport détaillé, puis un résumé
La tâche d’exemple comporte deux phases. Premièrement, générer un rapport détaillé sur un sujet donné. Deuxièmement, résumer ce rapport en cinq points. La deuxième phase ne voit jamais directement le sujet original ; elle travaille uniquement à partir du rapport produit par la première phase :
Input
↓
Explanation Prompt
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Summary Prompt
↓
LLM
↓
StrOutputParser
↓
5-Point Summary
Deux modèles de prompts assurent le fonctionnement. explaination_prompt (l’erreur d’orthographe se trouve dans le code, donc conservez-la si vous copiez les extraits) demande un rapport détaillé. summary_prompt prend ce rapport comme entrée et demande cinq points de synthèse.
La version Gemini
Voici le programme complet pour Gemini :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
# Prompt 1 : Explanation
explaination_prompt = PromptTemplate(
template='Generate a detailed report on {topic}',
input_variables=['topic']
)
# Prompt 2 : Summary
summary_prompt = PromptTemplate(
template='Generate a 5 pointer summary from the following text \n {text}',
input_variables=['text']
)
parser = StrOutputParser()
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
result = chain.invoke({"topic": "Generative AI"})
# print(result)
chain.get_graph().print_ascii()
Une instance de GoogleGenerativeAI sert les deux étapes, et le pipeline est une seule expression composée de six éléments : deux prompts, l’utilisation du modèle à deux reprises et celle du parseur à deux reprises.
Décortiquage du code Gemini
Les importations correspondent à cette chaîne simple :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
GoogleGenerativeAI pour le modèle, PromptTemplate pour les deux prompts réutilisables, StrOutputParser pour transformer chaque réponse en chaîne de caractères, et load_dotenv pour la configuration.
Les variables d’environnement sont chargées en premier, exactement comme avant :
load_dotenv()
Le modèle est créé une seule fois :
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
Il est tout à fait possible de réutiliser un seul objet modèle pour les deux étapes. Les enveloppes de modèle dans LangChain sont sans état par rapport aux appels individuels, ce qui permet à la même instance d’être référencée autant de fois que nécessaire dans une chaîne.
Le premier prompt accepte un topic et demande un rapport détaillé :
explaination_prompt = PromptTemplate(
template='Generate a detailed report on {topic}',
input_variables=['topic']
)
Avec cette entrée :
{"topic": "Generative AI"}
cela se présente comme suit :
Generate a detailed report on Generative AI
C’est au second prompt que se produit le transfert intéressant :
summary_prompt = PromptTemplate(
template='Generate a 5 pointer summary from the following text \n {text}',
input_variables=['text']
)
Il attend une variable nommée text, mais rien dans le dictionnaire des entrées ne porte ce nom. La valeur provient de la première étape : c’est le rapport généré par la première appel au modèle qui remplit cette case.
Cela fonctionne grâce au analyseur situé entre les différentes étapes :
parser = StrOutputParser()
Après la première appel au modèle, l’analyseur génère une chaîne de caractères simple. Lorsqu’un PromptTemplate contenant exactement une variable d’entrée reçoit une chaîne nue au lieu d’un dictionnaire, LCEL associe cette chaîne à la seule variable. Ainsi, le rapport se retrouve sous la forme {text} sans aucune association explicite. Si le deuxième prompt contenait deux variables, il faudrait construire soi-même le dictionnaire, par exemple à l’aide d’un petit dictionnaire de Runnables ou de RunnablePassthrough.assign.
Tout le processus se résume en une seule ligne :
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
En le lisant de gauche à droite, on obtient l’ordre d’exécution :
Explanation Prompt
↓
Gemini
↓
StrOutputParser
↓
Summary Prompt
↓
Gemini
↓
StrOutputParser
↓
Final Output
Chaque composant reçoit la sortie du composant précédent. Rien dans votre code ne modifie le rapport intermédiaire.
Le lancement nécessite un seul sujet :
result = chain.invoke({"topic": "Generative AI"})
Derrière cette appel, les deux étapes s’exécutent l’une après l’autre :
"Generative AI"
↓
Explanation Prompt
↓
Gemini
↓
StrOutputParser
↓
Detailed Report
↓
Summary Prompt
↓
Gemini
↓
StrOutputParser
↓
Final 5-Point Summary
Ce qui est retourné
La valeur finale est un résumé en cinq points sur l’IA générative, par exemple :
1. Generative AI is a branch of artificial intelligence that creates new content.
2. It can generate text, images, audio, video, and code.
3. Foundation models are an important part of modern Generative AI systems.
4. Generative AI is used across areas such as education, software development, and customer support.
5. It is enabling applications that can automate and simplify many creative and knowledge-based tasks.
Il convient de noter que ce résumé n’est pas écrit directement à partir du sujet. Gemini écrivait d’abord un rapport complet, et ce n’est que ce rapport qui est envoyé à la commande de résumé. Cette approche en deux étapes permet généralement d’obtenir un matériel plus riche à compresser, au prix d’un appel supplémentaire du modèle ainsi que de la latence et des tokens qui en découlent. Comme toujours, les formulations exactes varient d’une exécution à l’autre.
Même pipeline sur OpenAI
La version OpenAI conserve le flux de travail inchangé :
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI()
explaination_prompt = PromptTemplate(
template="Generate a detailed report on {topic}",
input_variables=["topic"]
)
summary_prompt = PromptTemplate(
template="Generate a 5 pointer summary from the following text \n {text}",
input_variables=["text"],
)
parser = StrOutputParser()
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
result = chain.invoke({"topic": "Generative AI"})
print(result)
chain.get_graph().print_ascii()
C’est la même structure en deux étapes : prompt de rapport, modèle, analyseur, prompt de résumé, modèle, analyseur. Le script importe également load_dotenv deux fois, ce qui n’est pas problématique mais peut être optimisé.
Cette fois, le modèle est créé sans arguments :
llm = ChatOpenAI()
En l’absence d’argument model, ChatOpenAI recourt au modèle par défaut de la bibliothèque, qui a changé au fil des mises à jour de langchain_openai. Dans les projets réels, indiquez explicitement le nom du modèle afin qu’une mise à jour ne modifie pas silencieusement son fonctionnement ou ses coûts.
Le prompt de rapport est identique à celui de la version Gemini :
explaination_prompt = PromptTemplate(
template="Generate a detailed report on {topic}",
input_variables=["topic"]
)
Il en va de même pour le prompt de résumé, qui attend à nouveau que le rapport soit présenté sous la forme {text}:
summary_prompt = PromptTemplate(
template="Generate a 5 pointer summary from the following text \n {text}",
input_variables=["text"],
)
C’est ici que le schéma séquentiel se révèle utile : le deuxième prompt est rempli automatiquement à partir du résultat de la première étape.
Le parseur est le même StrOutputParser:
parser = StrOutputParser()
Et la composition est identique, caractère par caractère :
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
En termes de flux, cela se lit comme suit :
Prompt 1
↓
OpenAI
↓
StrOutputParser
↓
Prompt 2
↓
OpenAI
↓
StrOutputParser
↓
Final Output
Seul le nœud de modèle diffère de la version Gemini.
L’appel transmet à nouveau le sujet. Notez que l’extrait présenté manque de sa parenthèse de fermeture ; le programme complet contient la ligne correcte :
result = chain.invoke({"topic": "Generative AI"}
La chaîne exécute ensuite les deux étapes séparément.
Ce qui est retourné
Le résultat est à nouveau un résumé en cinq points dérivé du rapport intermédiaire :
1. Generative AI creates new content by learning patterns from existing data.
2. It is powered by large foundation models trained on diverse datasets.
3. Generative AI can produce text, images, audio, video, and code.
4. It is being used in many industries and applications.
5. It is becoming an important part of modern AI-powered software.
Les détails varieront d’une exécution à l’autre.
Imprimer la structure de la chaîne
L’outil graphique fonctionne également avec des chaînes plus longues :
chain.get_graph().print_ascii()
Pour la chaîne séquentielle Gemini, le diagramme montre les deux étapes empilées verticalement :
PromptInput
*
*
PromptTemplate
*
*
GoogleGenerativeAI
*
*
StrOutputParser
*
*
PromptTemplate
*
*
GoogleGenerativeAI
*
*
StrOutputParser
*
*
StrOutputParserOutput
Le nœud du modèle apparaît deux fois parce que le même objet de modèle est utilisé en deux endroits du pipeline, et le graphe reflète les emplacements plutôt que des objets uniques.
Comparaison des deux implémentations
Les deux versions suivent la même structure séquentielle ; le fournisseur est la seule différence réelle. Le pipeline Gemini :
Input
↓
Explanation Prompt
↓
GoogleGenerativeAI
↓
StrOutputParser
↓
Summary Prompt
↓
GoogleGenerativeAI
↓
StrOutputParser
↓
Output
Le pipeline OpenAI :
Input
↓
Explanation Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
Summary Prompt
↓
ChatOpenAI
↓
StrOutputParser
↓
Output
Les prompts, le analyseur et la disposition de la chaîne sont partagés. La construction du modèle est la seule étape qui change :
# Google Gemini
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
# OpenAI
llm = ChatOpenAI()
La chaîne composée elle-même reste identique d’un fournisseur à l’autre :
chain = explaination_prompt | llm | parser | summary_prompt | llm | parser
De manière abstraite, une chaîne séquentielle ressemble à ceci :
Prompt 1
↓
LLM 1
↓
Parser
↓
Prompt 2
↓
LLM 2
↓
Parser
↓
Output
La différence avec une simple chaîne est qu’il existe plusieurs étapes, et chaque étape après la première dépend de celle qui la précède.
Le parcours complet des données
En général, la chaîne séquentielle transfère les données de cette manière :
Input
↓
PromptTemplate 1
↓
LLM 1
↓
StrOutputParser
↓
Detailed Report
↓
PromptTemplate 2
↓
LLM 2
↓
StrOutputParser
↓
Final Output
Trace avec l’entrée de l’IA générative, y compris les prompts affichés :
{"topic": "Generative AI"}
↓
Explanation Prompt
↓
"Generate a detailed
report on Generative AI"
↓
LLM
↓
StrOutputParser
↓
Detailed Report
↓
Summary Prompt
↓
"Generate a 5 pointer
summary from the following text"
↓
LLM
↓
StrOutputParser
↓
5-Point Summary
La première appel au modèle constitue une étape intermédiaire et non la réponse finale. Son résultat sert de matière première pour l’opération suivante. Vous pouvez continuer à superposer des étapes lorsque le flux de travail en nécessite davantage :
Prompt
↓
LLM
↓
Parser
↓
Prompt
↓
LLM
↓
Parser
↓
Prompt
↓
LLM
↓
Output
Peu importe la longueur du pipeline, l’ordre reste fixe et chaque étape attend celle qui précède. Cela signifie également que la latence s’accumule : trois appels au modèle en séquence coûtent approximativement la somme de leurs temps de réponse. Il est également utile d’ajouter une protection contre les résultats intermédiaires trop longs, car un rapport très volumineux est directement intégré au prompt suivant, ce qui augmente son nombre de tokens.
Chaînes parallèles : diffusion à partir de la même entrée
Une chaîne parallèle exécute plusieurs sous-chains côte à côte. Alors qu’une chaîne séquentielle attend que chaque étape soit terminée avant de commencer la suivante, une chaîne parallèle lance simultanément des sous-chains indépendants :
┌──→ Sub-Chain 1 ──→ Output 1
│
Input ──────────────┼──→ Sub-Chain 2 ──→ Output 2
│
└──→ Sub-Chain 3 ──→ Output 3
↓
Combined Output
Comme les sous-chains sont indépendants, aucun d’eux ne voit la sortie des autres. Le fonctionnement est simple :
- Chaque sous-chain reçoit la même entrée et produit sa propre sortie.
- Tous les sous-chains sont lancés en même temps.
- L’étape parallèle attend que tous les sous-chains aient terminé.
- Leurs résultats sont rassemblés en une valeur combinée.
Cette valeur combinée est un dictionnaire. Chaque clé correspond au nom que vous avez donné à un sous-chain, et chaque valeur représente sa sortie :
{
"facts": "...",
"definition": "...",
"use_case": "..."
}
Exemple : trois points de vue sur un même sujet
Lorsqu’on aborde un sujet comme l’intelligence artificielle, l’objectif est de produire trois éléments à partir de la même entrée : cinq faits intéressants, une définition concise et un cas d’usage concret. Aucun de ces éléments ne dépend des autres, donc les traiter l’un après l’autre ne ferait que gaspiller du temps. Ils sont plutôt traités en parallèle :
Input
↓
RunnableParallel
/ | \
/ | \
↓ ↓ ↓
Facts Definition Use Case
↓ ↓ ↓
LLM LLM LLM
↓ ↓ ↓
Parser Parser Parser
\ | /
\ | /
↓ ↓ ↓
Combined Result
Trois sous-processus s’occupent de cette tâche : facts pour les cinq faits, definition pour la définition courte, et use_case pour l’application pratique. Ils sont combinés à l’aide de RunnableParallel. La première implémentation utilise Gemini pour les trois tâches ; la seconde mélange OpenAI et Anthropic et ajoute une étape de fusion.
Pourquoi exécuter les sous-processus en parallèle
Il y a deux raisons de recourir à ce schéma.
Le premier facteur est la vitesse. Les appels aux modèles sont dominés par le temps de transmission réseau et celui de génération, donc les appels indépendants bénéficient grandement de la concurrence. Imaginez trois sous-tâches qui prennent chacune environ deux secondes. Exécutées l’une après l’autre, leur durée totale s’additionne :
Chain 1 → 2 seconds
Chain 2 → 2 seconds
Chain 3 → 2 seconds
Total ≈ 6 seconds
Exécutées en parallèle, la durée totale correspond approximativement à celle de la tâche la plus lente :
Chain 1 ─┐
Chain 2 ─┼→ execute together → ≈ 2 seconds
Chain 3 ─┘
Les économies réelles dépendent des limites de vitesse imposées par le fournisseur et du degré d’égalité dans l’achèvement des appels, mais pour des tâches indépendantes, l’amélioration est généralement significative. Le deuxième facteur est structurel : les tâches partagent une même entrée mais n’ont pas besoin des résultats des autres. Par exemple, à partir d’un seul document, on peut vouloir un résumé, une liste des sujets clés et un ensemble de questions d’étude. Chaque tâche peut donc traiter directement le document :
Document
↓
┌───────┼───────┐
↓ ↓ ↓
Summary Topics Questions
Le résumé n’a pas besoin de s’intégrer à l’extraction du sujet ou au générateur de questions. Lorsque les tâches sont liées de cette manière, forcer leur exécution en séquence augmente les délais et lie des étapes qui ne présentent aucune véritable dépendance. Lorsqu’elles dépendent réellement l’une de l’autre, le parallélisme n’est pas l’outil adapté ; une chaîne séquentielle constitue alors la représentation fidèle.
La version Gemini
Voici la répartition en trois voies pour Gemini :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel
from dotenv import load_dotenv
load_dotenv()
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
parser = StrOutputParser()
# Prompt 1: Facts
fact_prompt = PromptTemplate(
input_variables=["topic"],
template="Give five interesting short facts about: {topic}"
)
# Prompt 2: Definition
definition_prompt = PromptTemplate(
input_variables=["topic"],
template="Give a concise definition of: {topic}"
)
# Prompt 3: Use Case
usecase_prompt = PromptTemplate(
input_variables=["topic"],
template="What is a real-world use case of: {topic}?"
)
# Run all chains in parallel
parallel_chain = RunnableParallel({
"facts": fact_prompt | llm | parser,
"definition": definition_prompt | llm | parser,
"use_case": usecase_prompt | llm | parser
})
# Invoke parallel chain
result = parallel_chain.invoke({"topic": "Artificial Intelligence"})
# print(type(result))
# print(result)
# print("\n📌 Facts:\n", result["facts"])
# print("\n📘 Definition:\n", result["definition"])
# print("\n🛠️ Use Case:\n", result["use_case"])
parallel_chain.get_graph().print_ascii()
Trois modèles de prompts, un modèle et un analyseur sont combinés en trois petites chaînes, que RunnableParallel exécute ensuite simultanément sur l’entrée Intelligence Artificielle. Plusieurs lignes print sont commentées ; les activer permet de visualiser le type de résultat ainsi que chaque champ.
Décortiquage du code Gemini
Les importations ajoutent un nouvel élément :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableParallel
En plus du wrapper de modèle, PromptTemplate et StrOutputParser, le script importe RunnableParallel provenant de langchain_core.runnables, qui est l’élément chargé d’exécuter les sous-chains en parallèle.
Un seul modèle Gemini 2.5 Flash est créé :
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
Les trois sous-chains le partagent. Il est sécurisé de partager un seul objet modèle entre les branches parallèles, car chaque appel reste indépendant.
Un seul analyseur est également partagé :
parser = StrOutputParser()
La première demande concerne cinq faits courts :
fact_prompt = PromptTemplate(
input_variables=["topic"],
template="Give five interesting short facts about: {topic}"
)
La deuxième demande une définition concise :
definition_prompt = PromptTemplate(
input_variables=["topic"],
template="Give a concise definition of: {topic}"
)
La troisième demande un exemple d’application dans le monde réel :
usecase_prompt = PromptTemplate(
input_variables=["topic"],
template="What is a real-world use case of: {topic}?"
)
Toutes les trois demandes utilisent le même nom de variable :
{topic}
Ce nom partagé permet à un seul dictionnaire d’entrée de alimenter chaque branche. Les tâches elles-mêmes restent complètement séparées.
Voici maintenant l’élément central, la chaîne parallèle :
parallel_chain = RunnableParallel({
"facts": fact_prompt | llm | parser,
"definition": definition_prompt | llm | parser,
"use_case": usecase_prompt | llm | parser
})
RunnableParallel prend un dictionnaire dont les valeurs sont des objets Runnable. Les clés deviennent alors les clés de la sortie :
facts
definition
use_case
Chaque valeur constitue une petite chaîne complète indépendante :
fact_prompt → LLM → parser
definition_prompt → LLM → parser
usecase_prompt → LLM → parser
Puisqu’aucune d’elles ne dépend d’une autre, elles peuvent toutes s’exécuter en même temps. Avec une méthode invoke synchrone, LangChain exécute les branches sur un pool de threads ; avec ainvoke, elles s’exécutent en tant que tâches asynchrones concurrentes.
La chaîne est invoquée une seule fois :
result = parallel_chain.invoke({"topic": "Artificial Intelligence"})
avec une seule entrée :
{"topic": "Artificial Intelligence"}
Ce dictionnaire est transmis à chaque branche. Conceptuellement :
Artificial Intelligence
↓
RunnableParallel
/ | \
↓ ↓ ↓
Facts Definition Use Case
↓ ↓ ↓
LLM LLM LLM
↓ ↓ ↓
Parser Parser Parser
\ | /
\ | /
Combined Result
Qu’est-ce qui revient
Le résultat est un dictionnaire contenant une entrée par sous-chaine :
{
"facts": "1. Artificial Intelligence enables machines to perform tasks that normally require human intelligence.\n2. ...",
"definition": "Artificial Intelligence is the field of creating systems that can perform tasks requiring human-like intelligence.",
"use_case": "A real-world use case of AI is customer support chatbots that can understand and respond to user queries."
}
Le texte contenu dans chaque champ variera d’une exécution à l’autre, mais sa structure reste stable, et c’est là que dépend votre code :
result["facts"]
result["definition"]
result["use_case"]
Chaque clé renvoie la sortie de la branche correspondante, permettant ainsi au code suivant d’obtenir précisément la partie dont il a besoin.
Mélange des fournisseurs et fusion des résultats
La deuxième implémentation présente deux idées supplémentaires : différentes branches peuvent utiliser des fournisseurs distincts, et une étape parallèle peut elle-même faire partie d’un pipeline séquentiel plus large. Ici, une branche utilise OpenAI, l’autre Anthropic, et une dernière appel à OpenAI fusionne les deux résultats en un seul document :
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from dotenv import load_dotenv
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.schema.runnable import RunnableParallel
load_dotenv()
model1 = ChatOpenAI()
model2 = ChatAnthropic(model_name='claude-3-7-sonnet-20250219')
prompt1 = PromptTemplate(
template='Generate short and simple notes from the following text \n {text}',
input_variables=['text']
)
prompt2 = PromptTemplate(
template='Generate 5 short question answers from the following text \n {text}',
input_variables=['text']
)
prompt3 = PromptTemplate(
template='Merge the provided notes and quiz into a single document \n notes -> {notes} and quiz -> {quiz}',
input_variables=['notes', 'quiz']
)
parser = StrOutputParser()
parallel_chain = RunnableParallel({
'notes': prompt1 | model1 | parser,
'quiz': prompt2 | model2 | parser
})
merge_chain = prompt3 | model1 | parser
chain = parallel_chain | merge_chain
text = """
Support vector machines (SVMs) are a set of supervised learning methods used for classification, regression and outliers detection.
The advantages of support vector machines are:
Effective in high dimensional spaces.
Still effective in cases where number of dimensions is greater than the number of samples.
Uses a subset of training points in the decision function (called support vectors), so it is also memory efficient.
Versatile: different Kernel functions can be specified for the decision function. Common kernels are provided, but it is also possible to specify custom kernels.
The disadvantages of support vector machines include:
If the number of features is much greater than the number of samples, avoid over-fitting in choosing Kernel functions and regularization term is crucial.
SVMs do not directly provide probability estimates, these are calculated using an expensive five-fold cross-validation (see Scores and probabilities, below).
The support vector machines in scikit-learn support both dense (numpy.ndarray and convertible to that by numpy.asarray) and sparse (any scipy.sparse) sample vectors as input. However, to use an SVM to make predictions for sparse data, it must have been fit on such data. For optimal performance, use C-ordered numpy.ndarray (dense) or scipy.sparse.csr_matrix (sparse) with dtype=float64.
"""
result = chain.invoke({'text': text})
print(result)
chain.get_graph().print_ascii()
La entrée est un texte sur les machines à vecteurs de support. Une branche en extrait des notes de cours, l’autre crée un quiz composé de cinq questions, et la chaîne de fusion combine ces notes et ce quiz en un seul document.
Deux détails dans cette description méritent une mise en garde. Il importe RunnableParallel depuis langchain.schema.runnable, une ancienne voie d’accès antérieure à la séparation en langchain_core ; les versions actuelles le fournissent depuis langchain_core.runnables, comme dans l’exemple Gemini, et cette voie d’accès obsolète peut ne plus exister dans les versions récentes. De plus, il utilise claude-3-7-sonnet-20250219, un identifiant de modèle obsolète ; les fournisseurs retirent progressivement les anciennes versions, il convient donc de vérifier la liste actuelle des modèles Anthropic avant de l’utiliser à nouveau.
Deux objets de modèle sont créés :
model1 = ChatOpenAI()
model2 = ChatAnthropic(model_name='claude-3-7-sonnet-20250219')
ChatOpenAI écrit les notes puis effectue la fusion. ChatAnthropic rédige le quiz. Rien dans RunnableParallel ne nécessite que chaque branche utilise le même modèle, ce qui est pratique lorsque l’un des fournisseurs est plus performant ou moins cher pour une tâche spécifique.
L’instruction de création des notes transforme le texte d’entrée en notes courtes et simples :
prompt1 = PromptTemplate(
template='Generate short and simple notes from the following text \n {text}',
input_variables=['text']
)
L’instruction de création du quiz génère cinq paires courtes de questions et réponses à partir du même texte :
prompt2 = PromptTemplate(
template='Generate 5 short question answers from the following text \n {text}',
input_variables=['text']
)
Aucune des tâches n’a besoin du résultat de l’autre, ce qui en fait naturellement des branches parallèles.
La troisième instruction est différente : elle s’exécute après la distribution des tâches et a besoin des deux résultats :
prompt3 = PromptTemplate(
template='Merge the provided notes and quiz into a single document \n notes -> {notes} and quiz -> {quiz}',
input_variables=['notes', 'quiz']
)
Elle déclare deux variables d’entrée :
notes
quiz
Ces noms ne sont pas arbitraires. Ils doivent correspondre aux clés de l’étape parallèle, car le dictionnaire généré par la distribution est exactement ce que reçoit cette requête.
L’étape parallèle définit les deux branches :
parallel_chain = RunnableParallel({
'notes': prompt1 | model1 | parser,
'quiz': prompt2 | model2 | parser
})
Sa structure est une séparation bidirectionnelle qui se combine en un dictionnaire :
Input Text
↓
RunnableParallel
/ \
↓ ↓
Notes Chain Quiz Chain
↓ ↓
OpenAI Anthropic
↓ ↓
Parser Parser
\ /
↓ ↓
Dictionary
Conceptuellement, la valeur intermédiaire ressemble à ceci :
{
"notes": "...",
"quiz": "..."
}
La chaîne de fusion est une chaîne simple ordinaire :
merge_chain = prompt3 | model1 | parser
Elle consomme le dictionnaire provenant de l’étape parallèle, remplit {notes} et {quiz}, puis demande à OpenAI de composer un document.
Finalement, les deux étapes sont reliées par une autre canalisation :
chain = parallel_chain | merge_chain
Le résultat est un flux de travail hybride : parallèle au début, séquentiel à la fin :
Input Text
↓
RunnableParallel
/ \
↓ ↓
Notes Quiz
↓ ↓
OpenAI Anthropic
↓ ↓
Parser Parser
\ /
↓ ↓
Combined Dictionary
↓
Merge Prompt
↓
OpenAI
↓
Parser
↓
Final Output
Le détail comportemental clé est que les deux branches s’exécutent de manière indépendante, mais l’étape de fusion ne peut commencer qu’une fois que toutes deux ont terminé. La branche la plus lente détermine donc le rythme de tout le processus en étoile. Si l’une des branches échoue, l’étape parallèle est interrompue et la fusion ne s’exécute jamais ; il convient donc d’entourer les fournisseurs peu fiables de mécanismes de tentative répétée ou de solution de secours lorsque cela est important.
Tout le pipeline est invoqué avec le texte suivant :
result = chain.invoke({'text': text})
Les branches consacrées aux notes et au quiz traitent le texte en même temps. Lorsque toutes deux ont terminé, leurs résultats sont intégrés dans la chaîne de fusion, et c’est le résultat texte obtenu qui est retourné par la fonction invoke.
Affichage de la structure de la chaîne
L’outil de visualisation du graphe montre également l’organisation en étoile :
parallel_chain.get_graph().print_ascii()
Pour la chaîne Gemini à trois branches, le diagramme illustre la séparation et la réunion des flux :
Parallel<...>Input
/ | \
/ | \
PromptTemplate PromptTemplate PromptTemplate
↓ ↓ ↓
LLM LLM LLM
↓ ↓ ↓
StrOutputParser StrOutputParser StrOutputParser
\ | /
\ | /
Parallel<...>Output
Les nœuds Parallel<...>Input et Parallel<...>Output indiquent l’endroit où les données d’entrée sont distribuées et où les résultats sont regroupés dans un dictionnaire.
Comparaison des deux implémentations
Les deux versions illustrent la même idée : des sous-chaines indépendantes fonctionnant en même temps. La version Gemini est une structure pure de type fan-out avec trois branches sur un seul modèle :
Input
↓
RunnableParallel
├── Facts → Gemini → Parser
├── Definition → Gemini → Parser
└── Use Case → Gemini → Parser
↓
Dictionary Output
La version utilisant des fournisseurs mixtes se divise en deux branches sur des fournisseurs différents, avant d’ajouter une étape de fusion :
Input
↓
RunnableParallel
├── Notes → OpenAI → Parser
└── Quiz → Anthropic → Parser
↓
Dictionary Output
↓
Merge Chain
↓
OpenAI
↓
Output
En dessous des deux se trouve la même structure de base :
RunnableParallel({
"chain_1": chain_1,
"chain_2": chain_2,
"chain_3": chain_3
})
Les clés désignent les sous-chains ; les valeurs sont les sous-chains eux-mêmes. Comme raccourci, lorsque un dictionnaire ordinaire de Runnables apparaît à l’intérieur d’une expression pipe, LCEL le convertit automatiquement en RunnableParallel, de sorte que l’on voit souvent ce dictionnaire écrit sans la classe explicite.
Le chemin des données complet
Dans sa forme générale, une chaîne parallèle distribue une entrée et rassemble un dictionnaire :
Input
↓
RunnableParallel
/ | \
↓ ↓ ↓
Sub-chain Sub-chain Sub-chain
↓ ↓ ↓
Output Output Output
\ | /
\ | /
↓ ↓ ↓
Dictionary
Pour l’exemple d’intelligence artificielle sur Gemini :
{"topic": "Artificial Intelligence"}
↓
RunnableParallel
/ | \
↓ ↓ ↓
Facts Definition Use Case
↓ ↓ ↓
Gemini Gemini Gemini
↓ ↓ ↓
Parser Parser Parser
\ | /
\ | /
Dictionary
La valeur finale possède une clé par branche :
{
"facts": "...",
"definition": "...",
"use_case": "..."
}
C’est là la différence essentielle avec une chaîne séquentielle. Dans une chaîne séquentielle, chaque étape consomme la sortie de l’étape précédente :
A → B → C → D
Dans une chaîne parallèle, des étapes indépendantes s’exécutent en même temps sur la même entrée et leurs résultats sont fusionnés :
┌→ A ─┐
Input ─┼→ B ─┼→ Combined Output
└→ C ─┘
Utilisez le parallélisme pour les tâches qui partagent des entrées mais pas de résultats, et conservez la composition séquentielle pour les tâches qui s’appuient réellement sur des étapes précédentes. Mélanger les deux, comme le montre l’exemple des notes et du quiz, est courant et tout à fait idiomatique.
Chaînes conditionnelles : redirection des entrées vers la bonne sous-chaine
Une chaîne conditionnelle choisit son étape suivante au moment de l’exécution. Une chaîne séquentielle exécute toujours chaque étape dans un ordre fixe, tandis qu’une chaîne parallèle exécute toutes les branches en même temps. Une chaîne conditionnelle évalue une condition sur l’entrée et ne met en œuvre que la branche qui correspond :
Input
↓
Condition Check
↓
┌───────────────┐
│ │
Yes No
↓ ↓
Chain 1 Chain 2
↓ ↓
Output 1 Output 2
\ /
\ /
Final Output
Dans LangChain, cela s’exprime avec RunnableBranch. On y fournit une liste de paires, chaque paire associant une condition à un élément Runnable, ainsi qu’un élément Runnable par défaut à la fin. Les conditions sont testées dans l’ordre ; la première qui renvoie True détermine quel élément Runnable sera exécuté, et si aucune condition ne correspond, c’est le par défaut qui gère l’entrée.
Exemple : routage selon la longueur du texte
Cet exemple traite le texte différemment en fonction de sa longueur :
- Si le texte compte plus de 50 mots, un résumé court est généré.
- Sinon, l’idée exprimée dans le texte est développée en détail.
Le flux de travail résultant :
Input Text
↓
Condition Check
↓
Is word count > 50?
↓
┌───────┴───────┐
Yes No
↓ ↓
Summary Extend
Chain Chain
↓ ↓
LLM LLM
↓ ↓
Parser Parser
\ /
\ /
Final Output
Deux prompts alimentent les branches : summary_prompt demande un bref résumé, tandis que extend_prompt demande au modèle de développer l’idée. Une petite fonction, is_long_text(), compte le nombre de mots, et RunnableBranch utilise sa réponse pour choisir la chaîne à suivre. Comme pour les autres modèles, il existe une version Gemini et une version OpenAI.
En forme générale, un pipeline conditionnel bidirectionnel se présente ainsi :
User Input
↓
Condition Check
↓
┌───────┴───────┐
Yes No
↓ ↓
Prompt 1 Prompt 2
↓ ↓
LLM LLM
↓ ↓
Parser Parser
\ /
└──────┬───────┘
↓
Final Output
Pour toute entrée donnée, une seule branche s’exécute.
Suivi d’une entrée concrète
L’entrée de exemple est une définition en une seule phrase du machine learning, la décrivant comme un sous-domaine de l’IA dans lequel des méthodes statistiques permettent aux ordinateurs d’apprendre à partir de données.
Cette phrase compte 22 mots, bien en dessous du seuil de 50 mots, donc la condition renvoie False et c’est la chaîne d’extension qui est choisie :
Input Text
↓
Condition Check
↓
22 > 50?
↓
No
↓
Extend Chain
↓
Gemini 2.5 Flash
↓
StrOutputParser
↓
Extended Text
Lorsque l’on insère un passage de plus de 50 mots, la condition devient True, ce qui envoie l’entrée à la chaîne de résumé plutôt qu’à une autre.
Quand l’acheminement conditionnel est utile
L’acheminement est pratique chaque fois que le même type d’entrée nécessite un traitement différent en fonction d’une certaine propriété :
- Longueur : condenser les textes longs, développer ceux qui sont courts.
- Sentiment : envoyer des retours positifs et négatifs à des chaînes de réponse distinctes.
- Type de demande : diriger chaque catégorie de demande vers une chaîne spécialisée.
La propriété importante est de ne pas payer pour des branches inutiles. Seul le chemin sélectionné s’exécute :
Input
↓
Condition
↓
Choose one branch
↓
Execute selected chain
↓
Output
Cela rend le flux de travail adaptable d’une manière impossible avec une séquence fixe, et cela permet d’économiser des appels au modèle par rapport à l’exécution de toutes les branches puis au rejet de leurs résultats.
La version Gemini
Voici le programme de routage complet sur Gemini :
from langchain_google_genai import GoogleGenerativeAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch
from dotenv import load_dotenv
load_dotenv()
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
parser = StrOutputParser()
# Prompt 1 : Summary
summary_prompt = PromptTemplate(
input_variables=["text"],
template="Summarize the following text briefly:\n\n{text}"
)
# Prompt 2 : Extend
extend_prompt = PromptTemplate(
input_variables=["text"],
template="Extend the idea in the following long text:\n\n{text}"
)
# Chain 1 : Summary
summary_chain = summary_prompt | llm | parser
# Chain 2 : Extend
extend_chain = extend_prompt | llm | parser
def is_long_text(input: dict) -> bool:
return len(input["text"].split()) > 50
conditional_chain = RunnableBranch(
(lambda x: is_long_text(x), summary_chain),
extend_chain
)
text_input = {
"text": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to learn from data."
}
result = conditional_chain.invoke(text_input)
# print("\nOutput:\n")
# print(result)
conditional_chain.get_graph().print_ascii()
Il définit deux petites chaînes, summary_chain et extend_chain, ainsi qu’un prédicat de comptage de mots, puis permet à RunnableBranch de choisir entre ces chaînes en fonction de ce prédicat.
Décortiquage du code Gemini
Le modèle est à nouveau Gemini 2.5 Flash :
llm = GoogleGenerativeAI(model="gemini-2.5-flash")
Un StrOutputParser transforme chaque réponse en chaîne de caractères :
parser = StrOutputParser()
La demande de résumé sollicite un résumé succinct :
summary_prompt = PromptTemplate(
input_variables=["text"],
template="Summarize the following text briefly:\n\n{text}"
)
La demande d’extension est utilisée sur l’autre chemin :
extend_prompt = PromptTemplate(
input_variables=["text"],
template="Extend the idea in the following long text:\n\n{text}"
)
Examinez attentivement sa formulation : elle fait référence à « le texte suivant », or cette branche ne s’exécute que pour des entrées courtes. Ce manque de cohérence ne provoque aucun problème, mais elle envoie au modèle un indice trompeur. La reformuler en quelque chose comme « Développez l’idée présentée dans le texte suivant » constitue une petite correction utile à apporter à votre propre version.
Chaque prompt est combiné avec le modèle et l’analyseur pour former sa propre chaîne :
summary_chain = summary_prompt | llm | parser
extend_chain = extend_prompt | llm | parser
Cela génère deux chemins indépendants :
Summary Prompt → Gemini → Parser
Extend Prompt → Gemini → Parser
La étape conditionnelle décide lequel d’entre eux s’exécute.
La condition est une fonction Python ordinaire :
def is_long_text(input: dict) -> bool:
return len(input["text"].split()) > 50
Elle divise input[“text”] en fonction des espaces blancs et compte les éléments obtenus. Son contrat est simple :
True → if word count > 50
False → otherwise
La séparation par les espaces blancs donne un comptage approximatif du nombre de mots ; la ponctuation reste attachée aux mots et les termes composés d’hyphènes sont comptés comme un seul. Cela convient pour le routage par longueur, mais si vous tenez vraiment compte des limites du modèle, comptez les tokens plutôt que les mots. Notez également que le nom du paramètre input masque une fonction intégrée de Python ; cela fonctionne, mais un nom comme data est plus clair.
Avec ces éléments en place, RunnableBranch assemble le routeur :
conditional_chain = RunnableBranch(
(lambda x: is_long_text(x), summary_chain),
extend_chain
)
Le premier argument est une tuple qui représente une règle :
If is_long_text(x) is True
↓
Execute summary_chain
L’argument final, qui n’est pas encadré dans une tuple, correspond à la valeur par défaut :
Otherwise
↓
Execute extend_chain
Le wrapper lambda n’est pas nécessaire. RunnableBranch accepte n’importe quelle fonction appelable comme condition, donc passer directement is_long_text fonctionne tout aussi bien. Dans son ensemble, le routage se présente comme ceci :
Input
↓
is_long_text()
/ \
True False
↓ ↓
summary_chain extend_chain
↓ ↓
Gemini Gemini
↓ ↓
Parser Parser
\ /
\ /
Output
Le fonctionnement nécessite le dictionnaire d’entrée suivant :
result = conditional_chain.invoke(text_input)
La condition est vérifiée en premier, et seul le branch sélectionné effectue une appel au modèle.
Résultat obtenu
Avec un exemple d’entrée de 22 mots, le prédicat renvoie :
is_long_text() → False
Ainsi, extend_chain gère l’entrée. Conceptuellement :
Input Text
↓
Word Count Check
↓
False
↓
extend_chain
↓
Gemini 2.5 Flash
↓
StrOutputParser
↓
Extended Text
Si l’on fournit un texte de plus de 50 mots, le prédicat renvoie :
is_long_text() → True
Cela modifie le chemin vers :
Input Text
↓
Word Count Check
↓
True
↓
summary_chain
↓
Gemini 2.5 Flash
↓
StrOutputParser
↓
Short Summary
Un seul pipeline, deux itinéraires possibles, déterminés par l’entrée elle-même.
Même routeur sur OpenAI
La version OpenAI conserve la logique de routage et ne change que le modèle :
from langchain_openai import ChatOpenAI
from langchain_core.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnableBranch
from dotenv import load_dotenv
load_dotenv()
llm = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()
# Prompt 1 : Summary
summary_prompt = PromptTemplate(
input_variables=["text"],
template="Summarize the following text briefly:\n\n{text}"
)
# Prompt 2 : Extend
extend_prompt = PromptTemplate(
input_variables=["text"],
template="Extend the idea in the following long text:\n\n{text}"
)
# Chain 1 : Summary
summary_chain = summary_prompt | llm | parser
# Chain 2 : Extend
extend_chain = extend_prompt | llm | parser
def is_long_text(input: dict) -> bool:
return len(input["text"].split()) > 50
conditional_chain = RunnableBranch(
(lambda x: is_long_text(x), summary_chain),
extend_chain
)
text_input = {
"text": "Machine learning is a field of artificial intelligence that uses statistical techniques to give computer systems the ability to learn from data."
}
result = conditional_chain.invoke(text_input)
print(result)
conditional_chain.get_graph().print_ascii()
Deux chaînes, un prédicat, une RunnableBranch, identique à celui utilisé avec Gemini.
Le modèle est gpt-4o-mini :
llm = ChatOpenAI(model="gpt-4o-mini")
Les deux modèles de prompt restent inchangés, l’un pour résumer et l’autre pour étendre :
summary_prompt = PromptTemplate(
input_variables=["text"],
template="Summarize the following text briefly:\n\n{text}"
)
extend_prompt = PromptTemplate(
input_variables=["text"],
template="Extend the idea in the following long text:\n\n{text}"
)
Les chaînes sont composées exactement comme avant :
summary_chain = summary_prompt | llm | parser
extend_chain = extend_prompt | llm | parser
Désormais, les deux chemins passent par OpenAI :
Summary Prompt → OpenAI → StrOutputParser
Extend Prompt → OpenAI → StrOutputParser
Le prédicat est toujours la même vérification de 50 mots :
def is_long_text(input: dict) -> bool:
return len(input["text"].split()) > 50
Le routage est construit de la même manière :
conditional_chain = RunnableBranch(
(lambda x: is_long_text(x), summary_chain),
extend_chain
)
Un résultat True envoie l’entrée vers summary_chain ; tout le reste est envoyé vers extend_chain.
La chaîne est invoquée et le résultat affiché :
result = conditional_chain.invoke(text_input)
print(result)
Pour la phrase d’exemple, le prédicat donne à nouveau pour résultat :
is_long_text() → False
ainsi la chaîne d’extension s’exécute et renvoie une explication étendue de l’entrée, similaire à ceci :
Machine learning enables computer systems to learn patterns
from data and improve their performance without being explicitly
programmed for every task. It is widely used in applications
such as recommendation systems, image recognition, and predictive
analytics.
Le texte exact dépend du modèle et changera à chaque exécution.
Imprimer la structure de la chaîne
Le même outil graphique fonctionne également pour les routeurs :
conditional_chain.get_graph().print_ascii()
L’élément notable du diagramme affiché est le nœud Branch. Il s’agit du point de décision où LangChain évalue les conditions et transmet l’entrée à l’une des chaînes candidates.
En quoi les deux branches diffèrent-elles
Chaque branche a sa propre responsabilité. La branche de résumé :
summary_prompt
↓
Gemini
↓
StrOutputParser
↓
Short Summary
La branche d’extension :
extend_prompt
↓
Gemini
↓
StrOutputParser
↓
Extended Text
Le prédicat choisit entre elles :
Input
↓
is_long_text()
/ \
True False
↓ ↓
Summary Chain Extend Chain
C’est là la principale différence par rapport à une chaîne parallèle : les branches ne s’exécutent pas toutes en même temps. Seul le chemin dont la condition est remplie s’exécute, ce qui permet de garder les coûts et les délais proportionnels au travail réellement nécessaire.
Routeur basé sur la classification propre du modèle
Les conditions ne se limitent pas aux contrôles simples tels que le comptage de mots. Un modèle plus courant et efficace consiste à faire en sorte qu’une première chaîne classe l’entrée avant d’orienter le traitement selon cette classification. Dans un exemple de sentiment, les retours reçus sont étiquetés avec l’une de deux valeurs :
positive
negative
et RunnableBranch les transmet à la chaîne de réponse correspondante :
Feedback
↓
Sentiment Classifier
↓
┌───────────────┐
│ │
Positive Negative
│ │
↓ ↓
Positive Negative
Response Response
│ │
└───────┬───────┘
↓
Output
Une méthode fiable pour construire le classificateur consiste à faire en sorte que le modèle renvoie une sortie structurée validée par un modèle Pydantic, par exemple un champ restreint aux valeurs positive ou negative. Les conditions des branches vérifient alors ce champ plutôt que de parser du texte libre, ce qui évite les surprises lorsque le modèle répond par « Positive. » avec une majuscule et un point. Il est toujours conseillé de conserver une branche par défaut pertinente, car un classificateur finira tôt ou tard par renvoyer quelque chose que l’on n’avait pas anticipé.
Cela montre qu’une condition peut dépendre des informations produites par une chaîne antérieure, et non seulement de l’entrée brute. Pour un routage plus complexe, la documentation actuelle de LangChain décrit également le routage à l’aide d’une fonction personnalisée enveloppée dans RunnableLambda qui retourne la chaîne à exécuter ; il est utile de vérifier quelle approche les documents recommandent pour votre version.
Le chemin complet des données
Le schéma conditionnel général est :
Input
↓
Condition Check
↓
┌───────┴───────┐
↓ ↓
Condition Condition
True False
↓ ↓
Chain 1 Chain 2
↓ ↓
LLM LLM
↓ ↓
Parser Parser
\ /
\ /
└─────┬───────┘
↓
Output
Pour l’exemple de longueur de texte :
Machine Learning Text
↓
is_long_text()
↓
Word Count > 50?
/ \
Yes No
↓ ↓
Summary Extend
Chain Chain
↓ ↓
Gemini Gemini
↓ ↓
Parser Parser
\ /
\ /
Output
Les entrées courtes suivent un chemin :
Word Count ≤ 50
↓
Extend Chain
Les entrées longues suivent l’autre chemin :
Word Count > 50
↓
Summary Chain
En résumé, voici les quatre formes côte à côte :
Simple Chain
A → B → C
Sequential Chain
A → B → C → D → E
Parallel Chain
┌→ B ─┐
A ────┼→ C ─┼→ Output
└→ D ─┘
Conditional Chain
↓
A
↓
Condition
/ \
B C
Une chaîne conditionnelle permet à un flux de travail d’LLM de prendre des décisions, en exécutant différentes logiques en fonction de l’entrée ou d’une valeur calculée précédemment dans le pipeline.
Conseils pratiques avant l’envoi
Quelques problèmes récurrents méritent d’être gardés à l’esprit en passant des exemples au code réel :
- Spécifiez les noms des modèles. Les constructeurs sans argument de modèle héritent de la valeur par défaut utilisée par l’intégration installée, et les identifiants obsolètes finissent par être supprimés.
- Gardez le parseur. Les modèles de chat renvoient des objets de message ;
StrOutputParserfournit à chaque étape suivante une valeur de typestrprévisible. - Faites attention aux transferts de données. Une chaîne de caractères brute ne remplit automatiquement un prompt que si ce dernier contient exactement une variable. Pour plus de flexibilité, créez le dictionnaire explicitement.
- Associez les clés aux variables. Lors d’une distribution suivie d’une fusion, les clés parallèles doivent correspondre aux noms des variables du prompt de fusion.
langchain_core.runnables, et non depuis les chemins obsolètes de langchain.schema.get_graph().print_ascii() (avec grandalf installé) est un moyen simple de vérifier que une chaîne complexe est configurée comme prévu.Pour une présentation plus complète montrant ces chaînes en parallèle avec la récupération d’informations, les outils et les agents fonctionnant sur des modèles locaux, consultez LangChain 1.x en pratique : chaînes, RAG, outils et agents localement.
Points clés
- Une chaîne est un pipeline déclaré dans lequel la sortie de chaque composant alimente le suivant ; l’opérateur
|la construit à partir de n’importe quels éléments exécutables. - Les chaînes simples impliquent un seul passage en boucle ; les chaînes séquentielles empilent plusieurs passages, ajoutant du délai à chaque étape.
RunnableParallelexécute des sous-chaînes indépendantes en parallèle et renvoie un dictionnaire dont les clés sont les noms de branche, que les étapes ultérieures peuvent utiliser en se référant au nom de la variable.RunnableBranchévalue les conditions dans l’ordre et ne lance que la première branche correspondante, en recourant à une valeur par défaut si nécessaire.- Les conditions peuvent être des vérifications simples sur l’entrée ou des décisions basées sur une classification structurée d’un modèle antérieur.
- Puisque le modèle n’est qu’un seul composant, passer de Gemini à OpenAI ou Anthropic ne nécessite généralement qu’une seule modification, tandis que les prompts, les analyseurs et la structure restent inchangés.
Tous les quatre modèles reposent sur la même base : l’interface Runnable, qui est ce qui permet à la fois aux prompts, aux modèles, aux analyseurs et à vos propres fonctions d’être composés. Comprendre directement cette interface constitue la prochaine étape naturelle une fois que ces structures vous sembleront familières.