How to Build Your First AI Agent in Python (in About 40 Lines)
A beginner-friendly, working tutorial: give a language model a tool, write the agent loop, and see how AI agents really work. Includes tested Python code, how tool calling works, and what to add before production.
Nokku Learn Team3 min read
AI agent frameworks can make agents look complicated. Underneath, almost every agent is a short loop: ask the model what to do, run the tool it asks for, give it the result, repeat. In this tutorial you'll build that loop yourself in plain Python, in about 40 lines, and see exactly how agents work.
The Idea: The Model Asks, Your Code Acts
A language model on its own can only write text. Tool calling lets it do more: you describe functions to the model, and when it needs one, it replies with a structured request like "call get_order_status with order_id="A1001"". Crucially, the model never runs anything itself. Your code decides whether and how to run each tool, and that's where you add safety checks.
The Complete Agent
Here's the whole thing. We ran this exact code against the live API before publishing.
first_agent.pyimport json import anthropic client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY MODEL = "claude-sonnet-5" # 1. A tool: a normal Python function, plus a description the model reads ORDERS = {"A1001": {"status": "shipped", "eta": "2026-10-02"}, "A1002": {"status": "processing", "eta": "2026-10-06"}} def get_order_status(order_id: str) -> dict: order = ORDERS.get(order_id.upper()) return order or {"error": f"No order {order_id}. IDs look like A1001."} TOOLS = [{ "name": "get_order_status", "description": "Look up one order's shipping status and estimated delivery date.", "input_schema": { "type": "object", "properties": {"order_id": {"type": "string", "description": "For example A1001"}}, "required": ["order_id"], }, }] FUNCTIONS = {"get_order_status": get_order_status} # 2. The agent loop: call the model, run the tools it asks for, repeat def run_agent(goal: str, max_steps: int = 5) -> str: messages = [{"role": "user", "content": goal}] for _ in range(max_steps): response = client.messages.create(model=MODEL, max_tokens=1000, tools=TOOLS, messages=messages) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": # the model has finished return "".join(b.text for b in response.content if b.type == "text") results = [] for block in response.content: if block.type == "tool_use": output = FUNCTIONS[block.name](**block.input) results.append({"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(output)}) messages.append({"role": "user", "content": results}) return "Stopped: step limit reached." if __name__ == "__main__": print(run_agent("Where are my orders A1001 and A1002?"))
Running it prints something like:
textHere's the status of both orders: - Order A1001: Shipped, estimated delivery on October 2, 2026. - Order A1002: Processing, estimated delivery on October 6, 2026 (not yet shipped).
How It Works, Step by Step
1. Define a tool. A tool is an ordinary function plus a description the model reads: a name, what it does, and a JSON schema for its inputs. The description matters a lot. The model decides when to use a tool based on it, so say what the tool returns and when to use it.
2. Send the goal and the tools. Each call to client.messages.create includes the conversation so far and the list of tools.
3. Check stop_reason. If it's tool_use, the model wants tools run. Anything else means it has written its final answer.
4. Run every requested tool. The model can ask for several tools at once (here, one lookup per order). Run each and collect the results.
5. Send results back, matched by id. Each result is a tool_result block with the tool_use_id of the call it answers, in a user message. The model's own turn (with its tool_use blocks) must stay in the history before it.
6. Repeat until done. That's the loop. The model chooses each next step from what it has learned so far, which is what makes it an agent rather than a script.
Common Mistakes
| Mistake | What happens | Fix |
|---|---|---|
Reading response.content[0].text | Crashes when the reply starts with a tool call or a thinking block | Select blocks by type |
| Forgetting to append the assistant's turn | API error: the tool result refers to nothing | Append response.content before the results |
| Only handling the first tool call | API error when the model calls tools in parallel | Answer every tool_use block |
| No step limit | Runaway loops and costs | Always cap the loop |
| Tools that raise exceptions | The whole agent crashes | Return a helpful error message the model can act on |
Before You Put an Agent in Production
This example is deliberately minimal. Real agents also need:
- Error handling that turns failures into messages the model can recover from
- Guardrails enforced in code: permissions, spending limits and approval for risky actions
- Defences against prompt injection, where instructions hidden in data try to hijack the agent
- Evaluations: a test suite of real tasks, run on every change
- Cost and latency controls: budgets, prompt caching and routing simple requests to smaller models
- Logging of every step, so you can debug and audit what happened
Key Takeaways
- An AI agent is a loop: the model picks a tool, your code runs it, the result goes back, and repeat.
- The model requests actions; your code performs them, which is where safety lives.
- Answer every tool call with a matching
tool_result, and always limit the steps. - Production agents add guardrails, evaluations, cost controls and logging on top of this core.