Most LangChain users still wire logic with legacy Chain subclasses that obscure control flow and complicate streaming. To build first lcel chain langchain, you should adopt the LangChain Expression Language (LCEL), which models every component as a Runnable and composes them with a | operator. This guide walks through a working example you can run against any OpenAI-compatible endpoint, from empty environment to verified output.
Step 1: Install the minimal package set
LangChain fragmented its monolith into focused packages in 2024. You do not need langchain itself for a basic chain—langchain-core provides the LCEL primitives, and langchain-openai provides the model wrapper.
pip install langchain-core==0.3.* langchain-openai==0.2.* python-dotenv
Pin majors to avoid surprise breaking changes. If you are on an older environment with langchain<0.1, uninstall it first; the legacy LLMChain will shadow the new imports.
Step 2: Configure a model backend
LCEL chains are backend-agnostic, but the easiest path is ChatOpenAI from langchain-openai. Point it at your provider with environment variables. If you do not want to juggle multiple API keys, you can target n4n.ai’s OpenAI-compatible endpoint—it fronts 240+ models and applies automatic fallback when a provider is rate-limited.
import os
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
load_dotenv() # pulls OPENAI_API_KEY and OPENAI_BASE_URL from .env
# Default OpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
# Alternative: n4n.ai or any OpenAI-compatible gateway
# llm = ChatOpenAI(
# model="anthropic/claude-3.5-sonnet",
# base_url="https://api.n4n.ai/v1",
# api_key=os.environ["OPENAI_API_KEY"],
# )
The base_url override is the only change required to swap providers. LCEL does not care where tokens come from.
Step 3: Define a prompt template
Prompts are Runnables too. Use ChatPromptTemplate to enforce a message structure instead of hand-concatenating strings.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages(
[
("system", "You are a senior engineer. Answer concisely."),
("user", "Explain {concept} in one sentence."),
]
)
The {concept} placeholder is filled at invocation time. Never interpolate user input with f-strings—let the template handle escaping and message roles.
Step 4: Compose the chain with the pipe operator
This is the core of LCEL. You build first lcel chain langchain by piping a prompt into a model into an output parser.
from langchain_core.output_parsers import StrOutputParser
chain = prompt | llm | StrOutputParser()
Why the pipe works
Each object implements the Runnable protocol: invoke, stream, batch, ainvoke, etc. The | operator returns a RunnableSequence that forwards the output of one step to the next. No hidden globals, no chain.run() magic.
If you need parallel branches, use RunnableParallel:
from langchain_core.runnables import RunnableParallel
parallel = RunnableParallel(
concise=prompt | llm | StrOutputParser(),
verbose=prompt.with_messages([("system", "Explain in detail.")]) | llm | StrOutputParser(),
)
Step 5: Invoke the chain synchronously
Call .invoke() with a dict matching your template variables.
result = chain.invoke({"concept": "LCEL"})
print(result)
Expect a single string. If you passed a list of dicts, you would get a list of strings—but use batch for that (see Step 6).
Step 6: Stream and batch for real workloads
Synchronous invoke blocks. Production systems need streaming and concurrency.
# Stream tokens as they arrive
for chunk in chain.stream({"concept": "vector databases"}):
print(chunk, end="", flush=True)
# Process multiple inputs concurrently
inputs = [{"concept": "retrieval augmented generation"},
{"concept": "function calling"}]
outputs = chain.batch(inputs)
The same chain object supports .ainvoke, .astream, and .abatch for asyncio loops. You did not write any async code; LCEL provides it by default.
Step 7: Add fallback and observability
Models fail. Wrap your chain with with_fallbacks to degrade gracefully.
fallback_llm = ChatOpenAI(model="gpt-3.5-turbo")
robust_chain = chain.with_fallbacks([prompt | fallback_llm | StrOutputParser()])
try:
text = robust_chain.invoke({"concept": "semantic caching"})
except Exception as e:
print("Both primaries failed:", e)
For metering, pass metadata or tags to the model constructor; LangChain forwards them to callback handlers. If you route through a gateway that honors client routing directives, set model to a qualified name and the gateway selects the backend.
When you build first lcel chain langchain with streaming and fallback, you get a unit that behaves like a function but survives partial outages.
Step 8: Verify the chain end to end
Write a tiny test to confirm shape and content. This doubles as documentation.
def test_chain_returns_string():
out = chain.invoke({"concept": "LCEL"})
assert isinstance(out, str)
assert len(out) > 0
assert "LangChain" in out or "chain" in out.lower()
Run with pytest. Success means the test passes and the streamed output prints without raising. If you see ValidationError, check that your dict keys match the template variables exactly.
Common failure modes
- Missing
base_urlwhen using a non-OpenAI provider: tokens never arrive, timeout at TLS. - Legacy
LLMChainimport lingering in the environment: you get.run()instead of|. - Forgetting
StrOutputParser:invokereturns anAIMessageobject, not a string, and downstream string ops break.
Step 9: Extend with custom runnables
The mental model you adopt when you build first lcel chain langchain pays off when you need custom logic. Any Python callable can become a RunnableLambda.
from langchain_core.runnables import RunnableLambda
def trim(text: str) -> str:
return text.strip()
trimmer = RunnableLambda(trim)
extended_chain = chain | trimmer
print(extended_chain.invoke({"concept": "embeddings"}))
You can also use RunnableBranch for conditional routing, or RunnableWithMessageHistory to add memory. None of these require leaving LCEL.
Verify success in production
A chain is not done until it logs. Attach a callback to emit per-token usage:
from langchain_core.callbacks import StreamingStdOutCallbackHandler
llm_with_cb = ChatOpenAI(
model="gpt-4o-mini",
streaming=True,
callbacks=[StreamingStdOutCallbackHandler()],
)
stream_chain = prompt | llm_with_cb | StrOutputParser()
stream_chain.invoke({"concept": "rate limits"})
If you meter through a gateway, the per-token usage appears in your provider dashboard without extra code. The chain above prints tokens to stdout and returns the full string—confirm both happen.
To build first lcel chain langchain that you can trust, start with the nine steps above, keep the Runnable contract in mind, and resist the urge to drop back to legacy chains. The pipe syntax is not sugar; it is the composition boundary that makes streaming, batching, and fallback first-class.