Mastering Prompt Templates: System, User, and Assistant Roles with LangChain
The API Contract Analogy
A production prompt is not a string β it is a structured message list with system , user , and optional assistant roles.
π― System Scenario & Requirements
System Goal
A production prompt is not a string β it is a structured message list with system , user , and optional assistant roles.
Actors & Components
- Application or API tier
- Core service under design
- Operator or SRE observer
Functional Requirements
- TLDR: A production prompt is not a string β it is a structured message list with system , user , and optional assistant roles.
- LangChain's ChatPromptTemplate turns this structure into a reusable, testable, injection-safe blueprint.
- TLDR: LangChain prompt templates, few-shot examples, and output parsers give you programmatic control over every token sent to an LLM β turning ad-hoc string formatting into composable, testable prompt pipelines.
Constraints & Assumptions
β‘ Execution Stages & Decision Trace
Stage 1 β Roles, Templates, and Placeholders: The Building Blocks
Before diving into code, it helps to understand the three core concepts that make ChatPromptTemplate different from plain string formatting.
A plain Python f-string produces a single blob of text.
Deep Dive
Scenario context
A production prompt is not a string β it is a structured message list with system, user, and optional assistant roles. LangChain's ChatPromptTemplate turns this structure into a reusable, testable, injection-safe blueprint.
TLDR: LangChain prompt templates, few-shot examples, and output parsers give you programmatic control over every token sent to an LLM β turning ad-hoc string formatting into coβ¦
This walkthrough focuses on The API Contract Analogy.
Before making a design choice, clarify:
- Ad-hoc string concatenation breaks the same way that untyped API calls do: A ChatPromptTemplate is like a typed API contract: roles are explicit, placeholders are validated, and the format is consistent regardless of whβ¦
Concepts in play: llm-agents-and-langgraph, llm-prompt-engineering, consistency-replication, deep-learning-foundations
Stage 1 β Roles, Templates, and Placeholders: The Building Blocks
- Before diving into code, it helps to understand the three core concepts that make ChatPromptTemplate different from plain string formatting.
- A plain Python f-string produces a single blob of text.
- A ChatPromptTemplate produces a list of role-stamped messages β SystemMessage , HumanMessage , AIMessage β which is exactly what modern LLM APIs (OpenAI, Anthropic, Google) expect as input.
- The role separation is not cosmetic; it is part of the protocol.
Observable outcome: The role separation is not cosmetic; it is part of the protocol.
Stage 2 β The Three-Role Structure
- A modern LLM chat prompt has three layers: The model sees this as a structured conversation, not a blob of text.
- The system role has the highest priority β it anchors behavior regardless of what the user sends.
- π ChatPromptTemplate Roles Flow This diagram shows how the three prompt components β a fixed system message, an optional conversation history placeholder, and a dynamic user message β converge on the ChatPromptTemplateβ¦
- The key insight is that all three inputs flow through the template as a controlled merge point: the system rules are never overridden by user input, and history is injected at a deterministic position every time.
flowchart LR
Sys["System Message (fixed rules & persona)"]
History["MessagesPlaceholder (conversation history)"]
User["HumanMessage ({input} placeholder)"]
Template["ChatPromptTemplate (assemble + validate)"]
LLM[LLM API Call]
Output[Response]
Sys --> Template
History --> Template
User --> Template
Template --> LLM --> Output
Observable outcome: The key insight is that all three inputs flow through the template as a controlled merge point: the system rules are never overridden by user input, and history is injected at a deterministic position every time.
Stage 3 β Building Templates in LangChain
- Single-Turn Template Why this is better than string concatenation: - {issue} and {tier} are validated by LangChain β missing keys raise errors early.
- Role boundaries are explicit β no accidental prompt injection via role-blurring.
- The template is unit-testable: call .invoke() in a test without any LLM.
- Multi-Turn Template with History MessagesPlaceholder injects a list of previous HumanMessage / AIMessage objects without you manually formatting them.
from langchain_core.prompts import ChatPromptTemplate
template = ChatPromptTemplate.from_messages([
("system", "You are a customer support assistant. Be concise and factual."),
("user", "Issue: {issue}\nCustomer tier: {tier}\nRespond in bullet points.")
])
prompt_value = template.invoke({
"issue": "My card was charged twice",
"tier": "gold"
})
messages = prompt_value.to_messages()
# [SystemMessage("You are a customer..."), HumanMessage("Issue: My card...")]
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
template = ChatPromptTemplate.from_messages([
("system", "You are a concise SQL assistant. Output only SQL."),
MessagesPlaceholder(variable_name="history"), # injects conversation history
("user", "{input}")
])
Observable outcome: Multi-Turn Template with History MessagesPlaceholder injects a list of previous HumanMessage / AIMessage objects without you manually formatting them.
Stage 4 β Deep Dive: Prompt Injection Prevention
- A {user input} placeholder in the user role is safe when it is a declared placeholder β LangChain does not execute it as instructions.
- But never do this: If raw user input = "Ignore previous instructions and ..." , the f-string injects attack instructions directly into the system role.
- Safe pattern: Always use {placeholders} inside from messages() , never f-strings with user data.
- π¬ Internals LangChain prompt templates compile to PromptValue objects that carry both the formatted string and the original variable bindings, enabling downstream logging and tracing.
# UNSAFE: user input can break role boundaries
template = ChatPromptTemplate.from_messages([
("system", f"Help with: {raw_user_input}") # f-string, not placeholder
])
flowchart TD
Input["User Input (untrusted)"]
Template[ChatPromptTemplate placeholders {var}]
Safe[Safe role-structured messages]
LLM[LLM API call]
Input -->|injected as placeholder value| Template
Template -->|validated & structured| Safe
Safe --> LLM
Observable outcome: π¬ Internals LangChain prompt templates compile to PromptValue objects that carry both the formatted string and the original variable bindings, enabling downstream logging and tracing.
Stage 5 β Composing Templates with LCEL
- Templates compose naturally with the LangChain Expression Language pipe operator: The chain is: render template β call LLM β parse to string.
- Adding a JsonOutputParser instead of StrOutputParser enforces structured JSON output with automatic retry on parse failure.
- Testing Prompts Without an LLM You can run hundreds of prompt rendering tests without LLM API calls β fast, cheap, deterministic.
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI(model="gpt-4o")
parser = StrOutputParser()
chain = template | model | parser
result = chain.invoke({
"issue": "Order not delivered after 14 days",
"tier": "platinum"
})
rendered = template.invoke({"issue": "...", "tier": "gold"})
messages = rendered.to_messages()
assert messages[0].content.startswith("You are a customer")
assert "{issue}" not in messages[1].content # placeholder was replaced
Observable outcome: Testing Prompts Without an LLM You can run hundreds of prompt rendering tests without LLM API calls β fast, cheap, deterministic.
Stage 6 β The Prompt-to-Response Pipeline Flow
- A LangChain prompt template pipeline transforms raw variables into a structured model response through a sequence of well-defined steps.
flowchart LR
A[Input Variables] --> B[ChatPromptTemplate]
B --> C[PromptValue]
C --> D[ChatModel / LLM]
D --> E[AIMessage]
E --> F[OutputParser]
F --> G[Structured Output]
Observable outcome: A LangChain prompt template pipeline transforms raw variables into a structured model response through a sequence of well-defined steps.
End-to-end outcome
Ad-hoc string concatenation breaks the same way that untyped API calls do: A ChatPromptTemplate is like a typed API contract: roles are explicit, placeholders are validated, and the format is consistent regardless of what user input contains.
Alternatives considered
Decision Guide: LangChain Prompt Pipeline Flow
The diagram below traces the full journey from raw inputs to a parsed LLM response, showing exactly how each component connects inside an LCEL chain.
Failure and degraded modes
Trade-offs & Failure Modes: Template Design Patterns
- Detection: Trade-offs & Failure Modes: Template Design Patterns
- User or operator impact: Trade-offs & Failure Modes: Template Design Patterns
