Building an AI Agent From Scratch Using the Anthropic API: A Complete Guide Published October 15, 2025 • 12 min read • View as Markdown J Juan Carlos Olamendy Senior AI Engineer | Building & Telling Stories about AI/ML Systems | Software Engineer The Power Is In Your Hands Most developers spend weeks wrestling with bloated agent frameworks, burning through $2,000+ in API costs while debugging black-box abstractions they don't understand. Here's the truth: you don't need complex frameworks to build a production-ready AI agent. In this guide, you'll build a fully functional agent from scratch in under 200 lines of Python—complete with tool integration, memory management, and multi-step reasoning. No expensive platforms. No mysterious abstractions. Just clean, extensible code you control entirely. By the end, you'll have an autonomous agent that breaks down complex problems, uses external tools, and maintains conversation context—all while understanding exactly how every component works. You'll learn the core patterns for tool design, the agentic loop that enables autonomous behavior, and battle-tested strategies for memory management and error handling. Let's build something real. Setting Up Your Development Environment Let's get your workspace ready for agent development. You'll need Python 3.8 or higher and a few essential libraries. First, install the required packages and set up your API credentials. import os import json from typing import List, Dict, Any, Optional from dotenv import load_dotenv import anthropic load_dotenv() Create a .env file in your project root with your Anthropic API key: ANTHROPIC_API_KEY=your_api_key_here You can obtain your API key from the Anthropic Console at console.anthropic.com . Keep this key secure and never commit it to version control. Building a Modular Tool System Tools are the hands and eyes of your agent—they allow it to interact with the world beyond text generation. Let's build a flexible tool system that can be easily extended with new capabilities. We'll create a weather information tool that demonstrates real-world utility. Designing the Tool Interface Every tool needs a consistent interface so the agent can discover and use it effectively. We'll define a base structure that all tools must implement. class BaseTool : """Base class for all agent tools""" def get_name ( self ) -> str: """Returns the tool name""" raise NotImplementedError def get_description ( self ) -> str: """Returns what the tool does""" raise NotImplementedError def get_parameters ( self ) -> Dict[str, Any]: """Returns the parameter schema for the tool""" raise NotImplementedError def execute ( self, **kwargs ) -> Dict[str, Any]: """Executes the tool with given parameters""" raise NotImplementedError This base class establishes a contract that every tool must fulfill. The agent will call get_name() , get_description() , and get_parameters() to understand what the tool does. When the agent decides to use a tool, it calls execute() with the appropriate parameters. Implementing a Weather Information Tool Now let's build a concrete tool implementation that provides weather information. This tool simulates weather data retrieval—in production, you'd connect to a real weather API. class WeatherTool ( BaseTool ): """A tool for retrieving weather information""" def get_name ( self ) -> str: return "get_weather" def get_description ( self ) -> str: return "Retrieves current weather information for a specified city. Use this when users ask about weather conditions, temperature, or forecasts." def get_parameters ( self ) -> Dict[str, Any]: return { "type" : "object" , "properties" : { "city" : { "type" : "string" , "description" : "The city name (e.g., 'New York', 'London', 'Tokyo')" }, "unit" : { "type" : "string" , "enum" : [ "celsius" , "fahrenheit" ], "description" : "Temperature unit preference" } }, "required" : [ "city" ] } def execute ( self, city: str, unit: str = "celsius" ) -> Dict[str, Any]: """ Simulates weather data retrieval. In production, this would call a real weather API. """ # Simulated weather data weather_database = { "new york" : { "temp" : 22 , "condition" : "Partly cloudy" , "humidity" : 65 }, "london" : { "temp" : 15 , "condition" : "Rainy" , "humidity" : 80 }, "tokyo" : { "temp" : 28 , "condition" : "Sunny" , "humidity" : 55 }, "paris" : { "temp" : 18 , "condition" : "Overcast" , "humidity" : 70 }, } city_key = city.lower() if city_key not in weather_database: return { "error" : f"Weather data not available for {city} " , "available_cities" : list(weather_database.keys()) } data = weather_database[city_key] temp = data[ "temp" ] # Convert temperature if needed if unit == "fahrenheit" : temp = (temp * 9 / 5 ) + 32 return { "city" : city, "temperature" : round(temp, 1 ), "unit" : unit, "condition" : data[ "condition" ], "humidity" : data[ "humidity" ] } This tool demonstrates proper error handling and parameter validation. Notice how we provide detailed descriptions for each parameter—this helps Claude understand how to use the tool correctly. Creating the Tool Registry We need a system to manage multiple tools and convert them into the format Claude expects. Let's build a tool registry that handles this transformation. def create_tool_schema ( tool: BaseTool ) -> Dict[str, Any]: """ Converts a tool into Claude's expected schema format. Args: tool: The tool instance to convert Returns: A dictionary matching Claude's tool schema specification """ return { "name" : tool.get_name(), "description" : tool.get_description(), "input_schema" : tool.get_parameters() } def register_tools ( tools: List[BaseTool] ) -> tuple[List[Dict], Dict[str, BaseTool]]: """ Registers multiple tools and creates lookup structures. Args: tools: List of tool instances to register Returns: A tuple of (tool_schemas, tool_map) for agent use """ tool_schemas = [create_tool_schema(tool) for tool in tools] tool_map = {tool.get_name(): tool for tool in tools} return tool_schemas, tool_map These utility functions separate the concerns of tool definition and tool registration. The create_tool_schema() function transforms our Python classes into the JSON schema format that Claude understands. The register_tools() function creates both the schema list for API calls and a lookup dictionary for executing tools. Building the Agent Core Now we'll construct the agent's brain—the component that manages conversation flow and orchestrates tool usage. The agent needs to maintain conversation history, handle API interactions, and manage the request-response cycle. Initializing the Agent Let's create the agent class with proper initialization and configuration. class Agent : """ An AI agent powered by Claude that can use tools to solve problems. """ def __init__ ( self, tools: Optional[List[BaseTool]] = None, model: str = "claude-sonnet-4-20250514" , system_prompt: Optional[str] = None ): """ Initialize the agent with tools and configuration. Args: tools: List of tool instances the agent can use model: Claude model identifier system_prompt: Custom system instructions for the agent """ self.client = anthropic.Anthropic(api_key=os.getenv( "ANTHROPIC_API_KEY" )) self.model = model self.conversation_history: List[Dict[str, Any]] = [] # Set default system prompt if none provided self.system_prompt = system_prompt or self._get_default_system_prompt() # Register tools if tools: self.tool_schemas, self.tool_map = register_tools(tools) else : self.tool_schemas, self.tool_map = [], {} def _get_default_system_prompt ( self ) -> str: """Returns the default system prompt for the agent""" return """You are a helpful AI assistant that solves problems systematically. When faced with complex questions, break them down into smaller steps. Use available tools when you need to retrieve information or perform actions. Think through problems logically and explain your reasoning. Always verify your answers before presenting them to the user.""" The initialization method sets up all the essential components. We store the API client, model configuration, conversation history, and tool registry. The system prompt defines the agent's behavior and personality—customize this to match your use case. Implementing the Chat Interface The chat method is where the magic happens—it sends messages to Claude and receives responses. def send_message ( self, user_message: str ) -> anthropic.types.Message: """ Sends a user message to Claude and returns the response. Args: user_message: The user's input message Returns: Claude's response message object """ # Add user message to conversation history self.conversation_history.append({ "role" : "user" , "content" : user_message }) # Prepare API call parameters api_params = { "model" : self.model, "max_tokens" : 4096 , "system" : self.system_prompt, "messages" : self.conversation_history } # Add tools if available if self.tool_schemas: api_params[ "tools" ] = self.tool_schemas # Call Claude API response = self.client.messages.create(**api_params) # Add assistant's response to conversation history self.conversation_history.append({ "role" : "assistant" , "content" : response.content }) return response This method handles the complete request-response cycle. We append the user's message to history, prepare the API parameters, make the call, and store Claude's response. Notice how we conditionally include tools only when they're available—this keeps the API call clean for tool-less agents. Extracting Text Responses Claude's responses can contain multiple content blocks, including text and tool use blocks. We need a helper function to extract just the text. def get_text_response ( response: anthropic.types.Message ) -> str: """ Extracts text content from Claude's response. Args: response: The message object from Claude Returns: Concatenated text from all text content blocks """ text_parts = [] for block in response.content: if block.type == "text" : text_parts.append(block.text) return "\n" .join(text_parts) This function iterates through all content blocks and collects text. When Claude uses tools, it might include both thinking (text) and tool calls in the same response. Implementing the Agentic Loop The agentic loop is where your agent becomes truly autonomous. This loop allows the agent to use tools, evaluate results, and continue reasoning until it reaches a final answer. Unlike a simple chatbot that responds once, an agent can perform multiple actions in sequence to solve complex problems. Detecting Tool Usage First, we need a function to identify when Claude wants to use tools. def extract_tool_uses ( response: anthropic.types.Message ) -> List[Dict[str, Any]]: """ Extracts all tool use blocks from Claude's response. Args: response: The message object from Claude Returns: List of dictionaries containing tool use information """ tool_uses = [] for block in response.content: if block.type == "tool_use" : tool_uses.append({ "id" : block.id, "name" : block.name, "input" : block.input }) return tool_uses When Claude decides it needs a tool, it includes a tool_use block in its response. This function finds all such blocks and extracts the relevant information. Each tool use has an ID (for tracking), a name (which tool to use), and input (the parameters). Executing Tools Safely Tool execution is where your agent interacts with the outside world—this requires careful error handling. def execute_tool_safely ( tool: BaseTool, tool_name: str, tool_input: Dict[str, Any] ) -> Dict[str, Any]: """ Executes a tool with error handling and logging. Args: tool: The tool instance to execute tool_name: Name of the tool (for logging) tool_input: Parameters to pass to the tool Returns: The tool's execution result or error information """ try : print( f" → Executing {tool_name} with input: {tool_input} " ) result = tool.execute(**tool_input) print( f" ✓ Tool execution successful" ) return result except Exception as e: error_msg = f"Tool execution failed: {str(e)} " print( f" ✗ {error_msg} " ) return { "error" : error_msg} This function wraps tool execution in try-except blocks. If something goes wrong (invalid parameters, network errors, etc.), we capture the error and return it as a result. The agent can then see what went wrong and potentially retry or adjust its approach. Processing Tool Results After executing tools, we need to format the results for Claude. def create_tool_result_content ( tool_uses: List[Dict[str, Any]], tool_map: Dict[str, BaseTool] ) -> List[Dict[str, Any]]: """ Executes tools and formats results for Claude. Args: tool_uses: List of tool use requests from Claude tool_map: Mapping of tool names to tool instances Returns: List of tool result objects formatted for the API """ tool_results = [] for tool_use in tool_uses: tool_name = tool_use[ "name" ] tool_id = tool_use[ "id" ] tool_input = tool_use[ "input" ] # Execute the tool tool = tool_map[tool_name] result = execute_tool_safely(tool, tool_name, tool_input) # Format result for Claude tool_results.append({ "type" : "tool_result" , "tool_use_id" : tool_id, "content" : json.dumps(result) }) return tool_results Claude expects tool results in a specific format. Each result must include the original tool use ID (so Claude knows which call it corresponds to) and the content as a JSON string. Building the Main Agent Loop Now we combine everything into a loop that handles multi-turn tool usage. def run_agent_loop ( agent: Agent, initial_message: str, max_iterations: int = 10 , verbose: bool = True ) -> str: """ Runs the agent in a loop, handling tool use until completion. Args: agent: The agent instance to run initial_message: The user's initial query max_iterations: Maximum iterations to prevent infinite loops verbose: Whether to print detailed execution logs Returns: The final text response from the agent """ iteration = 0 current_message = initial_message if verbose: print( f"\n { '=' * 80 } " ) print( f"AGENT EXECUTION START" ) print( f" { '=' * 80 } " ) print( f"User Query: {initial_message} \n" ) while iteration < max_iterations: iteration += 1 if verbose: print( f"\n--- Iteration {iteration} ---" ) # Send message to Claude response = agent.send_message(current_message) # Check if Claude wants to use tools tool_uses = extract_tool_uses(response) if not tool_uses: # No tools needed - we have the final answer final_response = get_text_response(response) if verbose: print( f"\n { '=' * 80 } " ) print( f"AGENT EXECUTION COMPLETE" ) print( f" { '=' * 80 } " ) print( f"Final Response: {final_response} \n" ) return final_response # Execute tools and prepare results if verbose: print( f"Claude wants to use {len(tool_uses)} tool(s):" ) tool_results = create_tool_result_content(tool_uses, agent.tool_map) # Send tool results back to Claude current_message = tool_results return "Maximum iterations reached without completion." This function orchestrates the entire agent execution. It loops until Claude provides a final answer (no tool use) or hits the iteration limit. Each iteration sends a message, checks for tool use, executes any requested tools, and feeds the results back. Complete Implementation Example Let's bring everything together into a working example that demonstrates the full capabilities of our agent system. def create_weather_agent () -> Agent: """ Factory function to create a weather-enabled agent. Returns: Configured agent instance with weather tool """ weather_tool = WeatherTool() custom_system_prompt = """You are a helpful weather assistant. When users ask about weather, use the get_weather tool to retrieve accurate information. Present weather data in a clear, conversational manner. If users don't specify a temperature unit, use celsius by default. Always mention the weather condition along with the temperature.""" return Agent( tools=[weather_tool], system_prompt=custom_system_prompt ) def main (): """Main execution function demonstrating agent capabilities""" # Create agent with weather tool agent = create_weather_agent() # Test Case 1: Simple weather query print( "\n" + "=" * 80 ) print( "TEST 1: Simple Weather Query" ) print( "=" * 80 ) response = run_agent_loop( agent, "What's the weather like in Tokyo?" , verbose= True ) print(response) # Test Case 2: Multi-city comparison print( "\n" + "=" * 80 ) print( "TEST 2: Multi-City Comparison" ) print( "=" * 80 ) response = run_agent_loop( agent, "Compare the weather in London and Paris. Which city is warmer?" , verbose= True ) print(response) # Test Case 3: Temperature unit conversion print( "\n" + "=" * 80 ) print( "TEST 3: Temperature Unit Preference" ) print( "=" * 80 ) response = run_agent_loop( agent, "What's the temperature in New York in Fahrenheit?" , verbose= True ) print(response) if __name__ == "__main__" : main() This complete example shows how to create specialized agents, run them through various scenarios, and monitor their performance. Conclusion You've just built a complete AI agent system from scratch using the Anthropic API. This agent can reason through complex problems, use tools to gather information, maintain conversation context, and provide verified answers. The modular architecture we've created allows you to easily add new tools, customize behavior, and scale to production environments. The key takeaways from this guide include understanding that agents are more than chatbots—they're autonomous systems that can take actions. Tool design is crucial—well-designed tools with clear schemas enable Claude to use them effectively. The agentic loop enables multi-step reasoning and complex problem-solving. Memory management keeps conversations efficient and cost-effective. From here, you can extend your agent with additional tools like database queries, API integrations, file operations, or web searches. You can implement streaming responses for better UX, add conversation summarization for long-running sessions, or build multi-agent systems where specialized agents collaborate. The future of AI isn't just in better models—it's in better agent architectures that leverage those models effectively. You now have the foundation to build production-ready AI agents that can solve real-world problems autonomously. Start experimenting with your own tools and use cases. The possibilities are limitless. PS: If you like this article, share it with others ♻️ Would help a lot ❤️ And feel free to follow me for more content like this. # artificial-intelligence # agentic-ai # llm