LangChain Prompt Templates and LCEL Chains
A LangChain chain is a pipeline of steps joined with the pipe operator: a prompt template fills in variables, the chat model generates, and an output parser turns the reply into a useful type. This style is called LCEL, the LangChain Expression Language. It gives you invoke, stream and batch for free on anything you pipe together.
Prompt templates
A template is a prompt with named holes. ChatPromptTemplate.from_messages builds one from role and text pairs.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a {tone} technical writer."),
("human", "Summarize this in two sentences:\n\n{text}"),
])
value = prompt.invoke({"tone": "friendly", "text": "Docker packages apps into containers."})
print(value.to_messages())
The result is a list of messages with the variables filled in. Templates catch missing variables early, which is safer than building strings by hand.
Join steps with the pipe operator
from dotenv import load_dotenv
from langchain.chat_models import init_chat_model
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
load_dotenv()
prompt = ChatPromptTemplate.from_messages([
("system", "You are a {tone} technical writer."),
("human", "Summarize this in two sentences:\n\n{text}"),
])
model = init_chat_model("anthropic:claude-sonnet-5-5", temperature=0)
chain = prompt | model | StrOutputParser()
print(chain.invoke({
"tone": "friendly",
"text": "Kubernetes schedules containers across a cluster of machines.",
}))
Each piece is a Runnable. The output of one becomes the input of the next. StrOutputParser extracts the text from the AIMessage, so the chain returns a plain string. Expect two friendly sentences about Kubernetes.
Batch and stream
Because the chain is a Runnable, you get other call styles without extra code:
texts = [
{"tone": "formal", "text": "Redis is an in-memory data store."},
{"tone": "casual", "text": "Git tracks changes to files over time."},
]
for answer in chain.batch(texts):
print(answer)
for piece in chain.stream(texts[0]):
print(piece, end="")
batch runs the inputs concurrently, and stream yields the output piece by piece. Async versions are abatch and astream.
Add your own step
Any function can join a chain when wrapped in RunnableLambda:
from langchain_core.runnables import RunnableLambda
shout = RunnableLambda(lambda s: s.upper())
loud_chain = chain | shout
print(loud_chain.invoke(texts[0]))
Chains are not agents
A chain follows a fixed path that you wrote: prompt, model, parser. The model never chooses what happens next. An agent is different because the model decides whether to call tools and when to stop. Use a chain when the steps are known in advance, such as summarizing, classifying or extracting. Use an agent when the number of steps depends on the input. Many good applications mix both: a fixed chain for a stage, and an agent for the open-ended stage.
Common mistakes
- Forgetting that curly braces in a template are variables. To print a literal brace, double it:
{{and}}. This bites when you paste JSON into a template. - Passing a plain string to a chain that expects a dictionary of variables.
- Building one huge prompt instead of chaining two focused ones.
- Using chains for tasks that need tool use; move to an agent for those.
Next, you will give the model tools, which is the step that turns a chain into an agent.
Few-shot examples and message placeholders
Templates can hold more than one system and human message. A common need is a slot for conversation history. MessagesPlaceholder reserves a place in the template where a list of messages is inserted at run time. Chains that need memory use it to receive earlier turns, while the rest of the prompt stays fixed. You can also include a few worked examples as alternating human and assistant messages, which often steers format better than a long description does.
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
chat_prompt = ChatPromptTemplate.from_messages([
("system", "You classify support tickets as billing, bug or other."),
("human", "I was charged twice"),
("ai", "billing"),
MessagesPlaceholder("history"),
("human", "{ticket}"),
])
print(chat_prompt.invoke({"history": [], "ticket": "The app crashes on start"}))
The empty history list simply adds nothing. Pass real messages and they appear between the example and the new ticket.
Why templates beat string formatting
Plain f-strings work for a prototype, but templates give you validation, reuse and tracing. A template knows its input variables, so a missing one fails immediately with a clear error rather than sending a half-filled prompt to the model. Tracing tools record the template and the values separately, which makes it far easier to see what changed between a good run and a bad one. Prompts are code that you will tune for weeks, so keep them in named variables or modules, not scattered across call sites.
Comments
No comments yet. Have a question or a tip? Be the first to share it.