You write your own tool-use loops against the Anthropic and OpenAI APIs. This is what LangChain and LangGraph actually add on top — and the small mental model you need to read a LangGraph and not be surprised.
Two names, one stack. As of their v1.0 releases on 22 October 2025, LangChain and LangGraph are no longer competitors or even really separate tools — they're two altitudes of the same thing.3 Get that relationship straight first; everything else hangs off it.
LangChain is the high-level way to build an agent — "the fastest way to build an AI agent," provider-agnostic, batteries included.3 LangGraph is the low-level framework and runtime underneath it, for "highly custom and controllable agents… production-grade, long-running."3
The kicker: in 1.0, LangChain's agents run on the LangGraph runtime. You start high-level and "seamlessly drop down to LangGraph when you need more control."3 It's one ladder, not two products.
When you build an agent against the raw API today, you hand-write a loop. It looks like this, in spirit:
# the loop you already write by hand messages = [user_msg] while True: resp = client.messages.create(model=..., tools=tools, messages=messages) if resp.stop_reason != "tool_use": break # model gave a final answer for call in tool_calls(resp): result = run_tool(call) # you dispatch + execute messages.append(tool_result(result)) messages.append(resp) # feed it all back, loop again
LangChain's headline abstraction, create_agent, is that loop — standardized. The docs describe it in exactly three steps: send a request to the model with tools and prompt; the model returns either tool calls (execute them, add the results) or a final answer; repeat until done.3 So the first honest answer to "what does it add?" is: it deletes the boilerplate loop you keep rewriting — and standardizes the messy parts around it.
The real value isn't the loop — it's the normalization. LangChain 1.0 introduces standardized content blocks (a .content_blocks property) giving "consistent content types across providers" — reasoning traces, citations, tool calls — so the same code works against Anthropic, OpenAI, and others without you special-casing each provider's response shape.3 If you've ever written if provider == "openai" branches, that's the pain it targets.
The hand-written loop above has a hidden ceiling. It's a straight line: call model → run tools → repeat. The moment you want structure — branch here, loop back there, run two things in parallel, pause for a human, survive a crash and resume — you start hand-rolling a state machine. LangGraph is that state machine, done properly.
| Concern | Raw API (today) | LangChain | LangGraph |
|---|---|---|---|
| Mental model | A while loop you own | A prebuilt agent loop (create_agent) | A graph of nodes & edges — a state machine1 |
| Control flow | Linear; you code every branch | The standard agent loop, configured | Loops, branches, parallelism, dynamic routing1 |
| State | A messages list you mutate | Managed for you | An explicit, typed State object with reducers1 |
| Durability | None — crash loses everything | Inherited from LangGraph | Checkpointers: persist & resume at any point1 |
| Human-in-the-loop | You build it | Inherited from LangGraph | First-class interrupts / pause for review1 |
| You reach for it when… | It's a quick script | A standard agent is enough | You need control, durability, weird shapes |
Maximum control, maximum boilerplate. Every capability above the basic loop is yours to build.
Stop rewriting the loop; get provider-agnostic messages. Most agents start (and stay) here.
The runtime under LangChain. Drop here for branching, persistence, human-in-the-loop, multi-agent.
Your mission is reading fluency: open someone's StateGraph and narrate what it does. You only need four nouns. The docs put the whole thing in five words: "nodes do the work, edges tell what to do next."1
StateGraph — the graph itselfThe primary graph class, parameterized by a State type. You add nodes and edges to it, then call .compile() — which validates the structure and turns on runtime features like checkpointers and breakpoints.1
State — the shared memoryA schema (a TypedDict, dataclass, or Pydantic model) that flows through the graph. Each key can have a reducer saying how updates apply: default is overwrite; operator.add or add_messages means append/accumulate.1
messages list — but typed, and the reducer is the rule you'd hand-write for "append vs replace."Nodes — the workPlain Python functions. Each receives the current state (plus optional config/runtime), does something, and returns just the slice of state it wants to update.1
Edges — the routingNormal edges (add_edge) are fixed hops. Conditional edges (add_conditional_edges) call a function to decide where to go next. Execution starts at a virtual START node and ends at END.1
if stop_reason == "tool_use" branch — promoted to a first-class, drawable arrow.Under the hood it's message passing in the style of Google's Pregel system: execution runs in discrete super-steps where active nodes run (in parallel if more than one is active), pass messages along edges, and go inactive. When no node is active and no message is in flight, the graph stops.1 You rarely need this detail to read a graph — but it's why parallel branches and loops "just work."
Here's a minimal agent graph. Don't memorize the API — just narrate the flow out loud: what's the state, what do the two nodes do, and where does the loop happen?
from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from typing import Annotated, TypedDict class State(TypedDict): messages: Annotated[list, add_messages] # reducer = append, not overwrite def call_model(state): return {"messages": [llm.invoke(state["messages"])]} def route(state): last = state["messages"][-1] return "tools" if last.tool_calls else END # the branch g = StateGraph(State) g.add_node("model", call_model) g.add_node("tools", tool_node) g.add_edge(START, "model") g.add_conditional_edges("model", route) # model → tools, or → END g.add_edge("tools", "model") # tools loop back to model agent = g.compile()
The answer: state is a growing message list; model calls the LLM, tools runs any requested tools; route sends control back to the model if there were tool calls, else stops. That tools → model edge is your while True — drawn as a cycle instead of written as a loop.
This space moved fast and old tutorials lie. Pre-1.0 LangChain pushed LLMChain, initialize_agent, and AgentExecutor — those are legacy. The current idiom is create_agent (high level) and StateGraph (low level). If an example imports AgentExecutor, it predates the world you're learning.3
Judgement, not trivia. One per question; reasoning appears instantly.
1. A teammate says "we'll use LangChain or LangGraph, haven't decided." What's the sharpest correction?
create_agent is built on the LangGraph runtime. It's one ladder: LangChain on top, LangGraph underneath. The decision is how low you need to go, not which to adopt.2. You're reading a StateGraph and see messages: Annotated[list, add_messages]. What is add_messages doing?
add_messages (like operator.add) accumulates — which is why the conversation grows instead of being replaced each step.3. Your hand-written agent works, but the client now needs it to pause for human approval before sending an email and resume after a crash. Which altitude earns its keep?
That's the orientation pass. You said you weren't sure where to focus — now you've seen the map, here are the three honest drill-downs. Tell me which itch is strongest and I'll build explainer 0002 around it: