Complete LangGraph Tutorial: Building Complex AI Agents

# Tutorial Lengkap LangGraph: Membangun AI Agents yang Kompleks LangGraph adalah library dari LangChain untuk membangun stateful, multi-actor applications dengan Large Language Models (LLMs). Dengan...

By Ruby Abdullah · · tutorial
PythonLangGraphLangChainAI AgentsLLMMulti-Agent

Complete LangGraph Tutorial: Building Complex AI Agents

LangGraph is a library from LangChain for building stateful, multi-actor applications with Large Language Models (LLMs). With LangGraph, you can create complex AI agents with flexible control flow, state management, and the ability to run multiple agents simultaneously.

What is LangGraph?

LangGraph extends LangChain by providing:

  • Stateful graphs: Store and manage state between steps
  • Cyclic flows: Support for loops and conditionals
  • Human-in-the-loop: Manual interaction during workflow
  • Persistence: Save state for resume later
  • Streaming: Real-time streaming responses

When to use LangGraph:
  • Building AI agents with multi-step reasoning
  • Workflows requiring conditionals and loops
  • Applications needing human approval
  • Multi-agent systems
  • Complex RAG pipelines

Installation

pip install langgraph langchain langchain-openai

Basic Concepts

1. Graph Components

LangGraph consists of:

  • State: Data managed throughout execution
  • Nodes: Functions that process and modify state
  • Edges: Connections between nodes (conditional or fixed)

2. Basic Graph Structure

from typing import TypedDict, Annotated

from langgraph.graph import StateGraph, START, END

Define state

class State(TypedDict):

messages: list

currentstep: str

Define nodes

def nodea(state: State) -> State:

state["messages"].append("Processed by Node A")

state["currentstep"] = "a"

return state

def nodeb(state: State) -> State:

state["messages"].append("Processed by Node B")

state["currentstep"] = "b"

return state

Build graph

graph = StateGraph(State)

graph.addnode("nodea", nodea)

graph.addnode("nodeb", nodeb)

Add edges

graph.addedge(START, "nodea")

graph.addedge("nodea", "nodeb")

graph.addedge("nodeb", END)

Compile

app = graph.compile()

Run

result = app.invoke({"messages": [], "currentstep": ""})

print(result)

Building a Simple Chatbot

1. Basic Chatbot with Memory

from typing import Annotated

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import addmessages

from langchainopenai import ChatOpenAI

from langchaincore.messages import HumanMessage, AIMessage

State with message history

class ChatState(TypedDict):

messages: Annotated[list, addmessages]

Initialize LLM

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

Chatbot node

def chatbot(state: ChatState) -> ChatState:

response = llm.invoke(state["messages"])

return {"messages": [response]}

Build graph

graph = StateGraph(ChatState)

graph.addnode("chatbot", chatbot)

graph.addedge(START, "chatbot")

graph.addedge("chatbot", END)

app = graph.compile()

Interactive chat

def chat(userinput: str, history: list = None):

if history is None:

history = []

history.append(HumanMessage(content=userinput))

result = app.invoke({"messages": history})

return result["messages"]

Usage

messages = chat("Hello, who are you?")

print(messages[-1].content)

messages = chat("What can you do?", messages)

print(messages[-1].content)

2. Chatbot with Tools

from typing import Annotated

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import addmessages

from langgraph.prebuilt import ToolNode, toolscondition

from langchainopenai import ChatOpenAI

from langchaincore.tools import tool

Define tools

@tool

def searchweb(query: str) -> str:

"""Search the web for information."""

# Simulate web search

return f"Search results for: {query}"

@tool

def calculator(expression: str) -> str:

"""Calculate a mathematical expression."""

try:

result = eval(expression)

return f"Result: {result}"

except:

return "Error calculating expression"

@tool

def getweather(city: str) -> str:

"""Get current weather for a city."""

# Simulate weather API

return f"Weather in {city}: 25°C, Sunny"

tools = [searchweb, calculator, getweather]

State

class AgentState(TypedDict):

messages: Annotated[list, addmessages]

LLM with tools

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

llmwithtools = llm.bindtools(tools)

Agent node

def agent(state: AgentState) -> AgentState:

response = llmwithtools.invoke(state["messages"])

return {"messages": [response]}

Build graph

graph = StateGraph(AgentState)

Add nodes

graph.addnode("agent", agent)

graph.addnode("tools", ToolNode(tools))

Add edges

graph.addedge(START, "agent")

graph.addconditionaledges(

"agent",

toolscondition, # Routes to "tools" if tool call, else END

)

graph.addedge("tools", "agent")

app = graph.compile()

Test

from langchaincore.messages import HumanMessage

result = app.invoke({

"messages": [HumanMessage(content="What's the weather in Jakarta?")]

})

print(result["messages"][-1].content)

result = app.invoke({

"messages": [HumanMessage(content="Calculate 25 4 + 100")]

})

print(result["messages"][-1].content)

ReAct Agent Pattern

ReAct (Reasoning + Acting) is a popular pattern for AI agents.

from typing import Annotated, Literal

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import addmessages

from langchainopenai import ChatOpenAI

from langchaincore.tools import tool

from langchaincore.messages import HumanMessage, AIMessage, ToolMessage

Tools

@tool

def searchdatabase(query: str) -> str:

"""Search internal database for information."""

# Simulate database search

data = {

"producta": "Price: $100, Stock: 50",

"productb": "Price: $200, Stock: 25",

"usercount": "Total users: 10,000"

}

for key, value in data.items():

if key in query.lower():

return value

return "No results found"

@tool

def sendemail(to: str, subject: str, body: str) -> str:

"""Send an email to a recipient."""

return f"Email sent to {to} with subject: {subject}"

tools = [searchdatabase, sendemail]

State

class ReActState(TypedDict):

messages: Annotated[list, addmessages]

iteration: int

LLM

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

llmwithtools = llm.bindtools(tools)

Nodes

def reasoning(state: ReActState) -> ReActState:

"""Agent reasoning step"""

response = llmwithtools.invoke(state["messages"])

return {

"messages": [response],

"iteration": state.get("iteration", 0) + 1

}

def acting(state: ReActState) -> ReActState:

"""Execute tool calls"""

lastmessage = state["messages"][-1]

toolresults = []

for toolcall in lastmessage.toolcalls:

toolname = toolcall["name"]

toolargs = toolcall["args"]

# Find and execute tool

for t in tools:

if t.name == toolname:

result = t.invoke(toolargs)

toolresults.append(

ToolMessage(content=result, toolcallid=toolcall["id"])

)

break

return {"messages": toolresults}

def shouldcontinue(state: ReActState) -> Literal["acting", "end"]:

"""Determine if we should continue or end"""

lastmessage = state["messages"][-1]

# Check iteration limit

if state.get("iteration", 0) >= 5:

return "end"

# Check if there are tool calls

if hasattr(lastmessage, "toolcalls") and lastmessage.toolcalls:

return "acting"

return "end"

Build graph

graph = StateGraph(ReActState)

graph.addnode("reasoning", reasoning)

graph.addnode("acting", acting)

graph.addedge(START, "reasoning")

graph.addconditionaledges(

"reasoning",

shouldcontinue,

{"acting": "acting", "end": END}

)

graph.addedge("acting", "reasoning")

app = graph.compile()

Test

result = app.invoke({

"messages": [HumanMessage(content="Search for producta info and then send it to user@example.com")],

"iteration": 0

})

for msg in result["messages"]:

print(f"{type(msg).name}: {msg.content[:100]}...")

Multi-Agent Systems

1. Supervisor Pattern

from typing import Annotated, Literal

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import addmessages

from langchainopenai import ChatOpenAI

from langchaincore.messages import HumanMessage, AIMessage

State

class MultiAgentState(TypedDict):

messages: Annotated[list, addmessages]

nextagent: str

Agents

def researcher(state: MultiAgentState) -> MultiAgentState:

"""Research agent - gathers information"""

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

system = """You are a research agent. Your job is to gather and analyze information.

Be thorough but concise in your findings."""

messages = [{"role": "system", "content": system}] + [

{"role": m.type, "content": m.content} for m in state["messages"]

]

response = llm.invoke(messages)

return {"messages": [AIMessage(content=f"[Researcher]: {response.content}")]}

def writer(state: MultiAgentState) -> MultiAgentState:

"""Writer agent - creates content"""

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

system = """You are a writer agent. Your job is to create well-written content

based on the research provided. Be creative and engaging."""

messages = [{"role": "system", "content": system}] + [

{"role": m.type, "content": m.content} for m in state["messages"]

]

response = llm.invoke(messages)

return {"messages": [AIMessage(content=f"[Writer]: {response.content}")]}

def reviewer(state: MultiAgentState) -> MultiAgentState:

"""Reviewer agent - reviews and improves content"""

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

system = """You are a reviewer agent. Your job is to review content and provide

constructive feedback or improvements."""

messages = [{"role": "system", "content": system}] + [

{"role": m.type, "content": m.content} for m in state["messages"]

]

response = llm.invoke(messages)

return {"messages": [AIMessage(content=f"[Reviewer]: {response.content}")]}

def supervisor(state: MultiAgentState) -> MultiAgentState:

"""Supervisor - decides which agent to call next"""

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

system = """You are a supervisor managing a team of agents:

  • researcher: gathers information
  • writer: creates content
  • reviewer: reviews content

Based on the conversation, decide which agent should work next.

Respond with ONLY one of: researcher, writer, reviewer, or FINISH"""

messages = [{"role": "system", "content": system}] + [

{"role": m.type, "content": m.content} for m in state["messages"]

]

response = llm.invoke(messages)

nextagent = response.content.strip().lower()

return {"nextagent": nextagent}

def routesupervisor(state: MultiAgentState) -> Literal["researcher", "writer", "reviewer", "end"]:

"""Route based on supervisor decision"""

nextagent = state.get("nextagent", "").lower()

if "finish" in nextagent:

return "end"

elif "researcher" in nextagent:

return "researcher"

elif "writer" in nextagent:

return "writer"

elif "reviewer" in nextagent:

return "reviewer"

else:

return "end"

Build graph

graph = StateGraph(MultiAgentState)

graph.addnode("supervisor", supervisor)

graph.addnode("researcher", researcher)

graph.addnode("writer", writer)

graph.addnode("reviewer", reviewer)

graph.addedge(START, "supervisor")

graph.addconditionaledges(

"supervisor",

routesupervisor,

{

"researcher": "researcher",

"writer": "writer",

"reviewer": "reviewer",

"end": END

}

)

graph.addedge("researcher", "supervisor")

graph.addedge("writer", "supervisor")

graph.addedge("reviewer", "supervisor")

app = graph.compile()

Test

result = app.invoke({

"messages": [HumanMessage(content="Write a blog post about AI in healthcare")],

"nextagent": ""

})

for msg in result["messages"]:

print(f"\n{msg.content}\n{'='50}")

Human-in-the-Loop

1. Interrupt for Approval

from typing import Annotated

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langgraph.graph.message import addmessages

from langgraph.checkpoint.memory import MemorySaver

from langchainopenai import ChatOpenAI

from langchaincore.messages import HumanMessage

State

class ApprovalState(TypedDict):

messages: Annotated[list, addmessages]

draft: str

approved: bool

Nodes

def createdraft(state: ApprovalState) -> ApprovalState:

llm = ChatOpenAI(model="gpt-4o-mini")

response = llm.invoke(state["messages"])

return {"draft": response.content, "approved": False}

def processapproval(state: ApprovalState) -> ApprovalState:

# This node processes after human approval

return {"messages": [AIMessage(content=f"Final approved content: {state['draft']}")]}

def checkapproval(state: ApprovalState) -> str:

if state.get("approved", False):

return "process"

return "wait"

Build graph

graph = StateGraph(ApprovalState)

graph.addnode("createdraft", createdraft)

graph.addnode("processapproval", processapproval)

graph.addedge(START, "createdraft")

graph.addconditionaledges(

"createdraft",

checkapproval,

{"process": "processapproval", "wait": END}

)

graph.addedge("processapproval", END)

Compile with checkpointer for persistence

memory = MemorySaver()

app = graph.compile(checkpointer=memory, interruptbefore=["processapproval"])

Usage with human approval

config = {"configurable": {"threadid": "1"}}

Step 1: Create draft

result = app.invoke(

{"messages": [HumanMessage(content="Write an email to client about project delay")]},

config

)

print("Draft created:", result["draft"])

Step 2: Human reviews and approves

In real app, this would be UI interaction

print("\n[Human approves the draft]\n")

Step 3: Continue with approval

result = app.invoke(

{"approved": True},

config

)

print("Final:", result["messages"][-1].content)

Persistence and Checkpointing

1. Memory Saver (In-Memory)

from langgraph.checkpoint.memory import MemorySaver

memory = MemorySaver()

app = graph.compile(checkpointer=memory)

Run with threadid

config = {"configurable": {"threadid": "user123"}}

result = app.invoke({"messages": [HumanMessage(content="Hello")]}, config)

Continue conversation

result = app.invoke({"messages": [HumanMessage(content="What did I say?")]}, config)

2. SQLite Persistence

from langgraph.checkpoint.sqlite import SqliteSaver

Create SQLite checkpointer

with SqliteSaver.fromconnstring(":memory:") as checkpointer:

app = graph.compile(checkpointer=checkpointer)

config = {"configurable": {"threadid": "session1"}}

result = app.invoke({"messages": [HumanMessage(content="Hello")]}, config)

3. PostgreSQL Persistence

from langgraph.checkpoint.postgres import PostgresSaver

connectionstring = "postgresql://user:password@localhost/langgraph"

with PostgresSaver.fromconnstring(connectionstring) as checkpointer:

app = graph.compile(checkpointer=checkpointer)

config = {"configurable": {"threadid": "persistentsession"}}

result = app.invoke({"messages": [HumanMessage(content="Hello")]}, config)

Streaming

1. Stream Events

from langchaincore.messages import HumanMessage

Stream all events

for event in app.stream({"messages": [HumanMessage(content="Tell me a story")]}):

print(event)

print("---")

2. Stream with Mode

# Stream values (state after each node)

for state in app.stream(

{"messages": [HumanMessage(content="Hello")]},

streammode="values"

):

print(state["messages"][-1].content)

Stream updates (only changes)

for update in app.stream(

{"messages": [HumanMessage(content="Hello")]},

streammode="updates"

):

print(update)

3. Async Streaming

import asyncio

async def streamresponse():

async for event in app.astream(

{"messages": [HumanMessage(content="Hello")]}

):

print(event)

asyncio.run(streamresponse())

RAG with LangGraph

from typing import Annotated, List

from typingextensions import TypedDict

from langgraph.graph import StateGraph, START, END

from langchainopenai import ChatOpenAI, OpenAIEmbeddings

from langchaincommunity.vectorstores import FAISS

from langchaincore.messages import HumanMessage

State

class RAGState(TypedDict):

question: str

context: List[str]

answer: str

Setup vector store

embeddings = OpenAIEmbeddings()

documents = [

"LangGraph is a library for building stateful AI applications.",

"It supports cyclic flows and human-in-the-loop interactions.",

"LangGraph extends LangChain with graph-based workflows.",

]

vectorstore = FAISS.fromtexts(documents, embeddings)

retriever = vectorstore.asretriever(k=2)

Nodes

def retrieve(state: RAGState) -> RAGState:

"""Retrieve relevant documents"""

docs = retriever.invoke(state["question"])

return {"context": [doc.pagecontent for doc in docs]}

def generate(state: RAGState) -> RAGState:

"""Generate answer based on context"""

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

context = "\n".join(state["context"])

prompt = f"""Answer the question based on the context.

Context:

{context}

Question: {state["question"]}

Answer:"""

response = llm.invoke(prompt)

return {"answer": response.content}

Build graph

graph = StateGraph(RAGState)

graph.addnode("retrieve", retrieve)

graph.addnode("generate", generate)

graph.addedge(START, "retrieve")

graph.addedge("retrieve", "generate")

graph.addedge("generate", END)

app = graph.compile()

Test

result = app.invoke({"question": "What is LangGraph?"})

print(f"Answer: {result['answer']}")

Best Practices

1. Error Handling

from langgraph.graph import StateGraph

def safenode(state):

try:

# Your logic here

result = riskyoperation()

return {"result": result, "error": None}

except Exception as e:

return {"result": None, "error": str(e)}

def errorrouter(state):

if state.get("error"):

return "handleerror"

return "continue"

2. Timeout and Retry

import asyncio

from tenacity import retry, stopafterattempt, waitexponential

@retry(stop=stopafterattempt(3), wait=waitexponential(multiplier=1, min=4, max=10))

def nodewithretry(state):

response = llm.invoke(state["messages"])

return {"messages": [response]}

3. Logging

import logging

logging.basicConfig(level=logging.INFO)

logger = logging.getLogger(name)

def logged_node(state):

logger.info(f"Processing state: {state}")

result = process(state)

logger.info(f"Result: {result}")

return result

Conclusion

LangGraph provides a powerful framework for building complex AI agents with:

  • State Management: Managing data throughout workflow
  • Flexible Control Flow: Conditional edges and cycles
  • Human-in-the-Loop: Approval and manual interactions
  • Persistence: Saving state for resume
  • Multi-Agent: Coordinating multiple agents
  • Key takeaways:

    • Use TypedDict for type-safe state
    • Leverage conditional edges for dynamic routing
    • Implement checkpointing for production
    • Use streaming for real-time responses
    • Handle errors gracefully with fallbacks

    Related Articles

    LangChain Tutorial: The Most Popular Framework for Building LLM Applications

    Tutorial LangChain: Framework Paling Populer untuk Membangun Aplikasi LLM LangChain adalah framework open-source yang di...

    Zep Tutorial: Long-Term Memory for AI Agents with a Temporal Knowledge Graph

    Zep: Bikin AI Agent Punya Memori Jangka Panjang dengan Temporal Knowledge Graph Temen-temen, pernah ngobrol sama chatbot...

    Complete Tutorial: Google Agent Development Kit (ADK) for Building AI Agents with Python

    Tutorial Lengkap Google Agent Development Kit (ADK): Membangun AI Agent dengan Python Google Agent Development Kit (ADK)...

    Composio Tutorial: Tool Integration Platform for AI Agents

    Tutorial Composio: Platform Integrasi Tool untuk AI Agents Composio adalah platform open-source yang memungkinkan AI age...