title: "AI Agents" track: "practical-ai" difficulty: "intermediate" estimatedTime: "30 min" prerequisites: ["practical-ai/rag"] tags: ["agents", "tool-use", "function-calling", "react", "multi-agent", "planning"]
Introduction
An AI agent is a system that uses a language model as its reasoning engine to decide what actions to take, execute those actions, observe the results, and iterate until a task is complete. While a standard LLM call is a single input-output exchange, an agent operates in a loop — thinking, acting, and adapting.
Agents represent a fundamental shift from using LLMs as text generators to using them as autonomous decision-makers. They can browse the web, write and execute code, query databases, send emails, and orchestrate complex multi-step workflows.
The Agent Loop
Every agent follows the same basic cycle:
Observe → Think → Act → Observe → Think → Act → ... → Done
- Observe — receive the current state (user query, tool outputs, environment)
- Think — the LLM reasons about what to do next
- Act — execute a tool or produce a final response
- Repeat until the task is complete
def agent_loop(user_query, tools, max_steps=10):
"""Simplified agent loop."""
messages = [{"role": "user", "content": user_query}]
for step in range(max_steps):
# Think: ask the LLM what to do
response = call_llm(messages, tools=tools)
# Check if the agent wants to use a tool
if response.tool_use:
# Act: execute the tool
tool_name = response.tool_use.name
tool_input = response.tool_use.input
tool_result = execute_tool(tool_name, tool_input)
# Observe: add tool result to context
messages.append({"role": "assistant", "content": response})
messages.append({
"role": "user",
"content": [{"type": "tool_result", "content": tool_result}],
})
else:
# Done: the agent produced a final response
return response.text
return "Max steps reached without completing the task."
Tool Use and Function Calling
Tools are the agent's hands. They bridge the gap between the LLM's reasoning ability and the real world. Modern APIs support function calling — you define tools with schemas, and the model decides when and how to call them.
Defining Tools
import anthropic
client = anthropic.Anthropic()
# Define tools the agent can use
tools = [
{
"name": "get_weather",
"description": "Get the current weather for a city.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The city name, e.g., 'San Francisco'",
},
},
"required": ["city"],
},
},
{
"name": "search_web",
"description": "Search the web for current information.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query",
},
},
"required": ["query"],
},
},
]
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=[{
"role": "user",
"content": "What's the weather like in Tokyo today?",
}],
)
# The model will respond with a tool_use block
for block in response.content:
if block.type == "tool_use":
print(f"Tool: {block.name}")
print(f"Input: {block.input}")
# Tool: get_weather
# Input: {"city": "Tokyo"}
Executing Tools and Continuing
def run_agent(user_message, tools, tool_implementations):
"""Complete agent with tool execution."""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
tools=tools,
messages=messages,
)
# Check if we need to execute tools
tool_uses = [b for b in response.content if b.type == "tool_use"]
if not tool_uses:
# No tool calls — return the text response
return response.content[0].text
# Execute each tool and collect results
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_use in tool_uses:
# Run the actual tool
result = tool_implementations[tool_use.name](tool_use.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": str(result),
})
messages.append({"role": "user", "content": tool_results})
<Callout type="tip">
The quality of your tool descriptions is critical. The LLM decides which tool to use based on the name and description. Vague descriptions lead to wrong tool selections. Be specific: instead of "search for information," write "Search a product catalog by name, category, or price range. Returns up to 10 matching products."
</Callout>
The ReAct Pattern
ReAct (Reasoning + Acting) is a foundational pattern where the agent alternates between reasoning (thinking through the problem) and acting (using tools). The reasoning step is explicit in the output.
Question: What is the population of the capital of France?
Thought: I need to find the capital of France first, then its population.
Action: search_web("capital of France")
Observation: The capital of France is Paris.
Thought: Now I need the population of Paris.
Action: search_web("population of Paris 2024")
Observation: The population of Paris is approximately 2.1 million.
Thought: I now have the answer.
Answer: The population of the capital of France (Paris) is approximately
2.1 million people.
The key insight is that explicit reasoning improves tool selection and task decomposition. Without the thought step, agents make more errors in choosing which tool to use and when.
Planning and Reasoning
More complex agents use structured planning before executing:
Plan-and-Execute
system_prompt = """You are a research agent. When given a question:
1. PLAN: Break the question into sub-tasks
2. EXECUTE: Complete each sub-task using available tools
3. SYNTHESIZE: Combine results into a final answer
Always start by writing your plan before taking any actions."""
Self-Reflection
Agents can evaluate their own outputs and correct mistakes:
reflection_prompt = """Review your previous answer for:
1. Factual accuracy — are all claims supported by the retrieved data?
2. Completeness — did you address all parts of the question?
3. Coherence — does the answer flow logically?
If you find issues, fix them. If the answer is good, confirm it."""
Building a Simple Agent
Here is a complete, working agent that can do math and look up information:
import anthropic
import json
import math
client = anthropic.Anthropic()
# Tool implementations
def calculate(expression):
"""Safely evaluate a math expression."""
allowed = {
"abs": abs, "round": round,
"min": min, "max": max,
"sqrt": math.sqrt, "pow": pow,
"pi": math.pi, "e": math.e,
}
try:
return str(eval(expression, {"__builtins__": {}}, allowed))
except Exception as e:
return f"Error: {e}"
def get_current_date():
"""Get today's date."""
from datetime import date
return date.today().isoformat()
# Tool definitions for the API
tools = [
{
"name": "calculate",
"description": "Evaluate a mathematical expression. Supports +, -, *, /, sqrt, pow, pi, e.",
"input_schema": {
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression, e.g. 'sqrt(144) + pow(2, 10)'",
},
},
"required": ["expression"],
},
},
{
"name": "get_current_date",
"description": "Get today's date in ISO format (YYYY-MM-DD).",
"input_schema": {
"type": "object",
"properties": {},
},
},
]
tool_functions = {
"calculate": lambda args: calculate(args["expression"]),
"get_current_date": lambda args: get_current_date(),
}
def run_agent(question):
"""Run the agent to answer a question."""
messages = [{"role": "user", "content": question}]
print(f"\nQuestion: {question}")
print("-" * 50)
for step in range(5):
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=1024,
system="You are a helpful assistant with access to tools. Use them when needed.",
tools=tools,
messages=messages,
)
tool_uses = [b for b in response.content if b.type == "tool_use"]
text_blocks = [b for b in response.content if b.type == "text"]
if text_blocks:
for block in text_blocks:
print(f"Agent: {block.text}")
if not tool_uses:
return
messages.append({"role": "assistant", "content": response.content})
tool_results = []
for tool_use in tool_uses:
print(f" [Tool: {tool_use.name}({json.dumps(tool_use.input)})]")
result = tool_functions[tool_use.name](tool_use.input)
print(f" [Result: {result}]")
tool_results.append({
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": result,
})
messages.append({"role": "user", "content": tool_results})
# Try it
run_agent("What is the square root of 729 plus 2 to the power of 8?")
run_agent("What is today's date, and how many days until the end of the year?")
Multi-Agent Systems
Complex tasks benefit from multiple specialized agents working together:
Common Patterns
- Supervisor — one agent delegates tasks to specialist agents
- Pipeline — agents pass output sequentially (researcher -> writer -> reviewer)
- Debate — multiple agents propose solutions and critique each other
- Swarm — agents independently work on subtasks and merge results
# Simplified multi-agent: Researcher + Writer
def research_agent(topic):
"""Agent that gathers information."""
return run_agent_with_tools(
f"Research this topic thoroughly: {topic}",
tools=[search_web, read_page],
)
def writer_agent(research, style):
"""Agent that writes based on research."""
return run_agent_with_tools(
f"Write a {style} article based on this research:\n{research}",
tools=[], # Writer doesn't need tools
)
# Pipeline execution
research = research_agent("quantum computing breakthroughs 2024")
article = writer_agent(research, style="beginner-friendly blog post")
<Callout type="tip">
Start with a single agent and simple tools. Multi-agent systems add complexity — coordinating agents, handling failures, and managing shared state is significantly harder than a single agent loop. Only add agents when a single agent genuinely cannot handle the task.
</Callout>
Key Takeaways
- Agents use LLMs as reasoning engines in an observe-think-act loop
- Tool use (function calling) lets agents interact with the real world
- The ReAct pattern improves accuracy by alternating explicit reasoning with actions
- Good tool descriptions are essential — the LLM selects tools based on descriptions
- Multi-agent systems are powerful but complex; start with a single agent first
- Always set a maximum step limit to prevent infinite loops