TL;DR

  • Un passage plus rapide du POC au prod: Comme le décrit la documentation de langchain, "LCEL est un moyen déclaratif de composer facilement des chaînes ensemble. LCEL a été conçu dès le premier jour pour supporter la mise en production de prototypes, sans changement de code".

  • Création de chaînes personnalisées: LCEL simplifie le processus de création de chaînes personnalisées grâce à une nouvelle syntaxe.

  • Streaming et batch prêts à l'emploi: LCEL vous offre gratuitement des fonctionnalités batch, streaming et async.

  • Interface unifiée: Elle offre une parallélisation automatique, des capacités de typage et toute autre fonctionnalité que LangChain pourrait développer à l'avenir.

  • LCEL est l'avenir de LangChain: LCEL offre une nouvelle perspective sur le développement d'applications basées sur le LLM. Je vous recommande vivement de l'utiliser pour votre prochain projet LLM.

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 .

class="img-responsive

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 :

  • Nous avons créé une nouvelle chaîne is_real_product_chain() qui détecte si l'entrée de l'utilisateur peut être considérée comme un produit.

  • Nous mettons en œuvre des conditions if/else pour passer d'une chaîne à l'autre.

De nombreux problèmes commencent à se poser :

  • Le code est un peu redondant, car il contient de nombreux éléments de base.

  • Il est difficile de distinguer quelle chaîne LLMC est liée à quelle chaîne LLMC, nous devons tracer les entrées et les sorties pour la comprendre.

  • Nous pouvons facilement faire des erreurs sur les types de sortie des chaînes, par exemple, la sortie de is_a_product_chain() est un str qui devrait être évalué plus tard comme bool.

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 :

  • Il y a un opérateur | bizarre entre un PromptTemplate, un llm et un dictionnaire ! L'opérateur | est simplement là pour dire : "prendre le dictionnaire à gauche et le passer en entrée de l'objet à droite".

  • Pourquoi passons-nous la variable product à l'intérieur d'un dictionnaire au lieu d'une chaîne de caractères comme auparavant ? Si vous avez lu le #1, vous savez que l'opérateur | s'attend à ce que les entrées soient des dictionnaires, donc nous donnons l'argument produit à l'intérieur d'un dictionnaire.

  • Pourquoi y a-t-il un nom de fonction RunnablePassthrough() au lieu du nom de l'entreprise ? La fonction RunnablePassthrough() est un espace réservé pour dire : "nous n'avons pas le nom de l'entreprise pour l'instant, mais lorsque nous l'aurons, placez-le ici". J'expliquerai ce que signifie le terme "Runnable" dans les prochaines parties, pour l'instant il n'y a pas de problème à l'ignorer.

  • Why do we need a specific method .invoke() instead of writing chain({‘product’:’colorful socks’}) ?We will understand this in the next part, but it is a sneak peek to why LCEL makes industrialization easier!

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 :

  • La syntaxe est différente, c'est une évidence.

  • Des chaînes intermédiaires sont définies et appelées dans une chaîne plus importante, un peu comme des fonctions.

  • Les entrées et les sorties sont typées, presque comme des fonctions.

  • Cela ne ressemble pas à Python.

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 :

  • .invoke(): Vous voulez passer votre entrée et obtenir la sortie, rien de plus, rien de moins.

  • .batch(): Vous voulez passer plusieurs entrées pour obtenir plusieurs sorties, la parallélisation est gérée pour vous (plus rapide que d'appeler invoke 3 fois).

  • .stream(): Cela vous permet de commencer à imprimer le début de l'achèvement avant que l'achèvement complet ne soit terminé.

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 :

  • Pas entièrement conforme à la PEP: LCEL ne respecte pas totalement le PEP20, le Zen de Python, qui stipule que "l'explicite est meilleur que l'implicite". (De plus, la syntaxe de LCEL n'est pas considérée comme "pythonique" car elle ressemble à un langage différent, ce qui pourrait rendre LCEL moins intuitif pour certains développeurs de Pyhton qui pourraient refuser de l'utiliser.

  • LCEL est un langage spécifique à un domaine (DSL): Les utilisateurs sont censés avoir une certaine compréhension des invites, des chaînes ou des LLM afin d'exploiter la syntaxe de manière efficace.

  • Dépendances entre les entrées et les sorties: Les entrées intermédiaires et les sorties finales doivent être transmises du début à la fin. Par exemple, si vous souhaitez utiliser la sortie d'une étape intermédiaire comme sortie finale, vous devez la transmettre à toutes les étapes suivantes. Cela peut conduire à des arguments supplémentaires dans la plupart de vos chaînes, qui peuvent ne pas être utilisés mais qui sont nécessaires si vous voulez les accéder par le biais de la sortie.

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 :

  • Interface unifiée: Fournit une interface cohérente pour toutes les chaînes, facilitant l'industrialisation de votre code avec des modèles de flux, async, fallback, typage, configurations de temps d'exécution, etc...

  • Parallélisation automatique: Exécutez automatiquement plusieurs tâches en parallèle, en améliorant la vitesse d'exécution de vos chaînes et l'expérience de l'utilisateur.

  • Composabilité: Il vous permet de composer et de modifier facilement des chaînes, ce qui rend votre code plus souple et plus adaptable.

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.

class="img-responsive

Medium Blog par Artefact.

Cet article a été initialement publié sur Medium.com.
Suivez-nous sur notre Medium Blog !