Skip to main content
Articles/AI/prompting-techniques-langchain
Scenario Article

Mastering Prompt Templates: System, User, and Assistant Roles with LangChain

The API Contract Analogy

Abstract Algorithmsβ€’Mar 9, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

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

1

Stage 1 β€” Roles, Templates, and Placeholders: The Building Blocks

Action / Decision:

Before diving into code, it helps to understand the three core concepts that make ChatPromptTemplate different from plain string formatting.

Component Behavior:

A plain Python f-string produces a single blob of text.

Observable Output: 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.
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

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

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit