AI agents that can reason, use tools, and maintain state across interactions represent one of the most practical applications of large language models today. While many frameworks promise agent capabilities, LangGraph stands out by giving you explicit control over agent behavior through a graph-based architecture.

In this tutorial, you will build a functional AI agent from scratch using LangGraph. By the end, your agent will be able to search the web, perform calculations, and chain multiple reasoning steps together.

What is LangGraph?

LangGraph is a library built on top of LangChain that lets you define agent workflows as directed graphs. Each node in the graph represents a computation step (calling an LLM, executing a tool, making a decision), and edges define how control flows between them.

This graph-based approach solves a fundamental problem with traditional agent loops: unpredictability. Instead of relying on an LLM to decide the entire flow, you define the structure explicitly while letting the LLM handle reasoning within each node.

LangChain

Prerequisites

Before starting, make sure you have:

  • Python 3.10 or higher installed
  • An OpenAI API key (or any LLM provider supported by LangChain)
  • Basic familiarity with Python async/await patterns
  • A terminal and code editor ready

Step 1: Install Dependencies

Set up your project environment and install the required packages:

mkdir langgraph-agent && cd langgraph-agent
python -m venv venv
source venv/bin/activate
pip install langgraph langchain-openai langchain-community tavily-python

Create a .env file for your API keys:

OPENAI_API_KEY=your-openai-key-here
TAVILY_API_KEY=your-tavily-key-here

Step 2: Define Your Agent State

LangGraph agents operate on a shared state object. This state gets passed between nodes and accumulates information as the agent works:

from typing import Annotated, TypedDict
from langgraph.graph.message import add_messages
 
class AgentState(TypedDict):
    messages: Annotated[list, add_messages]
    tool_calls_count: int

The add_messages annotation tells LangGraph to append new messages rather than replacing the list. The tool_calls_count field tracks how many tools the agent has used, which is useful for setting limits.

Step 3: Set Up Tools

Define the tools your agent can use. Here we create a web search tool and a calculator:

from langchain_community.tools.tavily_search import TavilySearchResults
from langchain_core.tools import tool
 
search_tool = TavilySearchResults(max_results=3)
 
@tool
def calculator(expression: str) -> str:
    """Evaluate a mathematical expression. Use Python syntax."""
    try:
        result = eval(expression, {"__builtins__": {}}, {})
        return str(result)
    except Exception as e:
        return f"Error: {e}"
 
tools = [search_tool, calculator]

Step 4: Create the Agent Node

The agent node calls the LLM with the current conversation history and available tools:

from langchain_openai import ChatOpenAI
 
llm = ChatOpenAI(model="gpt-4o", temperature=0)
llm_with_tools = llm.bind_tools(tools)
 
def agent_node(state: AgentState) -> dict:
    """Call the LLM to decide what to do next."""
    messages = state["messages"]
    response = llm_with_tools.invoke(messages)
    return {"messages": [response], "tool_calls_count": state.get("tool_calls_count", 0)}

Step 5: Create the Tool Execution Node

When the LLM decides to use a tool, this node executes the chosen tool and returns the result:

from langgraph.prebuilt import ToolNode
 
tool_node = ToolNode(tools)

LangGraph's built-in ToolNode handles parsing the LLM's tool call request, executing the appropriate tool, and formatting the result as a message.

Step 6: Define the Routing Logic

The router determines whether the agent should continue using tools or return its final answer:

from langgraph.graph import END
 
def should_continue(state: AgentState) -> str:
    """Decide whether to call tools or finish."""
    last_message = state["messages"][-1]
 
    # If the LLM made tool calls, route to tool execution
    if last_message.tool_calls:
        # Safety limit: stop after 5 tool calls
        if state.get("tool_calls_count", 0) >= 5:
            return END
        return "tools"
 
    # No tool calls means the agent is done
    return END

Step 7: Assemble the Graph

Now connect all the pieces into a complete graph:

from langgraph.graph import StateGraph
 
# Create the graph
workflow = StateGraph(AgentState)
 
# Add nodes
workflow.add_node("agent", agent_node)
workflow.add_node("tools", tool_node)
 
# Set entry point
workflow.set_entry_point("agent")
 
# Add edges
workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", END: END})
workflow.add_edge("tools", "agent")
 
# Compile
agent = workflow.compile()

The flow is: start at the agent node, if it wants to use tools go to the tool node, then return to the agent node. Repeat until the agent produces a final answer.

Step 8: Run Your Agent

Put it all together and test your agent:

from langchain_core.messages import HumanMessage
 
async def main():
    inputs = {
        "messages": [HumanMessage(content="What is the population of France, and what is that number divided by 7?")],
        "tool_calls_count": 0
    }
 
    async for event in agent.astream(inputs, stream_mode="values"):
        last_msg = event["messages"][-1]
        print(f"[{last_msg.type}]: {last_msg.content[:200]}")
 
import asyncio
asyncio.run(main())

When you run this, you will see the agent search for France's population, extract the number, then use the calculator to divide it by 7.

Step 9: Add Memory for Multi-Turn Conversations

To make your agent remember previous interactions, add a checkpointer:

from langgraph.checkpoint.memory import MemorySaver
 
memory = MemorySaver()
agent_with_memory = workflow.compile(checkpointer=memory)
 
# Use thread_id to maintain conversation history
config = {"configurable": {"thread_id": "user-session-1"}}
 
response = await agent_with_memory.ainvoke(
    {"messages": [HumanMessage(content="Hello, my name is Alex")]},
    config=config
)
 
# In a follow-up call with the same thread_id, the agent remembers
response = await agent_with_memory.ainvoke(
    {"messages": [HumanMessage(content="What is my name?")]},
    config=config
)

Practical Tips

Start simple, then add complexity. Begin with a single tool and linear flow. Once that works reliably, add branching logic and additional tools.

Always set tool call limits. Without limits, agents can enter infinite loops. The tool_calls_count pattern shown above is a minimal safeguard. In production, also add timeout limits.

Use streaming for better UX. The astream method lets you show intermediate steps to users in real time, which is critical for agents that take multiple seconds to complete.

Test with deterministic inputs. When debugging, use questions with known answers so you can verify the agent's reasoning chain. Set temperature=0 during development.

Monitor token usage. Each loop iteration costs tokens. A complex query might trigger 4-5 tool calls, each with a full context window. Track costs from day one.

Where to Go Next

Once you have the basics working, consider these extensions:

  • Add human-in-the-loop approval for sensitive tool calls
  • Implement parallel tool execution for independent queries
  • Build sub-graphs for complex multi-stage workflows
  • Deploy with LangGraph Platform for production hosting

Start Building with LangChain

Access the full LangChain ecosystem including LangGraph, LangSmith for tracing, and pre-built agent templates.

Get Started Free