Agentic AI from Scratch, Part 1: Your First Agent in LangGraph
A tool-using agent as a state machine you can actually reason about
"Agent" sounds mystical. It isn't. An agent is a loop: the model thinks, decides to call a tool, observes the result, and repeats until it can answer. The hard part isn't the intelligence, it's the control flow. LangGraph makes that control flow explicit, which is exactly what you want in production.
This is Part 1 of Agentic AI from Scratch. We build a single tool-using agent as a state machine you can actually reason about. Part 2 scales it to multiple coordinated agents.
LangGraph models your agent as a graph of nodes (functions) and edges (transitions) over a shared state object. No hidden magic, you can see every step.
The State#
Everything starts with state. For a chat agent, state is the running list of messages.
from typing import Annotated, TypedDict
from langgraph.graph import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]The add_messages reducer means each node appends to the conversation instead of overwriting it. State accumulates as the agent works.
The Tools#
Tools are just functions with a clear schema. Here are two:
from langchain_core.tools import tool
@tool
def search_docs(query: str) -> str:
"""Search the knowledge base for a query and return the top match."""
return retrieve(query, k=1)[0].text
@tool
def calculator(expression: str) -> str:
"""Evaluate a basic arithmetic expression, e.g. '120 * 0.15'."""
return str(eval(expression, {"__builtins__": {}}))
tools = [search_docs, calculator]The Graph#
Now wire the loop. Two nodes (the model and the tools) with a conditional edge that decides whether to keep looping or stop.
from langgraph.graph import StateGraph, START, END
from langgraph.prebuilt import ToolNode
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model="claude-opus-4-8").bind_tools(tools)
def call_model(state: AgentState):
return {"messages": [model.invoke(state["messages"])]}
def should_continue(state: AgentState) -> str:
last = state["messages"][-1]
return "tools" if last.tool_calls else END
graph = StateGraph(AgentState)
graph.add_node("model", call_model)
graph.add_node("tools", ToolNode(tools))
graph.add_edge(START, "model")
graph.add_conditional_edges("model", should_continue, ["tools", END])
graph.add_edge("tools", "model") # observe -> think again
agent = graph.compile()Read the edges out loud and the loop appears: start → model → (tools → model)* → end. The model thinks; if it requested tools, run them and loop back; otherwise finish. That's the entire ReAct pattern, made visible.
result = agent.invoke({
"messages": [("user", "What's 15% of our 480 enterprise seats?")]
})
print(result["messages"][-1].content)Why the Explicit Graph Wins#
A one-line create_react_agent helper exists, and it's fine for demos. But in production you want to:
- Add guardrails: a node that validates tool inputs before execution.
- Cap iterations: break the loop after N steps so a confused agent can't spin forever.
- Persist state: attach a checkpointer for memory and resumability.
from langgraph.checkpoint.memory import MemorySaver
agent = graph.compile(checkpointer=MemorySaver())
agent.invoke(
{"messages": [("user", "Remember my name is Folarin.")]},
config={"configurable": {"thread_id": "user-123"}},
)With a checkpointer and a thread_id, the agent now has memory across turns, the same graph, one extra line.
Always bound the loop. A recursion_limit (or an explicit step counter in state) is the difference between a bug and a runaway bill.
What You've Got#
A single agent that reasons, uses tools, and remembers, built from parts you can inspect and test. No black box.
In Part 2 we go from one agent to many: a supervisor delegating to specialists, handoffs, and how to keep a multi-agent system debuggable when several models are talking at once.
Folarin Akinloye is an AI Engineer based in London, UK. He builds production-ready agentic AI systems, multi-agent architectures, and sophisticated RAG implementations, and writes about the engineering decisions behind them.
