Skip to main content
Articles/langchain/langgraph-101-building-your-first-stateful-agent
Series: AI Product Engineeringβ€’Part 8 of 10
Scenario Article

LangGraph 101: Building Your First Stateful Agent

The Stateless Chain Problem: Why Your Agent Forgets Everything

Abstract Algorithmsβ€’Mar 28, 2026
XLinkedInReddit
πŸ’‘ Executive Summary

LangGraph adds state, branching, and loops to LLM chains β€” build stateful agents with graphs, nodes, and typed state.

🎯 System Scenario & Requirements

System Goal

LangGraph adds state, branching, and loops to LLM chains β€” build stateful agents with graphs, nodes, and typed state.

Actors & Components

  • Application or API tier
  • Core service under design
  • Operator or SRE observer

Functional Requirements

  • TLDR: LangGraph adds state, branching, and loops to LLM chains β€” build stateful agents with graphs, nodes, and typed state.
  • You built a LangChain chain that answers questions.
  • Then you tried to make it ask a follow-up.

Constraints & Assumptions

  • LangGraph solves real problems, but adding it to every project introduces overhead that is not always justified.
  • Here is an honest look at both sides.

⚑ Execution Stages & Decision Trace

1

Stage 1 β€” LangGraph's Mental Model: Graphs, State, Nodes, and Edges

Action / Decision:

Before writing any code, you need three clear ideas in your head.

Component Behavior:

LangGraph is built from exactly these three concepts β€” nothing more.

Observable Output: State is a Python dictionary typed with TypedDict .
Read Full Stage in Deep Dive ↓
πŸ“–

Deep Dive

Detailed Architectural Analysis

Scenario context

LangGraph adds state, branching, and loops to LLM chains β€” build stateful agents with graphs, nodes, and typed state.


You built a LangChain chain that answers questions. Then you tried to make it ask a follow-up. The chain had no idea what it just said.

This is the stateless chain problem β€” and it trips up almost every developer who moves beyond basic LangChain tutorials. Here is what the failure looks…

This walkthrough focuses on The Stateless Chain Problem: Why Your Agent Forgets Everything.

Before making a design choice, clarify:

  • You built a LangChain chain that answers questions.
  • Then you tried to make it ask a follow-up.
  • The chain had no idea what it just said.

Concepts in play: llm-agents-and-langgraph, graph-traversal-algorithms, sorting-and-searching-algorithms, event-driven-architecture

Stage 1 β€” LangGraph's Mental Model: Graphs, State, Nodes, and Edges

  • Before writing any code, you need three clear ideas in your head.
  • LangGraph is built from exactly these three concepts β€” nothing more.
  • State is a Python dictionary typed with TypedDict .
  • Every node in the graph can read it and write partial updates to it.

Observable outcome: Every node in the graph can read it and write partial updates to it.

Stage 2 β€” Building Blocks: TypedDict State, Nodes, and How Edges Connect Them

  • Let's make everything above concrete with runnable code.
  • Here is the minimum viable LangGraph agent β€” a two-node graph that greets a user and then summarizes the greeting.
  • Step 1 β€” Define your state schema with TypedDict The Annotated[list[str], operator.add] syntax is the first thing that surprises most developers.
  • When a node returns {"messages": ["Hello!"]} , LangGraph does not replace the entire messages list β€” it appends to it.
from typing import Annotated
from typing_extensions import TypedDict
import operator

class AgentState(TypedDict):
    messages: Annotated[list[str], operator.add]
    # operator.add means "append new values; never overwrite the whole list"
def greet_node(state: AgentState) -> dict:
    """Add a greeting to the message list."""
    return {"messages": ["Hi! How can I help you today?"]}

def summarize_node(state: AgentState) -> dict:
    """Summarize everything that has been said so far."""
    history = " | ".join(state["messages"])
    return {"messages": [f"[Summary]: {history}"]}

Observable outcome: When a node returns {"messages": ["Hello!"]} , LangGraph does not replace the entire messages list β€” it appends to it.

Stage 3 β€” Deep Dive: How LangGraph Compiles and Executes Your Graph

  • The Internals: Pregel-Style Execution Under the Hood When you call StateGraph.compile() , LangGraph validates the graph structure (checks for disconnected nodes, missing entry points, unreachable END ) and produces an e…
  • At each superstep, LangGraph identifies the currently active nodes, runs them, collects their partial state updates, and merges those updates into the shared state dict before advancing to the next step.
  • The merge strategy for each field is controlled by its annotated reducer β€” operator.add for append, or overwrite-by-default for unannotated fields.
  • LangGraph also serializes the full state after each superstep.
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
graph = builder.compile(checkpointer=checkpointer)

# Each run gets a thread_id β€” state is persisted per thread
config = {"configurable": {"thread_id": "user-session-42"}}
result = graph.invoke({"messages": []}, config=config)
for step in graph.stream({"messages": []}):
    node_name = list(step.keys())[0]
    print(f"βœ… Node '{node_name}' completed: {step[node_name]}")

Observable outcome: LangGraph also serializes the full state after each superstep.

  • Now let's build the worked example.
  • The agent takes a user question, decides whether it needs to search the web before answering, runs a search if needed, and synthesizes the final response.
  • Here is the graph structure: The router LLM inspects the question and sets a needs search flag.
  • If true, the search node runs and injects results into state.
graph TD
    A([ User Question]) --> B[router_node Decide: search needed?]
    B -->|needs_search = true| C[search_node Fetch results]
    B -->|needs_search = false| D[answer_node Synthesize answer]
    C --> D
    D --> E([END])

    style A fill:#e3f2fd,stroke:#1976D2,color:#000
    style E fill:#e8f5e9,stroke:#388E3C,color:#000
    style B fill:#fff8e1,stroke:#F57F17,color:#000
    style C fill:#f3e5f5,stroke:#7B1FA2,color:#000
    style D fill:#e8f5e9,stroke:#2E7D32,color:#000
import operator
from typing import Annotated
from typing_extensions import TypedDict
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, BaseMessage
from langgraph.graph import StateGraph, END

# --- 1. State schema ---
class ResearchState(TypedDict):
    messages: Annotated[list[BaseMessage], operator.add]
    search_results: str
    needs_search: bool

# --- 2. LLM (swap freely β€” see the LLM-agnostic section below) ---
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# --- 3. Node: decide whether to search ---
def router_node(state: ResearchState) -> dict:
    question = state["messages"][-1].content
    prompt = (
        f"Does answering this question require current web information?\n"
        f"Question: {question}\n"
        f"Reply with only: YES or NO"
    )
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"needs_search": "YES" in response.content.upper()}

# --- 4. Node: run the search ---
def search_node(state: ResearchState) -> dict:
    question = state["messages"][-1].content
    # Replace this stub with TavilySearch, Serper, or Bing API in production
    results = f"[Search stub] Top results for: '{question}' β€” relevant facts retrieved."
    return {"search_results": results}

# --- 5. Node: synthesize the answer ---
def answer_node(state: ResearchState) -> dict:
    context_messages = list(state["messages"])
    if state.get("search_results"):
        context_messages.append(
            HumanMessage(content=f"Use this context to answer: {state['search_results']}")
        )
    context_messages.append(HumanMessage(content="Now answer the original question."))
    response = llm.invoke(context_messages)
    return {"messages": [AIMessage(content=response.content)]}

# --- 6. Routing function ---
def route_search(state: ResearchState) -> str:
    return "search_node" if state.get("needs_search") else "answer_node"

# --- 7. Build and compile the graph ---
builder = StateGraph(ResearchState)

builder.add_node("router_node", router_node)
builder.add_node("search_node", search_node)
builder.add_node("answer_node", answer_node)

builder.set_entry_point("router_node")
builder.add_conditional_edges("router_node", route_search, {
    "search_node": "search_node",
    "answer_node": "answer_node",
})
builder.add_edge("search_node", "answer_node")
builder.add_edge("answer_node", END)

graph = builder.compile()

# --- 8. Run it ---
result = graph.invoke({
    "messages": [HumanMessage(content="What is the current price of Bitcoin?")],
    "search_results": "",
    "needs_search": False,
})
print(result["messages"][-1].content)

Observable outcome: If true, the search node runs and injects results into state.

Stage 5 β€” Real-World Applications: Where Stateful Graphs Show Up in Production

  • LangGraph is not a toy framework.
  • Stateful graphs power several real production patterns: Customer support routing β€” A support bot classifies incoming messages: simple FAQs route directly to an answer node; complex issues enter a multi-turn diagnostic l…
  • State tracks the conversation history, the issue category, and an escalation flag across every turn without external session storage.
  • Code generation with self-repair β€” An agent generates code, runs a linter node, reads the linter output from state, and conditionally loops back to the LLM to fix errors.

Observable outcome: Code generation with self-repair β€” An agent generates code, runs a linter node, reads the linter output from state, and conditionally loops back to the LLM to fix errors.

Stage 6 β€” Practical Example: Making the Graph LLM-Agnostic

  • One of LangGraph's cleanest design decisions is that nodes are plain Python functions with no opinion about which LLM you use.
  • Swapping providers requires changing exactly one line β€” the llm assignment at the top.
  • This scenario was chosen because it is the most common "day 2" question new LangGraph users ask β€” and showing it is a one-liner is the best answer.
  • As you read through the three provider options, watch how every other line (the research node , the StateGraph , the compile() call) stays identical across all three β€” that invariance is LangGraph's LLM-agnostic promise…
# Option 1: OpenAI β€” best default for production
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

# Option 2: Anthropic Claude β€” great for long-context reasoning
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-3-haiku-20240307", temperature=0)

# Option 3: Ollama β€” fully local, zero API cost, great for development
from langchain_ollama import ChatOllama
llm = ChatOllama(model="llama3.2", temperature=0)

# The graph, nodes, state schema, and edges are IDENTICAL for all three.
def make_router_node(llm):
    """Factory that binds a specific LLM into the router node."""
    def router_node(state: ResearchState) -> dict:
        question = state["messages"][-1].content
        prompt = f"Does answering this require web search?\nQuestion: {question}\nReply YES or NO"
        response = llm.invoke([HumanMessage(content=prompt)])
        return {"needs_search": "YES" in response.content.upper()}
    return router_node

# At graph build time, pick your LLM and inject it:
builder.add_node("router_node", make_router_node(llm))

Observable outcome: As you read through the three provider options, watch how every other line (the research node , the StateGraph , the compile() call) stays identical across all three β€” that invariance is LangGraph's LLM-agnostic promise…

End-to-end outcome

After working through the stages, you should be able to explain:

  • Building your first stateful agent surfaces several lessons that are not obvious from the documentation: 1.
  • Compile once, invoke many times.

Alternatives considered

Decision Guide: StateGraph vs LCEL Chain vs LangChain AgentExecutor

The key signal is decision-making inside the loop .

Failure and degraded modes

Trade-offs and Failure Modes: When LangGraph Helps and When It Adds Complexity

  • Detection: LangGraph solves real problems, but adding it to every project introduces overhead that is not always justified.
  • User or operator impact: LangGraph solves real problems, but adding it to every project introduces overhead that is not always justified.
Series Navigation β€’ Part 8 of 10

AI Product Engineering

View Full Series Overview β†’

Share this case study

Discuss architecture trade-offs and lessons with your team.

Share:XLinkedInReddit