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.
- Composability —
prompt | model | parseronly 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:
PromptTemplate— single string with{vars}, produces a string. Used with legacy "LLM" models or for simple cases.ChatPromptTemplate— list of(role, template)tuples, produces a list of messages. Used with chat models (which is almost always).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 aPromptValue..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
MessagesPlaceholderwhen 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
PromptTemplateandChatPromptTemplate? A:PromptTemplateproduces a single string (for legacy text-completion models).ChatPromptTemplateproduces a list of role-tagged messages (for chat models). UseChatPromptTemplatewith any GPT-4 / Claude / Gemini model. -
Q: Why use
MessagesPlaceholderinstead of just a{history}variable? A:{history}would interpolate into a single string, losing the role structure (who said what).MessagesPlaceholderinjects 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 withxpre-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
Escape the literal braces in a template that contains {}
Expected: {user_id}: Alice
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.