En bref
En moins d'un an, LangChain est devenue l'une des bibliothèques Python les plus utilisées pour interagir avec les grands modèles de langage (LLM), mais elle servait principalement à réaliser des preuves de concept (POC), car elle ne permettait pas de créer des applications complexes et évolutives.
Tout a changé en août 2023 avec la sortie de LangChain Expression Language (LCEL), une nouvelle syntaxe qui comble le fossé entre les POC et la production. Cet article vous guidera à travers les tenants et aboutissants de LCEL, en vous montrant comment il simplifie la création de chaînes personnalisées et pourquoi vous devez l'apprendre si vous développez des applications LLM!
Prompts, modèles de langage de grande envergure et chaînes : rafraîchissons-nous la mémoire
Avant de nous plonger dans la syntaxe de LCEL, je pense qu’il est utile de nous rafraîchir la mémoire sur certains concepts de LangChain, tels que les LLM, les prompts ou encore les chaînes.
LLM: Dans LangChain, un LLM est une abstraction du modèle utilisé pour générer des compléments, comme OpenAI GPT-3.5, Claude, etc.
Consigne: Il s'agit de l'entrée de l'objet LLM, qui posera des questions au LLM et lui indiquera ses objectifs.
Chaîne: il s'agit d'une séquence d'appels à un modèle de langage (LLM) ou de toute étape data .

Maintenant que les définitions sont réglées, imaginons que nous voulions créer une entreprise ! Il nous faut un nom vraiment sympa et accrocheur, ainsi qu'un modèle économique pour gagner de l'argent !
Exemple — Nom de l'entreprise et modèle économique avec les anciennes chaînes
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
USER_INPUT = "chaussettes colorées"
llm = OpenAI(temperature=0)
prompt_template_product = "Quel serait un bon nom pour une entreprise qui fabrique ?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
company_name_output = company_name_chain(USER_INPUT)
prompt_template_business = "Propose-moi la meilleure idée de modèle économique pour mon entreprise nommée : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
business_model_output = business_model_chain(company_name_output["text"])
print(nom_de_l'entreprise_résultat)
print(modèle_économique_résultat)
>>>
>>>
C'est assez simple à suivre ; on constate certes quelques redondances, mais cela reste gérable.
Ajoutons quelques options de personnalisation en gérant les cas où l'utilisateur n'utilise pas notre chaîne comme prévu.
Peut-être que l'utilisateur saisira quelque chose qui n'a absolument rien à voir avec l'objectif de notre chaîne ? Dans ce cas, nous voulons le détecter et réagir de manière appropriée.
Exemple — Personnalisation et routage avec d'anciennes chaînes
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
import ast
USER_INPUT = "Harrison Chase"
llm = OpenAI(temperature=0)
# —- Même code que précédemment
prompt_template_product = "Quel serait un bon nom pour une entreprise qui fabrique ?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
prompt_template_business = "Propose-moi la meilleure idée de modèle économique pour mon entreprise nommée : "
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
# —- Nouveau code
prompt_template_is_product = (
"Votre objectif est de déterminer si la saisie de l'utilisateur correspond à un nom de produit plausible"
"Les questions, les formules de politesse, les phrases longues, les noms de célébrités ou toute autre saisie non pertinente ne sont pas considérés comme des noms de produits"
"Saisie : n"
"Répondez uniquement par « Vrai » ou « Faux », sans rien ajouter d'autre"
)
prompt_template_cannot_respond = (
"Vous ne pouvez pas répondre à la saisie de l'utilisateur : n"
"Demandez à l'utilisateur de saisir le nom d'un produit afin que vous puissiez créer une entreprise à partir de celui-ci.n"
)
cannot_respond_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_cannot_respond))
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
is_a_product_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_is_product))
# Si l'on utilise bool sur une chaîne non vide, le résultat sera True ; il faut donc utiliser `literal_eval`
is_a_product = ast.literal_eval(is_a_product_chain(USER_INPUT)["text"])
if is_a_product:
company_name_output = company_name_chain(USER_INPUT)
business_model_output = business_model_chain(company_name_output["text"])
print(business_model_output)
else:
print(cannot_respond_chain(USER_INPUT))
Cela devient un peu plus difficile à comprendre, résumons donc :
Plusieurs problèmes commencent à se poser :
Qu'est-ce que le langage d'expression LangChain (LCEL) ?
LCEL est une interface et une syntaxe unifiées permettant d'écrire des chaînes modulaires prêtes à l'emploi ; il y a beaucoup à approfondir pour en saisir toute la portée.
Nous allons d'abord essayer de comprendre la nouvelle syntaxe en réécrivant la chaîne présentée précédemment.
Exemple — Nom de l'entreprise et modèle économique avec LCEL
from langchain_core.runnables import RunnablePassthrough
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
USER_INPUT = "chaussettes colorées"
llm = OpenAI(temperature=0)
prompt_template_product = "Quel serait un bon nom pour une entreprise qui fabrique ?"
prompt_template_business = "Donnez-moi la meilleure idée de modèle économique pour mon entreprise nommée : "
chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
)
business_model_output = chain.invoke()
Beaucoup de code inhabituel, en seulement quelques lignes :
Mais est-ce vraiment plus modulable de créer des chaînes de cette manière ?
Mettons cela à l'épreuve en ajoutant la fonction is_a_product_chain() et la branche de contrôle si la saisie de l'utilisateur n'est pas correcte. Nous pouvons même définir le type de la chaîne avec Python Typing ; faisons-le par souci de bonne pratique.
Exemple — Personnalisation et routage avec LCEL
from typing import Dict
from langchain_core.runnables import RunnablePassthrough, RunnableBranch
from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.output_parsers import BooleanOutputParser
from langchain.community.llms import OpenAI
USER_INPUT = "Harrrison Chase"
llm = OpenAI(temperature=0)
prompt_template_product = "Quel serait un bon nom pour une entreprise qui fabrique ?"
prompt_template_cannot_respond = (
"Vous ne pouvez pas répondre à la saisie de l'utilisateur : n"
"Demandez à l'utilisateur de saisir le nom d'un produit afin que vous puissiez en faire une entreprise.n"
)
prompt_template_business = "Donnez-moi la meilleure idée de modèle économique pour mon entreprise nommée : "
prompt_template_is_product = (
"Votre objectif est de déterminer si la saisie de l'utilisateur correspond à un nom de produit plausible"
"Les questions, les formules de politesse, les phrases longues, les noms de célébrités ou toute autre saisie non pertinente ne sont pas considérés comme des noms de produits"
"saisie : n"
"Répondez uniquement par « Vrai » ou « Faux », rien de plus"
)
answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
|
| PromptTemplate.from_template(prompt_template_business)
| llm
).with_types(input_type=Dict[str, str], output_type=str)
is_product_chain = (
PromptTemplate.from_template(prompt_template_is_product)
| llm
| BooleanOutputParser(true_val='True', false_val='False')
).with_types(input_type=Dict[str, str], output_type=bool)
cannot_respond_chain = (
PromptTemplate.from_template(prompt_template_cannot_respond) | llm
).with_types(input_type=Dict[str, str], output_type=str)
full_chain = RunnableBranch(
(is_product_chain, answer_user_chain),
cannot_respond_chain
).with_types(input_type=Dict[str, str], output_type=str)
print(full_chain.invoke())
Énumérons les différences :
Pourquoi le LCEL est-il plus adapté à l'industrialisation ?
Si j’avais lu cet article jusqu’à ce moment précis et que quelqu’un m’avait demandé si j’étais convaincu par le LCEL, j’aurais probablement répondu non. La syntaxe est trop différente, et je pourrais sans doute organiser mon code en fonctions pour obtenir un résultat presque identique. Mais je suis là, en train d’écrire cet article, donc il doit y avoir autre chose.
Utilisation immédiate des fonctions invoke, stream et batch
En utilisant LCEL, votre chaîne bénéficie automatiquement des avantages suivants :
my_chain = prompt | llm
# ———invoke——— #
result_with_invoke = my_chain.invoke(« hello world ! »)
# ———lot——— #
result_with_batch = my_chain.batch([« hello », « world », « ! »])
# ———stream——— #
for chunk in my_chain.stream("hello world!"):
print(chunk, flush=True, end="")
Lorsque vous effectuez une itération, vous pouvez utiliser la méthode `invoke` pour faciliter le processus de développement. Mais lorsque vous affichez le résultat de votre chaîne dans une interface utilisateur, vous souhaitez diffuser la réponse en continu. Vous pouvez désormais utiliser la méthode `stream` sans avoir à réécrire quoi que ce soit.
Méthodes asynchrones prêtes à l'emploi
La plupart du temps, le front-end et le back-end de votre application sont distincts, ce qui signifie que le front-end envoie une requête au back-end. Si vous avez plusieurs utilisateurs, vous devrez peut-être traiter plusieurs requêtes simultanément au niveau du back-end.
Comme la majeure partie du code dans LangChain consiste simplement à attendre entre deux appels d'API, nous pouvons tirer parti du code asynchrone pour améliorer l'évolutivité de l'API. Si vous souhaitez comprendre pourquoi c'est important, je vous recommande de lire l'exemple « Concurrent Burgers » dans la documentation FastAPI.
Inutile de vous soucier de la mise en œuvre, car les méthodes asynchrones sont déjà disponibles si vous utilisez LCEL :
.ainvoke() / .abatch() / .astream: versions asynchrones des méthodes invoke, batch et stream.
Je vous recommande également de consulter la page « Pourquoi utiliser LCEL » de la documentation LangChain, qui contient des exemples pour chaque méthode synchrone et asynchrone.
Langchain a mis en place ces fonctionnalités « prêtes à l'emploi » en créant une interface unifiée appelée « Runnable ». À présent, pour tirer pleinement parti de LCEL, nous devons examiner de plus près cette nouvelle interface Runnable.
L'interface Runnable
Tous les objets que nous avons utilisés jusqu’à présent dans la syntaxe LCEL sont des Runnables. Il s’agit d’un objet Python créé par LangChain ; cet objet hérite automatiquement de toutes les fonctionnalités dont nous avons parlé précédemment, et bien d’autres encore. En utilisant la syntaxe LCEL, nous composons un nouveau Runnable à chaque étape, ce qui signifie que l’objet final créé sera lui aussi un Runnable. Vous pouvez en savoir plus sur cette interface dans la documentation officielle.
Tous les objets du code ci-dessous sont soit des objets de type `Runnable`, soit des dictionnaires qui sont automatiquement convertis en objets de type `Runnable` :
from langchain_core.runnables import RunnablePassthrough, RunnableParallel
from langchain.prompts import PromptTemplate
from langchain_community.llms import OpenAI
chain_number_one = (
PromptTemplate.from_template(prompt_template_product)
| llm
| # <— THIS WILL CHANGE
| PromptTemplate.from_template(prompt_template_business)
| llm
)
chain_number_two = (
PromptTemplate.from_template(prompt_template_product)
| llm
| RunnableParallel(company=RunnablePassthrough()) # <— THIS CHANGED
| PromptTemplate.from_template(prompt_template_business)
| llm
)
print(chain_number_one == chain_number_two)
>>> True
Pourquoi utilisons-nous RunnableParallel() et non pas simplement Runnable() ?
En effet, chaque Runnable contenu dans un RunnableParallel est exécuté en parallèle. Cela signifie que si votre Runnable comporte trois étapes indépendantes, celles-ci s'exécuteront simultanément sur différents threads de votre machine, ce qui améliorera gratuitement la vitesse de votre chaîne !
Inconvénients du LCEL
Malgré ses avantages, le LCEL présente toutefois certains inconvénients potentiels :
Conclusion
En conclusion, le LangChain Expression Language (LCEL) est un outil puissant qui apporte un regard neuf sur le développement d'applications Python. Malgré sa syntaxe peu conventionnelle, je recommande vivement d'utiliser le LCEL pour les raisons suivantes :
Pour aller plus loin…
L'abstraction exécutable
Dans certains cas, je pense qu’il est important de comprendre l’abstraction mise en œuvre par LangChain pour permettre le fonctionnement de la syntaxe LCEL.
Vous pouvez facilement réimplémenter les fonctionnalités de base de Runnable comme suit :
class Runnable :
def __init__(self, func) :
self.func = func
def __or__(self, other) :
def chained_func(*args, **kwargs) :
# self.func est à gauche, other est à droite
return other(self.func(*args, **kwargs))
return Runnable(chained_func)
def __call__(self, *args, **kwargs) :
return self.func(*args, **kwargs)
def add_ten(x) :
return x + 10
def divide_by_two(x) :
return x / 2
runnable_add_ten = Runnable(add_ten)
runnable_divide_by_two = Runnable(divide_by_two)
chain = runnable_add_ten | runnable_divide_by_two
result = chain(8) # (8+10) / 2 = 9.0 devrait être la réponse
print(result)
>>> 9.0
Un Runnable est simplement un objet Python dont la méthode .__or__() a été redéfinie.
Concrètement, LangChain a ajouté de nombreuses fonctionnalités, telles que la conversion de dictionnaires en Runnable, des capacités de typage, des options de configuration, ainsi que des méthodes invoke, batch, stream et async !
Alors, pourquoi ne pas essayer LCEL pour votre prochain projet ?
Si vous souhaitez en savoir plus, je vous recommande vivement de consulter le guide pratique LangChain sur LCEL.

BLOG






