DS DevShelfHub Projects · AI tools
Tutorials / LangChain / Chains & LCEL
LangChain Intermediate · 12 min read Page 4 of 20

LangChain Chains and LCEL: Compose LLM Pipelines in Python

By DevShelfHub

The old LLMChain and SequentialChain patterns, the modern LCEL pipe syntax, parallel steps, streaming, and when to reach for each.

Series progress4 / 20
LangChain chains and LCEL pipe syntax composing an LLM pipeline in Python

The old way — LLMChain

Before LCEL, you built chains by passing components as constructor arguments. You will still see this pattern in older tutorials and documentation.

python
from langchain.chains import LLMChain
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = ChatOpenAI(model="gpt-4o-mini")
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a helpful assistant."),
    ("human", "{question}"),
])

chain = LLMChain(llm=llm, prompt=prompt)
result = chain.invoke({"question": "What is LangChain?"})
print(result["text"])

The problem: every chain type had a slightly different API, different input/output keys, and different ways to compose. It became messy quickly.

SequentialChain

To run two chains in sequence — where the output of the first becomes the input of the second — you used SequentialChain.

python
from langchain.chains import SimpleSequentialChain

chain1 = LLMChain(llm=llm, prompt=summarise_prompt)
chain2 = LLMChain(llm=llm, prompt=translate_prompt)

pipeline = SimpleSequentialChain(chains=[chain1, chain2])
result = pipeline.invoke("A long English article...")

Works, but fragile. If you need to pass extra values or skip a step based on a condition, you quickly hit the limits of this pattern.

The modern way — LCEL

LCEL (LangChain Expression Language) replaces all the old chain classes with a single, unified composition model. The | operator connects any two Runnables — and every LangChain component implements the Runnable interface.

python
from langchain_core.output_parsers import StrOutputParser

chain = prompt | llm | StrOutputParser()
result = chain.invoke({"question": "What is LangChain?"})

The output of prompt (a list of messages) is passed directly into llm. The output of llm (an AIMessage) is passed into StrOutputParser, which extracts the text.

Two-step pipeline with LCEL

python
summarise_chain = summarise_prompt | llm | StrOutputParser()
translate_chain = translate_prompt | llm | StrOutputParser()

pipeline = summarise_chain | translate_chain
result = pipeline.invoke({"text": "A long English article..."})

Chains compose into longer chains — the output of one is the input of the next, no glue code needed.

RunnablePassthrough & RunnableParallel

Sometimes you need to forward input values alongside LLM outputs, or run two chains at the same time. LCEL has built-in helpers for both.

RunnablePassthrough — forward the original input

python
from langchain_core.runnables import RunnablePassthrough

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | llm
    | StrOutputParser()
)

RunnablePassthrough() passes the original input value through unchanged. Here the user's question is forwarded into the prompt alongside the retrieved context.

RunnableParallel — run steps in parallel

python
from langchain_core.runnables import RunnableParallel

parallel = RunnableParallel({
    "joke":    joke_chain,
    "poem":    poem_chain,
})

result = parallel.invoke({"topic": "cats"})
print(result["joke"])
print(result["poem"])

Both chains run concurrently. The result is a dict with both outputs. Total latency is max(joke_time, poem_time), not the sum.

Streaming

Every LCEL chain supports streaming out of the box. Replace .invoke() with .stream() and iterate over the chunks as they arrive.

python
chain = prompt | llm | StrOutputParser()

for chunk in chain.stream({"question": "Explain transformers"}):
    print(chunk, end="", flush=True)

Tokens print to the terminal as they arrive — no waiting for the full response. This is how you build streaming chat UIs with LangChain. For async use, call .astream() instead.

Batch processing

To run a chain over many inputs at once, use .batch(). LangChain handles concurrency automatically.

python
questions = [
    {"question": "What is Python?"},
    {"question": "What is Rust?"},
    {"question": "What is Go?"},
]

results = chain.batch(questions)  # runs concurrently

By default LangChain runs up to 5 concurrent calls. Pass max_concurrency=N to .batch() to override.

The full Runnable interface

Method What it does Async variant
.invoke() Single input → single output .ainvoke()
.stream() Single input → iterator of chunks .astream()
.batch() List of inputs → list of outputs (concurrent) .abatch()

Because every component shares this interface, you can swap any component in a chain without changing any other code.

When to use chains vs agents

Use a chain when…

  • The steps are fixed and known at build time
  • You need predictable, deterministic behavior
  • Latency and cost are critical
  • You're building a production pipeline

Use an agent when…

  • The steps depend on what the LLM discovers
  • You need the model to decide which tool to call
  • The task is open-ended or exploratory
  • You're prototyping or building an assistant

Quick summary

  • LLMChain and SequentialChain are legacy — still work but avoid in new code
  • LCEL: prompt | llm | parser — composable, uniform, no glue code
  • RunnablePassthrough forwards original inputs; RunnableParallel runs branches concurrently
  • Every chain supports .invoke(), .stream(), .batch() and their async variants
  • Use chains for fixed steps; use agents when the LLM must decide the steps at runtime

LangChain Chains and LCEL FAQ

What is the difference between LLMChain and LCEL?

LLMChain is the legacy approach where you pass components as constructor arguments — each chain type had a different API and input/output keys. LCEL (LangChain Expression Language) replaces all of them with one composition model using the pipe operator, such as prompt | llm | parser. LCEL is the recommended way to build chains today.

What is RunnablePassthrough used for in LangChain?

RunnablePassthrough forwards the original input value through a chain unchanged. It is most common in RAG, where you pass the user's question straight into the prompt alongside the retrieved context, for example {"context": retriever, "question": RunnablePassthrough()}.

What is RunnableParallel in LangChain?

RunnableParallel runs multiple chains concurrently and returns their outputs as a dict. Because the branches run at the same time, total latency is the slowest branch rather than the sum of all branches — useful when you need several independent generations from the same input.

How do I stream output from a LangChain chain?

Every LCEL chain supports streaming out of the box. Replace .invoke() with .stream() and iterate over the chunks as they arrive, printing each token immediately. For async applications, use .astream() instead. This is how you build streaming chat UIs with LangChain.

Should I use a chain or an agent in LangChain?

Use a chain when the steps are fixed and known at build time and you need predictable, low-latency behavior — ideal for production pipelines. Use an agent when the steps depend on what the LLM discovers and the model must decide which tool to call, which suits open-ended or exploratory tasks.

Quick jump: API Reference