Site icon Use AI the right way

Langchain LCEL explained the easy way

LCEL representation

Learn how to use Langchain Expression Language (LCEL) to create robust and production-ready chains while keeping a simple code base.

Introduction

Increasingly, AI-based apps are becoming more and more complex with the integration of so many new tools. All these integrations mean more complexity. and more difficulty to have a fast, robust, production-ready application. To tackle these problems, Langchain created LCLE which stands for LangChain Langage Expression and and is the solution they developed for creating production-ready code.
Let’s explain this right away!

Langchain LCEL

From the beginning, Langchain created the concept of chains, equivalent to pipelines or sequence of tasks (see this link for for information). It was powerful and simple but as the chains became increasingly complicated, the code was became a nightmare to update and maintain.
And so, the creation of Langchain LCEL(link).

Here’s a one-liner definition of LCEL:
LangChain Expression Language (LCEL) is a declarative system designed for easily building and deploying multi-step computational chains, from simple prototypes to complex, production-level applications.
Here are the interesting parts of this definition (that we will see later one):

Ultimately, this means that you will have a very powerful and elegant solution, capable of handling most use cases, although it will initially be complex to set up. There is always a tradeoff in any solution, and in the case of Langchain, it involves a steep learning curve.

LCEL declarative system

The LangChain Expression Language (LCEL) is a declarative system which means it focuses on defining the desired results or goals without explicitly programming the steps to achieve them.
It simplifies the process of setting up complex computational tasks by allowing users to state “what” outcome is needed rather than detailing “how” to achieve it.

Let’s take the following example to explain all this:

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
model = OpenAI(model="gpt-3.5-turbo")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

chain.invoke({"topic": "ice cream"})

As you can see, we create a prompt, a model and a way to parse the output., which are completely standard.
Here’s where the magic happens: chain = prompt | model | output_parser
and here’s what this means:

At no point, you code how the input of one component go to the next, you just wrote what you wanted which is the basic of a declarative system.

Multi-step computational chains

When you begin a complex LLM use case, you will soon arrive at a point where you will have multiple chains that do specific things (calling an API, getting the data from a retriever, handling the chat history). Each of these chains will require different inputs and sometimes the output from on or more other chains. So you will have a complex workflow with chains with different dependencies.
And this is what LCEL allows you to do, to link all these chains together into one single chain.

Let’s take the classical exemple of a RAG chain. The principe behind a RAG chain is to embed the question to get the most relevant chunks of text from your vector store and put it inside the prompt with the question.
This means your prompt will need not only the question but also the retrieved data. In this case, in the logic of the code, you will have a fork before the prompt where you will, in one fork, get the retriever’s data and the other one, pass the question.

Here’s a representation of the workflow:

Let’s implement this and see how it works:

from langchain_community.vectorstores import FAISS
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import OpenAIEmbeddings

vectorstore = FAISS.from_texts(
    ["Metadocs loves coding with Langchain", "bears like to eat honey"],
    embedding=OpenAIEmbeddings(),
)
retriever = vectorstore.as_retriever()

template = """Answer the question based only on the following context:
{context}

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

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

chain.invoke("What does Metadocs loves to do?")

As you can see, we have 2 simultaneous forks in the process where the rest of the process needs the output for each of the fork to continue. And so, the 2 forks needs to be launched in parallel.
All the magic is done here:

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

Here’s what is happening:

And so, when you are at the PromptTemplate object, the input you will receive is this: {"context": RETRIEVED_DATA, "question": QUESTION}.

Runnable interface

In the previous part, I talked about multi-step computational chains but in the fork of the example I used, I only used a RunnablePassthrough and a retriever and not chains.
This is not an error at all. In fact, this is a core principal of the inner working of LCEL.

All the components of Langchain implement the Runnable interface.
An interface, in coding principals, is a defined contract or blueprint that specifies the methods a class should implement, guiding how objects interact in a system without dictating the specific implementation details.
In this case, the Runnable interface define a set of methods that all higher level components need to implement to be used. We will not go too deep inside but here’s some useful methods:

If we take the previous example, here’s some interesting things:

RunnablePassthrough().invoke("How are you ?") # will print "Who are you ?"

retriever.invoke("How are you ?") # will print the retrieved data for "How are you ?"

setup_and_retrieval = RunnableParallel(
    {"context": retriever, "question": RunnablePassthrough()}
)
setup_and_retrieval.invoke("How are you ?") # Will return {"context": RETRIEVED_DATA, "question": How are you ?}

chain = setup_and_retrieval | prompt | model | output_parser
chain.invoke("How are you ?") # This will launch the chain for "How are you ?" 

This also means that all higher level components, from the simple RunnablePassthrough to the biggest chain are chains. So whenever you are using LCEL, or even Langchain in general, you are using and composing chains for the smallest things. This is the beauty of LCEL.

Langchain LCEL production ready features

Now that we have see the beauty of LCEL, we need to list all the features that make Langchain LCEL production ready:

The really interesting point is that you have all these features without changing your whole code completely.

Langchain LCEL defaults

We talked a lot about all the good points of LCEL but we need to talk about all the defaults too:

Conclusion

Langchain LCEL is an incredible upgrade to Langchain which makes it so much more production-ready. It allows developer to easily create a streaming and async compatible with the same code and with typing and validation features. The downside is that is more complicated to learn but the end results is that you will have something that you can in a real world application.

Afterward

I hope this tutorial helped you and taught you many things. I will update this post with more nuggets from time to time. Don’t forget to check my other post as I write a lot of cool posts on practical stuff in AI.

Cheers !

Exit mobile version