Tutorials & Guides
How to Build a Tool-Calling AI Agent in Raw Python (Without the LangChain Bloat)
Skip the complex orchestration libraries. Discover how to build a robust, transparent, and lightning-fast tool-calling agent using only the official OpenAI SDK and vanilla Python.
Updated 8/21/2026
AI orchestration frameworks have a massive popularity problem. While packages like LangChain or CrewAI are brilliant for quickly throwing together a weekend prototype, they frequently turn into a maintenance nightmare when you try to scale them in production. They introduce deep inheritance trees, abstract away raw API calls behind layers of custom classes, and make debugging a simple payload feel like navigating an archaeological dig.
When something goes wrong with a tool-calling agent, you need to know exactly what payload was sent to the model and exactly what it returned.
By building your agent loop in vanilla Python using the official openai package, you maintain absolute control over your state, execution, and error handling. Let's look at how the entire loop actually works, and build a fully functional, transparent tool-calling agent in under 100 lines of clean code.
Understanding the Core Loop: What Makes an Agent Tick?
Before writing code, we need to clarify what an "agent" actually is in the context of tool calling. It isn't a magical, sentient entity. It is a simple while-loop.
At each tick of the loop, we: 1. Send the conversation history (including our system prompt, tools list, and user query) to OpenAI. 2. Read the model's response. 3. If the model responds with a normal text message, we show it to the user and stop. 4. If the model responds with a request to call a tool, we pause, execute that specific function locally in Python, add the result back to the conversation history, and loop back to step 1.
For a deeper look at terminology like tool schemas and execution context, feel free to browse our comprehensive AI Glossary.
Step 1: Setting Up Your Python Environment
We only need a single dependency for this build: the official OpenAI SDK.
`bash
pip install openai
`
Make sure your environment variable is set up correctly in your terminal:
`bash
export OPENAI_API_KEY="your-api-key-here"
`
Step 2: Defining Our Local Tools
Let's write a couple of simple Python functions that our agent can execute. To keep things practical, we will write a mock database lookup tool and a simple math calculator.
`python
import json
from openai import OpenAI
Initialize the OpenAI client client = OpenAI()
Define the actual python functions the agent can run def get_user_status(user_id: str) -> str: """Simulates looking up a user in a local database.""" database = { "usr_100": {"name": "Alice", "status": "Active", "tier": "Premium"}, "usr_200": {"name": "Bob", "status": "Suspended", "tier": "Free"} } user = database.get(user_id) if user: return json.dumps(user) return json.dumps({"error": "User not found"})
def calculate_discount(tier: str, price: float) -> str:
"""Calculates order discounts based on user tier."""
if tier.lower() == "premium":
discount = 0.20
else:
discount = 0.0
final_price = price * (1 - discount)
return json.dumps({"original_price": price, "discount": discount, "final_price": final_price})
`
Step 3: Writing the Tool Schemas
Next, we have to describe these Python functions to OpenAI using JSON Schema. This is how the model understands when and how to invoke our code.
`python
tools_definition = [
{
"type": "function",
"function": {
"name": "get_user_status",
"description": "Retrieve user status and billing tier from the database using their user_id.",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string", "description": "The unique user identifier, e.g., usr_100"}
},
"required": ["user_id"]
}
}
},
{
"type": "function",
"function": {
"name": "calculate_discount",
"description": "Calculate final order price based on customer subscription tier and base price.",
"parameters": {
"type": "object",
"properties": {
"tier": {"type": "string", "description": "The billing tier of the user (e.g. Premium, Free)"},
"price": {"type": "number", "description": "The raw order price before discount"}
},
"required": ["tier", "price"]
}
}
}
]
Map function name strings to our actual Python callables available_tools = { "get_user_status": get_user_status, "calculate_discount": calculate_discount } ```
Step 4: The Clean Agent Loop
Now, let's assemble the agent runner loop. This loop runs recursively, resolving any requested tools until the LLM decides it has all the information it needs to construct a final answer.
`python
def run_agent(user_prompt: str):
# Initialize history with the system message and user query
messages = [
{
"role": "system",
"content": "You are a precise billing support agent. Solve the user's issue step-by-step using the provided tools."
},
{
"role": "user",
"content": user_prompt
}
]
print(f"🚀 Starting Agent Loop with prompt: '{user_prompt}'\n")
while True: # Query the model with the current conversation state and tool definitions response = client.chat.completions.create( model="gpt-4o-mini", messages=messages, tools=tools_definition, tool_choice="auto" )
response_message = response.choices[0].message tool_calls = response_message.tool_calls
If the model does not want to call any tools, it is ready to give us the final answer if not tool_calls: print("🏁 Agent finished executing tools.") print(f"Response: {response_message.content}\n") break
Add the assistant's message (containing the tool request details) to the history messages.append(response_message)
Loop through each tool call requested by the model for tool_call in tool_calls: function_name = tool_call.function.name function_to_call = available_tools[function_name] function_args = json.loads(tool_call.function.arguments) print(f"🔧 LLM requested tool execution: {function_name}() with arguments {function_args}") # Execute our local Python function tool_output = function_to_call(**function_args) print(f"📤 Tool output obtained: {tool_output}")
Append the tool's result to the message history so the LLM can read it messages.append({ "tool_call_id": tool_call.id, "role": "tool", "name": function_name, "content": tool_output }) print("🔄 Re-submitting history to LLM to process tool results...\n") ```
Step 5: Testing Our Agent
Let's run a complex prompt that forces the model to chain multiple tool calls together sequentially. The agent will first need to find the user's details, identify their membership tier, and then run the discount calculator before responding.
`python
if __name__ == "__main__":
run_agent("Can you check the final price of a 150 dollar item for customer usr_100?")
`
If you execute this script, you will see a clean step-by-step console log detailing exactly how the model reasons, triggers the database query, receives the tier details, evaluates the math formula, and delivers a final output. No hidden abstractions, no magic, and zero framework overhead.
Should you run into API connection issues or need help managing context window lengths with more massive payloads, take a look at the official guides over at OpenAI Support.
Building your agents this way ensures your codebase remains incredibly easy to debug, scale, and maintain—proving that sometimes, simpler really is better.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.