Skip to content

Prompts & Prompt Templates

1. Why this matters

In production, the same prompt is reused across thousands of requests with different variables. You need:

  • Validation — fail fast if a required variable is missing.
  • Reusability — define once, fill many.
  • Composabilityprompt | model | parser only works if the prompt is a Runnable.
  • Versioning — keep prompts in code, in git, reviewable like any other artifact.

Prompt templates give you all four.

2. Mental model

Three flavors, escalating in power:

  1. PromptTemplate — single string with {vars}, produces a string. Used with legacy "LLM" models or for simple cases.
  2. ChatPromptTemplate — list of (role, template) tuples, produces a list of messages. Used with chat models (which is almost always).
  3. FewShotPromptTemplate — wraps an example list + a prefix/suffix template, used when you want to show the model 2–10 input/output pairs before the real query.

All three are Runnables — they slot into LCEL.

3. Architecture / Flow

flowchart LR
    V[Variables<br/>dict] --> T[PromptTemplate]
    T --> P[PromptValue]
    P --> M[Chat Model]
    M --> R[AIMessage]

    style T fill:#e8f0fe

A "PromptValue" is just a typed wrapper — call .to_string() for text models or .to_messages() for chat models.

4. Core concepts

  • {variable} — placeholder in the template, filled by the input dict.
  • from_template(text) — build from a single template string. Variables are auto-detected.
  • from_messages([("system", "..."), ("human", "...")]) — build a chat prompt from a list of (role, template) tuples.
  • .invoke({"var": "value"}) — fill the template, returns a PromptValue.
  • .partial(var="value") — pre-fill some variables, return a new template needing the rest.
  • MessagesPlaceholder("history") — slot for a list of prior messages (used for memory).
  • FewShotPromptTemplate — automate the "here are 5 examples, now answer:" pattern.

5. Code — minimal working example

from langchain_core.prompts import PromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

prompt = PromptTemplate.from_template(
    "Translate this to {language}:\n\n{text}"
)

chain = prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()

print(chain.invoke({"language": "French", "text": "Good morning."}))

Chat-style with system + human:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a {style} writing assistant."),
    ("human", "Write a tweet about {topic}."),
])

chain = prompt | ChatOpenAI(model="gpt-4o-mini") | StrOutputParser()
chain.invoke({"style": "witty", "topic": "vector databases"})

6. Code — real-world pattern

Chat prompt with memory (MessagesPlaceholder) + few-shot examples + a partial:

from langchain_core.prompts import (
    ChatPromptTemplate,
    MessagesPlaceholder,
    FewShotChatMessagePromptTemplate,
)
from langchain_core.messages import HumanMessage, AIMessage

# 1. Few-shot examples teach the model the format
examples = [
    {"input": "I love this!",  "output": "positive"},
    {"input": "Worst purchase ever.", "output": "negative"},
    {"input": "It's okay.",     "output": "neutral"},
]
example_prompt = ChatPromptTemplate.from_messages([
    ("human", "{input}"),
    ("ai", "{output}"),
])
few_shot = FewShotChatMessagePromptTemplate(
    example_prompt=example_prompt,
    examples=examples,
)

# 2. Final prompt: system + examples + conversation history + new input
prompt = ChatPromptTemplate.from_messages([
    ("system", "Classify sentiment as positive/negative/neutral. App: {app_name}"),
    few_shot,
    MessagesPlaceholder("history"),
    ("human", "{input}"),
])

# 3. Pre-fill app_name once — the chain only needs `history` + `input` from now on
prompt = prompt.partial(app_name="ShopX")

# 4. Wire it up
history = [HumanMessage("It arrived broken."), AIMessage("negative")]
filled = prompt.invoke({"history": history, "input": "Five stars!"})

for msg in filled.to_messages():
    print(msg.type, "→", msg.content)

7. Common pitfalls

  • { and } in literal text get parsed as variables. If your template needs literal curly braces (e.g., JSON examples in the system prompt), escape them: {{ and }}.
  • Mixing positional args with from_template. Always pass a dict to .invoke({...}), never positional args.
  • Forgetting MessagesPlaceholder when wiring memory. Without it, the chain has no slot to inject the conversation history.
  • Putting too many variables in one template. If a template has 8 variables it's a code smell — split into smaller composable templates.
  • Stuffing huge context strings into prompts manually. Use a retriever + {context} placeholder instead — better caching, easier to test.

8. When to use vs not use

Use this When
PromptTemplate Single-turn text-completion style (rare in 2025+)
ChatPromptTemplate Default for chat models — use this 95% of the time
FewShotPromptTemplate Classification / extraction where 3–10 examples dramatically improve quality
Plain f-strings Never in chains — you lose validation, composability, and tracing

9. Cheatsheet

# Create
PromptTemplate.from_template("Tell me about {topic}")
ChatPromptTemplate.from_messages([
    ("system", "..."),
    ("human", "...{var}..."),
    ("ai", "..."),
])
ChatPromptTemplate.from_template("...{var}...")   # all-human shortcut

# Fill
prompt.invoke({"var": "value"})        # → PromptValue
prompt.format_messages(var="value")    # → list[BaseMessage]
prompt.format(var="value")             # → str (text models)

# Modify
prompt.partial(static_var="fixed")     # pre-fill some vars
prompt + another_prompt                # concat templates

# Slots
MessagesPlaceholder("history")          # injects a list of messages
MessagesPlaceholder("ctx", optional=True)  # ok if missing

# Escape literal braces
"Return JSON like {{\"key\": \"value\"}}"

10. Q&A — recall test

  • Q: Difference between PromptTemplate and ChatPromptTemplate? A: PromptTemplate produces a single string (for legacy text-completion models). ChatPromptTemplate produces a list of role-tagged messages (for chat models). Use ChatPromptTemplate with any GPT-4 / Claude / Gemini model.

  • Q: Why use MessagesPlaceholder instead of just a {history} variable? A: {history} would interpolate into a single string, losing the role structure (who said what). MessagesPlaceholder injects an actual list of typed messages, preserving the conversation shape.

  • Q: When do you reach for FewShotPromptTemplate? A: When zero-shot performance is poor on a classification / extraction / structured-output task and you have 3–10 labeled examples that consistently improve outputs.

  • Q: What does prompt.partial(x="...") do? A: Returns a new prompt with x pre-filled. Useful for binding app-wide constants (model role, tenant ID) so chains don't have to pass them every invoke.

  • Q: How do you include literal { in a template? A: Escape as {{ (and }} for }).

Practice

What does this print?

Expected: Translate Hello to French

template = "Translate {text} to {lang}"
print(template.format(text="Hello", lang="French"))

Escape the literal braces in a template that contains {}

Expected: {user_id}: Alice

template = "{user_id}: {name}"           # bug: BOTH look like placeholders, but we only want name as a real var
# We want literal "{user_id}" in the output
result = template.format(name="Alice")
print(result)                            # crashes with KeyError

Quiz — Quick check

What you remember

Q1. Why use ChatPromptTemplate instead of plain string formatting?

  • It's faster
  • It produces a list of Message objects with proper role tags (system/user/assistant) for chat models
  • Required by LangChain
  • It validates content

Why: Chat models expect structured messages. ChatPromptTemplate.from_messages([("system", ...), ("human", "{input}")]) produces the right format and substitutes variables.

Q2. What's MessagesPlaceholder for?

  • Injecting a list of prior chat messages (the conversation history) into the prompt
  • Showing a loading spinner
  • Reserving space for future messages
  • Required for streaming

Why: A typical multi-turn chat prompt has system + history + new user message. MessagesPlaceholder("history") is where prior turns get spliced in.

Q3. What's the right way to include a literal { in your template?

  • \{
  • {{
  • "{"
  • Use a different character

Why: Same rule as Python's .format(). Double the brace to escape. {{ this would be {literal} }}.

Common doubts

Where should I store prompts — in code or in a file?

For prototyping, hardcoded is fine. For production, store in a file (YAML/JSON), version-controlled, and load via load_prompt. Even better: use a prompt registry (LangSmith Hub) for non-developers to edit prompts without code deploys.

Should I use few-shot examples in the prompt?

For classification or structured-output tasks, yes — a few examples can dramatically improve accuracy. For open-ended generation, often unnecessary and they can constrain creativity. Use FewShotPromptTemplate for clean composition.

How long can my system prompt be?

Technically up to the model's context window (often 128K+ tokens). Practically: keep it under ~1000 tokens. Long prompts increase cost per request and can dilute the model's focus. If your system prompt feels bloated, refactor: maybe some logic belongs in tools or retrieval instead.