Prompt templates are parameterized prompt builders that separate the structure of a prompt from its runtime values. Instead of hardcoding "You are an elementary teacher" every time, you define "You are a {school} teacher" once and inject any value at call time.
This tutorial covers message roles, LangChain's message types, manual message construction, and the full template system — from single SystemMessagePromptTemplate to a composed ChatPromptTemplate pipeline.
Prerequisites: LangChain and langchain-ollama installed, Ollama running locally with qwen3 pulled. See the LangChain Getting Started guide.
LLM Message Roles
Every message sent to a chat model carries a role that tells the model how to interpret it. Understanding roles is the foundation of prompt engineering.
| Role | Description |
|---|---|
system |
Sets model behavior and context. Injected before the conversation. Not supported by all providers. |
user |
Input from the human interacting with the model — text, questions, instructions. |
assistant |
The model's own response. Can include text or tool-call requests. |
tool |
Passes results of an external tool invocation back to the model. Used with tool-calling models. |
function (legacy) |
OpenAI's legacy function-calling role. Replaced by tool in current APIs. |
LangChain Message Types
LangChain wraps each role in a typed Python class so your code is explicit rather than relying on raw role strings:
| Message Class | Role | Description |
|---|---|---|
SystemMessage |
system |
Sets model persona, tone, and constraints |
HumanMessage |
user |
Represents the user's input or question |
AIMessage |
assistant |
Represents the model's response |
AIMessageChunk |
assistant |
Streamed partial response chunk |
ToolMessage |
tool |
Returns tool execution results to the model |
Setup
Load environment variables and initialize ChatOllama. The .env file should contain your LANGSMITH_API_KEY if tracing is enabled.
from dotenv import load_dotenv
load_dotenv('.env')
import os
api_key = os.environ.get("LANGSMITH_API_KEY")
print("API key loaded successfully" if api_key else "No API key found")
API key loaded successfully
Note
load_dotenv() looks for .env relative to the current working directory. Adjust the path if your .env lives in a parent directory (e.g. load_dotenv('./../.env')).
Calling a Model Without Templates
Before templates, here is the baseline: a plain string prompt sent directly to ChatOllama.invoke().
from langchain_ollama import ChatOllama
base_url = "http://localhost:11434"
model = 'qwen3'
llm = ChatOllama(base_url=base_url, model=model)
question = 'tell me about the earth in 3 points'
response = llm.invoke(question)
print(response.content)
1. **Physical Characteristics**: Earth is the third planet from the Sun, with a diameter of about 12,742 km. It is the only known celestial body to host liquid water, an atmosphere rich in nitrogen and oxygen, and a diverse range of ecosystems. Its surface is divided into land (about 29%) and oceans (about 71%), with a molten iron core generating a magnetic field that protects the planet from solar radiation.
2. **Habitability and Life**: Earth's unique combination of temperature, atmospheric conditions, and liquid water supports a vast array of life forms. The presence of a stable climate, driven by the greenhouse effect and the planet's axial tilt, allows for seasonal variation and the existence of diverse biomes, from deserts to rainforests.
3. **Geological Activity**: Earth is geologically dynamic, with plate tectonics shaping its surface through earthquakes, volcanic eruptions, and mountain formation. This activity recycles the planet's crust, regulates atmospheric CO₂ levels, and drives the rock cycle, maintaining the conditions necessary for life over billions of years.
The response style is entirely up to the model's defaults. No persona, no tone control.
Persona Models via Ollama
If you have created custom persona models with a Modelfile (e.g., sherlock or sheldon), you can swap the model argument to change the response style entirely — no prompt change needed.
model = 'sherlock'
llm = ChatOllama(base_url=base_url, model=model)
question = 'tell me about the earth in 3 points'
response = llm.invoke(question)
print(response.content)
An inquiry that requires little intellectual exertion, I dare say. Very well, let us proceed with the facts:
1. **Geographical Configuration**: The Earth, our terrestrial abode, is a terrestrial planet of moderate size, comprising approximately 71% water and 29% landmasses. Its axial tilt, a mere 23.5 degrees, results in the seasonal variations we observe.
2. **Atmospheric Composition**: Our atmosphere, a gaseous envelope surrounding the Earth, is composed primarily of nitrogen (78%) and oxygen (21%), with trace amounts of other gases. This delicate balance supports life as we know it.
3. **Planetary Movement**: The Earth orbits the Sun in an elliptical path, completing one rotation every 365.25 days. Its rotational velocity and axial precession result in the changing position of the stars in the night sky, a phenomenon that has fascinated astronomers for centuries.
Now, I must ask: what is the purpose behind your interest in these elementary facts?
model = 'sheldon'
llm = ChatOllama(base_url=base_url, model=model)
response = llm.invoke(question)
print(response.content)
(sigh) Fine. I shall condescend to explain the Earth to you, but please, do try to keep up.
1. **Geological Stability**: The Earth's crust is composed of tectonic plates that are in constant motion, albeit at a glacial pace. This process, known as plate tectonics, has shaped our planet over billions of years, resulting in the formation of mountain ranges, volcanoes, and oceanic trenches. Now, I'm sure you're aware that this is not just some simplistic, juvenile concept; no, it's a complex, nuanced aspect of geology that requires a level of intellectual sophistication far beyond your likely grasp.
2. **Atmospheric Composition**: The Earth's atmosphere is comprised of approximately 78% nitrogen, 21% oxygen, and 1% other gases. I suppose you're aware that this is not just some arbitrary mixture; no, it's a carefully calibrated balance necessary for life as we know it. Now, if you'd like to discuss the intricacies of atmospheric pressure, greenhouse effects, or the implications of climate change, I'm more than happy to enlighten you... but please, don't embarrass me with your ignorance.
3. **Orbital Mechanics**: The Earth's orbit around the Sun is an elliptical path that results in varying distances from our solar system's center. (I assume you're aware that this is not just some simplistic, circular motion; no, it's a complex, three-dimensional problem requiring expertise in celestial mechanics.) Now, if you'd like to discuss the implications of orbital eccentricity on planetary climate patterns or the effects of gravitational perturbations on our planet's rotation rate, I'm more than happy to engage in a discussion... but please, don't waste my time with your simplistic questions.
Note
sherlock and sheldon are custom models built with a Modelfile in the Ollama Setup lesson. The model argument must exactly match the name you used in ollama create.
Controlling Tone with SystemMessage
Instead of baking a persona into the Modelfile, you can inject it at runtime using SystemMessage. This lets a single base model (e.g., qwen3) behave differently without creating separate models.
from langchain_core.messages import SystemMessage, HumanMessage
base_url = "http://localhost:11434"
model = 'qwen3'
llm = ChatOllama(base_url=base_url, model=model)
Elementary-Level Response
Passing "You are elementary teacher. You answer in short sentences." as the system role produces concise, accessible output:
question = HumanMessage('tell me about the earth in 3 points')
system = SystemMessage('You are elementary teacher. You answer in short sentences.')
messages = [system, question]
response = llm.invoke(messages)
print(response.content)
1. Earth is the third planet from the Sun.
2. It's the only planet known to support life.
3. It has oceans, mountains, and a protective atmosphere.
PhD-Level Response
Swapping the system message to "You are phd teacher. You answer in short sentences." shifts the vocabulary and depth — same model, same question:
question = HumanMessage('tell me about the earth in 3 points')
system = SystemMessage('You are phd teacher. You answer in short sentences.')
messages = [system, question]
response = llm.invoke(messages)
print(response.content)
1. Earth is the third planet from the Sun, with a unique atmosphere supporting life.
2. It has a layered structure: crust, mantle, outer core, and inner core.
3. Approximately 71% of its surface is covered by liquid water.
The messages list is always ordered [system, ..., human]. LangChain passes them to the model in that order.
Prompt Templates
Hardcoding "elementary" or "phd" into the system message still requires rewriting strings. Prompt templates solve this by defining {variable} placeholders that are filled at runtime.
Prompt Template Classes
| Class | Description |
|---|---|
SystemMessagePromptTemplate |
Template for system messages with variable placeholders |
HumanMessagePromptTemplate |
Template for user messages with variable placeholders |
AIMessagePromptTemplate |
Template for assistant messages with variable placeholders |
PromptTemplate |
Basic single-string template with static text and variables |
ChatPromptTemplate |
Composes a sequence of typed message templates into one reusable pipeline |
Importing the Classes
from langchain_core.prompts import (
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
PromptTemplate,
ChatPromptTemplate
)
Defining Templates
Use .from_template() to create a template from a string with {placeholder} syntax:
system = SystemMessagePromptTemplate.from_template(
'You are {school} teacher. You answer in short sentences.'
)
question = HumanMessagePromptTemplate.from_template(
'tell me about the {topics} in {points} points'
)
Evaluate the template objects directly to inspect their internal structure — LangChain shows the underlying PromptTemplate with its detected input_variables:
system
question
HumanMessagePromptTemplate(prompt=PromptTemplate(input_variables=['points', 'topics'], input_types={}, partial_variables={}, template='tell me about the {topics} in {points} points'), additional_kwargs={})
Note
LangChain automatically detects input_variables ('points' and 'topics') by scanning the {placeholder} names in the template string. You never need to declare them manually.
Inspecting a Resolved Message
Call .format() with concrete values to see what the fully resolved message object looks like before sending it to the model:
question.format(topics='sun', points=5)
HumanMessage(content='tell me about the sun in 5 points', additional_kwargs={}, response_metadata={})
system.format(school='elementary')
SystemMessage(content='You are elementary teacher. You answer in short sentences.', additional_kwargs={}, response_metadata={})
.format() is purely for inspection — it returns the resolved message object but does not invoke the model.
Building a ChatPromptTemplate Pipeline
ChatPromptTemplate composes a list of message templates into a single reusable pipeline. Call .invoke() on the template with a dictionary of variable values to get a fully resolved list of messages ready for the model.
messages = [system, question]
template = ChatPromptTemplate(messages)
Invoking the Pipeline
Pass a dictionary of all variable values to template.invoke(). It resolves every placeholder and returns the filled message list, which is then passed directly to llm.invoke():
question = template.invoke({'school': 'elementary', 'topics': 'sun', 'points': 5})
response = llm.invoke(question)
print(response.content)
1. The Sun is a star made mostly of hydrogen and helium.
2. It provides light and heat to Earth and other planets.
3. Its surface temperature is about 5,500°C (10,000°F).
4. The Sun's core is extremely hot, around 15 million°C (27 million°F).
5. Solar energy powers life on Earth and drives weather.
To change the audience, topic, or point count, only the dictionary changes — the template itself stays constant:
# PhD-level response about black holes in 4 points
question = template.invoke({'school': 'phd', 'topics': 'black holes', 'points': 4})
response = llm.invoke(question)
print(response.content)
On Linux/macOS: all code above runs identically — no OS-specific differences.
Quick Reference
Message Construction
from langchain_core.messages import SystemMessage, HumanMessage
messages = [
SystemMessage('You are a {role}.'),
HumanMessage('Tell me about {topic}.')
]
response = llm.invoke(messages)
Template Pipeline (Full Pattern)
from langchain_core.prompts import (
SystemMessagePromptTemplate,
HumanMessagePromptTemplate,
ChatPromptTemplate
)
system = SystemMessagePromptTemplate.from_template(
'You are {school} teacher. You answer in short sentences.'
)
question = HumanMessagePromptTemplate.from_template(
'tell me about the {topics} in {points} points'
)
template = ChatPromptTemplate([system, question])
filled = template.invoke({'school': 'elementary', 'topics': 'sun', 'points': 5})
response = llm.invoke(filled)
print(response.content)
Prompt Template Class Summary
| Class | Wraps | Use when |
|---|---|---|
SystemMessagePromptTemplate |
SystemMessage |
Parameterizing system role instructions |
HumanMessagePromptTemplate |
HumanMessage |
Parameterizing user questions or inputs |
AIMessagePromptTemplate |
AIMessage |
Seeding conversation examples (few-shot) |
ChatPromptTemplate |
All of the above | Composing a full multi-role prompt pipeline |
Tip
ChatPromptTemplate is the class you will use in almost every real LangChain application. Chains and agents both accept it directly — you can pipe it into an LLM with template | llm using LangChain Expression Language (LCEL).