TL;DR
LangChain est devenu l'une des bibliothèques Python les plus utilisées pour interagir avec les LLM en moins d'un an, mais LangChain était surtout une bibliothèque pour les POC car elle n'avait pas la capacité de créer des applications complexes et évolutives.
Tout a changé en août 2023 avec la sortie du LangChain Expression Language (LCEL), une nouvelle syntaxe qui permet de passer du POC à la production. Cet article vous guidera à travers les tenants et les aboutissants du LCEL, en vous montrant comment il simplifie la création de chaînes personnalisées et pourquoi vous devez l'apprendre si vous construisez des applications LLM!
Prompts, LLM et chaînes, rafraîchissons-nous la mémoire
Avant de plonger dans la syntaxe LCEL, je pense qu'il est utile de se rafraîchir la mémoire sur les concepts LangChain tels que LLM et Prompt ou même une chaîne.
LLM: Dans langchain, llm est une abstraction autour du modèle utilisé pour faire les complétions comme openai gpt3.5, claude, etc...
Invite: 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 LLM ou à toute autre étape de traitement data .
Maintenant que les définitions sont terminées, supposons que nous voulions créer une entreprise ! Nous avons besoin d'un nom vraiment cool et accrocheur et d'un modèle d'entreprise pour gagner de l'argent !
Exemple - Nom de l'entreprise et modèle d'entreprise 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 est un bon nom pour une entreprise qui fabrique {produit} ?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
company_name_output = company_name_chain(USER_INPUT)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
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_sortie)
print(sortie_modèle_d'entreprise)
>>> {‘product’: ‘colorful socks’, ‘text’: ‘Socktastic!’}
>>> {‘company’: ‘Socktastic!’,’text’: “A subscription-based service offering a monthly delivery…”}
C'est assez facile à suivre, il y a un peu de redondance, mais c'est gérable.
Ajoutons un peu de personnalisation en traitant les cas où l'utilisateur n'utilise pas notre chaîne comme prévu.
Peut-être l'utilisateur va-t-il saisir quelque chose qui n'a 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 est un bon nom pour une entreprise qui fabrique {produit} ?"
company_name_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_product))
prompt_template_business = “Give me the best business model idea for my company named: {company}”
business_model_chain = LLMChain(llm=llm, prompt=PromptTemplate.from_template(prompt_template_business))
# -- Nouveau code
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.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 nous utilisons bool sur une chaîne non vide, ce sera True, donc nous avons besoin de `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(sortie_modèle_entreprise)
else :
print(cannot_respond_chain(USER_INPUT))
Cela devient un peu plus difficile à comprendre, mais résumons :
De nombreux problèmes commencent à se poser :
Qu'est-ce que le LangChain Expression Language (LCEL) ?
LCEL est une interface et une syntaxe unifiées permettant d'écrire des chaînes composables prêtes à la production.
Nous allons d'abord essayer de comprendre la nouvelle syntaxe en réécrivant la chaîne de tout à l'heure.
Exemple - Nom de l'entreprise et modèle d'entreprise 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 = “What is a good name for a company that makes {product}?”
prompt_template_business = “Give me the best business model idea for my company named: {company}”
chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| PromptTemplate.from_template(prompt_template_business)
| llm
)
business_model_output = chain.invoke({‘product’: ‘colorful socks’})
Beaucoup de code inhabituel, en quelques lignes seulement :
Mais est-il vraiment plus facile de créer des chaînes de cette manière ?
Mettons-le à l'épreuve en ajoutant la fonction is_a_product_chain() et le branchement si l'entrée de l'utilisateur n'est pas correcte. Nous pouvons même taper la chaîne avec Python Typing, faisons-le comme une 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 = “What is a good name for a company that makes {product}?”
prompt_template_cannot_respond = (
“You cannot respond to the user input: {product}n”
“Ask the user to input the name of a product in order for you to make a company out of it.n”
)
prompt_template_business = “Give me the best business model idea for my company named: {company}”
prompt_template_is_product = (
“Your goal is to find if the input of the user is a plausible product namen”
“Questions, greetings, long sentences, celebrities or other non relevant inputs are not considered productsn”
“input: {product}n”
“Answer only by ‘True’ or ‘False’ and nothing moren”
)
answer_user_chain = (
PromptTemplate.from_template(prompt_template_product)
| llm
| {‘company’: RunnablePassthrough()}
| 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({‘product’: USER_INPUT}))
Dressons la liste des différences :
Pourquoi la LCEL est-elle meilleure pour l'industrialisation ?
Si je lisais cet article jusqu'à ce point précis, et que quelqu'un me demandait si j'étais convaincu par LCEL, je dirais probablement non. La syntaxe est trop différente, et je peux probablement organiser mon code en fonctions pour obtenir presque exactement le même code. Mais je suis ici, en train d'écrire cet article, alors il doit y avoir quelque chose de plus.
Invoke, stream et batch prêts à l'emploi
En utilisant LCEL, votre chaîne a automatiquement :
ma_chaîne = prompt | llm
# ---invoke--- #
result_with_invoke = my_chain.invoke("hello world !")
# ---batch--- #
result_with_batch = my_chain.batch(["hello", "world", " !"])
# ---stream--- #
for chunk in my_chain.stream("hello world !") :
print(chunk, flush=True, end="")
Lorsque vous itérez, vous pouvez utiliser la méthode invoke pour faciliter le processus de développement. Mais lorsque vous affichez la sortie de votre chaîne dans une interface utilisateur, vous voulez streamer la réponse. Vous pouvez maintenant utiliser la méthode stream sans réécrire quoi que ce soit.
Méthodes asynchrones prêtes à l'emploi
La plupart du temps, le frontend et le backend de votre application seront séparés, ce qui signifie que le frontend fera une requête au backend. Si vous avez plusieurs utilisateurs, vous pouvez avoir besoin de gérer plusieurs requêtes en même temps sur votre backend.
Puisque la plupart du code de LangChain ne fait qu'attendre entre les appels à l'API, nous pouvons exploiter le code asynchrone pour améliorer l'évolutivité de l'API. Si vous voulez comprendre pourquoi c'est important, je vous recommande de lire l'histoire des hamburgers concurrents dans la documentation de FastAPI.
Il n'est pas nécessaire de se préoccuper de l'implémentation, car les méthodes asynchrones sont déjà disponibles si vous utilisez LCEL :
.ainvoke() / .abatch() / .astream: versions asynchrones de invoke, batch et stream.
Je recommande également de lire la page Why use LCEL de la documentation LangChain avec des exemples pour chaque méthode sync / async.
Langchain a réalisé ces fonctionnalités "out of the box" en créant une interface unifiée appelée "Runnable". Maintenant, pour tirer pleinement parti de LCEL, nous devons nous pencher sur ce qu'est cette nouvelle interface Runnable.
L'interface Runnable
Tous les objets que nous avons utilisés dans la syntaxe LCEL jusqu'à présent 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 de 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 également un Runnable. Vous pouvez en apprendre plus sur l'interface dans la documentation officielle.
Tous les objets du code ci-dessous sont soit des Runnables, soit des dictionnaires qui sont automatiquement convertis en Runnables :
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
| {‘company’: RunnablePassthrough()} # <— 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(numéro_de_chaîne_un == numéro_de_chaîne_deux)
>>> Vrai
Pourquoi utiliser RunnableParallel() et pas simplement Runnable() ?
En effet, chaque Runnable à l'intérieur d'un RunnableParallel est exécuté en parallèle. Cela signifie que si vous avez 3 étapes indépendantes dans votre Runnable, elles s'exécuteront en même temps sur différents threads de votre machine, améliorant ainsi la vitesse de votre chaîne gratuitement !
Inconvénients de la LCEL
Malgré ses avantages, la LCEL présente quelques inconvénients potentiels :
Conclusion
En conclusion, LangChain Expression Language (LCEL) est un outil puissant qui apporte une nouvelle perspective à la construction d'applications Python. Malgré sa syntaxe peu conventionnelle, je recommande vivement l'utilisation de 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 que LangChain a mise en œuvre pour faire fonctionner la syntaxe LCEL.
Vous pouvez facilement réimplémenter les fonctionnalités de base de Runnable comme suit :
classe 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 dans lequel la méthode .__or__() a été écrasée.
En pratique, LangChain a ajouté de nombreuses fonctionnalités telles que la conversion de dictionnaires en Runnable, des capacités de typage, des capacités de configuration, et des méthodes invoke, batch, stream et async !
Alors, pourquoi ne pas essayer LCEL dans votre prochain projet ?
Si vous souhaitez en savoir plus, je vous recommande vivement de consulter le livre de recettes LangChain sur LCEL.