Building Your First LLM Chain
LangChain is the framework our team uses to build LLM-powered applications. This guide walks you through the core concepts and has you running a working chain in minutes.
If you know Python and want to build your first application that connects to a large language model, this is the place to start.
What Is LangChainβ
LangChain is a Python framework for building applications powered by large language models. It provides a consistent interface across model providers, composable building blocks for constructing pipelines, and tooling for debugging and evaluation.
Why We Use Itβ
- Provider-agnostic abstractions β switch between OpenAI, Anthropic, Azure OpenAI, and others with minimal code changes.
- Compositional primitives β chain prompts, models, parsers, and tools together using a consistent API.
- Observability β integrates with LangSmith for tracing, debugging, and evaluating LLM calls in production.
- Ecosystem β a large library of pre-built integrations for retrieval, memory, agents, and more.
High-Level Architectureβ
Every LangChain application is composed of building blocks that connect to form a pipeline:
User Input β Prompt Template β Chat Model β Output Parser β Structured Result
β β
(fills variables) (OpenAI, Anthropic, Azure...)
The modern way to compose these blocks is LCEL (LangChain Expression Language), which we cover in detail below. Under the hood, LangChain's higher-level constructs β including agents β are built on LCEL.
Core Conceptsβ
Before writing code, understand the five building blocks you will combine.
Modelsβ
Models are the reasoning engine. LangChain supports two model types:
- ChatModels β models that understand conversation structure (system, human, AI messages). This is what you will use 99% of the time.
- LLMs β simpler text-in, text-out models (legacy, rarely needed today).
You initialize a model by specifying a provider and model name:
from langchain.chat_models import init_chat_model
# Initialize from a string identifier
model = init_chat_model("gpt-4o")
# Or initialize directly from a provider class
from langchain_openai import ChatOpenAI
model = ChatOpenAI(model="gpt-4o")
Always set your API key via environment variables, never hardcode them. LangChain reads OPENAI_API_KEY, ANTHROPIC_API_KEY, AZURE_OPENAI_API_KEY, etc. automatically.
Promptsβ
Prompts structure the input sent to the model. LangChain provides two prompt template types:
PromptTemplateβ for simple string prompts with variable injection.ChatPromptTemplateβ for structured conversations with system, human, and AI messages.
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
("system", "You are a helpful assistant that answers in {language}."),
("human", "{question}"),
])
Output Parsersβ
Output parsers convert the model's free-text response into structured data β JSON, Pydantic models, or custom formats. They sit between the model and your application code:
Model response (string) β Output Parser β Structured data (dict, BaseModel, β¦)
Chainsβ
A chain is a sequence of components wired together. With LCEL, you compose chains using the | (pipe) operator:
chain = prompt | model | parser
This reads naturally: the prompt feeds into the model, and the model output feeds into the parser.
Toolsβ
Tools give the model access to external functionality β databases, APIs, file systems, and custom Python functions. A tool is a callable with a name, description, and input schema:
from langchain.tools import tool
@tool
def search_knowledge_base(query: str) -> str:
"""Search the internal knowledge base for relevant documents."""
# Your implementation here
return "Results..."
The model reads the tool's description to decide when to call it.
Your First Chainβ
Let's build a working chain from scratch. This chain will take a user question, send it through a prompt template to an LLM, and parse the response into structured JSON.
Step 1 β Install Dependenciesβ
We use uv for package management:
uv init llm-chain-demo
cd llm-chain-demo
# Install LangChain with your preferred provider
uv add "langchain[openai]"
# Or for Anthropic: uv add "langchain[anthropic]"
# Or for Azure: uv add "langchain[openai]"
uv sync
Step 2 β Set Environment Variablesβ
Export your API key before running any code:
- OpenAI
- Azure OpenAI
- Anthropic
export OPENAI_API_KEY="sk-..."
export AZURE_OPENAI_API_KEY="..."
export AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com"
export AZURE_OPENAI_DEPLOYMENT_NAME="gpt-4o"
export OPENAI_API_VERSION="2024-08-01-preview"
export ANTHROPIC_API_KEY="sk-ant-..."
Step 3 β Build the Chainβ
Create a file chain.py and build a chain that classifies a customer support ticket:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
# --- Define the output schema ---
class TicketClassification(BaseModel):
category: str = Field(description="Category: billing, technical, account, general")
urgency: str = Field(description="Urgency level: low, medium, high, critical")
summary: str = Field(description="One-sentence summary of the issue")
# --- Initialize the model ---
# Swap the model string to change providers
model = init_chat_model("gpt-4o")
# --- Build the prompt ---
prompt = ChatPromptTemplate.from_messages([
(
"system",
"You are a support ticket classifier. Analyze the ticket and return "
"a JSON object with category, urgency, and summary fields.",
),
("human", "Classify this ticket:\n{ticket_text}"),
])
# --- Build the output parser ---
parser = JsonOutputParser(pydantic_object=TicketClassification)
# --- Compose the chain with LCEL ---
chain = prompt | model | parser
# --- Run it ---
result = chain.invoke({"ticket_text": "My credit card was charged twice for the same subscription."})
print(result)
# TicketClassification(category='billing', urgency='high', summary='Customer reports duplicate charge for subscription.')
Step 4 β Run Itβ
uv run python chain.py
You should see a TicketClassification object with populated fields. The chain did three things in sequence:
- Prompt template filled the
{ticket_text}variable and built a message list. - Model received the messages and generated a response.
- Output parser converted the response into a Pydantic model instance.
To switch providers, change the model string and the environment variable. For Azure OpenAI, use init_chat_model("azure_openai:gpt-4o", azure_deployment="your-deployment"). For Anthropic, use init_chat_model("claude-sonnet-4-20250514").
Prompt Templatesβ
Prompt templates are how you parameterize and reuse prompts. Let's go deeper.
String Prompts vs. Chat Promptsβ
PromptTemplate produces a single string. It works but does not leverage the model's understanding of conversation structure:
from langchain_core.prompts import PromptTemplate
string_prompt = PromptTemplate.from_template(
"Translate the following text to {target_language}:\n{source_text}"
)
result = string_prompt.format(target_language="French", source_text="Hello, how are you?")
# "Translate the following text to French:\nHello, how are you?"
ChatPromptTemplate produces a list of messages with roles. This is the recommended approach because modern models are trained on multi-turn conversations and respond better to structured prompts:
from langchain_core.prompts import ChatPromptTemplate
chat_prompt = ChatPromptTemplate.from_messages([
("system", "You are a professional translator. Translate accurately and maintain tone."),
("human", "Translate to {target_language}:\n{source_text}"),
])
messages = chat_prompt.invoke({"target_language": "French", "source_text": "Hello, how are you?"})
# [SystemMessage(content="You are a professional translator..."),
# HumanMessage(content="Translate to French:\nHello, how are you?")]
Message Typesβ
Chat prompts support four message roles:
| Role | Purpose |
|---|---|
system | Sets behavior, tone, and constraints for the model. |
human | The user's input or question. |
ai | A previous assistant response (used for few-shot examples). |
tool | Output from a tool call (used in agent loops). |
Variable Injection and Few-Shot Examplesβ
Variables are injected using {variable_name} syntax. For few-shot prompting, include example exchanges as ai and human messages:
from langchain_core.prompts import ChatPromptTemplate
few_shot_prompt = ChatPromptTemplate.from_messages([
("system", "Classify the sentiment of each review as positive, negative, or neutral."),
# Few-shot example 1
("human", "The product broke after one day. Terrible quality."),
("ai", "negative"),
# Few-shot example 2
("human", "Decent for the price. Nothing special."),
("ai", "neutral"),
# Actual user input
("human", "{review}"),
])
result = few_shot_prompt.invoke({"review": "Absolutely love it! Best purchase this year."})
Few-shot examples are powerful because they give the model concrete patterns to follow. Place them between the system message and the user input for maximum effect.
Prompt Management Best Practicesβ
- Keep prompts in version control. Store template strings in files or a prompt registry, not scattered across code.
- Parameterize, do not hardcode. Use variables for anything that changes between invocations β language, tone, domain-specific terms.
- Test prompts independently. Validate prompt templates produce the expected message structure before chaining them to models.
- Use the system message for behavior, not data. The system message should define how the model behaves; put what it works on in the human message.
Output Parsingβ
Getting structured data from an LLM is one of the most important patterns in production systems. LangChain provides several approaches.
JSON Output Parsingβ
The JsonOutputParser instructs the model to return valid JSON and parses the response:
from langchain_core.output_parsers import JsonOutputParser
# Basic JSON parsing β no schema validation
parser = JsonOutputParser()
# The parser provides a format instruction you can include in your prompt
format_instruction = parser.get_format_instructions()
# "The output should be formatted as a JSON instance that conforms to the JSON schema..."
prompt = ChatPromptTemplate.from_messages([
("system", f"Return your answer as JSON.\n\n{format_instruction}"),
("human", "List three Python web frameworks with their primary use case."),
])
chain = prompt | model | parser
result = chain.invoke({})
# [{'framework': 'Django', 'use_case': 'Full-stack web applications'}, ...]
Pydantic Output Parsingβ
For production code, Pydantic models give you type safety and validation:
from pydantic import BaseModel, Field
from langchain_core.output_parsers import JsonOutputParser
class ReviewAnalysis(BaseModel):
sentiment: str = Field(description="One of: positive, negative, neutral")
score: float = Field(description="Rating from 0.0 to 5.0")
key_topics: list[str] = Field(description="Main topics mentioned")
actionable: bool = Field(description="Whether the review requires action")
parser = JsonOutputParser(pydantic_object=ReviewAnalysis)
prompt = ChatPromptTemplate.from_messages([
("system", "Analyze the customer review and return structured data."),
("human", "{review_text}"),
])
chain = prompt | model | parser
result = chain.invoke({"review_text": "Great product but shipping took forever. Packaging was damaged."})
print(result.sentiment) # "mixed" or "neutral"
print(result.score) # 3.0
print(result.key_topics) # ["product quality", "shipping", "packaging"]
When using JsonOutputParser(pydantic_object=...), the parser automatically includes the Pydantic schema in the prompt instructions, so the model knows exactly what structure to produce.
Custom Output Parsersβ
Sometimes built-in parsers are not enough. You can create a custom parser by implementing the OutputParser interface:
from langchain_core.output_parsers import BaseOutputParser
class SentimentScoreParser(BaseOutputParser[float]):
"""Extract a sentiment score from free-text responses."""
def parse(self, text: str) -> float:
# Extract a number from the model's response
import re
match = re.search(r"(-?\d+\.?\d*)", text)
if match:
score = float(match.group(1))
return max(-1.0, min(1.0, score)) # Clamp to [-1, 1]
return 0.0 # Default neutral
@property
def _output_type(self) -> type:
return float
parser = SentimentScoreParser()
prompt = ChatPromptTemplate.from_messages([
("system", "Rate the sentiment of this text as a number from -1.0 (very negative) to 1.0 (very positive). Return only the number."),
("human", "{text}"),
])
chain = prompt | model | parser
result = chain.invoke({"text": "This is the worst experience I have ever had."})
print(result) # -0.9 or similar
Chainsβ
Chains are the backbone of LangChain applications. They let you compose components into pipelines that transform input into output.
LCEL β LangChain Expression Languageβ
LCEL is the recommended way to build chains. It uses the | pipe operator to compose Runnable components. Every prompt, model, and parser in LangChain is a Runnable:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
model = init_chat_model("gpt-4o")
prompt = ChatPromptTemplate.from_messages([
("system", "You are a concise summarizer."),
("human", "Summarize this in one sentence: {text}"),
])
# StrOutputParser extracts the text content from the AI message
chain = prompt | model | StrOutputParser()
result = chain.invoke({"text": "A long article about the history of computing..."})
print(result)
The chain reads left to right: prompt β model β parser. Each stage's output becomes the next stage's input.
The .pipe() Methodβ
As an alternative to the | operator, you can use .pipe() for the same composition:
chain = prompt.pipe(model).pipe(StrOutputParser())
Both | and .pipe() produce identical chains. Use whichever reads better in your codebase.
RunnablePassthrough and RunnableLambdaβ
Two utilities make chains more powerful:
RunnablePassthrough lets you pass data through a chain unchanged, or add new keys to the input dict:
from langchain_core.runnables import RunnablePassthrough
# Pass through with additional context
chain = (
RunnablePassthrough.assign(summary=lambda x: short_summary(x["document"]))
| prompt
| model
| StrOutputParser()
)
# The prompt now receives both "document" and "summary" variables
RunnableLambda wraps any Python callable as a Runnable, letting you inject custom logic:
from langchain_core.runnables import RunnableLambda
def preprocess(text: str) -> str:
return text.strip().lower()
def postprocess(response: str) -> str:
return response.replace("\n", " ").strip()
chain = (
RunnableLambda(preprocess)
| prompt
| model
| RunnableLambda(postprocess)
)
Chaining Multiple Stepsβ
Real-world chains often have multiple stages. Here is a chain that translates and then summarizes:
from langchain.chat_models import init_chat_model
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
model = init_chat_model("gpt-4o")
translate_prompt = ChatPromptTemplate.from_messages([
("system", "Translate the following text to English."),
("human", "{text}"),
])
summarize_prompt = ChatPromptTemplate.from_messages([
("system", "Summarize the following text in one sentence."),
("human", "{text}"),
])
# Chain: translate β summarize
chain = (
{"text": lambda x: x["input_text"]}
| translate_prompt
| model
| StrOutputParser()
| {"text": lambda x: x}
| summarize_prompt
| model
| StrOutputParser()
)
result = chain.invoke({"input_text": "Le changement climatique est l'un des plus grands dΓ©fis..."})
print(result)
# "Climate change is one of the greatest challenges facing humanity today."
Keep chains readable. If a chain grows beyond a few stages, extract intermediate steps into named functions or separate chain variables.
Next Stepsβ
You now understand the core building blocks and have a working chain. Here is where to go next:
- Prompt Engineering Best Practices β our team's conventions for writing reliable, maintainable prompts.
- LangGraph Documentation β for building stateful, multi-step agentic workflows with human-in-the-loop support.
- LangChain Models Reference β detailed provider setup, model parameters, and structured output configuration.
- LangChain Tools Reference β how to define tools, access runtime context, and integrate external APIs.
Set up LangSmith tracing early. Trace every chain invocation in development so you can debug prompts, measure latency, and evaluate output quality before things reach production.