Models, Prompts, Chains, Tools: The Four Primitives
Understand the four core LangChain abstractions: language models, prompt templates, chains, and tools. How they compose to build AI applications.
The Four Primitives
Every LangChain application is built from four composable building blocks:
- Models ā Wrappers around LLMs (OpenAI, Anthropic, etc.)
- Prompts ā Templates that format inputs for the model
- Chains ā Sequences that connect prompts, models, and processing steps
- Tools ā Functions the model can call to take actions
Understanding how these four primitives compose is the foundation of LangChain development.
Primitive 1: Models
Models are the LLM wrappers. LangChain provides a unified interface regardless of provider:
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_community.llms import Ollama
# All have the same interface: .invoke(), .stream(), .batch()
openai_model = ChatOpenAI(model="gpt-4o", temperature=0)
claude_model = ChatAnthropic(model="claude-sonnet-4-6", temperature=0)
local_model = Ollama(model="llama3")
# Call any model the same way
response = openai_model.invoke("What is the mechanism of warfarin?")
print(response.content)
# Two categories of models
from langchain_openai import OpenAI, ChatOpenAI
# LLM: text-in, text-out (legacy)
llm = OpenAI(model="gpt-3.5-turbo-instruct")
text_response = llm.invoke("The capital of France is")
# ChatModel: messages-in, message-out (modern)
chat = ChatOpenAI(model="gpt-4o")
from langchain_core.messages import HumanMessage
message_response = chat.invoke([HumanMessage(content="What is 2+2?")])Why models are abstracted: You can swap providers without rewriting chains. A chain built for OpenAI works with Claude by changing one line.
Primitive 2: Prompts
Prompts are reusable, parameterized templates:
from langchain_core.prompts import ChatPromptTemplate, PromptTemplate
# Simple string template
simple_prompt = PromptTemplate.from_template(
"Explain {concept} in terms a {audience} would understand."
)
# Chat prompt (system + human turns)
chat_prompt = ChatPromptTemplate.from_messages([
("system", "You are a clinical pharmacist who gives precise, evidence-based answers."),
("human", "What is the recommended dose of {drug} for {condition}?"),
])
# Format the prompt
formatted = chat_prompt.format_messages(drug="warfarin", condition="atrial fibrillation")
# Returns: [SystemMessage(...), HumanMessage(...)]
# Invoke with a model
model = ChatOpenAI(model="gpt-4o", temperature=0)
response = (chat_prompt | model).invoke({
"drug": "warfarin",
"condition": "atrial fibrillation",
})
print(response.content)Why prompts are abstracted: Separate your prompt logic from your model calls. Reuse the same template with different models. Version control prompt templates independently.
Primitive 3: Chains
Chains connect multiple primitives into a pipeline. The modern way uses LCEL (LangChain Expression Language) with the | pipe operator:
from langchain_core.output_parsers import StrOutputParser
# Simple chain: prompt | model | output_parser
chain = chat_prompt | model | StrOutputParser()
# Invoke the chain
result = chain.invoke({"drug": "metformin", "condition": "type 2 diabetes"})
print(result) # Plain string output
# Sequential chain: output of step 1 feeds step 2
from langchain_core.runnables import RunnablePassthrough
diagnosis_prompt = ChatPromptTemplate.from_messages([
("system", "You are a clinical decision support assistant."),
("human", "Given this drug information:\n{drug_info}\n\nWhat monitoring is needed for a patient starting this drug?"),
])
# Step 1: Get drug info
drug_info_chain = chat_prompt | model | StrOutputParser()
# Step 2: Get monitoring requirements from step 1's output
monitoring_chain = (
{"drug_info": drug_info_chain}
| diagnosis_prompt
| model
| StrOutputParser()
)
result = monitoring_chain.invoke({
"drug": "warfarin",
"condition": "atrial fibrillation",
})Why chains are abstracted: Compose complex pipelines from simple pieces. The | syntax is intuitive ā data flows left to right.
Primitive 4: Tools
Tools are functions the model can call. They bridge the LLM to external systems:
from langchain_core.tools import tool
@tool
def get_drug_information(drug_name: str) -> str:
"""
Retrieve clinical information about a drug from the pharmacy database.
Use this to get dosing, indications, and contraindications.
"""
# In production: query a real drug database
drug_db = {
"warfarin": "Anticoagulant. Dose: 2-10mg daily. Monitor INR. CYP2C9 metabolized.",
"metformin": "Biguanide antidiabetic. Dose: 500-2550mg/day. Contraindicated in renal failure.",
}
return drug_db.get(drug_name.lower(), f"Drug '{drug_name}' not found in database.")
@tool
def check_drug_interaction(drug_a: str, drug_b: str) -> str:
"""
Check for interactions between two drugs.
Returns interaction severity and clinical notes.
"""
# Simplified example
interactions = {
("warfarin", "aspirin"): "Major ā increased bleeding risk. Monitor closely.",
("metformin", "alcohol"): "Moderate ā increased lactic acidosis risk.",
}
key = tuple(sorted([drug_a.lower(), drug_b.lower()]))
return interactions.get(key, "No known major interaction documented.")
# Tools are bound to models for agent use
model_with_tools = ChatOpenAI(model="gpt-4o").bind_tools([
get_drug_information,
check_drug_interaction,
])
# The model can now decide to call these tools
response = model_with_tools.invoke(
"What is the interaction between warfarin and aspirin?"
)
print(response.tool_calls) # Contains the tool call if model decided to call itHow the Four Primitives Compose
from langchain.agents import create_tool_calling_agent, AgentExecutor
# A complete agent uses all four primitives:
# 1. Model: ChatOpenAI ā the reasoning engine
# 2. Prompt: system + messages template ā instructions + chat history
# 3. Tools: drug_info + interaction checker ā what the model can do
# 4. Chain (AgentExecutor): connects model ā tools ā model in a loop
system_prompt = ChatPromptTemplate.from_messages([
("system", "You are a clinical pharmacist. Use your tools to answer drug questions accurately."),
("placeholder", "{chat_history}"),
("human", "{input}"),
("placeholder", "{agent_scratchpad}"),
])
tools = [get_drug_information, check_drug_interaction]
model = ChatOpenAI(model="gpt-4o", temperature=0)
agent = create_tool_calling_agent(model, tools, system_prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({
"input": "I have a patient on warfarin who needs aspirin. What should I know?",
"chat_history": [],
})
print(result["output"])Primitive Comparison Summary
| Primitive | Purpose | Composable with | Key class | |---|---|---|---| | Model | Text/message generation | Prompt, Parser | ChatOpenAI, ChatAnthropic | | Prompt | Format inputs | Model | ChatPromptTemplate | | Chain (LCEL) | Connect primitives | Everything | RunnableSequence (via pipe) | | Tool | External actions | Model (via bind_tools) | @tool decorator |
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.