In Part 1, we installed LangChain and sent our first message to a chat model. That's fine for a one-off question, but real applications need repeatable, reusable prompts - and the ability to connect several steps into a pipeline. That's what we're covering today: prompt templates and LCEL (LangChain Expression Language).
Recap: The Series So Far
- Part 1 - What LangChain is & your first script
- Part 2 (this article) - Prompts, models, and LCEL chains
- Part 3 - Tools and building your first agent
- Part 4 - RAG: teaching your agent to read documents
- Part 5 - Memory and middleware
- Part 6 - Debugging and observing agents with LangSmith
Why You Need Prompt Templates
Hardcoding a prompt as a plain string works until you need to reuse it with different inputs. Compare:
# Without a template - repetitive and error-prone
prompt1 = "Explain machine learning in 3 bullet points."
prompt2 = "Explain quantum computing in 3 bullet points."
# With a template - define the structure once, reuse it forever
from langchain_core.prompts import PromptTemplate
prompt = PromptTemplate.from_template(
"Explain {topic} in 3 bullet points."
)
A PromptTemplate is just a string with {placeholders} that get filled in at run time. It looks trivial here, but it becomes essential once your prompts grow to include instructions, examples, and formatting rules that you don't want to retype every time.
Chat Prompts with Roles
Most modern LLMs work with role-based messages (system, human, ai), not a single flat string. LangChain's ChatPromptTemplate mirrors that:
from langchain_core.prompts import ChatPromptTemplate
chat_prompt = ChatPromptTemplate.from_messages([
("system", "You are a friendly Python tutor who explains things simply."),
("human", "Explain {topic} in 3 bullet points."),
])
The system message sets the model's persona and ground rules once, and every user question inherits that behavior - this is how you give an app a consistent "voice."
Introducing LCEL: The Pipe Operator
Here's where LangChain starts to feel genuinely different from calling an API directly. LCEL lets you connect components with the | operator, the same way you'd pipe commands together in a Unix shell.
from dotenv import load_dotenv
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain.chat_models import init_chat_model
load_dotenv()
prompt = ChatPromptTemplate.from_messages([
("system", "You are a friendly Python tutor who explains things simply."),
("human", "Explain {topic} in 3 bullet points."),
])
model = init_chat_model("gpt-5", model_provider="openai")
parser = StrOutputParser()
chain = prompt | model | parser
result = chain.invoke({"topic": "list comprehensions"})
print(result)
Read the chain = prompt | model | parser line like a sentence: "Take the input, run it through the prompt template, send that to the model, then parse the model's reply into a plain string."
Each piece - prompt, model, parser - is a Runnable, LangChain's standard interface for "something that takes an input and produces an output." Because everything implements the same interface, you can swap any piece out (a different model, a different parser, an extra step) without rewriting the rest of the chain.
What Does the Output Parser Do?
Without StrOutputParser, chain.invoke(...) would return a full Message object (the same one you saw in Part 1) instead of plain text. The parser is a small, focused Runnable that unwraps .content for you, so result above is already a ready-to-use string.
Multi-Step Chains
The real payoff of LCEL is chaining more than two steps. Let's build a two-stage pipeline: first generate a story title, then use that title to write a one-line pitch.
title_prompt = ChatPromptTemplate.from_template(
"Write a short, catchy title for a story about {topic}."
)
pitch_prompt = ChatPromptTemplate.from_template(
"Write a one-sentence pitch for a story titled '{title}'."
)
title_chain = title_prompt | model | parser
pitch_chain = pitch_prompt | model | parser
topic = "a robot learning to paint"
title = title_chain.invoke({"topic": topic})
pitch = pitch_chain.invoke({"title": title})
print("Title:", title)
print("Pitch:", pitch)
Notice the output of title_chain feeds directly into the input of pitch_chain. As your pipelines grow, you can compose these with RunnablePassthrough and dictionary-shaped steps to pass multiple values through several stages at once - we'll use that pattern when we build a RAG pipeline in Part 4.
Streaming Output
One more Runnable superpower: every chain built with LCEL automatically supports streaming, batching, and async execution, without extra code.
for chunk in chain.stream({"topic": "recursion"}):
print(chunk, end="", flush=True)
This is the same chain object from earlier - just call .stream() instead of .invoke() and you get tokens as they're generated, which is exactly how ChatGPT-style typing effects are built.
What's Next
You now know how to structure prompts and connect steps into a pipeline - the two ingredients every LangChain app is built from. In Part 3, we'll give a model tools it can call on its own, and use create_agent to build a real decision-making agent, not just a fixed pipeline.
This is Part 2 of a 6-part LangChain beginner series. Catch up on Part 1 if you missed it, and follow along for the rest of the series.

Top comments (0)