← Back to Agentic AI map
Lesson 2.3 · Building Agents with LangChain

Prompt Templates

Reusable prompts with variables, system and human messages, and few-shot examples.

prompts

What you will be able to do

  • Turn a hard-coded prompt into a template with variables
  • Fill a template, and see exactly what reaches the model
  • Separate behaviour (system) from the request (human) with from_messages
  • Use few-shot examples to show the model the answer you want
  • Avoid the template traps: missing variables, leaking indentation, and literal braces

The idea, in plain English

In Lesson 2.2 the prompt was a fixed string: "Explain Docker in simple English." To explain Kubernetes, Redis, and FastAPI too, you would copy it three times. A prompt template keeps the wording once and leaves gaps for what changes: "Explain {topic} in simple English."

At run time you supply the values - chain.invoke({"topic": "Docker"}) - and LangChain fills the gaps before anything reaches the model. The model never sees {topic}; it sees "Explain Docker in simple English." The template is the reusable structure; the dictionary is the data.

Templates also carry behaviour. A system message says how to answer, a human message carries the request, and few-shot examples show what a good answer looks like. In our runs, two short examples cut the model’s answers from over 180 words to about 10.

Every output here comes from a real run against a local model (llama3; the code says llama3.1, the course model).

Worked example: A reusable "explain like I am 5" prompt.

workflowFrom template to modelstep 1 / 4

1 - The template is written once

Fixed wording, with gaps in braces. ChatPromptTemplate reads the gaps as input variables: ["level", "topic"].

variables
topic, level
written
once
reused for
every topic
sent yet
nothing

Step through one call. Watch where the values go in - and notice the model only ever sees the finished text.

Templates and variables

ChatPromptTemplate.from_template("Explain {topic} in simple English.") finds every name in braces and records it as an input variable - here, just topic. A template can have as many as you need: "Explain {topic} to a {level} developer" has two.

Values come in a dictionary whose keys match the names. Leave one out and the chain stops before the model with a KeyError naming it: "missing variables {’level’}". Extra keys are ignored silently.

To see exactly what the model will get, invoke the template on its own: prompt.invoke({"topic": "Docker"}) returns the finished messages. It costs nothing - no model is called.

Why not an f-string?

f"Explain {topic}" fills the gap the moment the line runs, so it is not reusable - it is just a string. A template stays a template until it is invoked, knows which variables it needs and reports the missing ones, and joins onto a model with |.

It can also be filled in stages. prompt.partial(level="beginner") returns a new template with level already set, leaving only topic - handy when one value is fixed for a whole app and the other changes per request.

System and human messages

from_messages builds a template from roles: a system message for behaviour - "You are a programming teacher. Use simple English." - and a human message for the request - "Explain {topic}." Either can contain variables.

Keeping them apart means the behaviour is written once and reused for every request, and the request stays short. It is Module 1’s system and user roles, with gaps.

Few-shot examples

Instructions describe the answer you want; examples show it. Put two or three question-and-answer pairs in front of the real question, and the model copies their length, tone, and structure.

The effect is large. Without examples, llama3 answered "What is Kubernetes?" in 224 words, "What is an API?" in 183, and "What is PostgreSQL?" in 324. With two one-sentence examples in front, the same questions took 10, 21, and 9 words.

Examples teach a pattern, not facts. They should be correct anyway - the model may lean on them - and they should look like the answers you actually want, because that is what you will get.

Answer length, same questions, temperature 0
What is Kubernetes?No examples: 224 words. Two examples: 10 words.
What is an API?No examples: 183 words. Two examples: 21 words.
What is PostgreSQL?No examples: 324 words. Two examples: 9 words.

FewShotChatMessagePromptTemplate

Writing examples into one long string works. LangChain’s FewShotChatMessagePromptTemplate does it more cleanly: give it a list of example dictionaries and a small template for one example, and it turns them into real human and AI turns - exactly as if the conversation had happened.

Our run produced system, human ("What is Docker?"), ai, human ("What is Redis?"), ai, and finally the real question - and the answer came back in 15 words. Keeping examples as data also means you can store them, swap them, or choose different ones per request.

Three template traps

Literal braces. Anything in braces is a variable - including the braces in a JSON example. ’Reply as JSON like {"name": "..."}’ creates a variable called "name", builds without complaint, and fails at invoke with "missing variables". Double the braces - {{"name": "..."}} - to mean literal braces. This bites every JSON prompt from Lesson 1.3 you move into a template.

Indentation. A system message written as an indented triple-quoted string is sent exactly as written: "\n You are a programming teacher.\n ..." - leading newline, eight spaces per line. It rarely changes the answer, but it is noise in every call. Start the text at the left margin, or strip it.

Missing variables. Every name in braces needs a key in the dictionary. Check prompt.input_variables when in doubt.

Watch out: A template with literal braces builds fine and only fails when it runs - with an error about a variable you did not mean to create. Double any brace that is not a variable.

Step-by-step code

One template, any topic
from langchain_core.prompts import ChatPromptTemplate from langchain_ollama import ChatOllama llm = ChatOllama(model="llama3.1", temperature=0) prompt = ChatPromptTemplate.from_template("Explain {topic} in simple English.") chain = prompt | llm for topic in ["Docker", "Redis", "FastAPI"]: print(chain.invoke({"topic": topic}).content)
What the model actually receives
prompt = ChatPromptTemplate.from_template("Explain {topic} for a {level} developer.") print(prompt.input_variables) # ['level', 'topic'] print(prompt.invoke({"topic": "Redis", "level": "beginner"}).to_messages()) # [HumanMessage(content='Explain Redis for a beginner developer.')] prompt.invoke({"topic": "Redis"}) # KeyError: Input to ChatPromptTemplate is missing variables {'level'}. # Expected: ['level', 'topic'] Received: ['topic'] beginner = prompt.partial(level="beginner") print(beginner.input_variables) # ['topic']
The "explain like I am 5" prompt
eli5 = ChatPromptTemplate.from_template( "Explain {topic} like I am 5 years old, in 3 short sentences." ) | llm print(eli5.invoke({"topic": "a database"}).content)
Output - from a real run
Here's an explanation of a database that a 5-year-old can understand: A database is like a super cool, magic bookshelf that can hold lots and lots of information, like pictures and words. You can put things on the bookshelf, like a picture of your favorite toy, and then you can find it again later by looking at the bookshelf. It's like a special place where you can store all your favorite things and find them again whenever you want!
System behaviour, human request
prompt = ChatPromptTemplate.from_messages([ ("system", "You are a programming teacher. Explain concepts in simple English. " "Keep the answer concise. Give one real-world example."), ("human", "Explain {topic} for a {level} developer."), ]) chain = prompt | llm print(chain.invoke({"topic": "Redis", "level": "beginner"}).content)
Few-shot examples in the template
few_shot = ChatPromptTemplate.from_template("""You explain programming concepts in one short sentence. Question: What is Docker? Answer: Docker packages applications and their dependencies into containers. Question: What is Redis? Answer: Redis is an in-memory data store commonly used for caching. Question: {question} Answer:""") chain = few_shot | llm print(chain.invoke({"question": "What is Kubernetes?"}).content)
With and without the examples - from real runs
no examples two examples "What is Kubernetes?" 224 words 10 words "What is an API?" 183 words 21 words "What is PostgreSQL?" 324 words 9 words With examples: Kubernetes automates the deployment, scaling, and management of containerized applications. PostgreSQL is a powerful open-source relational database management system.
Examples as data: FewShotChatMessagePromptTemplate
from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate examples = [ {"question": "What is Docker?", "answer": "Docker packages applications and their dependencies into containers."}, {"question": "What is Redis?", "answer": "Redis is an in-memory data store commonly used for caching."}, ] shots = FewShotChatMessagePromptTemplate( examples=examples, example_prompt=ChatPromptTemplate.from_messages([("human", "{question}"), ("ai", "{answer}")]), ) prompt = ChatPromptTemplate.from_messages([ ("system", "You explain programming concepts in one short sentence."), shots, ("human", "{question}"), ]) print((prompt | llm).invoke({"question": "What is Kubernetes?"}).content)
What it sends, and the answer
system You explain programming concepts in one short sentence. human What is Docker? ai Docker packages applications and their dependencies into containers. human What is Redis? ai Redis is an in-memory data store commonly used for caching. human What is Kubernetes? Kubernetes is a container orchestration system that automates the deployment, scaling, and management of containers. (15 words)
Two traps, checked
# Literal braces become variables bad = ChatPromptTemplate.from_template('Reply as JSON like {"name": "..."}. Person: {person}') print(bad.input_variables) # ['"name"', 'person'] - not what you meant bad.invoke({"person": "Priya"}) # KeyError: missing variables {'"name"'} good = ChatPromptTemplate.from_template('Reply as JSON like {{"name": "..."}}. Person: {person}') print(good.invoke({"person": "Priya"}).to_messages()[0].content) # Reply as JSON like {"name": "..."}. Person: Priya # Indented triple-quoted text is sent as written indented = ChatPromptTemplate.from_messages([("system", """ You are a programming teacher. """), ("human", "{q}")]) print(repr(indented.invoke({"q": "x"}).to_messages()[0].content)) # '\n You are a programming teacher.\n '

Tip: prompt.invoke(values) shows the exact messages the model will receive, without calling the model. Check it whenever an answer surprises you.

Prompt templates at a glance

from_template

One human message with variables.

ChatPromptTemplate.from_template("Explain {topic}.")
from_messages

Several roles, each with variables.

[("system", "..."), ("human", "Explain {topic}.")]
input_variables

The names the template needs.

prompt.input_variables  # ['topic']
invoke

Fill the template - no model call.

prompt.invoke({"topic": "Docker"})
partial

Pre-fill some variables.

prompt.partial(level="beginner")
Few-shot

Examples as real conversation turns.

FewShotChatMessagePromptTemplate(examples=..., example_prompt=...)
Literal braces

Double them.

{{"name": "..."}}

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

Same template, new topic

“Run the ELI5 chain for "the internet", "a CPU", and "an API".”

Measure few-shot

“Ask three questions with and without the examples and count the words.”

See the prompt

“Print prompt.invoke(values).to_messages() before every model call for one session.”

Break it

“Put a JSON example in a template without doubling the braces, and read the error.”

What usually goes wrong

Forgetting a variable

Every name in braces needs a value; the chain stops with a KeyError before the model.

✗ chain.invoke({"topic": "Docker"})  # template also needs {level}
✓ chain.invoke({"topic": "Docker", "level": "beginner"})
Literal braces in a template

Braces in a JSON or code example become variables. Double them.

✗ 'Reply as JSON like {"name": "..."}'
✓ 'Reply as JSON like {{"name": "..."}}'
Indented triple-quoted prompts

The indentation and the leading newline are sent to the model on every call.

✗ ("system", """
        You are a teacher.
        """)
✓ ("system", "You are a teacher.")
Using an f-string instead of a template

An f-string is filled immediately, so it cannot be reused, checked, or chained.

✗ f"Explain {topic} simply."
✓ ChatPromptTemplate.from_template("Explain {topic} simply.")
Examples that do not look like the answer you want

The model copies the examples’ length and style. Long, chatty examples get long, chatty answers.

Key points

  • A prompt template is reusable wording with variables in braces.
  • invoke({...}) fills the variables; the model only ever sees the finished text.
  • from_messages separates behaviour (system) from the request (human).
  • Few-shot examples show the pattern - two examples cut answers from 180+ words to about 10.
  • FewShotChatMessagePromptTemplate turns example data into real conversation turns.
  • Double literal braces, and do not indent triple-quoted prompts.
  • prompt.invoke() shows exactly what the model will receive, without calling it.

Quick check before you move on

What is a prompt template?
A reusable prompt with variables, filled with values at run time.
What does {topic} represent?
A template variable - a gap filled from the dictionary passed to invoke.
What does chain.invoke({"topic": "Docker"}) do?
Fills {topic} with "Docker", sends the finished prompt to the model, and returns its reply.
What is few-shot prompting?
Giving the model a few examples of the answers you want, so it follows their pattern.
Why separate system instructions from the human message?
So the behaviour is written once and reused, while the request changes per call.

Quiz

  1. 1.

    A template contains ’Reply as JSON like {"name": "..."}’ and a {person} variable. What happens?

  2. 2.

    You pass a key the template does not use. What happens?

  3. 3.

    What does prompt.partial(level="beginner") return?

  4. 4.

    Why did two short examples shorten the answers so much?

Interview questions

Why use a prompt template instead of building strings by hand?

It separates the reusable prompt structure from run-time data, validates that every variable is supplied, can be partially filled, and composes with models and parsers. That makes prompts easier to reuse, test, and maintain.

What is few-shot prompting, and when would you use it?

Including a few input-output examples in the prompt so the model follows their format, length, and style. Use it when instructions alone do not produce consistent output - especially for format and tone.

What is a common bug when moving prompts into templates?

Literal braces - in JSON or code examples - are parsed as variables. The template builds but fails at run time. Escape them by doubling.

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...