A LangChain chain runs one way, start to finish. Real agents need to loop: call a model, run a tool, feed the result back, and go again until the work is done. LangGraph models that as a graph of nodes and edges with shared state, and it is what LangChain's own agents run on under the hood.
Why use a graph instead of a chain?
A chain is a one-way pipe. A graph adds the three things an agent needs:
- Cycles · the model and its tools can pass control back and forth until a stopping condition is met.
- Control flow · conditional edges route to different nodes based on what just happened.
- Shared state · every node reads and updates one state object as it runs.
What are the building blocks of a LangGraph?
LangGraph has a small vocabulary:
- State · a typed object that flows through the graph. For chat agents the
key piece is a
messageslist with a reducer that appends new messages.Common gotchaThe easy mistake is declaringmessagesas a plainlistwithout theadd_messagesreducer.Without it every node return overwrites the whole history instead of appending, so the model loses the prior turns and the tool loop forgets what it just did. Annotate the field asAnnotated[list, add_messages]so each update merges in. - Nodes · functions that take the state and return an update to it.
- Edges · connect nodes. A plain edge always goes to the next node; a conditional edge picks the next node based on the state.
- START and END · the entry and exit sentinels.
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from typing_extensions import TypedDict, Annotated
class State(TypedDict):
messages: Annotated[list, add_messages]How do I build an agent loop in LangGraph?
Here is a complete agent: a model node, a tool node, and a conditional edge that loops back whenever the model wants to call a tool.
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langchain_anthropic import ChatAnthropic
from langchain.tools import tool
from typing_extensions import TypedDict, Annotated
@tool
def get_weather(city: str) -> str:
"""Return the weather for a city."""
return f"Sunny in {city}"
tools = [get_weather]
model = ChatAnthropic(model="claude-sonnet-4-6").bind_tools(tools)
class State(TypedDict):
messages: Annotated[list, add_messages]
def call_model(state: State):
return {"messages": [model.invoke(state["messages"])]}
def should_continue(state: State):
last = state["messages"][-1]
return "tools" if last.tool_calls else END
builder = StateGraph(State)
builder.add_node("model", call_model)
builder.add_node("tools", ToolNode(tools))
builder.add_edge(START, "model")
builder.add_conditional_edges("model", should_continue, ["tools", END])
builder.add_edge("tools", "model") # loop back
graph = builder.compile()
graph.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})The shape is the whole idea: the model runs, and if it asked for a tool, control goes to the tool node and then back to the model. That cycle is what a flat chain cannot express.
Do I need to build the graph by hand?
For a standard agent you rarely need to wire the graph yourself. LangChain ships
a create_agent helper that builds this exact loop for you:
from langchain.agents import create_agent
agent = create_agent(
model="anthropic:claude-sonnet-4-6",
tools=[get_weather],
system_prompt="You are a helpful assistant.",
)
agent.invoke({"messages": [{"role": "user", "content": "Weather in SF?"}]})Drop down to a hand-built StateGraph when you need control the prebuilt agent
does not give you: parallel nodes, custom branching, retries, or a supervisor
coordinating several workers.
How do I add persistence and human-in-the-loop?
Two features turn a toy into something usable.
A checkpointer saves state between calls, keyed by a thread id, so a
conversation survives across invocations:Common gotchaAttaching a checkpointer but leaving the thread_id out of the config silently breaks persistence: with no key to load, every call starts a fresh conversation.Always pass the same thread_id to resume, and a new one to start a separate conversation.
from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(checkpointer=MemorySaver())
config = {"configurable": {"thread_id": "conversation-1"}}
graph.invoke(inputs, config)An interrupt pauses the graph to ask a human, then resumes with their answer:
from langgraph.types import interrupt, Command
def review(state: State):
decision = interrupt({"question": "Approve this action?"})
return {"messages": [{"role": "user", "content": decision}]}
# First call pauses at the interrupt; resume with the human's value:
graph.invoke(Command(resume="yes, proceed"), config)Interrupts need a checkpointer and a thread id, because pausing means saving the whole state and picking it back up later.
With state, loops, and pauses, you can build capable agents, and keep your agent on track shows how to stop them looping forever. The next two guides move to Anthropic's own Claude Agent SDK for doing this with less wiring.