Skip to main content

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")
tip

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:

export OPENAI_API_KEY="sk-..."

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:

  1. Prompt template filled the {ticket_text} variable and built a message list.
  2. Model received the messages and generated a response.
  3. Output parser converted the response into a Pydantic model instance.
tip

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:

RolePurpose
systemSets behavior, tone, and constraints for the model.
humanThe user's input or question.
aiA previous assistant response (used for few-shot examples).
toolOutput 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."})
info

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"]
tip

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."
warning

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:

tip

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.