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

STARTcall_modelENDrun_toolsno tool callshas tool calls

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.

DimensionLangChainLangGraph
ParadigmSequential pipeline (A then B then C)Directed graph (nodes + conditional edges)
Cycles / loopsNot supportedFirst-class feature
State managementImplicit (passed between chain steps)Explicit TypedDict shared across all nodes
Human-in-the-loopAwkward to implementBuilt-in via checkpointers and interrupt()
PersistenceManualNative checkpointing (SQLite, Postgres)
Multi-agentLimitedFirst-class: sub-graphs as nodes
Best forRAG, document Q&A, sequential pipelinesComplex agents, retry logic, long-running workflows
ComplexityLow to mediumMedium to high
Learning curveModerate (LCEL, component model)Steeper (graph model, state reducers, checkpointers)
RelationshipStandalone or used inside LangGraph nodesExtends 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 IScheduler interface 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]"
Explore the library:PyPI: core-genaiDocumentation

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
Software Engineering in AI EraLesson 10