Learnixo

LangChain Mastery · Lesson 11 of 33

PromptTemplate and ChatPromptTemplate

Two Types of Prompts

LangChain has two prompt classes for two model paradigms:

  • PromptTemplate — for completion/text models (legacy LLMs, text-in text-out)
  • ChatPromptTemplate — for chat models (message-in message-out, the modern standard)

In practice, you'll use ChatPromptTemplate almost exclusively since GPT-4o, Claude, and Gemini are all chat models.

Python
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate

# PromptTemplate: single string with {variables}
text_prompt = PromptTemplate(
    input_variables=["drug", "condition"],
    template="What is the dose of {drug} for {condition}?",
)
formatted_text = text_prompt.format(drug="warfarin", condition="AFib")
# "What is the dose of warfarin for AFib?"

# ChatPromptTemplate: list of (role, content) tuples
chat_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist."),
    ("human", "What is the dose of {drug} for {condition}?"),
])
formatted_chat = chat_prompt.format_messages(drug="warfarin", condition="AFib")
# [SystemMessage(content="You are..."), HumanMessage(content="What is the dose...")]

ChatPromptTemplate Construction Patterns

Python
from langchain_core.messages import SystemMessage, HumanMessage, AIMessage

# Pattern 1: from_messages with tuples (most common)
prompt_a = ChatPromptTemplate.from_messages([
    ("system", "You are a {role}."),
    ("human", "{question}"),
])

# Pattern 2: from_messages with Message objects
prompt_b = ChatPromptTemplate.from_messages([
    SystemMessage(content="You are a clinical pharmacist."),
    HumanMessage(content="Explain {drug}."),
])

# Pattern 3: from_template (single human message)
prompt_c = ChatPromptTemplate.from_template("Explain {drug} in simple terms.")
# Equivalent to: ChatPromptTemplate.from_messages([("human", "Explain {drug}...")])

# Pattern 4: from_messages with placeholder (for chat history)
prompt_d = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist."),
    ("placeholder", "{chat_history}"),   # Insert list of messages here
    ("human", "{question}"),
])
# Used in conversational chains  chat_history expands to a list of messages

# Pattern 5: with few-shot examples
prompt_e = ChatPromptTemplate.from_messages([
    ("system", "Answer drug questions precisely."),
    ("human", "What is the mechanism of aspirin?"),
    ("ai", "Aspirin inhibits COX-1 and COX-2 enzymes, reducing prostaglandin synthesis."),
    ("human", "What is the mechanism of {drug}?"),   # Template variable in last message
])

Input Variables and Validation

Python
prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a {specialty} specialist."),
    ("human", "Patient: {patient_age} years old. Drug: {drug}. {question}"),
])

# Check required variables
print(prompt.input_variables)
# ["specialty", "patient_age", "drug", "question"]

# LangChain raises if required variables are missing
try:
    prompt.format_messages(drug="warfarin", question="dosing?")
    # Raises: KeyError: 'specialty' and 'patient_age'
except Exception as e:
    print(f"Error: {e}")

# Correct invocation
messages = prompt.format_messages(
    specialty="clinical pharmacology",
    patient_age=72,
    drug="warfarin",
    question="What is the appropriate starting dose?",
)

Partial Variables

"Bake in" some variables upfront, leaving others for runtime:

Python
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()
model = ChatOpenAI(model="gpt-4o", temperature=0)

base_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a {specialty} specialist. Today is {date}."),
    ("human", "{question}"),
])

# Partial: fix specialty and date, leave question for runtime
import datetime

clinical_pharmacist_prompt = base_prompt.partial(
    specialty="clinical pharmacist",
    date=datetime.date.today().isoformat(),
)

# Now only needs "question"
print(clinical_pharmacist_prompt.input_variables)  # ["question"]

chain = clinical_pharmacist_prompt | model | parser
result = chain.invoke({"question": "What is the warfarin dose for AFib?"})

Template Composition

Build complex prompts from smaller parts:

Python
from langchain_core.prompts import PipelinePromptTemplate

# Compose prompts from reusable parts
system_part = PromptTemplate.from_template(
    "You are a {role}. Your guidelines follow {standard}."
)

safety_part = PromptTemplate.from_template(
    "Safety rule: {safety_rule}"
)

question_part = PromptTemplate.from_template("{question}")

# Full prompt pipeline
full_template = PromptTemplate.from_template(
    "{system}\n\n{safety}\n\nQuestion: {question_text}"
)

pipeline_prompt = PipelinePromptTemplate(
    final_prompt=full_template,
    pipeline_prompts=[
        ("system", system_part),
        ("safety", safety_part),
        ("question_text", question_part),
    ],
)

result_prompt = pipeline_prompt.format(
    role="clinical pharmacist",
    standard="ASHP guidelines",
    safety_rule="Always recommend professional consultation for specific patient cases.",
    question="What is warfarin dosing?",
)
print(result_prompt)

Format Instructions for Structured Output

Python
from langchain_core.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class DrugProfile(BaseModel):
    name: str = Field(description="Generic drug name")
    drug_class: str = Field(description="Pharmacological class")
    mechanism: str = Field(description="Mechanism of action in one sentence")
    half_life: str = Field(description="Elimination half-life")
    monitoring: list[str] = Field(description="Required clinical monitoring")

# PydanticOutputParser generates format instructions
parser = PydanticOutputParser(pydantic_object=DrugProfile)

# Inject format instructions into prompt
structured_prompt = ChatPromptTemplate.from_messages([
    ("system", "Extract drug information. {format_instructions}"),
    ("human", "Drug: {drug}"),
]).partial(format_instructions=parser.get_format_instructions())

# The format instructions tell the model exactly what JSON to return
print(structured_prompt.format_messages(drug="warfarin")[0].content[:200])
# "Extract drug information. The output should be formatted as a JSON instance..."

chain = structured_prompt | ChatOpenAI(model="gpt-4o") | parser
profile = chain.invoke({"drug": "warfarin"})
print(profile.name)        # str
print(profile.monitoring)  # list[str]

Dynamic Templates

Build templates programmatically based on runtime conditions:

Python
def build_clinical_prompt(
    specialty: str,
    include_safety_warning: bool = True,
    response_style: str = "technical",
) -> ChatPromptTemplate:
    """Build a clinical prompt customized to context."""
    messages = [
        ("system", f"You are a {specialty} specialist."),
    ]

    if include_safety_warning:
        messages.append((
            "system",
            "Important: For all clinical recommendations, note that individual patient assessment is required. "
            "This is for educational purposes only."
        ))

    if response_style == "technical":
        messages.append(("system", "Use precise medical terminology and cite evidence levels."))
    elif response_style == "patient_friendly":
        messages.append(("system", "Use simple language suitable for patients. Avoid jargon."))

    messages.append(("human", "{question}"))
    return ChatPromptTemplate.from_messages(messages)

# Build for different use cases
clinician_prompt = build_clinical_prompt("clinical pharmacology", include_safety_warning=False)
patient_prompt = build_clinical_prompt("pharmacist", response_style="patient_friendly")

clinician_chain = clinician_prompt | model | parser
patient_chain = patient_prompt | model | parser

Prompt Best Practices

Python
# 1. Use system messages for role, constraints, and output format
# 2. Use human messages for the actual query
# 3. Keep {variables} minimal  every variable is a possible injection point
# 4. Use .partial() for configuration, not .invoke() with fixed values
# 5. Validate input variables before building chains

def safe_invoke(chain, inputs: dict, required_keys: list[str]) -> str:
    """Validate inputs before invoking chain."""
    missing = [k for k in required_keys if k not in inputs or not inputs[k]]
    if missing:
        raise ValueError(f"Missing required inputs: {missing}")
    return chain.invoke(inputs)

# 6. For multi-turn: use MessagesPlaceholder (placeholder) for chat history
from langchain_core.prompts import MessagesPlaceholder

conversational_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist."),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{question}"),
])