LangChain Expression Language (LCEL) isn’t just a new way to write chains; it’s a fundamental shift in how LangChain components interact, making them composable building blocks rather than rigid sequences.
Let’s see LCEL in action. Imagine you want to build a simple Q&A system that first retrieves relevant documents, then summarizes them, and finally answers a question based on that summary.
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableSequence
from langchain_community.llms import OpenAI
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OpenAIEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.runnables import RunnablePassthrough
# 1. Load and split documents
loader = WebBaseLoader("https://lilianweng.github.io/posts/2023-06-23-agent/")
docs = loader.load()
text_splitter = RecursiveCharacterTextSplitter()
splits = text_splitter.split_documents(docs)
# 2. Create embeddings and vector store
vectorstore = FAISS.from_documents(splits, OpenAIEmbeddings())
retriever = vectorstore.as_retriever()
# 3. Define prompt templates
template = """Use the following context to answer the question:
{context}
Question: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
# 4. Define the LLM
llm = OpenAI(temperature=0)
# 5. Build the LCEL chain
# This is where the magic happens: RunnableSequence chains components together
chain = RunnableSequence(
{"context": retriever, "question": RunnablePassthrough()},
prompt,
llm,
)
# 6. Invoke the chain
question = "What is LangChain Expression Language?"
response = chain.invoke(question)
print(response)
This code does more than just execute a series of steps. It defines a runnable pipeline. The retriever is a runnable, prompt is a runnable, llm is a runnable. RunnableSequence orchestrates them, and RunnablePassthrough allows us to inject the initial question into the context dictionary.
The core problem LCEL solves is the rigidity and lack of composability in older "Chains." Before LCEL, if you wanted to add a new step, modify an existing one, or branch your logic, you often had to rewrite significant portions of your chain. LCEL treats everything as a Runnable. This means you can use the pipe operator (|) to connect them, creating dynamic and flexible sequences.
Internally, each Runnable has an invoke method. When you invoke a RunnableSequence, it calls the invoke method of each component in order, passing the output of one as the input to the next. This is true even for complex runnables like the retriever. When retriever is called with the question, it internally uses its embedding model to find relevant documents.
The "context" in the prompt template is a key example of LCEL’s power. We’re not just passing a string; we’re passing the result of the retriever runnable. LCEL handles the mapping and formatting needed to inject this context into the prompt. The RunnablePassthrough() is crucial here; it ensures the original question is also available to be passed into the prompt, alongside the context retrieved.
The most surprising aspect of LCEL is how it abstracts away the underlying execution details for many common operations. You don’t explicitly manage fetching documents, embedding them, or formatting prompts when using LCEL. You declare the intent – "I want to retrieve documents, then use them in a prompt, then send that to an LLM" – and LCEL handles the plumbing. This makes complex RAG (Retrieval Augmented Generation) pipelines, for instance, remarkably concise.
The next step is understanding how to use RunnableBranch for conditional logic and RunnableParallel for concurrent execution within your LCEL graphs.