04. ChatPromptTemplate
ChatPromptTemplate constructs prompts made of discrete message objects rather than a single string. This matches how chat models actually process input—they see role-labeled messages (system, user, assistant), not raw concatenation. Using the right template type matters because the model interprets positional cues differently depending on whether it receives a single prompt string or a structured message history.
LangChain defines three message role types in langchain_core.messages:
from langchain_core.messages import (
SystemMessage,
HumanMessage,
AIMessage,
ToolMessage,
ChatMessage,
)
# SystemMessage: global instructions (always first or interleaved depending on model)
# HumanMessage: user input turns
# AIMessage: previous model responses (for conversation memory)
# ToolMessage: output from tool/function calls
# ChatMessage: a generic role message (rarely needed)
Build a ChatPromptTemplate with explicit message tuples:
from langchain_core.messages import SystemMessage, HumanMessage
from langchain_core.prompts import ChatPromptTemplate
chat_template = ChatPromptTemplate.from_messages([
("system", "You are a {assistant_role} assistant. Always answer in {language}."),
("human", "The user said: {user_input}\n\nWhat is a good response?"),
])
rendered = chat_template.invoke({
"assistant_role": "helpful",
"language": "Spanish",
"user_input": "Where's the nearest coffee shop?"
})
for msg in rendered.to_messages():
print(f"[{msg.type}] {msg.content}")
# [system] You are a helpful assistant. Always answer in Spanish.
# [human] The user said: Where's the nearest coffee shop?
# What is a good response?
The ChatPromptTemplate also accepts message objects directly, which is useful when constructing responses from stored histories:
# Pre-populate with a conversation history
from langchain_core.messages import AIMessage
history_template = ChatPromptTemplate.from_messages([
("system", "You are a helpful code reviewer."),
AIMessage(content="I've written this function. Can you review it?"),
("human", "{new_code}"),
])
When you inspect the rendered PromptValue from a ChatPromptTemplate, you get a list of BaseMessage objects. These are what ChatOllama.invoke() accepts natively:
response = llm.invoke(rendered)
print(response.content)
The connection between template and model is direct: ChatPromptTemplate → PromptValue → list of BaseMessage → ChatOllama.
Failure mode with message ordering: some operators insert HumanMessage objects into a ChatPromptTemplate expecting them to be treated differently from hardcoded strings. They are not. The template renders all messages at invoke time, not at model call time. If you want to append a user's new message to an existing message history, concatenate the message lists before passing to the model—not inside the template construction.
Another failure mode: SystemMessage injection. Ollama models do not all handle system messages consistently. Some models trained with Llama 3-style instruction tuning expect system prompts to be prepended, while others ignore them entirely. Test your specific model with a system prompt that includes a verifiable token (e.g., "Always end responses with the word BANANA."). If the model ignores it, fall back to including the instruction in the first HumanMessage or using the ChatOllama system parameter directly.
Construct a ChatPromptTemplate with a system message defining a translator role, two human messages (the first expressing a subject, the second asking for a different translation style), and a render that captures both full message sequences. Print the role and content of each message in the rendered output.