What Makes an Agent Different from a Chatbot
You've been using chatbots your whole career. You type, it answers. One round trip. The model has no memory of what it's doing between turns and no ability to take action in the world.
An agent is different at a structural level.
- You ask a question
- It answers once
- You're done
- No tools, no actions
- Stateless by default
- You give it a goal
- It plans and acts in a loop
- Uses tools to get information
- Evaluates results, decides next step
- Stops when the task is complete
Receive goal user input
The agent receives a natural language goal. No need to specify exactly how to accomplish it.
Choose and call a tool stop_reason: tool_use
Claude decides which tool to use and returns a structured tool call. Your code executes it.
Receive tool result tool_result
You send the result back to Claude as a new message. It can now use that information.
Decide: use another tool or finish? loop continues
Claude evaluates its progress. It may call another tool, combine results, or decide it has enough to answer.
Return final answer stop_reason: end_turn
When Claude has everything it needs, it returns a text response. The loop exits.
Today you're building this loop from scratch. By the end of this lesson, you'll have an agent that checks weather, does math, and reads files — and decides on its own which capability to use.
Giving Claude Tools
Tools are the key concept. You define them as structured JSON schemas. Claude reads the schema and decides when and how to use each tool. You write the implementation. Claude writes the calls.
Here's the setup code — tool definitions plus the functions that actually run them:
import anthropic
import json
client = anthropic.Anthropic()
# Define the tools Claude can use
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city",
"input_schema": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "City name"}
},
"required": ["city"]
}
},
{
"name": "calculate",
"description": "Perform a mathematical calculation",
"input_schema": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Math expression to evaluate"}
},
"required": ["expression"]
}
}
]
# Tool implementations
def get_weather(city):
# In production, call a real weather API here
weathers = {
"new york": "72F, sunny",
"london": "58F, cloudy",
"tokyo": "68F, clear"
}
return weathers.get(city.lower(), f"65F, partly cloudy in {city}")
def calculate(expression):
try:
return str(eval(expression))
except:
return "Error: invalid expression"
# Dispatcher: routes tool calls to the right function
def run_tool(name, input_data):
if name == "get_weather":
return get_weather(input_data["city"])
elif name == "calculate":
return calculate(input_data["expression"])
return "Unknown tool"
Anatomy of a Tool Definition
"name"
The identifier Claude uses when it decides to call this tool. Keep it lowercase with underscores — treat it like a function name.
"description"
This is what Claude reads to decide whether to use the tool. Write it clearly. A good description is the difference between Claude using the right tool and the wrong one.
"input_schema"
JSON Schema for the tool's parameters. Claude uses this to structure its tool call correctly. The "required" array lists parameters the model must always provide.
run_tool()
Your dispatcher function. When Claude decides to use a tool, it returns the tool name and input. You call run_tool() to execute the actual Python function and get the result.
Claude never runs your code. It tells you what tool to call and with what arguments. You execute it and hand the result back. This separation is what makes agents safe and auditable — you always control what code runs.
The Agent Loop
This is the core. Add this function to your file from Section 2. Read it carefully — every line matters.
def agent(goal):
print(f"Agent goal: {goal}\n")
messages = [{"role": "user", "content": goal}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages
)
# Did Claude want to use a tool?
if response.stop_reason == "tool_use":
for block in response.content:
if block.type == "tool_use":
tool_name = block.name
tool_input = block.input
print(f"Agent using tool: {tool_name}({json.dumps(tool_input)})")
# Run the tool
result = run_tool(tool_name, tool_input)
print(f"Tool result: {result}\n")
# Add Claude's response + the tool result to message history
messages.append({"role": "assistant", "content": response.content})
messages.append({
"role": "user",
"content": [{
"type": "tool_result",
"tool_use_id": block.id,
"content": result
}]
})
else:
# Claude is done — extract and return the final text answer
final = next(b.text for b in response.content if hasattr(b, "text"))
print(f"Agent answer: {final}")
return final
# Test it
agent("What's the weather in New York and Tokyo? Then calculate the average of their temperatures.")
Run this and watch what happens in your terminal:
→Agent using tool: get_weather({"city": "New York"})
Tool result: 72F, sunny
→Agent using tool: get_weather({"city": "Tokyo"})
Tool result: 68F, clear
→Agent using tool: calculate({"expression": "(72 + 68) / 2"})
Tool result: 70.0
Agent answer: New York is 72°F and sunny, Tokyo is 68°F and clear. The average temperature between the two cities is 70°F.
This is the core pattern behind every AI agent in the world. Claude looks at the goal, decides which tool to use, gets the result, and decides what to do next. It keeps looping until the task is complete. The code changes dramatically between projects — but this while loop stays the same.
The Critical Lines
stop_reason == "tool_use"
This is how you know Claude wants to call a tool rather than respond to the user. It's the branching condition that makes the loop work.
block.id
Every tool use has a unique ID. You must include this in the tool_result message so Claude knows which tool call the result belongs to. Miss this and the API will return an error.
messages.append(...)
You're building up the full conversation history manually. This is how Claude knows what it already tried. Both Claude's message and your tool result need to be appended.
while True
The loop runs until the else branch fires — meaning Claude stopped calling tools and returned a final answer. In production, add a max iteration count to prevent infinite loops on edge cases.
Add a File Reading Tool
Now extend the agent from Section 3. Add a third tool that reads files from disk. Now your agent can answer questions about documents without you telling it which file to look at — it figures that out.
Add this to your tools list and run_tool dispatcher:
# Add this dict to your tools list
{
"name": "read_file",
"description": "Read the contents of a file from disk",
"input_schema": {
"type": "object",
"properties": {
"filename": {"type": "string", "description": "Path to the file to read"}
},
"required": ["filename"]
}
}
# Add this function
def read_file(filename):
try:
with open(filename, "r") as f:
return f.read()[:5000] # Cap at 5000 chars to stay within token limits
except FileNotFoundError:
return f"Error: {filename} not found"
# Update run_tool to include the new tool
def run_tool(name, input_data):
if name == "get_weather":
return get_weather(input_data["city"])
elif name == "calculate":
return calculate(input_data["expression"])
elif name == "read_file":
return read_file(input_data["filename"])
return "Unknown tool"
# Now test — no need to tell it the file name
agent("Read the file 'notes.txt' and summarize the 3 most important points")
Claude has a context window — a maximum amount of text it can process at once. The full context is very large, but keeping individual tool results bounded prevents unexpected token costs and speeds up responses. In production you'd chunk large files and process them in sections.
Create a notes.txt file with several paragraphs of content — meeting notes, research, anything. Then run:
# Ask it to do multiple things at once
agent("Read notes.txt, summarize the 3 most important points, then tell me the weather in Chicago and calculate how many days are in 12 weeks.")
Watch the agent read the file, check the weather, and do the math — all in one loop, all without you telling it what order to do things in. It decides.
You now have an AI agent that can read files, check weather, and do math — and it decides on its own which tools to use. This is the foundation of every production agent system. The tools change. The loop doesn't.
What You Built Today
- Understood the structural difference between chatbots and agents
- Defined tools using JSON Schema — name, description, input_schema
- Implemented the agent loop: tool call → run → result → loop
- Used
stop_reason == "tool_use"to detect when Claude wants a tool - Properly threaded
tool_use_idthrough the message history - Added a file reading tool to give the agent document access
Add a Web Search Tool
Extend your agent with a fourth tool: web_search. For now, mock it with hardcoded results. Then ask the agent a question that requires both searching and calculating.
- Add a
web_searchtool with aqueryparameter to the tools list - Implement
def web_search(query)— return a hardcoded dict with fake results - Add it to
run_tool() - Test: "Search for the current S&P 500 value, then calculate how much $10,000 invested at that price would be worth if it goes up 8%"
- Bonus: swap the mock with a real search API (SerpAPI, Brave Search, or Tavily all have free tiers)