Learnixo
Back to blog
AI Systemsintermediate

Composing Complex Prompts from Parts

Build modular, reusable prompts in LangChain. Combine system instructions, few-shot examples, context blocks, and format requirements into composable prompt components.

Asma Hafeez KhanMay 16, 20265 min read
LangChainPrompt CompositionModular PromptsReusabilityTemplates
Share:𝕏

Why Compose Prompts?

A large system prompt is hard to:

  • Test — which part caused the bad output?
  • Reuse — same instructions in different chains
  • Update — changing one piece shouldn't break others
  • Version control — track changes to individual components

Composable prompts solve this by treating prompt components like code modules.


Approach 1: .partial() for Default Values

Python
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
import datetime

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

# Base prompt with all variables
base_prompt = ChatPromptTemplate.from_messages([
    ("system",
     "You are a {specialty} at {institution}. "
     "Today is {date}. "
     "Follow {guidelines_standard} guidelines."),
    ("human", "{question}"),
])

# Create specialized variants via partial()
clinical_pharmacist_prompt = base_prompt.partial(
    specialty="clinical pharmacist",
    institution="General Hospital",
    date=datetime.date.today().isoformat(),
    guidelines_standard="ASHP",
)

icu_pharmacist_prompt = base_prompt.partial(
    specialty="ICU clinical pharmacist",
    institution="Medical Center ICU",
    date=datetime.date.today().isoformat(),
    guidelines_standard="SCCM",
)

# Each variant only needs "question" at runtime
clinical_chain = clinical_pharmacist_prompt | model | parser
icu_chain = icu_pharmacist_prompt | model | parser

clinical_answer = clinical_chain.invoke({"question": "What is the warfarin dose for AFib?"})
icu_answer = icu_chain.invoke({"question": "What is the heparin protocol for DVT prophylaxis?"})

Approach 2: Building Blocks with Concatenation

Python
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import SystemMessage

# Reusable system instruction blocks
SAFETY_DISCLAIMER = (
    "Clinical decisions must be individualized. "
    "This information is for educational purposes. "
    "Professional consultation is required for patient care."
)

CITATION_REQUIREMENT = (
    "When making clinical claims, indicate the evidence level: "
    "(A) RCT evidence, (B) observational evidence, (C) expert consensus."
)

RESPONSE_FORMAT = (
    "Structure your response with:\n"
    "1. Direct answer\n"
    "2. Clinical rationale\n"
    "3. Monitoring/follow-up\n"
)

def build_clinical_prompt(
    role: str,
    include_safety: bool = True,
    include_citations: bool = True,
    include_format: bool = True,
) -> ChatPromptTemplate:
    """Build a clinical prompt from modular components."""
    system_parts = [f"You are a {role}."]

    if include_safety:
        system_parts.append(SAFETY_DISCLAIMER)
    if include_citations:
        system_parts.append(CITATION_REQUIREMENT)
    if include_format:
        system_parts.append(RESPONSE_FORMAT)

    system_content = " ".join(system_parts)

    return ChatPromptTemplate.from_messages([
        ("system", system_content),
        ("human", "{question}"),
    ])

# Different configurations
research_prompt = build_clinical_prompt(
    "clinical researcher",
    include_safety=False,
    include_citations=True,
)

patient_prompt = build_clinical_prompt(
    "pharmacist counseling patients",
    include_citations=False,
    include_format=False,
)

clinician_prompt = build_clinical_prompt(
    "clinical pharmacist",
    include_safety=True,
    include_citations=True,
    include_format=True,
)

Approach 3: MessagesPlaceholder for Dynamic Sections

Python
from langchain_core.prompts import MessagesPlaceholder, FewShotChatMessagePromptTemplate

# Compose from discrete placeholder sections
def build_full_clinical_prompt(
    role: str,
    examples: list[dict] = None,
    with_history: bool = True,
) -> ChatPromptTemplate:
    """Build a complete clinical prompt with optional few-shot and history."""
    messages = [
        ("system", f"You are a {role}. Answer with clinical precision."),
    ]

    # Inject few-shot examples if provided
    if examples:
        example_prompt = ChatPromptTemplate.from_messages([
            ("human", "{input}"),
            ("ai", "{output}"),
        ])
        few_shot_section = FewShotChatMessagePromptTemplate(
            examples=examples,
            example_prompt=example_prompt,
        )
        messages.append(few_shot_section)

    # Chat history placeholder
    if with_history:
        messages.append(MessagesPlaceholder("chat_history", optional=True))

    # Current question
    messages.append(("human", "{question}"))

    return ChatPromptTemplate.from_messages(messages)


# Demonstrate usage
examples = [
    {
        "input": "What is the mechanism of warfarin?",
        "output": "Warfarin inhibits VKORC1, blocking vitamin K recycling. Result: reduced synthesis of clotting factors II, VII, IX, X. (Evidence Level A)",
    },
]

full_prompt = build_full_clinical_prompt(
    role="clinical pharmacologist",
    examples=examples,
    with_history=True,
)

from langchain_core.messages import HumanMessage, AIMessage

chain = full_prompt | model | parser
result = chain.invoke({
    "question": "What is the mechanism of rivaroxaban?",
    "chat_history": [
        HumanMessage(content="What anticoagulants do you know?"),
        AIMessage(content="Warfarin, rivaroxaban, apixaban, dabigatran are major anticoagulants."),
    ],
})

Approach 4: Context Injection Pattern

Python
from langchain_core.runnables import RunnablePassthrough, RunnableLambda

# Pattern: inject retrieved context into prompt at runtime
CONTEXT_BLOCK_TEMPLATE = """Relevant clinical information:
{context}

---
Based on the above information, answer the following question.
If the information is insufficient, say so explicitly."""

rag_prompt = ChatPromptTemplate.from_messages([
    ("system", "You are a clinical pharmacist. Answer only from provided context."),
    ("human",
     f"{CONTEXT_BLOCK_TEMPLATE}\n\nQuestion: {{question}}"),
])

# The context variable gets filled with retrieved documents
def format_retrieved_docs(docs: list) -> str:
    return "\n\n".join([
        f"[Source {i+1}: {d.get('title', 'Unknown')}]\n{d['content']}"
        for i, d in enumerate(docs)
    ])

# RAG chain with dynamic context injection
rag_chain = (
    RunnablePassthrough.assign(
        context=lambda inputs: format_retrieved_docs(inputs.get("retrieved_docs", []))
    )
    | rag_prompt
    | model
    | parser
)

result = rag_chain.invoke({
    "question": "What monitoring is needed for warfarin?",
    "retrieved_docs": [
        {"title": "Warfarin FDA Label", "content": "Monitor INR every 1-4 weeks..."},
        {"title": "Lexicomp", "content": "Target INR for AFib: 2.0-3.0..."},
    ],
})

Approach 5: Prompt Registry

Python
from dataclasses import dataclass
from typing import Callable

@dataclass
class PromptConfig:
    """Configuration for a named prompt."""
    name: str
    description: str
    builder: Callable[[], ChatPromptTemplate]
    required_vars: list[str]
    optional_vars: list[str]


class PromptRegistry:
    """Central registry of prompt configurations."""

    def __init__(self):
        self._registry: dict[str, PromptConfig] = {}

    def register(self, config: PromptConfig) -> None:
        self._registry[config.name] = config

    def get(self, name: str) -> ChatPromptTemplate:
        if name not in self._registry:
            raise KeyError(f"Prompt '{name}' not in registry")
        return self._registry[name].builder()

    def list_prompts(self) -> list[str]:
        return list(self._registry.keys())


# Build the registry
registry = PromptRegistry()

registry.register(PromptConfig(
    name="clinical_pharmacist",
    description="Standard clinical pharmacist prompt",
    builder=lambda: build_clinical_prompt("clinical pharmacist"),
    required_vars=["question"],
    optional_vars=["chat_history"],
))

registry.register(PromptConfig(
    name="drug_interaction_specialist",
    description="Specialized for drug interaction queries",
    builder=lambda: ChatPromptTemplate.from_messages([
        ("system", "You are a clinical pharmacologist specializing in drug interactions. "
                   "Classify interactions as: Contraindicated / Major / Moderate / Minor. "
                   "Explain the mechanism for each interaction."),
        ("human", "{question}"),
    ]),
    required_vars=["question"],
    optional_vars=[],
))

registry.register(PromptConfig(
    name="patient_counselor",
    description="Patient-friendly explanations",
    builder=lambda: ChatPromptTemplate.from_messages([
        ("system", "You are a pharmacist counseling patients. Use plain language. "
                   "Avoid jargon. Focus on what the patient needs to know and do."),
        ("human", "{question}"),
    ]),
    required_vars=["question"],
    optional_vars=[],
))

# Use registry to build chains dynamically
def build_chain_for_prompt(prompt_name: str) -> "Chain":
    prompt = registry.get(prompt_name)
    return prompt | model | parser

# Select prompt based on context
def smart_chain(question: str, user_type: str) -> str:
    prompt_map = {
        "clinician": "clinical_pharmacist",
        "specialist": "drug_interaction_specialist",
        "patient": "patient_counselor",
    }
    prompt_name = prompt_map.get(user_type, "clinical_pharmacist")
    chain = build_chain_for_prompt(prompt_name)
    return chain.invoke({"question": question})

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.