LangChain & LangGraph
LangChain pipelines, LangGraph stateful agents, and framework selection guidance
Introduction
LangChain and LangGraph are two of the most widely adopted open-source frameworks for LLM-powered applications. While LangChain provides reusable building blocks for composing AI pipelines, LangGraph adds a graph-based execution model for complex, stateful workflows. But more tooling is not always better: knowing when to reach for these frameworks, and when a 20-line script does the job just as well is one of the most practical skills an AI developer can have.
Quick Navigation
1. What is LangChain?
Chains, LCEL, and the integration ecosystem
2. What is LangGraph?
StateGraph, cycles, and multi-agent workflows
3. Comparison
Side-by-side: paradigm, state, complexity
4. When to Use Each
Decision guide for LangChain, LangGraph, or plain scripts
5. The Case for Plain Scripts
When frameworks add more cost than value
1. What is LangChain?
LangChain is an open-source framework for composing LLM-powered applications from interchangeable components. Instead of writing bespoke glue code for every new project, LangChain gives you a standard set of building blocks, a common interface across 100+ model providers and vector stores, and a composition syntax (LCEL) that makes pipelines readable.
Core Primitives
Chat Models
Unified interface over any LLM provider: Anthropic, OpenAI, Bedrock, Ollama, Groq, and 100+ more. Swap providers without rewriting your chain.
Prompt Templates
Reusable, parameterized prompts that separate your instructions from your inputs. Supports system messages, few-shot examples, and dynamic slots.
Output Parsers
Post-process the LLM's text response: extract structured JSON, validate with Pydantic models, or split into lists.
Document Loaders
Ingest content from PDFs, web pages, databases, S3, Notion, and dozens of other sources into a common Document format.
Text Splitters
Chunk large documents into sizes suitable for embedding and retrieval, with configurable overlap to avoid cutting context mid-sentence.
Vector Stores
Integration layer for Chroma, Pinecone, pgvector, Weaviate, and others. Insert embeddings and run similarity searches with the same API regardless of backend.
LCEL: LangChain Expression Language
LCEL is the glue. It lets you compose any two Runnables (models, prompts, retrievers, parsers) using the | pipe operator, turning a pipeline into a single Runnable that supports streaming, batching, and async out of the box.
# LCEL composes components with the pipe operator
chain = prompt | llm | output_parser
# Each component is a Runnable - they share a standard interface:
# .invoke() - single call
# .batch() - parallel calls
# .stream() - streaming tokens
result = chain.invoke({"topic": "machine learning"})LangChain in Practice: A RAG Pipeline
The most common real-world use case for LangChain is a Retrieval-Augmented Generation (RAG) pipeline: retrieve relevant chunks from a vector store and inject them into a prompt before calling the LLM. LCEL makes the flow explicit and easy to read.
from langchain_anthropic import ChatAnthropic
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_chroma import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_core.runnables import RunnablePassthrough
# --- Build the retriever ---
vectorstore = Chroma(
collection_name="docs",
embedding_function=OpenAIEmbeddings(),
persist_directory="./chroma_db",
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 4})
# --- Define the prompt ---
prompt = ChatPromptTemplate.from_template("""
Answer the question using only the context below.
Context: {context}
Question: {question}
""")
# --- Compose the RAG chain with LCEL ---
llm = ChatAnthropic(model="claude-sonnet-4-6")
parser = StrOutputParser()
rag_chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| parser
)
# --- Invoke ---
answer = rag_chain.invoke("What is the return policy?")
print(answer)LangChain's Learning Curve
LangChain is powerful, but its abstraction layers can make debugging difficult. Errors often surface several layers deep with stack traces that reference internal framework code. Before adopting it, make sure the integration savings genuinely outweigh this overhead for your project.
2. What is LangGraph?
LangGraph is built on top of LangChain and extends it with a graph-based execution model. Where LangChain chains are linear sequences (A then B then C), LangGraph lets you define workflows as directed graphs where nodes are Python functions and edges can branch conditionally or loop back. This makes it possible to build agents that retry, self-correct, wait for human approval, or coordinate multiple sub-agents in parallel.
The StateGraph Mental Model
Every LangGraph application is a StateGraph. You define:
- State: a TypedDict that represents the data flowing through the graph. Every node reads and writes to it.
- Nodes: plain Python functions that receive the current state and return a partial update.
- Edges: static transitions between nodes, or conditional functions that return the name of the next node dynamically.
Because edges can point back to earlier nodes, LangGraph supports cycles, something a LangChain chain cannot do. An agent can call a tool, observe the result, decide it needs another tool call, loop back, and only move to the final node when it has enough information.
LangGraph Agent Loop
The model loops through tool calls until it has a final answer
LangGraph in Practice: A Tool-Calling Agent
from typing import Annotated, TypedDict
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import BaseMessage, HumanMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
# --- State definition ---
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# --- Tools ---
@tool
def search_web(query: str) -> str:
"""Search the web for up-to-date information."""
return f"[Search result for '{query}': placeholder response]"
tools = [search_web]
llm = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(tools)
# --- Nodes ---
def call_model(state: AgentState) -> AgentState:
response = llm.invoke(state["messages"])
return {"messages": [response]}
def run_tools(state: AgentState) -> AgentState:
last_msg = state["messages"][-1]
results = []
for call in last_msg.tool_calls:
tool_map = {t.name: t for t in tools}
result = tool_map[call["name"]].invoke(call["args"])
results.append(ToolMessage(content=result, tool_call_id=call["id"]))
return {"messages": results}
def should_continue(state: AgentState) -> str:
last_msg = state["messages"][-1]
return "run_tools" if last_msg.tool_calls else END
# --- Build the graph ---
graph = StateGraph(AgentState)
graph.add_node("call_model", call_model)
graph.add_node("run_tools", run_tools)
graph.add_edge(START, "call_model")
graph.add_conditional_edges("call_model", should_continue)
graph.add_edge("run_tools", "call_model")
agent = graph.compile()
# --- Run ---
result = agent.invoke({"messages": [HumanMessage("What happened in tech news today?")]})
print(result["messages"][-1].content)Key LangGraph Features
Cycles
Loops and retries are first-class. An agent can call tools indefinitely until a stopping condition is met.
Human-in-the-Loop
Pause the graph at any node and wait for human input or approval before continuing. Checkpointing saves state across the pause.
Persistence
Attach a checkpointer (SQLite, Postgres) to save and restore graph state. This enables multi-turn conversations and resumable workflows.
Multi-Agent
Compose multiple sub-graphs as nodes in a parent graph. Agents can hand off control to each other, run in parallel, or aggregate results.
3. LangChain vs LangGraph
LangGraph is not a replacement for LangChain, it is a layer on top. Most LangGraph applications still use LangChain components (ChatAnthropic, tools, prompts) inside their graph nodes. The distinction is about execution model: sequential chains versus stateful graphs.
| Dimension | LangChain | LangGraph |
|---|---|---|
| Paradigm | Sequential pipeline (A then B then C) | Directed graph (nodes + conditional edges) |
| Cycles / loops | Not supported | First-class feature |
| State management | Implicit (passed between chain steps) | Explicit TypedDict shared across all nodes |
| Human-in-the-loop | Awkward to implement | Built-in via checkpointers and interrupt() |
| Persistence | Manual | Native checkpointing (SQLite, Postgres) |
| Multi-agent | Limited | First-class: sub-graphs as nodes |
| Best for | RAG, document Q&A, sequential pipelines | Complex agents, retry logic, long-running workflows |
| Complexity | Low to medium | Medium to high |
| Learning curve | Moderate (LCEL, component model) | Steeper (graph model, state reducers, checkpointers) |
| Relationship | Standalone or used inside LangGraph nodes | Extends LangChain, uses its components internally |
They Work Together
In most production LangGraph apps, the nodes contain LangChain components. You might use a LangChain ChatAnthropic model, a LangChain tool bound to it, and a LangChain prompt template, all wired together inside a LangGraph StateGraph. Think of LangChain as the component library and LangGraph as the orchestration engine.
4. When to Use Each
Use LangChain when...
- You are building a RAG pipeline, retrieval, chunking, embedding, and generation in sequence
- Your workflow is linear and predictable: no branching, no retries, no loops
- You need fast integrations: your stack includes Pinecone, Chroma, OpenAI, Bedrock, or similar. LangChain already wraps them
- You are building a document Q&A system, summarization pipeline, or structured extraction job
- Your team is new to LLMs and wants guardrails and conventions rather than designing everything from scratch
Use LangGraph when...
- You need a tool-calling agent that can loop, calling tools, evaluating results, and deciding whether to continue
- Your workflow requires conditional branching: different paths based on model output or user input
- You need human-in-the-loop: pause the workflow, collect approval, then resume from the saved checkpoint
- You are orchestrating multiple agents that coordinate, hand off tasks, or run in parallel
- You need long-running stateful conversations that persist across sessions (e.g., a coding assistant with memory)
Avoid Both When...
- Your use case is a single API call. Adding a framework for one
messages.create()call is pure overhead - You are prototyping or experimenting, iterate fast with raw SDK calls, then introduce a framework if complexity warrants it
- The team does not need the integrations, if you already control your vector store, embedding logic, and model calls, the framework adds nothing
- You are hitting a performance bottleneck, framework layers add latency and memory overhead that a hand-optimized script avoids
5. The Case for Plain Scripts
There is a persistent temptation to reach for a framework the moment a task involves an LLM. Resist it. Every framework is a tradeoff: you gain convenience and standardized patterns, but you pay in added dependencies, more complex debugging, and a steeper onboarding curve for new engineers. For a surprisingly large class of problems, a plain Python script calling the Anthropic SDK directly is the right answer.
What a "Plain Script" Looks Like
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Summarize this text: ..."}],
)
print(response.content[0].text)That is the entire call. No chains, no graphs, no state reducers. If your task is "summarize this document" or "classify this input", ten lines with the raw SDK is simpler, faster, and easier to debug than an equivalent LangChain chain.
Signals That You Should Stay Simple
Single inference step
If your app calls the LLM once to produce an output, there is nothing to chain or graph. One function is enough.
Early exploration
You are still figuring out what the model can and cannot do. Plain calls let you iterate in seconds without fighting framework abstractions.
Full-stack control
You own the vector store, the embedding pipeline, and the model call. None of LangChain's integrations save you any work.
Latency or cost sensitivity
Framework overhead is small but non-zero. High-throughput or cost-optimized inference paths benefit from cutting every unnecessary layer.
Small team, long maintenance
Every dependency you add is a dependency your team must understand and upgrade. Fewer moving parts means fewer future surprises.
A Practical Rule of Thumb
Start with a plain script. If you find yourself writing custom retry logic, managing state across multiple model calls, or manually wiring in several external systems, that is when a framework earns its keep. Introduce LangChain when the integration savings are concrete, and add LangGraph when your workflow genuinely needs cycles or stateful branching.
Bonus: core-genai - Lightweight Provider Abstraction
If you want a clean abstraction over multiple LLM providers without the overhead of LangChain, core-genai is a lightweight library that fits the plain-scripts philosophy. It provides a single unified interface for Claude, Gemini, ChatGPT, and Grok, with built-in cost tracking and normalized batch operations, but adds none of the orchestration complexity that LangChain brings.
The library provides:
- Provider-agnostic IAgent interface - swap Claude for Gemini or ChatGPT by changing a single
create_agent()call; the rest of your code stays identical - Automatic cost calculation - every response includes USD cost via
agent.get_cost(output)and normalized token metadata - Batch inference support - a unified
ISchedulerinterface for submitting, polling, and extracting batch jobs across all supported providers - Minimal footprint - provider SDKs are optional extras; install only what you need:
pip install "core-genai[claude]"
Key Takeaways
- LangChain is a component library for LLM apps: prompt templates, output parsers, document loaders, vector store integrations, and a pipe-based composition syntax (LCEL)
- LangGraph extends LangChain with a graph execution model that supports cycles, conditional branching, human-in-the-loop, and persistent state across sessions
- They are complementary - LangGraph nodes typically use LangChain components internally. You rarely choose one over the other; you choose which layer fits the current problem
- LangChain shines for RAG pipelines, sequential document processing, and cases where prebuilt integrations save meaningful work
- LangGraph shines for agents with tool loops, multi-agent orchestration, and workflows that need pausing, resuming, or branching based on dynamic conditions
- Plain scripts win for single-step inference, prototyping, full-stack-controlled pipelines, and performance-sensitive paths where framework overhead is unjustified
- The best engineers default to the simplest solution and add framework complexity only when the problem genuinely requires it