Chains & Prompt Templates

Prompt templates, a simple prompt-model-parser chain, and why chaining steps together is useful.

Prompt templates

A prompt template is a reusable prompt string with placeholders for variables, so application code can fill in the specifics without rebuilding the prompt's structure and instructions every time.

Python
from langchain_core.prompts import ChatPromptTemplate

template = ChatPromptTemplate.from_template(
    "Summarize the following {document_type} in {sentence_count} sentences:\n\n{content}"
)

prompt = template.invoke({
    "document_type": "support ticket",
    "sentence_count": 2,
    "content": "Customer reports the app crashes on launch after the latest update...",
})

This keeps the instructional wrapper (tone, format, constraints) defined once, in one place, while the actual variable content changes per call — the same benefit a function signature gives you over inlining literal values everywhere.

A simple chain: prompt → model → output parser

A chain connects these pieces together so data flows from one step into the next automatically: a prompt template formats the input, the model generates a response, and an output parser converts the raw model output into a usable shape (plain text, a list, structured JSON).

Python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_openai import ChatOpenAI

prompt = ChatPromptTemplate.from_template("Translate this to French: {text}")
model = ChatOpenAI(model="gpt-4o-mini")
parser = StrOutputParser()

chain = prompt | model | parser  # the pipe operator composes the steps

result = chain.invoke({"text": "Where is the nearest train station?"})
# result -> "Où se trouve la gare la plus proche ?"

The | operator here builds a pipeline: the dictionary passed to .invoke() flows into the prompt template, the formatted prompt flows into the model, and the model's raw response flows into the parser, which returns a clean string instead of a raw API response object.

Why chaining is useful

The real value shows up once a task needs more than one step, where each step's output is the next step's input. Consider "summarize a document, then translate that summary into French" — two genuinely separate operations, each best done as its own focused prompt rather than one prompt trying to do both at once:

Python
summarize_prompt = ChatPromptTemplate.from_template(
    "Summarize this document in 2 sentences:\n\n{document}"
)
translate_prompt = ChatPromptTemplate.from_template(
    "Translate this text to French:\n\n{summary}"
)

summarize_chain = summarize_prompt | model | parser
translate_chain = translate_prompt | model | parser

summary = summarize_chain.invoke({"document": long_english_document})
french_summary = translate_chain.invoke({"summary": summary})

Splitting this into two composed chains, rather than one prompt asking for "a 2-sentence French summary" in a single pass, makes each step individually easier to test, debug, and improve — if the French output looks wrong, you can inspect summary on its own and immediately tell whether the problem was in summarizing or in translating.

Common mistakes

  • Cramming multiple distinct operations into a single mega-prompt instead of composing focused chains — it's harder to debug (which part went wrong?) and harder to reuse (the summarization prompt alone can't be reused elsewhere if it's fused with translation).
  • Ignoring the output parser step and manually string-parsing the model's raw response in application code — brittle, and exactly the boilerplate output parsers exist to standardize.
  • Hardcoding literal values into a prompt string instead of using template variables — makes the prompt impossible to reuse across different inputs without editing the template itself each time.