This is one of the most important concepts in LangChain.
What is a Chain?
A chain is a sequence of steps where the output of one step becomes the input to the next step.
In simple terms:
A chain connects multiple components together to accomplish a task.
Real-World Example of chain
1) Imagine you’re building an AI research assistant.
User gives the query, then we search the arXiv, retrieve the necessary documents related to query. Suppose it return 40 pages of documents. Then we give this 40 pages to LLM to summarize. Then we translate into a language that user can understand.
User Query -> Search arXiv -> Retrieve Abstract -> LLM Summary -> Translate to Hindi/Spanish -> Return Result
2) Suppose you want a chatbot that answers questions from PDFs.
Question -> Retriever -> Relevant Documents -> Prompt (Question + Relevant Documents) -> LLM -> Parser (parse the output) -> Answer
LangChain Chains — Complete Tutorial¶
Connect LLM steps like LEGO blocks¶
LangChain version: 1.3.6 | Python: 3.11
Prerequisites: OpenAI API key, basic Python knowledge
What We’ll Cover¶
| # | Concept | What You’ll Build |
|---|---|---|
| 1 | Why Chains? | The problem they solve |
| 2 | The \| Pipe Operator |
Your first chain |
| 3 | Output Parsers | Clean, usable output |
| 4 | Sequential Chains | Output of A → Input of B |
| 5 | Parallel Chains | Multiple chains at once |
| 6 | Branching Chains | Different paths based on input |
| 7 | Real-world Mini Project | Movie review analyzer |
# !pip install -q langchain langchain-openai langchain-community openai
# !pip install langchain-groq
Collecting langchain-groq Downloading langchain_groq-1.1.3-py3-none-any.whl.metadata (2.9 kB) Collecting groq<1.0.0,>=0.30.0 (from langchain-groq) Downloading groq-0.37.1-py3-none-any.whl.metadata (16 kB) Requirement already satisfied: langchain-core<2.0.0,>=1.4.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-groq) (1.4.1) Requirement already satisfied: anyio<5,>=3.5.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (4.13.0) Requirement already satisfied: distro<2,>=1.7.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (1.9.0) Requirement already satisfied: httpx<1,>=0.23.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (0.28.1) Requirement already satisfied: pydantic<3,>=1.9.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (2.13.4) Requirement already satisfied: sniffio in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (1.3.1) Requirement already satisfied: typing-extensions<5,>=4.10 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from groq<1.0.0,>=0.30.0->langchain-groq) (4.15.0) Requirement already satisfied: jsonpatch<2.0.0,>=1.33.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (1.33) Requirement already satisfied: langchain-protocol>=0.0.14 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (0.0.16) Requirement already satisfied: langsmith<1.0.0,>=0.3.45 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (0.8.9) Requirement already satisfied: packaging>=23.2.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (24.2) Requirement already satisfied: pyyaml<7.0.0,>=5.3.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (6.0.3) Requirement already satisfied: tenacity!=8.4.0,<10.0.0,>=8.1.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (9.1.4) Requirement already satisfied: uuid-utils<1.0,>=0.12.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langchain-core<2.0.0,>=1.4.0->langchain-groq) (0.16.0) Requirement already satisfied: idna>=2.8 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from anyio<5,>=3.5.0->groq<1.0.0,>=0.30.0->langchain-groq) (3.13) Requirement already satisfied: certifi in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from httpx<1,>=0.23.0->groq<1.0.0,>=0.30.0->langchain-groq) (2026.4.22) Requirement already satisfied: httpcore==1.* in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from httpx<1,>=0.23.0->groq<1.0.0,>=0.30.0->langchain-groq) (1.0.9) Requirement already satisfied: h11>=0.16 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from httpcore==1.*->httpx<1,>=0.23.0->groq<1.0.0,>=0.30.0->langchain-groq) (0.16.0) Requirement already satisfied: jsonpointer>=1.9 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from jsonpatch<2.0.0,>=1.33.0->langchain-core<2.0.0,>=1.4.0->langchain-groq) (3.1.1) Requirement already satisfied: orjson>=3.9.14 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (3.11.9) Requirement already satisfied: requests-toolbelt>=1.0.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (1.0.0) Requirement already satisfied: requests>=2.0.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (2.33.1) Requirement already satisfied: websockets>=15.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (15.0.1) Requirement already satisfied: xxhash>=3.0.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (3.7.0) Requirement already satisfied: zstandard>=0.23.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (0.25.0) Requirement already satisfied: annotated-types>=0.6.0 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from pydantic<3,>=1.9.0->groq<1.0.0,>=0.30.0->langchain-groq) (0.7.0) Requirement already satisfied: pydantic-core==2.46.4 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from pydantic<3,>=1.9.0->groq<1.0.0,>=0.30.0->langchain-groq) (2.46.4) Requirement already satisfied: typing-inspection>=0.4.2 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from pydantic<3,>=1.9.0->groq<1.0.0,>=0.30.0->langchain-groq) (0.4.2) Requirement already satisfied: charset_normalizer<4,>=2 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (3.4.7) Requirement already satisfied: urllib3<3,>=1.26 in c:\users\hi\desktop\projects\python_projects\tutorial\play_langchain_llamaindex_langgraph\.venv\lib\site-packages (from requests>=2.0.0->langsmith<1.0.0,>=0.3.45->langchain-core<2.0.0,>=1.4.0->langchain-groq) (2.7.0) Downloading langchain_groq-1.1.3-py3-none-any.whl (20 kB) Downloading groq-0.37.1-py3-none-any.whl (137 kB) ---------------------------------------- 0.0/137.5 kB ? eta -:--:-- -------------------------------------- - 133.1/137.5 kB 8.2 MB/s eta 0:00:01 ---------------------------------------- 137.5/137.5 kB 4.1 MB/s eta 0:00:00 Installing collected packages: groq, langchain-groq Successfully installed groq-0.37.1 langchain-groq-1.1.3
[notice] A new release of pip is available: 24.0 -> 26.1.2 [notice] To update, run: python.exe -m pip install --upgrade pip
import importlib.metadata
# List the distribution package names
packages = ["langchain", "langchain-community", "langchain-openai", "openai",
"langchain_core", "langchain-groq",]
for package in packages:
try:
version = importlib.metadata.version(package)
print(f"{package} version: {version}")
except importlib.metadata.PackageNotFoundError:
print(f"{package} is not installed in this environment.")
langchain version: 1.3.6 langchain-community version: 0.4.2 langchain-openai version: 1.2.2 openai version: 2.36.0 langchain_core version: 1.4.1 langchain-groq version: 1.1.3
import os
from dotenv import load_dotenv
load_dotenv() # read .env file
# TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")
True
import os
from langchain_openai import ChatOpenAI
from langchain_groq import ChatGroq
# OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
# llm = ChatOpenAI(api_key=OPENAI_API_KEY, model="gpt-3.5-turbo", temperature=0.7)
GROQ_API_KEY = os.environ["GROQ_API_KEY"]
llm = ChatGroq(api_key=GROQ_API_KEY, model="llama-3.3-70b-versatile", temperature=0.7)
# Quick check
response = llm.invoke("Say hello in one word.")
print(" Connected! Test response:", response.content)
Connected! Test response: Hello.
1️. Why Do We Need Chains?¶
The Problem¶
Imagine you want to:
- Take a topic from the user
- Generate a blog post about it
- Translate that blog post to French
- Trim the blog to under 300 words
Without chains, you’d write this:
# BAD : Manual, messy, hard to reuse
response1 = llm.invoke(f"Write a blog post about {topic}")
blog_post = response1.content
response2 = llm.invoke(f"Translate this to French: {blog_post}")
translated = response2.content
response3 = llm.invoke(f"Shorten this to under 300 words {translated}")
final_text = response3.content
With chains, the same thing becomes:
# Clean, readable, reusable
result = (write_chain | translate_chain | summarize_chain).invoke({"topic": topic})
What is a Chain?¶
A Chain is a sequence of steps connected together, where the output of one step becomes the input of the next.
Input
↓
Step 1: Prompt Template (formats your input)
↓
Step 2: LLM (generates a response)
↓
Step 3: Output Parser (cleans the response)
↓
Output
LangChain uses the | pipe operator to connect steps — just like Unix terminal pipes!
# Let's see the PROBLEM first — manual, no chains
# from langchain_openai import ChatOpenAI
# llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7)
topic = "black holes"
# Step 1: manual call
response1 = llm.invoke(f"Give one interesting fact about {topic}.")
fact = response1.content
# Step 2: manual call — using output of step 1
response2 = llm.invoke(f"Make this fact sound more exciting: {fact}")
exciting_fact = response2.content
print("Original fact:")
print(fact)
print("---------------------")
print("\nExciting version:")
print(exciting_fact)
print("\n Works, but imagine doing this for 10 steps...")
Original fact: One interesting fact about black holes is that their gravity is so strong that not even light can escape once it gets too close to the event horizon, which is the point of no return around a black hole. This is why they appear "black" to observers, as no light or radiation can escape to be seen. --------------------- Exciting version: Imagine being sucked into a cosmic abyss so powerful that not even the fastest thing in the universe - light - can break free from its grasp. That's the mesmerizing reality of black holes, where gravity is so incredibly strong that it warps the very fabric of space and time. Once you cross the point of no return, known as the event horizon, you're trapped forever, and not even a photon of light can escape to tell the tale. It's this mind-boggling phenomenon that renders black holes invisible to our eyes, shrouding them in an impenetrable veil of darkness, making them appear "black" to us - a haunting reminder of the unfathomable power that lies at the heart of these cosmic monsters. ⚠️ Works, but imagine doing this for 10 steps...
2️) The | Pipe Operator — Your First Chain¶
LCEL: LangChain Expression Language¶
LangChain introduced LCEL (LangChain Expression Language) — a clean way to build chains using the | pipe.
prompt | llm | parser
Reads left to right:
promptformats the input into a proper messagellmsends it to the model and gets a responseparserconverts the response into a clean string
The 3 Core Building Blocks¶
| Block | Import | What it does |
|---|---|---|
ChatPromptTemplate |
langchain_core.prompts |
Formats your input with variables |
ChatOpenAI |
langchain_openai |
Calls the LLM |
StrOutputParser |
langchain_core.output_parsers |
Extracts just the text string |
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
# ── Block 1: Prompt Template ───────────────────────────────────────
# {topic} is a variable — gets filled in when you invoke the chain
prompt = ChatPromptTemplate.from_template(
"Give one interesting fact about {topic} in exactly 2 sentences."
)
# See what the prompt looks like BEFORE sending to LLM
rendered = prompt.format_messages(topic="black holes")
print("Rendered prompt:")
for msg in rendered:
print(f" [{msg.type}]: {msg.content}")
Rendered prompt: [human]: Give one interesting fact about black holes in exactly 2 sentences.
# ── Block 2: The Parser
parser = StrOutputParser()
# Without parser: response is an AIMessage object
raw_response = llm.invoke(rendered)
print("Without parser:")
print(f" Type: {type(raw_response)}")
print(f" Value: {raw_response}")
print()
# With parser: response is a plain string
clean_response = parser.invoke(raw_response)
print("With parser:")
print(f" Type: {type(clean_response)}")
print(f" Value: {clean_response}")
Without parser:
Type: <class 'langchain_core.messages.ai.AIMessage'>
Value: content='One interesting fact about black holes is that they have a point of no return called the event horizon, where the gravitational pull is so strong that not even light can escape once it crosses the boundary. This unique characteristic makes black holes some of the most fascinating and mysterious objects in the universe, captivating the imagination of scientists and space enthusiasts alike.' additional_kwargs={} response_metadata={'token_usage': {'completion_tokens': 68, 'prompt_tokens': 48, 'total_tokens': 116, 'completion_time': 0.170966944, 'completion_tokens_details': None, 'prompt_time': 0.002449291, 'prompt_tokens_details': None, 'queue_time': 0.052521299, 'total_time': 0.173416235}, 'model_name': 'llama-3.3-70b-versatile', 'system_fingerprint': 'fp_dae98b5ecb', 'service_tier': 'on_demand', 'finish_reason': 'stop', 'logprobs': None, 'model_provider': 'groq'} id='lc_run--019ed51c-9960-79c2-b4c0-23a33dca3deb-0' tool_calls=[] invalid_tool_calls=[] usage_metadata={'input_tokens': 48, 'output_tokens': 68, 'total_tokens': 116}
With parser:
Type: <class 'langchain_core.messages.base.TextAccessor'>
Value: One interesting fact about black holes is that they have a point of no return called the event horizon, where the gravitational pull is so strong that not even light can escape once it crosses the boundary. This unique characteristic makes black holes some of the most fascinating and mysterious objects in the universe, captivating the imagination of scientists and space enthusiasts alike.
# ── Block 3: Connect with | to make a Chain
# This is the magic moment — three objects connected with |
chain = prompt | llm | StrOutputParser()
print("Chain type:", type(chain))
print()
# Invoke the chain — just pass the variable values
result = chain.invoke({"topic": "black holes"})
print("Result type:", type(result)) # plain string!
print("Result:", result)
Chain type: <class 'langchain_core.runnables.base.RunnableSequence'> Result type: <class 'langchain_core.messages.base.TextAccessor'> Result: One interesting fact about black holes is that they have a point of no return, known as the event horizon, where the gravitational pull is so strong that anything that crosses it will be trapped forever. This phenomenon is due to the incredibly dense mass of black holes, which warps the fabric of spacetime around them, creating an intense gravitational field that cannot be escaped once the event horizon is crossed.
# ──Advantage of above: Reusability — same chain, different inputs
topics = ["quantum computers", "Monte carlo Method", "topology"]
print(" Interesting Facts:\n")
for topic in topics:
fact = chain.invoke({"topic": topic})
print(f" {topic.title()}:")
print(f" {fact}")
print()
🌍 Interesting Facts: 🔷 Quantum Computers: Quantum computers have the potential to solve complex problems that are currently unsolvable with traditional computers, thanks to their ability to process vast amounts of information simultaneously using quantum bits or qubits. One interesting fact about quantum computers is that they can exist in multiple states at the same time, allowing them to explore an exponentially large solution space in parallel, which could lead to breakthroughs in fields such as cryptography and optimization. 🔷 Monte Carlo Method: The Monte Carlo method is a computational algorithm that relies on repeated random sampling to obtain numerical results, and one interesting fact about it is that it was named after the famous Casino de Monte-Carlo in Monaco, reflecting the method's use of random chance. This method has been widely used in various fields, including physics, engineering, and finance, to solve complex problems that are difficult to solve using traditional deterministic methods. 🔷 Topology: Topology is a branch of mathematics that studies the properties of shapes and spaces that are preserved under continuous deformations, such as stretching and bending, but not tearing or gluing. One interesting fact about topology is that a coffee mug and a doughnut are considered topologically equivalent, meaning they can be transformed into each other through continuous deformations without any tearing or gluing.
# ── Multiple variables in a prompt
# Prompts can have as many {variables} as you need
explain_chain = (
ChatPromptTemplate.from_template(
"Explain {concept} to someone who is {audience}. "
"Keep it under {sentences} sentences."
)
| llm
| StrOutputParser()
)
# Invoke with all three variables
result = explain_chain.invoke({
"concept": "machine learning",
"audience": "a 12-year-old",
"sentences": 3
})
print(result)
Machine learning is a way to teach computers to learn and make decisions on their own, kind of like how you learn new things in school. It works by showing the computer lots of examples of something, like pictures of dogs and cats, and then it can start to recognize patterns and make predictions, like "oh, that's a dog!" or "that's a cat!". This helps computers get better at doing tasks, like recognizing pictures or understanding what people are saying, without being explicitly told what to do.
# YOUR TURN
# Change the concept, audience, and sentences below and run!
result = explain_chain.invoke({
"concept": "the stock market", # ← change this
"audience": "a curious grandparent", # ← change this
"sentences": 3 # ← change this
})
print(result)
The stock market is like a place where people can buy and sell small pieces of companies, kind of like buying a tiny part of your favorite store. When you buy a "stock," you're essentially betting that the company will do well and become more valuable, so you can sell your piece for a higher price later. Think of it like owning a tiny piece of a lemonade stand - if the stand becomes super popular, the value of your piece might go up, and you could sell it for a profit.
3️) Output Parsers — Getting Useful Data Back¶
The LLM always returns raw text. Output parsers convert that text into something your code can actually use.
| Parser | Output type | Use when you need |
|---|---|---|
StrOutputParser |
str |
Plain text — most common |
JsonOutputParser |
dict |
Structured data |
CommaSeparatedListOutputParser |
list |
A list of items |
PydanticOutputParser |
Pydantic model | Typed, validated data |
from langchain_core.output_parsers import StrOutputParser, JsonOutputParser
from langchain_core.output_parsers import CommaSeparatedListOutputParser
# ── Parser 1: StrOutputParser (you've seen this)
str_chain = (
ChatPromptTemplate.from_template("Name the capital of {country}.")
| llm
| StrOutputParser()
)
result = str_chain.invoke({"country": "Japan"})
print(f"StrOutputParser → type: {type(result).__name__}")
print(f"value: {result}")
StrOutputParser → type: TextAccessor value: The capital of Japan is Tokyo.
# ── Parser 2: CommaSeparatedListOutputParser ─
list_parser = CommaSeparatedListOutputParser()
list_chain = (
ChatPromptTemplate.from_template(
"List 5 {category}. Return ONLY a comma-separated list, nothing else."
)
| llm
| list_parser
)
result = list_chain.invoke({"category": "programming languages"})
print(f"Type: {type(result).__name__}")
print(f"Value: {result}")
print(f"First item: {result[0]}")
print(f"Last item: {result[-1]}")
print(f"Count: {len(result)}")
Type: list Value: ['Java', 'Python', 'C++', 'JavaScript', 'Ruby'] First item: Java Last item: Ruby Count: 5
# ── Parser 3: JsonOutputParser
# Tell the LLM exactly what JSON shape you want back
json_chain = (
ChatPromptTemplate.from_template(
"""
Return information about {city} as JSON with these exact 4 keys:
name, country, population_millions, famous_for (list of 3 things).
Return ONLY valid JSON, no extra text.
"""
)
| llm
| JsonOutputParser()
)
result = json_chain.invoke({"city": "Tokyo"})
print(f"Type: {type(result).__name__}")
print(f"City: {result['name']}")
print(f"Country: {result['country']}")
print(f"Population: {result['population_millions']}M")
print(f"Famous for: {result['famous_for']}")
Type: dict City: Tokyo Country: Japan Population: 38.14M Famous for: ['Neon Districts', 'Sushi Restaurants', 'Ancient Temples']
# ── Parser 4: Pydantic — typed and validated output
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List
# Define the exact shape of data you want
class MovieReview(BaseModel):
title: str = Field(description="Movie title")
year: int = Field(description="Release year")
rating: float = Field(description="Rating out of 10")
pros: List[str] = Field(description="List of 2 things done well")
cons: List[str] = Field(description="List of 2 weaknesses")
verdict: str = Field(description="One sentence summary")
parser = JsonOutputParser(pydantic_object=MovieReview)
review_chain = (
ChatPromptTemplate.from_messages([
("system", "You are a film critic. Always respond in valid JSON."),
("human", "Review the movie '{title}'. {instructions}"),
])
| llm
| parser
)
result = review_chain.invoke({
"title": "Inception",
"instructions": parser.get_format_instructions()
})
print(f"{result['title']} ({result['year']}) — {result['rating']}/10")
print(f"Pros: {result['pros']}")
print(f"Cons: {result['cons']}")
print(f"Verdict: {result['verdict']}")
Inception (2010) — ⭐ 9.2/10 Pros: ['Mind-bending storyline with multiple layers', 'Exceptional visual effects and action sequences'] Cons: ['Complex plot may be difficult to follow for some viewers', 'Character development is somewhat lacking'] Verdict: Inception is a thought-provoking and visually stunning sci-fi action film that will keep you on the edge of your seat.
4️) Sequential Chains — Chaining LLM Calls Together¶
This is where chains get powerful. The output of one LLM call becomes the input of the next.
Topic
↓
Chain A: "Write a blog post about {topic}"
↓ (blog post text)
Chain B: "Summarize this into 3 bullet points: {blog_post}"
↓ (bullet points)
Chain C: "Translate these bullet points to Spanish: {bullets}"
↓
Final Output
LangChain uses RunnablePassthrough to route values between steps.
from langchain_core.runnables import RunnablePassthrough
# ── Simple 2-step sequential chain
# Step 1: Write a poem
write_poem = (
ChatPromptTemplate.from_template("Write a short 4-line poem about {topic}.")
| llm
| StrOutputParser()
)
# Step 2: Analyse the poem (receives poem as input)
analyse_poem = (
ChatPromptTemplate.from_template(
"Analyse this poem in one sentence — identify its mood and style:\n\n{poem}"
)
| llm
| StrOutputParser()
)
# Connect them: output of write_poem becomes "poem" in analyse_poem
sequential = (
{"poem": write_poem} # write_poem runs first, result stored as "poem"
| RunnablePassthrough.assign(analysis=analyse_poem) # analyse_poem runs second
)
result = sequential.invoke({"topic": "autumn leaves"})
print("POEM:")
print(result["poem"])
print("\nANALYSIS:")
print(result["analysis"])
POEM: Autumn leaves fall to the ground, Golden hues and crimson all around. Their gentle rustle fills the air, As nature's final dance begins to share. ANALYSIS: This poem has a serene and contemplative mood, and its style is characterized by a lyrical and descriptive tone, using sensory language to evoke a sense of wonder and appreciation for the beauty of autumn.
# ── 3-step pipeline: generate → improve → translate
# Step 1: Generate a joke
generate_joke = (
ChatPromptTemplate.from_template("Write a short, clean joke about {topic}.")
| llm | StrOutputParser()
)
# Step 2: Improve the joke (uses output of step 1)
improve_joke = (
ChatPromptTemplate.from_template(
"Make this joke funnier by adding a punchline twist:\n{joke}"
)
| llm | StrOutputParser()
)
# Step 3: Rate the improved joke (uses output of step 2)
rate_joke = (
ChatPromptTemplate.from_template(
"""Rate this joke on a scale of 1-10 for funniness and explain why in one sentence:
{improved_joke}"""
)
| llm | StrOutputParser()
)
# Wire them together
pipeline = (
{"joke": generate_joke, "topic": RunnablePassthrough()}
| RunnablePassthrough.assign(improved_joke=lambda x: improve_joke.invoke({"joke": x["joke"]}))
| RunnablePassthrough.assign(rating=lambda x: rate_joke.invoke({"improved_joke": x["improved_joke"]}))
)
result = pipeline.invoke({"topic": "programmers"})
print("ORIGINAL JOKE:")
print(result["joke"])
print("\n IMPROVED JOKE:")
print(result["improved_joke"])
print("\n RATING:")
print(result["rating"])
ORIGINAL JOKE: Why do programmers prefer dark mode? Because light attracts bugs. IMPROVED JOKE: Why do programmers prefer dark mode? Because light attracts bugs... and they already have enough to debug. RATING: I would rate this joke an 8 out of 10 for funniness because it cleverly plays on the double meaning of "bugs" as both insects attracted to light and coding errors that programmers must debug.
# ── Real use case: Study notes pipeline ─
# Input: a topic → Output: explanation + quiz question + memory tip
# Chain 1: Explain the topic
explain = (
ChatPromptTemplate.from_template(
"Explain {topic} clearly in 3 bullet points for a student."
)
| llm | StrOutputParser()
)
# Chain 2: Create a quiz question from the explanation
make_quiz = (
ChatPromptTemplate.from_template(
"Based on this explanation:\n{explanation}\n\n"
"Write ONE multiple choice question (A/B/C/D) with the answer marked."
)
| llm | StrOutputParser()
)
# Chain 3: Create a memory trick from the explanation
memory_trick = (
ChatPromptTemplate.from_template(
"Based on this explanation:\n{explanation}\n\n"
"Give ONE creative memory trick or acronym to remember this."
)
| llm | StrOutputParser()
)
# Full pipeline
study_pipeline = (
{"explanation": explain, "topic": RunnablePassthrough()}
| RunnablePassthrough.assign(quiz=lambda x: make_quiz.invoke({"explanation": x["explanation"]}))
| RunnablePassthrough.assign(tip=lambda x: memory_trick.invoke({"explanation": x["explanation"]}))
)
result = study_pipeline.invoke({"topic": "photosynthesis"})
print("📚 EXPLANATION:")
print(result["explanation"])
print("\n❓ QUIZ QUESTION:")
print(result["quiz"])
print("\n🧠 MEMORY TRICK:")
print(result["tip"])
📚 EXPLANATION: Here are three bullet points explaining photosynthesis clearly for a student: * **What is photosynthesis?**: Photosynthesis is the process by which plants, algae, and some bacteria use energy from sunlight to convert carbon dioxide and water into glucose (a type of sugar) and oxygen. This process is essential for life on Earth as it provides energy and organic compounds for plants to grow and develop. * **How does it work?**: During photosynthesis, plants absorb carbon dioxide from the air through their leaves and water from the soil through their roots. They then use energy from sunlight, which is absorbed by a green pigment called chlorophyll, to convert the carbon dioxide and water into glucose and oxygen. The glucose is used by the plant as food, while the oxygen is released into the air as a byproduct. * **Why is it important?**: Photosynthesis is crucial for life on Earth because it provides oxygen for animals to breathe and serves as the basis of the food chain. Without photosynthesis, plants would not be able to produce the energy they need to grow, and animals would not have the oxygen they need to survive. Additionally, photosynthesis helps to regulate the Earth's climate by removing carbon dioxide from the atmosphere and producing oxygen, which helps to maintain a healthy balance of gases in the air. ❓ QUIZ QUESTION: What is the byproduct released into the air during the process of photosynthesis? A) Glucose B) Carbon dioxide C) Water D) Oxygen Answer: D) Oxygen 🧠 MEMORY TRICK: To remember the key points of photosynthesis, you can use the acronym "PGO" which stands for: P - Plants (and other organisms) use energy from sunlight G - Glucose (and oxygen) are produced from carbon dioxide and water O - Oxygen is released into the air, supporting life on Earth Alternatively, you can also use a sentence like "Pete's Gorilla Owns" to associate with the first letter of each word, making it more memorable.
5️. Parallel Chains — Run Multiple Chains Simultaneously¶
Sometimes you don’t need the output of one chain to feed into another.
You just want to run several chains on the same input at the same time.
RunnableParallel does exactly that — all chains run concurrently, saving time.
┌─── Chain A: pros ────┐
Input ────────┼─── Chain B: cons ────┼──→ Combined Result
└─── Chain C: summary ─┘
from langchain_core.runnables import RunnableParallel
# Three chains that all take {topic} as input
pros_chain = (
ChatPromptTemplate.from_template("List 3 advantages of {topic}. Be concise.")
| llm | StrOutputParser()
)
cons_chain = (
ChatPromptTemplate.from_template("List 3 disadvantages of {topic}. Be concise.")
| llm | StrOutputParser()
)
verdict_chain = (
ChatPromptTemplate.from_template(
"Give a one-sentence balanced verdict on {topic}: is it worth it overall?"
)
| llm | StrOutputParser()
)
# Run all three at the same time
parallel = RunnableParallel(
pros=pros_chain,
cons=cons_chain,
verdict=verdict_chain
)
result = parallel.invoke({"topic": "remote work"})
print("PROS:")
print(result["pros"])
print("\nCONS:")
print(result["cons"])
print("\n VERDICT:")
print(result["verdict"])
PROS: 1. Increased flexibility 2. Reduced commuting time 3. Improved work-life balance CONS: 1. Social isolation 2. Difficulty in separating work and personal life 3. Limited face-to-face interaction and communication VERDICT: Remote work is worth it overall for many individuals and organizations, as it offers numerous benefits such as increased flexibility, productivity, and work-life balance, but it also presents challenges like social isolation, communication barriers, and blurred boundaries, requiring intentional effort to strike a balance and maximize its potential.
# ── Parallel saves time — let's measure it
import time
topic = "electric vehicles"
# Sequential (one by one)
start = time.time()
p1 = pros_chain.invoke({"topic": topic})
c1 = cons_chain.invoke({"topic": topic})
v1 = verdict_chain.invoke({"topic": topic})
sequential_time = time.time() - start
# Parallel (all at once)
start = time.time()
result = parallel.invoke({"topic": topic})
parallel_time = time.time() - start
print(f"Sequential: {sequential_time:.2f}s")
print(f"Parallel: {parallel_time:.2f}s")
print(f"Speedup: {sequential_time/parallel_time:.1f}x faster")
Sequential: 0.79s Parallel: 0.41s Speedup: 1.9x faster
# ── Parallel + Sequential combined ────────────────────────────────
# First run parallel chains, then feed combined results into a final chain
summary_chain = (
ChatPromptTemplate.from_template(
"""You have analysed {topic}. Here is the analysis:
PROS: {pros}
CONS: {cons}
VERDICT: {verdict}
Write a 3-sentence recommendation for whether someone should adopt {topic}."""
)
| llm | StrOutputParser()
)
# Step 1: parallel analysis → Step 2: final recommendation
full_analysis = parallel | RunnablePassthrough.assign(
recommendation=lambda x: summary_chain.invoke({
"topic": topic,
"pros": x["pros"],
"cons": x["cons"],
"verdict": x["verdict"]
})
)
result = full_analysis.invoke({"topic": "social media for teenagers"})
print("📋 FINAL RECOMMENDATION:")
print(result["recommendation"])
📋 FINAL RECOMMENDATION: It appears there's been a mix-up, as the provided analysis pertains to social media for teenagers, not electric vehicles. However, to provide a recommendation on electric vehicles, I would suggest that individuals consider adopting electric vehicles due to their environmental benefits, lower operating costs, and improving technology. Overall, electric vehicles can be a worthwhile investment for those looking for a sustainable and cost-effective transportation option, but it's essential to weigh the pros and cons, such as limited charging infrastructure and higher upfront costs, before making a decision.
6️) Branching Chains — Different Paths Based on Input¶
Sometimes you want the chain to take different routes depending on the input.
This is called a Router Chain.
Input: "What is 2+2?" → Route: MATH → Math explanation chain
Input: "Why is sky blue?" → Route: SCIENCE → Science explanation chain
Input: "Who wrote Hamlet?" → Route: HISTORY → History explanation chain
We use RunnableLambda to write custom routing logic in plain Python.
from langchain_core.runnables import RunnableLambda
# ── Define specialist chains ──────────────────────────────────────
math_chain = (
ChatPromptTemplate.from_template(
"You are a math tutor. Answer this clearly with steps: {question}"
)
| llm | StrOutputParser()
)
science_chain = (
ChatPromptTemplate.from_template(
"You are a science teacher. Explain this with a real-world example: {question}"
)
| llm | StrOutputParser()
)
history_chain = (
ChatPromptTemplate.from_template(
"You are a history professor. Answer this with historical context: {question}"
)
| llm | StrOutputParser()
)
general_chain = (
ChatPromptTemplate.from_template(
"You are a helpful assistant. Answer this question: {question}"
)
| llm | StrOutputParser()
)
# ── Step 1: Classify the question ────────────────────────────────
classify_chain = (
ChatPromptTemplate.from_template(
"""Classify this question into exactly ONE category: math, science, history, or general.
Question: {question}
Reply with ONLY the category word, nothing else."""
)
| llm | StrOutputParser()
)
# ── Step 2: Route to the right chain ─────────────────────────────
def route(inputs: dict) -> str:
category = classify_chain.invoke({"question": inputs["question"]}).strip().lower()
print(f" 📂 Classified as: {category}")
if "math" in category:
return math_chain.invoke(inputs)
elif "science" in category:
return science_chain.invoke(inputs)
elif "history" in category:
return history_chain.invoke(inputs)
else:
return general_chain.invoke(inputs)
# ── Step 3: Wrap in a chain ────────────────────────────────────────
router_chain = RunnableLambda(route)
# Test it with different questions
questions = [
"What is the quadratic formula?",
"Why do leaves change colour in autumn?",
"Who was the first President of the United States?",
"What are some tips for better sleep?",
]
for q in questions:
print(f"\n❓ Question: {q}")
answer = router_chain.invoke({"question": q})
print(f"💬 Answer: {answer[:200]}...")
❓ Question: What is the quadratic formula? 📂 Classified as: math 💬 Answer: The quadratic formula is a mathematical formula used to find the solutions (or roots) of a quadratic equation. A quadratic equation is in the form of: ax² + bx + c = 0 where a, b, and c are constant... ❓ Question: Why do leaves change colour in autumn? 📂 Classified as: science 💬 Answer: What a wonderful topic to explore. The changing colors of leaves in autumn is a fascinating phenomenon that's not only aesthetically pleasing but also rooted in science. You see, during the spring an... ❓ Question: Who was the first President of the United States? 📂 Classified as: history 💬 Answer: The first President of the United States was George Washington, a man of great character and leadership who played a pivotal role in shaping the country's early history. Born on February 22, 1732, in ... ❓ Question: What are some tips for better sleep? 📂 Classified as: general 💬 Answer: Improving sleep quality can have a significant impact on overall health and well-being. Here are some valuable tips for better sleep: 1. **Establish a bedtime routine**: Develop a calming pre-sleep r...
7️) Mini Project — Movie Review Analyzer¶
Let’s put everything together into a real, useful pipeline.
What it does:¶
- Takes a movie title as input
- Parallel: fetches plot summary + gets critical reception (two chains at once)
- Sequential: uses both outputs to generate a structured review
- Structured output: returns a typed Python object you can use in your app
This is exactly the kind of pipeline you’d build in a real product.
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
from typing import List
# ── Output schema ─────────────────────────────────────────────────
class FullReview(BaseModel):
title: str
genre: List[str]
one_line_summary: str
what_works: List[str] = Field(description="3 things done well")
what_doesnt: List[str] = Field(description="2 weaknesses")
similar_movies: List[str] = Field(description="3 movies to watch if you liked this")
score: float = Field(description="Score out of 10")
recommended_for: str = Field(description="Who would enjoy this most")
parser = JsonOutputParser(pydantic_object=FullReview)
# ── Parallel chains ────────────────────────────────────────────────
plot_chain = (
ChatPromptTemplate.from_template(
"Write a 2-sentence spoiler-free plot summary of the movie '{title}'."
)
| llm | StrOutputParser()
)
reception_chain = (
ChatPromptTemplate.from_template(
"Describe in 2 sentences how '{title}' was received by critics and audiences."
)
| llm | StrOutputParser()
)
parallel_research = RunnableParallel(
plot=plot_chain,
reception=reception_chain,
title=RunnablePassthrough() # pass title through unchanged
)
# ── Final review chain ─────────────────────────────────────────────
final_review_chain = (
ChatPromptTemplate.from_messages([
("system", "You are a film critic. Always respond in valid JSON only."),
("human", """Based on this research, write a full review of '{title}'.
Plot: {plot}
Critical Reception: {reception}
{format_instructions}"""),
])
| llm
| parser
)
# ── Full pipeline ──────────────────────────────────────────────────
def analyze_movie(title: str) -> dict:
# Step 1: parallel research
research = parallel_research.invoke({"title": title})
# Step 2: final review using research
review = final_review_chain.invoke({
"title": title,
"plot": research["plot"],
"reception": research["reception"],
"format_instructions": parser.get_format_instructions()
})
return review
# Run it!
review = analyze_movie("The Dark Knight")
print(f"🎬 {review['title']}")
print(f"🎭 Genre: {', '.join(review['genre'])}")
print(f"⭐ Score: {review['score']}/10")
print(f"\n📖 Summary: {review['one_line_summary']}")
print(f"\n✅ What Works:")
for w in review['what_works']:
print(f" • {w}")
print(f"\n❌ What Doesn't:")
for w in review['what_doesnt']:
print(f" • {w}")
print(f"\n🎯 Recommended For: {review['recommended_for']}")
print(f"\n🎞️ Watch Next: {', '.join(review['similar_movies'])}")
🎬 The Dark Knight 🎭 Genre: Action, Thriller, Superhero ⭐ Score: 9.5/10 📖 Summary: Batman faces off against the chaotic and unpredictable Joker in a battle to protect Gotham City ✅ What Works: • Heath Ledger's posthumous Oscar-winning performance as the Joker • The film's thought-provoking themes of justice, morality, and heroism • The well-directed action sequences and intense plot ❌ What Doesn't: • The movie's dark and intense tone may not be suitable for all audiences • Some viewers may find the Joker's chaotic actions and violence disturbing 🎯 Recommended For: Fans of superhero movies and those who enjoy intense, thought-provoking action films 🎞️ Watch Next: The Avengers, The Dark Knight Rises, Spider-Man: Into the Spider-Verse
# Try with any movie!
review = analyze_movie("Parasite") # ← change this
print(f"🎬 {review['title']} | ⭐ {review['score']}/10")
print(f"📖 {review['one_line_summary']}")
🎬 Parasite | ⭐ 9/10 📖 A poor family infiltrates the lives of a wealthy family with unexpected consequences, blurring the boundaries of class, loyalty, and morality
📖 Summary — Everything We Covered¶
# ── 1. BASIC CHAIN ──────────────────────────────────────────────
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
chain = ChatPromptTemplate.from_template("...{var}...") | llm | StrOutputParser()
result = chain.invoke({"var": "value"}) # result is a plain string
# ── 2. OUTPUT PARSERS ───────────────────────────────────────────
StrOutputParser() # → str
CommaSeparatedListOutputParser() # → list
JsonOutputParser(pydantic_object=MyModel) # → dict (typed)
# ── 3. SEQUENTIAL CHAIN ─────────────────────────────────────────
from langchain_core.runnables import RunnablePassthrough
pipeline = (
{"step1_output": chain_a, "original": RunnablePassthrough()}
| RunnablePassthrough.assign(step2_output=chain_b)
)
# ── 4. PARALLEL CHAIN ───────────────────────────────────────────
from langchain_core.runnables import RunnableParallel
parallel = RunnableParallel(result_a=chain_a, result_b=chain_b, result_c=chain_c)
result = parallel.invoke({"input": "..."})
# result = {"result_a": "...", "result_b": "...", "result_c": "..."}
# ── 5. BRANCHING CHAIN ──────────────────────────────────────────
from langchain_core.runnables import RunnableLambda
def route(inputs):
if condition:
return chain_a.invoke(inputs)
else:
return chain_b.invoke(inputs)
router = RunnableLambda(route)
Coming Up Next¶
| Topic | What You’ll Learn |
|---|---|
| 🔧 Tools | Give chains the ability to search the web, run code, call APIs |
| 🧠 Memory | Make chains remember previous conversations |
| 🤖 Agents | Let the LLM decide WHICH chain/tool to use |
