Introduction à LCEL

LCEL (LangChain Expression Language) est un puissant outil d'orchestration de flux de travail qui vous permet de construire des chaînes de tâches complexes à partir de composants de base et prend en charge des fonctionnalités prêtes à l'emploi telles que le traitement en continu, le traitement parallèle et la journalisation.

Exemple de base : Invite + Modèle + Analyseur de sortie

Dans cet exemple, nous allons démontrer comment utiliser LCEL (LangChain Expression Language) pour relier trois composants - un modèle d'invite, un modèle et un analyseur de sortie - afin de former un flux de travail complet pour implémenter la tâche de "raconter des blagues". Le code montre comment créer des chaînes, utiliser le symbole de pipeline | pour connecter différents composants, et présente le rôle de chaque composant ainsi que les résultats de sortie.

Tout d'abord, voyons comment relier le modèle d'invite et le modèle pour générer une blague sur un sujet spécifique :

Installer les dépendances

%pip install --upgrade --quiet langchain-core langchain-community langchain-openai
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

invite = ChatPromptTemplate.from_template("Dis-moi une blague à propos de {topic}")
modèle = ChatOpenAI(model="gpt-4")
analyseur_sortie = StrOutputParser()

chaîne = invite | modèle | analyseur_sortie

chaîne.invoke({"topic": "glaces"})

Sortie

"Pourquoi les fêtes n'invitent-elles pas les glaces ? Parce qu'elles fondent quand il fait chaud !"

Dans ce code, nous utilisons LCEL pour relier différents composants en une chaîne :

chaîne = invite | modèle | analyseur_sortie

Le symbole | ici est similaire à l'opérateur de tube Unix pipe operator, qui connecte différents composants et transmet la sortie d'un composant en entrée au composant suivant.

Dans cette chaîne, l'entrée de l'utilisateur est transmise au modèle d'invite, puis la sortie du modèle d'invite est transmise au modèle, et enfin la sortie du modèle est transmise à l'analyseur de sortie. Jetons un coup d'oeil à chaque composant séparément pour mieux comprendre ce qui se passe.

1. Invite

L'invite est un BasePromptTemplate qui accepte un dictionnaire de variables de modèle et génère une PromptValue. PromptValue est un objet encapsulé contenant l'invite, qui peut être transmis à LLM (en entrée sous forme de chaîne) ou à ChatModel (en entrée sous forme de séquences de messages). Il peut être utilisé avec n'importe quel type de modèle de langue car il définit la logique pour générer BaseMessage et générer des chaînes.

invite_value = invite.invoke({"topic": "glaces"})
invite_value

Sortie

ChatPromptValue(messages=[HumanMessage(content="Dis-moi une blague à propos de glaces")])

En dessous, nous convertissons le résultat formaté de l'invite en format message utilisé par les modèles de conversation :

invite_value.to_messages()

Sortie

[HumanMessage(content="Dis-moi une blague à propos de glaces")]

Il peut également être directement converti en chaîne :

invite_value.to_string()

Sortie

'Humain : Dis-moi une blague à propos de glaces.'

2. Modèle

Ensuite, transmettre la PromptValue au modèle. Dans cet exemple, notre modèle est un ChatModel, ce qui signifie qu'il produira un BaseMessage.

Essayons d'appeler le modèle directement :

message = modèle.invoke(invite_value)
message

Retourne :

AIMessage(content="Pourquoi les fêtes n'invitent-elles pas les glaces ? Parce qu'elles fondent quand il fait chaud !")

Si notre modèle est défini comme un type LLM, il produira une chaîne.

from langchain_openai.llms import OpenAI

llm = OpenAI(model="gpt-3.5-turbo-instruct")
llm.invoke(invite_value)

Le modèle retourne :

'\n\nBot: Pourquoi le camion de glaces est-il tombé en panne ? Parce qu'il a fondu de l'intérieur !'

3. Analyseur de sortie

Enfin, transmettre la sortie de notre modèle à l'analyseur_sortie, qui est un BaseOutputParser, ce qui signifie qu'il accepte une chaîne ou un BaseMessage en entrée. StrOutputParser convertit spécifiquement n'importe quelle entrée en une simple chaîne.

analyseur_sortie.invoke(message)
Pourquoi les fêtes n'invitent-elles pas les glaces ? \n\nParce qu'elles fondent quand il fait chaud !

4. L'ensemble du processus

Le processus d'exécution est le suivant :

  1. Appeler chain.invoke({"topic": "glace"}), ce qui revient à initialiser le workflow que nous avons défini et à passer le paramètre {"topic": "glace"} pour générer une blague sur "la glace."
  2. Passer le paramètre d'appel {"topic": "glace"} au premier composant de la chaîne, prompt, qui formate le modèle de prompt pour obtenir le prompt Dis-moi une petite blague sur la glace.
  3. Passer le prompt Dis-moi une petite blague sur la glace au model (modèle gpt4).
  4. Passer le résultat renvoyé par le model au parseur de sortie output_parser, qui formate le résultat du modèle et renvoie le contenu final.

Si vous êtes intéressé par la sortie de n'importe quel composant, vous pouvez tester une version plus petite de la chaîne à tout moment, comme prompt ou prompt | model, pour voir les résultats intermédiaires :

input = {"topic": "glace"}

prompt.invoke(input)

(prompt | model).invoke(input)

Exemple de recherche RAG

Ensuite, expliquons un exemple LCEL légèrement plus complexe. Nous allons exécuter un exemple de recherche améliorée pour générer des chaînes, afin d'ajouter des informations de contexte lors de la réponse aux questions.

from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai.chat_models import ChatOpenAI
from langchain_openai.embeddings import OpenAIEmbeddings

vectorstore = DocArrayInMemorySearch.from_texts(
    ["harrison a travaillé chez kensho", "les ours aiment manger du miel"],
    embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()

template = """Répondez à la question uniquement sur la base du contexte suivant :
{context}

Question : {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI()
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

chain.invoke("où a travaillé harrison ?")

Dans ce cas, la chaîne composée est :

chain = setup_and_retrieval | prompt | model | output_parser

En résumé, le modèle de prompt ci-dessus accepte context et question comme valeurs à remplacer dans le prompt. Avant de construire le modèle de prompt, nous voulons récupérer les documents pertinents à utiliser comme partie du contexte.

En guise de test, nous utilisons DocArrayInMemorySearch pour simuler une base de données vectorielle en mémoire, en définissant un récupérateur qui peut récupérer des documents similaires en fonction des requêtes. Il s'agit également d'un composant exécutable chainable, mais vous pouvez également essayer de l'exécuter séparément :

retriever.invoke("où a travaillé harrison ?")

Ensuite, nous utilisons RunnableParallel pour préparer l'entrée pour le prompt, rechercher des documents à l'aide du récupérateur, et passer la question de l'utilisateur en utilisant RunnablePassthrough :

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)

En résumé, la chaîne complète est :

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

Le processus est le suivant :

  1. Tout d'abord, créer un objet RunnableParallel contenant deux entrées. La première entrée context inclura les résultats de document extraits par le récupérateur. La deuxième entrée question contiendra la question originale de l'utilisateur. Pour passer la question, nous utilisons RunnablePassthrough pour copier cette entrée.
  2. Passer le dictionnaire de l'étape précédente au composant prompt. Il accepte l'entrée de l'utilisateur (c'est-à-dire question) ainsi que les documents récupérés (c'est-à-dire context), construit un prompt, et renvoie une PromptValue.
  3. Le composant model prend le prompt généré et le passe au modèle LLM d'OpenAI pour évaluation. La sortie générée par le modèle est un objet ChatMessage.
  4. Enfin, le composant output_parser prend un ChatMessage, le convertit en une chaîne Python, et le renvoie à partir de la méthode invoke.