Introduction
The barrier to writing your first AI agent in Python has collapsed. Ten minutes, five dependencies, a single Python file — and you have an agent that can read inputs, decide when to call tools, execute those tools (real Python functions you wrote), and respond intelligently.
This is the absolute-shortest path from zero to a working tool-calling agent in 2026, using LangChain + LangGraph + OpenAI. The example agent generates and persists fake user data — trivial in itself, but it shows every concept you’ll use to build agents that handle real work later.
📚 Table of contents
- What “agent” actually means here
- Setup with uv (60 seconds)
- The .env file and your OpenAI key
- Defining your first tools
- Why docstrings and type hints decide tool quality
- Wiring up the LangGraph agent
- The driver loop that makes it conversational
- Running it
- Common mistakes
- FAQs
What “agent” actually means here
Distinguishing an AI agent from a chatbot: an agent has tools. It can decide, mid-conversation, that the right answer requires calling a function — reading a file, querying a database, hitting an API, sending an email — and it can execute that call and incorporate the result into its response. A chatbot only talks; an agent acts.
The pattern is called ReAct (Reason + Act). The model
alternates between reasoning steps (“I need user data; the
generate_sample_users tool exists; I’ll call it”) and action steps
(actual tool calls). LangGraph’s create_react_agent wraps the whole loop into
one function call.
Setup with uv (60 seconds)
mkdir 10-min-agent && cd 10-min-agent
uv init .
uv add langchain langgraph langchain-openai python-dotenv
Five dependencies. uv installs all of them in seconds. Open main.py in your editor
(PyCharm, VS Code, Cursor — doesn’t matter).
The .env file and your OpenAI key
Create a .env file at the project root:
OPENAI_API_KEY=sk-...
Get the key from platform.openai.com/api-keys. Inference for this demo costs pennies.
Add .env to .gitignore immediately so it doesn’t land in a repo.
Defining your first tools
A tool is just a Python function decorated with @tool. The example agent will need
three: generate fake users, write JSON to disk, read JSON from disk.
from dotenv import load_dotenv
load_dotenv()
import json, random
from langchain_core.tools import tool
@tool
def write_json(file_path: str, data: dict) -> str:
"""Write a Python dictionary as JSON to a file with pretty formatting.
Use this when the user wants to save generated data persistently."""
try:
with open(file_path, "w") as f:
json.dump(data, f, indent=2)
return f"Successfully wrote data to {file_path}"
except Exception as e:
return f"Error writing file: {e}"
@tool
def read_json(file_path: str) -> str:
"""Read a JSON file and return its contents as a string.
Use this when the user asks about data stored in a JSON file."""
try:
with open(file_path, "r") as f:
return json.dumps(json.load(f), indent=2)
except FileNotFoundError:
return f"File {file_path} does not exist."
except Exception as e:
return f"Error reading file: {e}"
@tool
def generate_sample_users(
first_names: list[str], last_names: list[str],
domains: list[str], min_age: int = 18, max_age: int = 80,
) -> dict:
"""Generate sample user data with random names, emails, and ages.
Use when the user asks for fake or sample users for testing."""
users = []
for first, last in zip(first_names, last_names):
users.append({
"name": f"{first} {last}",
"email": f"{first.lower()}.{last.lower()}@{random.choice(domains)}",
"age": random.randint(min_age, max_age),
})
return {"users": users}
Why docstrings and type hints decide tool quality
The LLM never sees your function’s code. It only sees:
- The function name
- The parameter names and their type annotations
- The return type annotation
- The docstring
All of those become part of the tool spec sent to the model. Vague docstrings produce vague tool use. Missing type hints mean the model guesses at parameter shapes. Misleading names mean the wrong tool gets called.
The fix: write docstrings as if they’re instructions to a junior developer. Lead with what the tool does and when to use it. Specify parameter types explicitly. Keep return types accurate. Two extra minutes per tool, dramatically better agent behaviour.
Wiring up the LangGraph agent
Three ingredients: a list of tools, an LLM, and a system prompt. LangGraph’s
create_react_agent binds them together:
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent
tools = [write_json, read_json, generate_sample_users]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
system_message = (
"You are a data generator agent. You can create sample user data and save "
"it to JSON files. When the user asks for users, generate realistic names "
"and ages. When asked to save, write to the file path they provide. When "
"asked about saved data, read the file and answer from it."
)
agent = create_react_agent(llm, tools=tools, prompt=system_message)
Why temperature=0? Tool-calling agents
benefit from deterministic tool selection. Temperature above 0 makes the model occasionally call
the wrong tool for randomness reasons. Keep it at 0 unless you have a specific reason.
The driver loop that makes it conversational
The agent only does single-turn work in one invoke call. To make it conversational,
keep a message history and append on each turn:
def run_agent(user_input: str, history: list) -> tuple[str, list]:
messages = history + [{"role": "user", "content": user_input}]
try:
result = agent.invoke({"messages": messages}, {"recursion_limit": 50})
reply = result["messages"][-1].content
return reply, result["messages"]
except Exception as e:
return f"Agent error: {e}", history
def main():
history = []
print("Type 'quit' to exit.\n")
while True:
user_input = input("you > ").strip()
if user_input.lower() in {"quit", "exit"}:
break
if not user_input:
continue
reply, history = run_agent(user_input, history)
print(f"\nagent > {reply}\n")
if __name__ == "__main__":
main()
The recursion_limit bounds how many tool-calling steps the agent can do per turn.
50 is generous; bump higher for more complex tasks, lower if you want to catch runaway loops.
Running it
uv run main.py
Sample interactions:
- “Generate 5 random users with company.com emails.” — agent calls
generate_sample_users, returns the list inline. - “Save those users to users.json.” — agent calls
write_json, confirms. - “What’s the oldest user in users.json?” — agent calls
read_json, parses, answers.
The agent reasons about which tool to use without you specifying. That’s the entire point.
❌ Common mistakes
- Skipping type hints on tool functions. The agent can’t generate correct calls without them.
- Vague docstrings. “Helps with files” is useless; “Read a JSON file and return its contents as a string” is correct.
- Temperature above 0 on tool-calling models. Random tool choice is rarely what you want.
- Committing
.envto Git. Add it to.gitignoreon commit one. - Hardcoding the OpenAI key in code. Read from env vars, even for personal projects.
- Not handling errors inside tools. A tool that raises crashes the whole agent run; return error strings instead.
- Forgetting recursion limits. Without one, a buggy agent can loop indefinitely on tool errors.
💡 Pro tips
- Start with 2–3 tools. Adding more later is trivial; getting the first few right is what matters.
- Use Pydantic models for complex tool inputs — LangChain accepts them and the agent gets richer parameter validation.
- Switch to Claude (
ChatAnthropic) or Gemini (ChatGoogleGenerativeAI) by changing one import. Pricing and tool-calling quality vary; try a few for your use case. - For production, swap the in-memory history list for LangGraph’s checkpointer with a Postgres backend. Multi-user sessions, durable state.
- Wrap the agent in FastAPI when you want a real HTTP service. See the DevShelf deploy-FastAPI-AI-agent guide for the next step.
- Use
verbose=Trueon the agent to see every reasoning + tool-call step in the logs during development. Invaluable for debugging.
Conclusion
The minimum-viable AI agent is genuinely tiny. Five dependencies, three tool definitions, one
create_react_agent call, a small driver loop. The pattern scales unchanged to real
agents — you just write more interesting tools.
Next steps: swap the toy tools for something useful (database queries, API calls, file system operations), add persistent conversation memory, deploy as an API. Each one is a small extension of what you already have.
Related reading
-
Building AI Agents for Production — Day 2
The full LangChain tool-calling deep dive—built-in tools, the @tool decorator, StructuredTool, and the Yahoo Finance example that pairs with this 10-minute start.
-
Guardrails with LangChain: Safe AI Agents
Add PII middleware, content filters, and human-in-the-loop checkpoints to the agent you just built—the production safety layer for LangGraph agents.
-
Evaluating LLM Chatbots and RAG Pipelines
LangSmith and LLM-as-a-judge for measuring how well the agent actually performs—the evaluation step after your first working LangGraph agent.