AI & Agents

LangGraph Tutorial: Build Your First AI Agent in Python (with Human Approval)

A hands-on LangGraph tutorial: build a tool-using customer-support agent in Python with graph state, a tool loop, conversation memory and a human approval step before refunds.

BIGXSTAR Academy Team··12 min read
LangGraphAI AgentsAgentic AIPythonTutorialGenerative AI

In this tutorial you will build a small but realistic AI agent with LangGraph: a customer-support agent for an online shop that can look up orders, and that must ask a human for approval before issuing a refund.

Along the way you will learn the core LangGraph ideas you will use in every agent: state, nodes, edges, the tool loop, checkpoints and interrupts.

You should know: Python basics and what an LLM API is. If tool calling is new to you, skim our agentic AI roadmap first.


Why LangGraph?

You can build a simple agent with a while loop: call the model, run any tool it asks for, repeat. That works until you need:

  • To pause for a human and resume later, maybe hours later
  • To remember a conversation across requests
  • Branching logic: different paths for different situations
  • To see exactly which step did what when something goes wrong

LangGraph models your agent as a graph. Each step is a node, the paths between steps are edges, and all steps read and write a shared state. A checkpointer saves the state after every step, which is what makes pausing, resuming and memory possible.


Setup

bash
pip install -U langgraph langchain-anthropic

This tutorial uses Claude as the model through langchain-anthropic. Set ANTHROPIC_API_KEY in your environment. LangGraph works with other model providers too; only the model line changes.


Step 1: define the tools

Tools are ordinary Python functions. The @tool decorator turns the function name, docstring and type hints into a definition the model can read, so write clear docstrings.

python
from langchain_core.tools import tool
from langgraph.types import interrupt

ORDERS = {
    "ORD-1001": {"status": "delivered", "amount": 2499},
    "ORD-1002": {"status": "delivered", "amount": 1200},
    "ORD-1003": {"status": "processing", "amount": 799},
}


@tool
def get_order_status(order_id: str) -> str:
    """Look up an order's status and amount. Use this before answering any question about an order."""
    order = ORDERS.get(order_id.strip().upper())
    if order is None:
        return f"No order found with ID {order_id}."
    return f"Order {order_id} is {order['status']}. Amount paid: Rs {order['amount']}."


@tool
def issue_refund(order_id: str, amount: int) -> str:
    """Refund a delivered order. A human reviewer must approve every refund."""
    order = ORDERS.get(order_id.strip().upper())
    if order is None:
        return f"No order found with ID {order_id}."
    if amount > order["amount"]:
        return f"Refund of Rs {amount} exceeds the amount paid (Rs {order['amount']})."

    # Pause the whole graph here and wait for a human decision.
    decision = interrupt({"action": "refund", "order_id": order_id, "amount": amount})
    if decision != "approve":
        return f"The refund for {order_id} was not approved by the reviewer."
    return f"Refund of Rs {amount} issued for {order_id}."


tools = [get_order_status, issue_refund]

Two design choices worth copying:

  • Validation happens in code, not in the prompt. The model cannot talk its way past amount > order["amount"].
  • The approval gate is in the tool itself, so no path through the agent can issue a refund without a human.

Step 2: define the state and the model

The state is the data every node shares. For a chat agent, that is the list of messages. The add_messages reducer means nodes append messages instead of overwriting the list.

python
from typing import Annotated
from typing_extensions import TypedDict

from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage
from langgraph.graph.message import add_messages


class State(TypedDict):
    messages: Annotated[list, add_messages]


SYSTEM = SystemMessage(
    "You are a polite support agent for an online shop in India. "
    "Always look up an order before discussing it. "
    "Only offer refunds for delivered orders, and never promise a refund before it is approved."
)

model = ChatAnthropic(model="claude-opus-5-5", max_tokens=16000).bind_tools(tools)


def agent(state: State):
    reply = model.invoke([SYSTEM, *state["messages"]])
    return {"messages": [reply]}

bind_tools sends the tool definitions with every request, so the model can choose to call them.


Step 3: build the graph

python
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.graph import StateGraph, START
from langgraph.prebuilt import ToolNode, tools_condition

builder = StateGraph(State)
builder.add_node("agent", agent)
builder.add_node("tools", ToolNode(tools))

builder.add_edge(START, "agent")
# If the model asked for a tool, go to "tools"; otherwise finish.
builder.add_conditional_edges("agent", tools_condition)
# After running tools, go back to the model with the results.
builder.add_edge("tools", "agent")

graph = builder.compile(checkpointer=InMemorySaver())

That is the classic agent loop, drawn as a graph:

START → agent ──(tool call?)──► tools
          ▲                       │
          └───────────────────────┘
          │
          └──(no tool call)──► END
  • ToolNode runs whatever tools the model requested and adds the results to the state.
  • tools_condition checks the model's last message: tool calls go to "tools", a normal answer ends the run.
  • InMemorySaver saves the state after every step. In production you would use a database-backed checkpointer such as Postgres.

Step 4: run it

Every conversation gets a thread ID. The checkpointer uses it to remember the conversation.

python
config = {"configurable": {"thread_id": "customer-42"}}

result = graph.invoke(
    {"messages": [{"role": "user", "content": "Hi, where is my order ORD-1003?"}]},
    config,
)
print(result["messages"][-1].text)

The agent calls get_order_status, sees the order is still processing, and replies. Ask a follow-up on the same thread ID ("How much did I pay for it?") and it remembers which order you mean.


Step 5: the human approval step

Now ask for a refund:

python
from langgraph.types import Command

result = graph.invoke(
    {"messages": [{"role": "user", "content": "ORD-1002 arrived damaged. Please refund the full amount."}]},
    config,
)

if "__interrupt__" in result:
    request = result["__interrupt__"][0].value
    print("Needs approval:", request)          # {'action': 'refund', 'order_id': 'ORD-1002', 'amount': 1200}

    decision = input("Approve? (approve/reject): ").strip()
    result = graph.invoke(Command(resume=decision), config)

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

Here is what happens:

  1. The model looks up ORD-1002, then calls issue_refund.
  2. interrupt(...) pauses the graph and returns control to your code with the request details. The checkpointer has saved everything.
  3. In a real system you would show this in an admin dashboard or send it to Slack, and the reviewer might respond minutes or hours later.
  4. Command(resume=decision) resumes the graph exactly where it stopped. interrupt returns the reviewer's decision, and the tool continues.

One important detail: when the graph resumes, the paused node runs again from the top, and this time interrupt returns the decision instead of pausing. So keep code that has side effects (writing to a database, sending emails) after the interrupt call, never before it.


Where to take it next

This agent is small, but the structure is the same one used in production. To make it production-ready:

  • Add RAG as another tool, so the agent can answer policy questions. See What is RAG?
  • Move tools into an MCP server so other AI apps can use them too. See What is MCP?
  • Write an evaluation set: 50 realistic conversations with expected outcomes, including attempts to trick the agent into an unapproved refund
  • Add tracing so you can see each model and tool call for a given conversation
  • Use a persistent checkpointer (Postgres) so paused approvals survive restarts
  • Set limits: maximum steps per run and a cost budget

Common mistakes

  • Giving the agent broad tools like run_sql or send_email(to, body). Prefer narrow, validated tools.
  • Putting business rules only in the prompt. Prompts guide; code enforces.
  • No step limit. A confused agent can loop. LangGraph has a recursion limit; set it deliberately.
  • Testing only the happy path. Most failures come from unusual inputs, unclear requests and adversarial users.

Go from tutorial to production. Our Generative AI & AI Agents Engineering course builds on exactly this pattern: LangGraph state and memory, MCP tools, multi-agent workflows, human approval, evaluation, tracing and deployment, live online over 12 weeks with a deployed capstone.