Building AI Agents with LangGraph: A Practical Guide
Create stateful, multi-step AI agents that can reason, use tools, and maintain context across complex workflows.
Introduction
LangGraph extends LangChain with graph-based orchestration — enabling cycles, branching, and persistent state. This is essential for building agents that:
- Handle multi-turn conversations with memory
- Execute complex workflows with decision points
- Recover from errors and retry
- Human-in-the-loop approval flows
Why LangGraph Over Chains?
| Feature | LCEL Chains | LangGraph |
|---|---|---|
| Cycles/Loops | ❌ | ✅ |
| Branching | Limited | ✅ Full control |
| State Persistence | Manual | ✅ Built-in |
| Human-in-the-loop | Complex | ✅ Native |
| Debugging | Hard | ✅ Visual graph |
| Parallel Execution | ❌ | ✅ |
Installation
pip install langgraph langchain-openai langchain-anthropic tavily-python Core Concepts
1. State
The shared data structure that flows through the graph:
from typing import Annotated, List from langgraph.graph import StateGraph, MessagesState from langchain_core.messages import BaseMessage class AgentState(MessagesState): # Extend MessagesState with custom fields user_id: str session_data: dict tool_call_count: int = 0 2. Nodes
Functions that process state and return updates:
from langchain_core.runnables import RunnableConfig def agent_node(state: AgentState, config: RunnableConfig) -> AgentState: # Process messages, call LLM, return updated state return {"messages": [response]} 3. Edges
Control flow between nodes — conditional or unconditional:
def should_continue(state: AgentState) -> str: last_message = state["messages"][-1] if last_message.tool_calls: return "tools" return "end" Building a ReAct Agent with Tools
Complete Example
# agent.py from typing import Annotated, Literal from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages from langgraph.prebuilt import ToolNode from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.messages import HumanMessage, AIMessage, ToolMessage import json # 1. Define Tools @tool def search_web(query: str) -> str: """Search the web for current information.""" # In production, use Tavily, SerpAPI, etc. return f"Search results for: {query}" @tool def calculator(expression: str) -> str: """Evaluate a mathematical expression safely.""" try: # Safe eval for basic math allowed = set("0123456789+-*/(). ") if all(c in allowed for c in expression): return str(eval(expression)) return "Invalid expression" except Exception as e: return f"Error: {e}" @tool def get_weather(city: str) -> str: """Get current weather for a city.""" # Mock - replace with real API return f"Weather in {city}: Sunny, 22°C" tools = [search_web, calculator, get_weather] # 2. Define State class AgentState(MessagesState): pass # 3. Create LLM with Tools llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) llm_with_tools = llm.bind_tools(tools) # 4. Define Nodes def agent(state: AgentState) -> AgentState: """Main agent reasoning node.""" messages = state["messages"] response = llm_with_tools.invoke(messages) return {"messages": [response]} tool_node = ToolNode(tools) # 5. Build Graph workflow = StateGraph(AgentState) workflow.add_node("agent", agent) workflow.add_node("tools", tool_node) workflow.add_edge(START, "agent") workflow.add_conditional_edges( "agent", lambda state: "tools" if state["messages"][-1].tool_calls else "end", {"tools": "tools", "end": END} ) workflow.add_edge("tools", "agent") app = workflow.compile() # 6. Run if __name__ == "__main__": # Initial state inputs = { "messages": [ HumanMessage(content="What's 25 * 4 + 10? Also search for 'LangGraph tutorial'") ] } # Execute for chunk in app.stream(inputs): for node, output in chunk.items(): print(f"\n--- {node} ---") if "messages" in output: msg = output["messages"][-1] print(f"{msg.type}: {msg.content}") if msg.tool_calls: print(f"Tool calls: {msg.tool_calls}") Adding Memory & Persistence
Short-term Memory (Thread-level)
from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() app = workflow.compile(checkpointer=checkpointer) # Run with thread_id for conversation memory config = {"configurable": {"thread_id": "user-123"}} # First message result = app.invoke( {"messages": [HumanMessage(content="My name is Alice")]}, config=config ) # Second message - remembers name! result = app.invoke( {"messages": [HumanMessage(content="What's my name?")]}, config=config ) Long-term Memory (Cross-session)
from langgraph.checkpoint.sqlite import SqliteSaver # Persistent across restarts conn = sqlite3.connect("agent_memory.db", check_same_thread=False) checkpointer = SqliteSaver(conn) app = workflow.compile(checkpointer=checkpointer) Human-in-the-Loop Approval
from langgraph.types import interrupt, Command def sensitive_action(state: AgentState) -> AgentState: """Node that requires human approval.""" # Pause execution and wait for human input decision = interrupt({ "question": "Approve sending this email?", "draft": state["email_draft"] }) if decision["approved"]: # Send email logic here return {"messages": [AIMessage(content="Email sent!")]} else: return {"messages": [AIMessage(content="Email cancelled.")]} # Add to graph workflow.add_node("human_approval", sensitive_action) workflow.add_edge("agent", "human_approval") workflow.add_edge("human_approval", "agent") # Resume after human input app.invoke(Command(resume={"approved": True}), config=config) Multi-Agent Workflow
from langgraph.graph import StateGraph from typing import Literal class MultiAgentState(MessagesState): next_agent: str # Specialist agents def researcher(state: MultiAgentState) -> MultiAgentState: # Research logic return {"messages": [AIMessage(content="Research complete")], "next_agent": "writer"} def writer(state: MultiAgentState) -> MultiAgentState: # Writing logic return {"messages": [AIMessage(content="Article written")], "next_agent": "editor"} def editor(state: MultiAgentState) -> MultiAgentState: # Editing logic return {"messages": [AIMessage(content="Article edited")], "next_agent": "end"} # Router def router(state: MultiAgentState) -> Literal["researcher", "writer", "editor", "end"]: return state.get("next_agent", "end") # Build workflow = StateGraph(MultiAgentState) workflow.add_node("researcher", researcher) workflow.add_node("writer", writer) workflow.add_node("editor", editor) workflow.add_edge(START, "researcher") workflow.add_conditional_edges("researcher", router) workflow.add_conditional_edges("writer", router) workflow.add_conditional_edges("editor", router) app = workflow.compile() Streaming & Real-time Updates
# Stream tokens as they're generated for chunk in app.stream(inputs, stream_mode="messages"): msg, metadata = chunk if msg.content: print(msg.content, end="", flush=True) # Stream state updates for chunk in app.stream(inputs, stream_mode="values"): print(f"State keys: {chunk.keys()}") Debugging & Visualization
# Visualize graph from langgraph.graph import draw_graph # Save as PNG draw_graph(app.get_graph()).write_png("agent_graph.png") # Print Mermaid diagram print(app.get_graph().draw_mermaid()) Output:
graph TD START --> agent agent -->|tools| tools agent -->|end| END tools --> agent Production Checklist
- [ ] Error handling — Wrap nodes in try/catch, add retry logic
- [ ] Rate limiting — Use
RunnableConfigfor API limits - [ ] Observability — Add LangSmith tracing
- [ ] Validation — Validate tool inputs/outputs with Pydantic
- [ ] Timeouts — Set execution timeouts per node
- [ ] State schema — Use Pydantic for strict state validation
- [ ] Testing — Unit test each node independently
# Example: Node with retry and timeout from langgraph.graph import RetryPolicy workflow.add_node( "unreliable_api", unreliable_node, retry=RetryPolicy(max_attempts=3, backoff_factor=2) ) Advanced Patterns
1. Parallel Tool Execution
from langgraph.graph import StateGraph from concurrent.futures import ThreadPoolExecutor def parallel_tools(state: AgentState) -> AgentState: tool_calls = state["messages"][-1].tool_calls with ThreadPoolExecutor() as executor: results = list(executor.map(execute_tool, tool_calls)) return {"messages": [ToolMessage(content=r, tool_call_id=tc["id"]) for r, tc in zip(results, tool_calls)]} 2. Dynamic Tool Selection
def select_tools(state: AgentState) -> list: """Choose tools based on query type.""" query = state["messages"][-1].content.lower() if "calculate" in query: return [calculator] elif "weather" in query: return [get_weather] return [search_web] 3. Reflection & Self-Correction
def reflect(state: AgentState) -> AgentState: """Critique the last response and improve.""" critique_prompt = f"Critique this answer: {state['messages'][-1].content}" critique = llm.invoke([HumanMessage(content=critique_prompt)]) return {"messages": [critique]} workflow.add_node("reflect", reflect) workflow.add_edge("agent", "reflect") workflow.add_conditional_edges("reflect", lambda s: "agent" if "needs improvement" in s["messages"][-1].content.lower() else "end") Deployment
FastAPI Server
# server.py from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class ChatRequest(BaseModel): message: str thread_id: str = "default" @app.post("/chat") async def chat(request: ChatRequest): config = {"configurable": {"thread_id": request.thread_id}} result = agent_app.invoke( {"messages": [HumanMessage(content=request.message)]}, config=config ) return {"response": result["messages"][-1].content} Docker
FROM python:3.11-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000"] Resources
Part of the AI Agents series. Next: Multi-Agent Systems with LangGraph.