How to Build Your First Production AI Agent with LangGraph: A Step-by-Step Guide

Why LangGraph for Production Agents
By 2026, the question is no longer whether your organization will run AI agents in production. It is how you build them so they survive real users. LangChain's State of Agent Engineering 2025 report, which surveyed more than 1,300 developers and leaders, found that 57% of organizations now have agents in production, up from 51% the year before. Quality, not cost, is the number one blocker to shipping agents at scale, and observability has become table stakes with nearly 90% of teams tracing agent behavior.
LangGraph has become the default runtime for production agents because it makes control flow explicit. Instead of hiding the agent loop inside framework magic, you model your agent as a state machine: a graph of nodes, edges, and conditional branches that share a typed state object. Every node is a plain Python function you can unit test, and the whole graph is inspectable and checkpointable. That is the difference between a demo that works once and a system you can debug at 2 a.m.
This guide walks through building a production-ready agent with LangGraph, step by step: state graphs, nodes and edges, tool integration, memory, error handling, and deployment. By the end you will have a working pattern you can adapt to your own use case, whether that is a customer support assistant, a research agent, or an internal operations copilot.
Step 1: Model Your Agent as a State Graph
Everything in LangGraph flows through a shared state object. The state is a typed dictionary that every node reads and writes, and it is the single source of truth for the agent's conversation. Start by defining the state schema with TypedDict, and use Annotated reducers when you want nodes to append to a field instead of overwriting it.
from typing import Annotated, TypedDict, Literal
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
next: str
The add_messages reducer appends new messages to the running list, which is exactly the semantics you want for a conversation. The next field holds the name of the node to route to next, which powers the agent loop.
Step 2: Define Nodes and Edges
A node is a function that takes the state and returns a partial state update. The classic ReAct loop has two nodes: a model node that reasons and decides, and a tools node that executes. A conditional edge routes between them based on whether the model asked for a tool call.
def call_model(state: AgentState):
response = llm_with_tools.invoke(state["messages"])
return {"messages": [response], "next": "tools" if response.tool_calls else END}
def call_tool(state: AgentState):
for call in state["messages"][-1].tool_calls:
result = tools_by_name[call["name"]].invoke(call["args"])
state["messages"].append(ToolMessage(result, tool_call_id=call["id"]))
return {"messages": state["messages"], "next": "model"}
builder = StateGraph(AgentState)
builder.add_node("model", call_model)
builder.add_node("tools", call_tool)
builder.add_edge(START, "model")
builder.add_conditional_edges("model", lambda s: s["next"], {"tools": "tools", END: END})
builder.add_edge("tools", "model")
graph = builder.compile()
The conditional edge reads the next field and routes to the tools node when the model emitted tool calls, or terminates the loop when it is done. This is the entire agent loop, made explicit and testable.
Step 3: Add Tools with Function Calling
Tools are how your agent touches the real world: querying a database, calling an API, reading a file, or sending an email. LangGraph works with any tool that follows the function calling convention. The cleanest way is to decorate a plain function with @tool and let the schema be inferred from the type hints and docstring.
from langchain_core.tools import tool
@tool
def get_order_status(order_id: str) -> str:
"""Look up the current status of a customer order."""
# Call your order service here
return f"Order {order_id} is out for delivery, ETA tomorrow."
@tool
def escalate_to_human(order_id: str, reason: str) -> str:
"""Escalate an order issue to a human agent."""
# Create a ticket in your support system
return f"Escalated {order_id}: {reason}"
tools = [get_order_status, escalate_to_human]
llm_with_tools = llm.bind_tools(tools)
tools_by_name = {t.name: t for t in tools}
Keep tool schemas narrow and their descriptions precise. A tool with a vague description gets called with wrong arguments, and a tool with too much surface area becomes a prompt injection target. Every tool is a trust boundary: validate inputs, scope permissions, and never expose destructive operations to an unauthenticated agent.
Step 4: Add Memory, Short-Term and Long-Term
Memory is what turns a stateless API call into a conversation. LangGraph gives you short-term memory for free through checkpointing: every step of the graph is persisted, so you can resume a thread, pause, and time travel across states. Long-term memory, the knowledge your agent carries across sessions, needs a store.
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
checkpointer = MemorySaver() # short-term: per-thread conversation state
store = InMemoryStore() # long-term: replace with Postgres/Redis in prod
graph = builder.compile(checkpointer=checkpointer, store=store)
config = {"configurable": {"thread_id": "user-42"}}
result = graph.invoke({"messages": [{"role": "user", "content": "Where is my order?"}]}, config)
For production, swap MemorySaver for a Postgres checkpointer and the in-memory store for a real database. The thread_id scopes the conversation, so the same user always resumes in the same thread. Long-term memory, such as customer preferences or past decisions, lives in the store and is retrieved explicitly by your nodes when needed.
Step 5: Error Handling and Retries
Agents fail in ways regular software does not: the model returns malformed JSON, a tool times out, an API returns a 429, or the loop spins. Plan for all of it. Wrap tool calls in try/except and feed the error back to the model so it can recover, add a maximum iteration count to stop runaway loops, and use LangGraph's built-in retry policies on nodes.
from langgraph.pregel import RetryPolicy
builder.add_node("tools", call_tool, retry=RetryPolicy(max_attempts=3, retry_on=lambda e: isinstance(e, TimeoutError)))
MAX_STEPS = 10
def call_model(state: AgentState):
if len(state["messages"]) > MAX_STEPS * 2:
return {"messages": [SystemMessage("Stop. Escalate to a human.")], "next": END}
# ... normal model call
The retry policy handles transient failures, and the step cap turns an infinite loop into a graceful handoff to a human. Both are cheap to add and expensive to discover after an incident.
Step 6: Evaluate Before You Deploy
An agent that works on three happy-path examples will fail on the long tail. Build an evaluation harness before you deploy. LangSmith lets you run offline evaluations on a dataset of realistic inputs, score tool call correctness and final answers, and catch regressions when you change models or prompts.
# pseudocode for an offline eval run
dataset = load("order_status_cases.jsonl") # 200 realistic inputs
for case in dataset:
result = graph.invoke({"messages": [{"role": "user", "content": case["input"]}]}, config)
score = judge_llm.evaluate(result["messages"][-1].content, case["expected"])
record(case["id"], score)
Track three signals on every run: tool call success rate, whether the final answer satisfies the request, and latency per node. The 94% eval score trap is real: a high pass rate on your own test set tells you nothing about adversarial or out-of-distribution inputs, so keep a small set of hard cases that must never regress.
Step 7: Deploy with Human-in-the-Loop
Production agents need a human escape hatch. LangGraph's interrupt feature pauses the graph at a checkpoint and waits for a human decision, which is ideal for approvals, escalations, and high-stakes actions. Deploy behind an API endpoint, add authentication and rate limiting, and trace every run.
from langgraph.types import interrupt
def approval_node(state: AgentState):
decision = interrupt({"action": state["pending_action"]})
if decision == "approve":
return {"next": "execute"}
return {"next": "cancel"}
When the graph hits the interrupt, it returns a checkpoint the caller can resume with a decision. This gives you the safety of a human reviewer without blocking the whole pipeline.
Decision Matrix: When to Use LangGraph
LangGraph is not the right tool for every job. Use this matrix to decide where it fits.
| Scenario | LangGraph | Alternative |
|---|---|---|
| Complex branching control flow | Best fit | Plain function calling loop |
| Multi-agent orchestration | Best fit | CrewAI role-based teams |
| Strict structured output only | Overkill | PydanticAI |
| Single-shot Q&A over docs | Overkill | RAG pipeline without agent loop |
| Long-running, resumable tasks | Best fit | Custom job queue |
Reach for LangGraph when you have branching logic, tool loops, or multi-step workflows that need checkpointing and human review. Reach for something simpler when a single model call with structured output is enough.
Practical Takeaways
- Model the agent as a state machine: typed state, explicit nodes, conditional edges. It is testable and debuggable.
- Treat every tool as a trust boundary. Validate inputs and scope permissions.
- Use checkpointing for short-term memory and a real store for long-term memory.
- Add retries and a step cap before you deploy, not after an incident.
- Build an evaluation harness and track tool call success, answer quality, and latency.
- Keep a human-in-the-loop escape hatch for high-stakes actions.
Building production AI agents is a discipline, not a one-time script. The teams that succeed treat the agent as software: versioned, tested, monitored, and reviewed. If you are evaluating a framework or planning your first production agent, our AI solution team can help you move from prototype to a system that survives real users. We also provide custom software development, IT outsourcing and consulting, and website development services for teams that need experienced engineers on their side.
Nexie
PT Niaga Expert Teknologi