Free lesson · GenAI Agent Engineering
Build with LangGraph StateGraph
You can map the manual while-loop to LangGraph's StateGraph (nodes, edges, ToolNode, tools_condition, add reducer), use Pydantic Settings for agent config, and decide when to build raw vs use a framework.
Course: GenAI Agent Engineering · Chapter 16 · The Raw Agent Loop
Free to read — no subscription required.
Introduction
When you've hand-rolled an agent loop with explicit message lists, iteration counters, and a tool-dispatch branch, you've already built — by hand — every piece LangGraph's StateGraph gives you for free. Teams that try to scale a manual loop hit the same wall: state-overwrite bugs that drop tool results, no visual model for the control flow, and ad-hoc termination logic that breaks under retries. By the end of this lesson you'll be able to translate a manual agent loop into a LangGraph StateGraph, declare state with the right reducer, wire a conditional edge that routes between an LLM node and a tool node, and cap iterations with recursion_limit.
Key Terminology
- StateGraph — LangGraph's container for nodes and edges. You build it with a typed state shape, add nodes (functions that read state and return updates), wire edges, then
.compile()to get an executable. It replaces thewhileloop in a manual agent. - Reducer — the function LangGraph uses to merge a node's returned state into the running state.
Annotated[Sequence, add]says "append to the list"; the default is "replace." Forgetting the reducer is why messages silently disappear between nodes. - ToolNode — a prebuilt node that reads
tool_callsoff the latest message, executes the named tools, and returnsToolMessageresults into state. Replaces a hand-written tool dispatcher. - tools_condition — a prebuilt routing function for
add_conditional_edges. Returns"tools"when the latest assistant message has tool calls,ENDotherwise. It is theif msg.tool_calls:of the graph world. - recursion_limit — runtime cap on graph steps (default 25). Set via
.with_config(recursion_limit=N). Each agent→tools→agent cycle counts as two steps, so a 10-iteration cap maps torecursion_limit=20.
Concepts
A manual agent loop has three moving parts: a message list, an LLM call that may emit tool calls, and a branch that either runs tools and continues or returns the final answer. LangGraph keeps the same three parts but renames them: state, nodes, edges. The translation is mechanical once you see the mapping.
Graph as Execution Model
In a manual loop, control flow is implicit in Python's while and if statements. In LangGraph, control flow is data: nodes are functions, edges are routing rules, and the runtime walks the graph until it hits END. The agent node calls the LLM; the tools node executes any tool calls; a conditional edge decides which runs next (see Code Walkthrough for the wiring).
State with Reducers
The biggest trap when porting a manual loop is state overwrite. If you declare messages: list on your state without a reducer, every node that returns {"messages": [new_msg]} replaces the entire list. The fix is the add reducer: messages: Annotated[Sequence, add]. Now each node's returned list is appended to the running list — exactly what messages.append(...) did in the manual loop.
Reducers compose per key: you can attach add to messages and leave another key on the default replace behaviour. Custom reducers — any (current, update) -> merged function — handle cases like deduplication or capped histories.
Conditional Edges and Tool Routing
The manual loop's if assistant_msg.tool_calls: becomes add_conditional_edges(source, router_fn, {key: dest, ...}). tools_condition is the prebuilt router; it inspects the latest message and returns one of the keys you mapped. Pair it with ToolNode(tools) and a back-edge from "tools" to "agent", and you have the full loop expressed declaratively (see Code Walkthrough).
Code Walkthrough
Now that you've seen Graph as Execution Model, State with Reducers, and Conditional Edges and Tool Routing, this walkthrough turns them into working code.
The snippet below builds the full agent graph in one pass: the AgentState TypedDict with an add reducer, the agent node that calls the LLM, the prebuilt ToolNode, and the conditional edge wired with tools_condition. It also shows how to cap iterations with recursion_limit, the LangGraph analogue of a manual IterationTracker.
Code snippetpython
1from operator import add 2from typing import Annotated, Sequence, TypedDict 3 4from langchain_core.messages import HumanMessage 5from langchain_core.tools import tool 6from langchain_openai import ChatOpenAI 7from langgraph.graph import END, StateGraph 8from langgraph.prebuilt import ToolNode, tools_condition 9 10class AgentState(TypedDict): 11 # The `add` reducer appends each node's returned messages to the 12 # running list. Without it, every node would overwrite history. 13 messages: Annotated[Sequence, add] 14 15def build_agent(tools: list, max_iterations: int = 10): 16 llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) 17 18 def agent(state: AgentState) -> dict: 19 response = llm.invoke(state["messages"]) 20 # Return ONLY the new message; the reducer handles appending. 21 return {"messages": [response]} 22 23 graph = StateGraph(AgentState) 24 graph.add_node("agent", agent) 25 graph.add_node("tools", ToolNode(tools)) 26 graph.set_entry_point("agent") 27 28 # tools_condition returns "tools" when the latest message has 29 # tool_calls, otherwise END. Map those keys to destinations. 30 graph.add_conditional_edges( 31 "agent", 32 tools_condition, 33 {"tools": "tools", END: END}, 34 ) 35 # Back-edge: after tools run, hand control back to the LLM so it 36 # can read the ToolMessage results and either call more tools or 37 # produce the final answer. 38 graph.add_edge("tools", "agent") 39 40 # Each logical iteration = agent + tools = 2 graph steps. 41 return graph.compile().with_config( 42 recursion_limit=max_iterations * 2 43 ) 44 45@tool 46def get_weather(location: str) -> str: 47 """Return the current weather for a location.""" 48 return f"Weather in {location}: Sunny, 72F" 49 50if __name__ == "__main__": 51 agent_graph = build_agent([get_weather]) 52 result = agent_graph.invoke( 53 {"messages": [HumanMessage(content="What's the weather in NYC?")]} 54 ) 55 print(result["messages"][-1].content)
You'll know it works when the final printed message contains the weather string ("Sunny, 72F") and result["messages"] has three entries: the HumanMessage, an AIMessage carrying tool_calls, and a ToolMessage from ToolNode. If the history collapses to a single message, the add reducer is missing on AgentState.messages; if the graph errors with a recursion-limit exception, raise max_iterations or check that the back-edge from tools to agent is present.
Do's and Don'ts
Having walked through building with langgraph stategraph above, the following Do's and Don'ts distill it into practice.
Do's
- ✓Do annotate accumulating state with a reducer —
Annotated[Sequence, add]onmessagesis the difference between a working agent and one that silently loses tool results. - ✓Do use the prebuilt
ToolNodeandtools_condition— they parsetool_callsand buildToolMessageresults correctly; rolling your own re-introduces the dispatcher bugs you came to LangGraph to escape. - ✓Do set
recursion_limitexplicitly — the default of 25 silently truncates long agent runs; size it to your worst-case iteration count times two (agent + tools per cycle).
Don'ts
- ✗Don't return the full message list from a node — return only the new messages; the reducer appends. Returning the full list under an
addreducer duplicates every prior message on each step. - ✗Don't forget the back-edge from
toolstoagent— without it the graph terminates after the first tool call instead of letting the LLM read the tool result and continue. - ✗Don't mutate state in place inside a node — return a new dict. LangGraph's reducer model assumes immutability; in-place mutation defeats checkpoint replay and time-travel debugging.
This lesson is free to read. Its hands-on lab — real code, in a cloud IDE — is part of the GenAI Agent Engineering subscription.
From · cancel anytime