Building Your First LangGraph Agent: A Practical Beginner’s
A beginner's first LangGraph agent: why agents matter, what LangGraph adds to LangChain, project setup, then state, nodes, edges and a working LLM loop.
This article is published in English.
Sources: Building Your First LangGraph Agent: A Practical Beginner’s Guide
This walkthrough rebuilds the path from raw materials to a working system for: Building Your First LangGraph Agent: A Practical Beginner’s Guide. The focus is operable steps, explicit checks, and code that you can drop into a repo without guessing intent. For the Overview stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
Why Agents Matter
When working through the Why Agents Matter stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
User question → LLM → Answer
User question → Retrieve context → LLM → Answer
User question → Agent decides what to do → Calls tools → Updates state → Produces answer
What Is LangGraph?
When working through the What Is LangGraph stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
Project Setup
When working through the Project Setup stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through the Project Setup stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
pip install langgraph langchain langchain-openai langchain-community python-dotenv pydantic
pip install jupyter notebook ipython
OPENAI_API_KEY=your_api_key_here
import os
import json
from typing import Optional, List
from dotenv import load_dotenv
from pydantic import BaseModel
from langgraph.graph import StateGraph, START, END
from langchain.chat_models import init_chat_model
from langchain_core.messages import HumanMessage
from langchain.tools import tool
load_dotenv()
llm = init_chat_model("openai:gpt-4o-mini")
Step 1: Define the Agent State
The Step 1 Define the stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
class PlayerState(BaseModel):
question: str
selected_tools: Optional[List[str]] = None
name: Optional[str] = None
club: Optional[str] = None
country: Optional[str] = None
number: Optional[int] = None
rating: Optional[int] = None
goals: Optional[List[int]] = None
minutes_played: Optional[List[int]] = None
summary: Optional[str] = None
Step 2: Create Tools
The Step 2 Create Tools stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve.
Tool 1: Fetch Player Information
The Tool 1 Fetch Player stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Expose tools with narrow schemas and explicit side-effect labels. Hosts need to know which calls mutate state before they auto-approve. The Tool 1 Fetch Player stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
@tool
def fetch_player_information_tool(name: str):
"""
Returns the football club and country of a player.
"""
data = {
"Haaland": {
"club": "Manchester City",
"country": "Norway"
},
"Kane": {
"club": "Bayern Munich",
"country": "England"
},
"Lautaro": {
"club": "Inter Milan",
"country": "Argentina"
},
"Ronaldo": {
"club": "Al-Nassr",
"country": "Portugal"
}
}
return data.get(name, {
"club": "unknown",
"country": "unknown"
})
def fetch_player_information(state: PlayerState):
return fetch_player_information_tool.invoke({
"name": state.name
})
Tool 2: Fetch Jersey Number
For the Tool 2 Fetch Jersey stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.
@tool
def fetch_player_jersey_number_tool(name: str):
"""
Returns the jersey number of a football player.
"""
data = {
"Haaland": 9,
"Kane": 9,
"Lautaro": 10,
"Ronaldo": 7
}
return {
"number": data.get(name, 0)
}
def fetch_player_jersey_number(state: PlayerState):
return fetch_player_jersey_number_tool.invoke({
"name": state.name
})
Tool 3: Fetch Player Rating
For the Tool 3 Fetch Player stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Authenticate at the gateway and re-authorize at the data plane. A bearer token alone is not a tenancy boundary.
@tool
def fetch_player_rating_tool(name: str):
"""
Returns the player's football video game rating.
"""
data = {
"Haaland": 92,
"Kane": 89,
"Lautaro": 88,
"Ronaldo": 90
}
return {
"rating": data.get(name, 0)
}
def fetch_player_rating(state: PlayerState):
return fetch_player_rating_tool.invoke({
"name": state.name
})
Step 3: Add Data Retrieval Nodes
For the Step 3 Add Data stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Cite the passages that actually grounded the answer. Without citations, operators cannot tell hallucination from an indexing gap. For the Step 3 Add Data stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
Retrieve Goals
When working through the Retrieve Goals stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Measure recall on a fixed question set before tuning prompts. Prompt churn rarely fixes a weak retrieval surface.
def retrieve_goals(state: PlayerState):
data = {
"Haaland": [25, 40, 28, 33, 36],
"Kane": [33, 37, 41, 38, 29],
"Lautaro": [19, 25, 27, 24, 25],
"Ronaldo": [27, 32, 28, 30, 36]
}
return {
"goals": data.get(state.name, [0])
}
Retrieve Minutes Played
When working through the Retrieve Minutes Played stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Measure recall on a fixed question set before tuning prompts. Prompt churn rarely fixes a weak retrieval surface.
def retrieve_minutes_played(state: PlayerState):
data = {
"Haaland": [2108, 3102, 3156, 2617, 2758],
"Kane": [2924, 2850, 3133, 2784, 2680],
"Lautaro": [2445, 2498, 2519, 2773],
"Ronaldo": [3001, 2560, 2804, 2487, 2771]
}
return {
"minutes_played": data.get(state.name, [0])
}
Step 4: Extract the Player Name
When working through the Step 4 Extract the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through the Step 4 Extract the stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
def extract_name(state: PlayerState):
prompt = f"""
You are a football name extraction assistant.
Extract only the surname of the football player mentioned in the question.
Question:
{state.question}
Return only one word.
"""
response = llm.invoke([
HumanMessage(content=prompt)
]).content.strip()
return {
"name": response
}
Will Erling Haaland win the World Cup with Norway?
{"name": "Haaland"}
Step 5: Build a Planner Node
The Step 5 Build a stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
def planner(state: PlayerState):
prompt = f"""
You are a football assistant planner.
Available tools:
[
"fetch_player_jersey_number",
"fetch_player_information",
"fetch_player_rating"
]
User question:
{state.question}
Choose which tools are needed to answer the question.
Return only a valid JSON array of tool names.
Example:
["fetch_player_information", "fetch_player_rating"]
"""
response = llm.invoke([
HumanMessage(content=prompt)
]).content.strip()
try:
selected_tools = json.loads(response)
except Exception:
selected_tools = []
return {
"selected_tools": selected_tools
}
Step 6: Write the Final Summary
The Step 6 Write the stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts.
def write_summary(state: PlayerState):
player_data = {
"name": state.name,
"club": state.club,
"country": state.country,
"number": state.number,
"rating": state.rating,
"goals": state.goals,
"minutes_played": state.minutes_played
}
prompt = f"""
You are a football journalist.
Using the data below, write a concise markdown summary about the player.
Player data:
{json.dumps(player_data, indent=4)}
The summary should include:
- Full player name if it can be inferred
- Country with a flag emoji if available
- Club if available
- Jersey number if available
- Rating if available
- Total goals in the last three seasons
- Average minutes per goal over the available data
- A direct answer to the user's question
User question:
{state.question}
"""
response = llm.invoke([
HumanMessage(content=prompt)
]).content
return {
"summary": response
}
Step 7: Build the LangGraph Workflow
The Step 7 Build the stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Keep graph state flat and typed. Nested blobs hide which node wrote which field and break resume after interrupts. The Step 7 Build the stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
graph_builder = StateGraph(PlayerState)
graph_builder.add_node("extract_name", extract_name)
graph_builder.add_node("planner", planner)
graph_builder.add_node("fetch_player_jersey_number", fetch_player_jersey_number)
graph_builder.add_node("fetch_player_information", fetch_player_information)
graph_builder.add_node("fetch_player_rating", fetch_player_rating)
graph_builder.add_node("retrieve_goals", retrieve_goals)
graph_builder.add_node("retrieve_minutes_played", retrieve_minutes_played)
graph_builder.add_node("write_summary", write_summary)
Step 8: Add Edges
For the Step 8 Add Edges stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
graph_builder.add_edge(START, "extract_name")
graph_builder.add_edge("extract_name", "planner")
def route_tools(state: PlayerState):
if state.selected_tools:
return state.selected_tools
return ["retrieve_goals"]
graph_builder.add_conditional_edges(
"planner",
route_tools,
{
"fetch_player_jersey_number": "fetch_player_jersey_number",
"fetch_player_information": "fetch_player_information",
"fetch_player_rating": "fetch_player_rating",
"retrieve_goals": "retrieve_goals"
}
)
graph_builder.add_edge("fetch_player_jersey_number", "retrieve_goals")
graph_builder.add_edge("fetch_player_information", "retrieve_goals")
graph_builder.add_edge("fetch_player_rating", "retrieve_goals")
graph_builder.add_edge("retrieve_goals", "retrieve_minutes_played")
graph_builder.add_edge("retrieve_minutes_played", "write_summary")
graph_builder.add_edge("write_summary", END)
graph = graph_builder.compile()
Step 9: Run the Agent
For the Step 9 Run the stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness.
result = graph.invoke({
"question": "Will Haaland be able to win the FIFA World Cup for Norway based on his recent performance?"
})
print(result["summary"])
## Erling Haaland Performance Summary
- **Player:** Erling Haaland
- **Country:** Norway 🇳🇴
- **Club:** Manchester City
- **FIFA-style rating:** 92
- **Recent goals:** 28, 33, and 36 in the last three seasons
- **Total goals in last three seasons:** 97
- **Average minutes per goal:** Around 88 minutes per goal
Haaland’s individual numbers are outstanding. His scoring rate, consistency, and elite rating show that he is one of the most dangerous forwards in world football.
However, winning the FIFA World Cup depends on much more than one player. Norway would need strong squad depth, tactical discipline, defensive quality, and consistent performances throughout the tournament.
So, based on recent performance, Haaland can significantly improve Norway’s chances, but a World Cup title would still be difficult to predict from individual statistics alone.
result
Why This Example Matters
For the Why This Example Matters stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Put human approval on edges that spend money or change production data. Compile-time wiring does not equal business completeness. For the Why This Example Matters stage, define the inputs, the owner of the step, and the exit criteria before changing code. Operators should be able to re-run the step from a known checkpoint without guessing hidden state. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
1. State
When working through the 1 State stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node.
2. Tool Use
When working through the 2 Tool Use stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Document the happy path and the recovery path together. Retries, human gates, and dead-letter handling are part of the product, not later polish. Log tool name, args hash, latency, and outcome for every call. Debugging agent loops without that trail wastes hours.
3. Planning
When working through the 3 Planning stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Prefer small, testable units over sprawling scripts. When a step fails, the failure should point at a single responsibility rather than a tangled pipeline. Checkpoint after expensive steps. Resume should not re-bill the same LLM call when an operator retries a later node. When working through the 3 Planning stage, write down the contract first: required inputs, success signal, and what happens on partial failure. That checklist keeps later code changes honest. Record timings and token or query cost next to functional results. Cost visibility early prevents surprise bills when the path moves from demo to shared environments.
4. Conditional Routing
The 4 Conditional Routing stage works best when treated as a measurable surface. Capture one golden transcript, one failure case, and the rollback note before expanding scope. Keep configuration outside application code. Environment files, secret stores, and feature flags belong in one place operators can audit without reading the whole graph.