LCEL: Why LangChain Is a Composition Engine
Understanding chaining in AI workflows and how LangChain Expression Language (LCEL) simplifies it.
When people first start using language models, they usually write code like this:
send a prompt → get a response → done
That works for demos. It does not work for real applications.
Real AI systems are workflows, not single calls.
They often need to:
- call a model more than once
- prepare data before the model runs
- do something useful with the model's output afterward
The idea that connects all of this is called chaining.
What "Chaining" Really Means
Chaining simply means:
taking multiple steps and connecting them so the output of one step becomes the input of the next.
With language models, this idea shows up in a few common ways.
1. Making Multiple LLM Calls
Language models are powerful, but they are not deterministic. They can make mistakes, hallucinate, or miss details.
Because of this, many applications use more than one model call.
Common patterns include:
- breaking a big task into smaller steps
- asking one model to generate an answer
- asking another call to review, fix, or improve it
For example:
- plan → execute → verify
- draft → critique → rewrite
- reason → answer → double-check
Chaining makes these multi-step processes clear and manageable.
2. Preparing the Input to the Model
Very often, the model cannot work with raw user input alone.
Before calling the LLM, applications may need to:
- format a prompt template
- fetch documents or database records
- perform retrieval based on the user's query
- combine multiple sources of information
This means the model call is only one step in a larger chain.
The data must be transformed and enriched before the LLM ever sees it.
3. Using the Model's Output
Chaining does not stop when the model responds.
In many systems, the LLM's output is used to:
- generate Python code and then run it
- generate SQL and execute it on a database
- produce structured data for another service
- drive business logic or automation
In these cases, the LLM is part of a pipeline, not the end of it.
Why Chaining Feels So Natural with Language Models
All of this could be done with traditional code. So why do people keep reaching for chaining abstractions?
Because text has become a universal interface.
Language models take text as input and produce text as output. Prompts, responses, SQL, code, JSON—everything is text.
When everything is text flowing between steps, chaining becomes the most natural way to think.
This is why visual tools like Flowwise and LangFlow became popular, built on top of LangChain.
The Early LangChain Approach
LangChain started with pre-built chains like LLMChain and ConversationalRetrievalChain.
They helped get started but were hard to combine, customize, and lacked consistent async/batching/streaming support.
Developers wanted composable, consistent chains with built-in features. The old approach couldn't deliver cleanly.
So LangChain introduced LCEL.
LangChain Expression Language (LCEL)
LangChain Expression Language (LCEL) is a declarative way to build chains using the pipe (|) operator.
Instead of large classes, compose small building blocks. Describe data flow, not execution control.
Pipes Instead of Glue Code
LCEL uses the pipe to connect components:
prompt | modelThis passes the prompt's output directly to the model.
Example:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
model = ChatOpenAI()
prompt = ChatPromptTemplate.from_template("tell me a joke about {foo}")
chain = prompt | model
# Run it
result = chain.invoke({"foo": "bears"})
print(result.content)Try It Yourself
Install LangChain and try this simple chain:
pip install langchain langchain-openaiThen run:
from langchain.chat_models import ChatOpenAI
from langchain.prompts import ChatPromptTemplate
# Set your OpenAI API key
import os
os.environ["OPENAI_API_KEY"] = "your-api-key-here"
model = ChatOpenAI()
prompt = ChatPromptTemplate.from_template("Summarize this text: {text}")
chain = prompt | model
# Test it
result = chain.invoke({"text": "LangChain makes building AI apps easy."})
print(result.content)This creates a summarizer chain. Change the prompt or add more steps!
One Interface, Everywhere
Every LCEL chain supports:
Single execution
result = chain.invoke({"text": "Hello world"})Batch execution
results = chain.batch([{"text": "First"}, {"text": "Second"}])Streaming
for chunk in chain.stream({"text": "Long text"}):
print(chunk.content, end="")Async
result = await chain.ainvoke({"text": "Async call"})All built-in—no extra code needed.
Easy to Customize and Extend
LCEL chains are composable. Add steps easily:
from langchain.output_parsers import StrOutputParser
# Add output parsing
chain = prompt | model | StrOutputParser()Swap models, add retrieval, or combine chains.
A Real-World Example: Explanation vs Roast
Here is a real chain that explains a concept, then roasts its own explanation, and finally compliments it.
First, choose your LLM provider:
from langchain_ollama import ChatOllama
model = ChatOllama(
model="gemma3:4b",
temperature=1.2, # higher = more randomness
top_p=0.95, # allow wider token choices
top_k=50, # explore more candidate tokens
)Now build the pipeline:
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
parser = StrOutputParser()
explain = ChatPromptTemplate.from_template(
"Explain {topic} in ONE clear sentence."
)
#llms are too polite btw
roast = ChatPromptTemplate.from_template(
"You are a sharp stand-up comedian.\n"
"Write EXACTLY ONE savage, funny one-liner roasting the explanation.\n"
"No politeness. No explanations. No emojis. No markdown.\n"
"Be blunt, witty, and a little mean.\n\n"
"Explanation:\n{explanation}"
)
compliment = ChatPromptTemplate.from_template(
"Compliment the explanation in ONE warm sentence.\n"
"{explanation}"
)
chain = (
explain
| model
| parser
| {
"🧠 Explanation": lambda x: x,
"🔥 Roast": roast | model | parser,
"💖 Compliment": compliment | model | parser
}
)
result = chain.invoke({"topic": "context window in LLMs"})
# Pretty-print output
for section, content in result.items():
print(f"\n{section}\n{'-' * 40}\n{content}")
🧠 Explanation ---------------------------------------- The context window in an LLM refers to the amount of preceding text the model can consider when generating its next output, acting like a short-term memory for understanding and responding to prompts. 🔥 Roast ---------------------------------------- That explanation is just a fancy way of saying it forgets everything five seconds ago. 💖 Compliment ---------------------------------------- That's a wonderfully clear and approachable explanation of the context window – you’ve really captured the essence of how LLMs process information!
This pipeline demonstrates LCEL's power:
- Takes a
topicinput - Uses the
explainprompt to generate an explanation - Feeds that explanation into both a
roastandcomplimentprompt in parallel - Returns all three results in a structured dictionary
The pipeline runs automatically in parallel where possible, streaming results as they arrive.
The Mental Model to Remember
Think of LCEL as an expression language for AI workflows.
You write:
input | transform | model | transform | outputThe system handles execution, async, batching, streaming, tracing.
Once you think in flows, chaining becomes simple.
LCEL makes chaining clear, composable, and scalable.