All Posts
AI AgentsLLMsPythonTool Use

Building AI Agents with Tool Use: From Zero to a Working Research Agent

14 July 2025·9 min read·Harshit Gupta
TL;DR

A tool-use agent is an LLM in a loop: generate a response → if the response contains a tool call → execute the tool → feed the result back → repeat until done. The architecture is simple. The complexity is in error handling, tool design, loop termination conditions, and preventing runaway agents. This post builds a complete research agent with web search, summarization, and structured output.

What Is an AI Agent, Really?

Strip away the hype and an AI agent is straightforward: an LLM that can take actions in a loop, where the output of each step becomes the input to the next. The key addition over a single LLM call is tools — functions the model can invoke to interact with the world: search the web, query a database, call an API, run code, read a file.

The model doesn't actually execute tools. It generates a structured description of which tool to call and with what arguments. Your code executes the tool and returns the result. The model sees the result and decides what to do next. This cycle continues until the model decides the task is complete.

The Agent Loop

def run_agent(task: str, tools: list[dict], max_iterations: int = 10) -> str:
    messages = [{"role": "user", "content": task}]

    for iteration in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
            tool_choice="auto"
        )

        msg = response.choices[0].message
        messages.append(msg)  # Add assistant's response to history

        # If no tool calls — the agent is done
        if not msg.tool_calls:
            return msg.content

        # Execute each tool call
        for tool_call in msg.tool_calls:
            result = execute_tool(tool_call.function.name, tool_call.function.arguments)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": str(result)
            })

    return "Max iterations reached"  # Safety net
Always set max_iterations

An agent without a loop limit can run indefinitely if the model keeps calling tools without converging. Set a reasonable limit (5-15 for simple agents, up to 30 for complex research tasks) and handle the termination case explicitly. Without this guard, a buggy agent can rack up thousands of API calls and dollars in minutes.

Defining Tools for the Agent

Tools are defined as JSON Schema objects describing their name, purpose, and parameters. Clear, unambiguous descriptions are critical — the model decides which tool to call based entirely on these descriptions:

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "web_search",
            "description": "Search the web for current information about a topic. Use when you need facts, news, or information that may be more recent than your training data.",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "description": "The search query. Be specific — better queries return better results."
                    },
                    "max_results": {
                        "type": "integer",
                        "description": "Number of results to return (1-10). Default: 5.",
                        "default": 5
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "read_url",
            "description": "Fetch and read the full text content of a web page. Use after web_search to read specific articles in depth.",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "The URL to fetch"}
                },
                "required": ["url"]
            }
        }
    }
]

Implementing the Tool Functions

import requests
from bs4 import BeautifulSoup
import json

def web_search(query: str, max_results: int = 5) -> list[dict]:
    # Using SerpAPI, Tavily, or any search API
    response = requests.get(
        "https://api.tavily.com/search",
        json={"query": query, "max_results": max_results, "api_key": TAVILY_API_KEY}
    )
    results = response.json().get("results", [])
    return [{"title": r["title"], "url": r["url"], "snippet": r["content"]} for r in results]

def read_url(url: str) -> str:
    try:
        resp = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
        soup = BeautifulSoup(resp.text, "html.parser")
        # Remove noise — scripts, styles, nav elements
        for tag in soup(["script", "style", "nav", "header", "footer"]):
            tag.decompose()
        text = soup.get_text(separator="\n", strip=True)
        # Truncate to stay within context limits
        return text[:8000]
    except Exception as e:
        return f"Error reading URL: {str(e)}"

def execute_tool(name: str, arguments_json: str) -> str:
    args = json.loads(arguments_json)
    if name == "web_search":
        results = web_search(**args)
        return json.dumps(results, indent=2)
    elif name == "read_url":
        return read_url(**args)
    else:
        return f"Unknown tool: {name}"

Giving the Agent a Strong System Prompt

SYSTEM_PROMPT = """You are a research assistant agent. Your goal is to answer the user's question thoroughly and accurately using web search.

Process:
1. Break the question into sub-questions if needed
2. Search for each sub-question using web_search
3. Read full articles with read_url when a snippet is insufficient
4. Synthesize findings into a comprehensive, well-structured answer

Guidelines:
- Cite your sources with URLs
- If search results are insufficient, try different search queries before giving up
- Return only information you actually found — do not supplement with your training knowledge
- When done, present your answer clearly without further tool calls"""

Structured Output from Agents

For production agents, you often need structured output rather than free-form prose. The cleanest pattern: add a final "format" tool that the agent must call when done:

FINISH_TOOL = {
    "type": "function",
    "function": {
        "name": "submit_research_report",
        "description": "Call this tool when you have finished your research to submit the final structured report.",
        "parameters": {
            "type": "object",
            "properties": {
                "summary": {"type": "string", "description": "2-3 sentence executive summary"},
                "key_findings": {"type": "array", "items": {"type": "string"}},
                "sources": {"type": "array", "items": {"type": "string"}},
                "confidence": {"type": "number", "description": "0-1 confidence in findings"}
            },
            "required": ["summary", "key_findings", "sources", "confidence"]
        }
    }
}
Agentic workflows in 2025

The field is moving fast. Frameworks like LangGraph, CrewAI, and the Anthropic Agent SDK are making multi-agent architectures — where specialized agents hand off tasks to each other — increasingly practical. But understanding the core loop (generate → tool call → result → repeat) is essential before adding framework abstractions. Build one from scratch first.

Key Takeaways

  • An agent is just an LLM in a loop with tools — the architecture is simpler than the hype suggests
  • Always set max_iterations to prevent runaway agents and unbounded API costs
  • Tool descriptions are critical — the model selects tools based entirely on your descriptions
  • A "submit result" tool forces structured final output instead of free-form prose
  • Clear system prompts that define the agent's process dramatically improve reliability
  • Build a simple agent from scratch before reaching for LangChain/LangGraph
Back to All Posts

Written by Harshit Gupta

© 2026 Harshit Gupta · New Delhi, India