LCEL is the standard way to build deterministic LLM pipelines with streaming and batching built in.
The LangChain Expression Language (LCEL) composes runnables with |. Each step's output becomes the next step's input:
chain = prompt | model | StrOutputParser()
chain.invoke({"topic": "tides"})This creates a RunnableSequence, itself a runnable, so it gets the full interface for free: invoke, batch, stream, their async versions, and tracing in LangSmith.
Output parsers turn the AIMessage into something convenient:
StrOutputParser()returns the text.JsonOutputParser()parses JSON, including partial JSON while streaming.- For typed objects prefer
model.with_structured_output(...)(a later lesson), which uses the provider's native structured output or tool calling.
Plain Python functions in a chain are wrapped automatically: chain = prompt | model | StrOutputParser() | (lambda s: s.upper()).
Streaming flows through a chain: if every step supports it, tokens stream from the model through the parser to your loop.
LCEL is ideal for fixed pipelines (prompt, call, parse, post-process). When the flow needs loops, decisions or tool use, move to agents or LangGraph, which accept LCEL chains as nodes.
Example
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_template("Write a haiku about {topic}.")
# model = init_chat_model("openai:gpt-5-mini") # real model
model = GenericFakeChatModel(messages=iter([
"Moon pulls the ocean", "Waves fold into sleeping sand"]))
chain = prompt | model | StrOutputParser() | (lambda s: s.upper())
print(type(chain).__name__) # RunnableSequence
print(chain.invoke({"topic": "tides"})) # MOON PULLS THE OCEAN
parts = list((prompt | model | StrOutputParser()).stream({"topic": "sand"}))
print(len(parts) > 1, "".join(parts)) # True Waves fold into sleeping sandRemember
prompt | model | parserbuilds a RunnableSequence- Chains get invoke, batch, stream and async methods for free
- Plain functions in a chain are wrapped as runnables automatically
Watch out for
- Calling .invoke on each step manually instead of composing, losing streaming and tracing for the whole pipeline.