Vector Database — The Search Brain of RAG

Episode 4 20 minutes

Introduction: AI Without Limbs

Imagine putting a very smart person in a room with just a tiny opening. You pass questions through the opening, they answer. But they can’t leave the room, can’t touch anything, can’t access the internet. This is roughly the situation of an LLM without Tool Use.

Now imagine opening the door and saying: “Here are your tools — calculator, web browser, email, database. Use them whenever you need to.” That’s Tool Use.

In this episode, you’ll learn what Tool Use is, how it works, and how to build your own tools and give them to an Agent.

What Is Tool Use?

Tool Use (or Function Calling) is a mechanism that lets you define tools and give them to an LLM. When the LLM determines it needs a tool to answer a question, it tells you “I want to call this tool with these parameters.” You execute the tool, return the result, and the LLM uses that result.

Important note: The LLM doesn’t execute the tool itself. It only decides which tool to call and with what parameters. The actual execution is your code’s responsibility.

The Tool Use Cycle

The Tool Use cycle typically works like this:

  1. User asks a question
  2. LLM determines it needs a tool to answer
  3. LLM returns a “tool request” (tool name + parameters)
  4. Your code executes the tool
  5. Tool result is returned to the LLM
  6. LLM uses the result to provide the final answer
# Pseudocode for the Tool Use Cycle

user_asks("What's the weather in Tehran right now?")

# LLM decides:
# -> I should use the get_weather tool
# -> Parameter: city = "Tehran"

result = get_weather(city="Tehran")
# -> {"temp": 28, "condition": "Sunny"}

# LLM final answer:
# "The weather in Tehran is currently 28 degrees and sunny"

Defining Tools with JSON Schema

For the LLM to understand what tools are available, you need to define them in a specific format. This format is typically JSON Schema:

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather conditions for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name in English, e.g. Tehran"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Calculate a mathematical expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Math expression, e.g. 2+2 or sqrt(16)"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]
Important note: Tool descriptions are crucial! The LLM decides which tool to use based on these descriptions. The clearer the descriptions, the better the LLM’s decisions.

Practical Implementation with OpenAI API

Let’s build a complete example. We’ll create a simple Agent that can both check the weather and perform calculations:

import json
from openai import OpenAI

client = OpenAI()

# Define tools
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "City name in English"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "Calculate a math expression",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "Math expression like 2+2"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

# Actual tool implementations
def get_weather(city: str) -> dict:
    """Simulated weather API"""
    fake_data = {
        "Tehran": {"temp": 28, "condition": "Sunny"},
        "London": {"temp": 15, "condition": "Rainy"},
        "Tokyo": {"temp": 22, "condition": "Cloudy"},
    }
    return fake_data.get(city, {"temp": 20, "condition": "Unknown"})

def calculate(expression: str) -> str:
    """Simple calculator"""
    try:
        return str(eval(expression))
    except Exception as e:
        return f"Error: {e}"

# Map tool names to functions
tool_functions = {
    "get_weather": get_weather,
    "calculate": calculate,
}

def run_agent(user_message: str):
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": user_message}
    ]
    
    # Step 1: Send to LLM
    response = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools,
    )
    
    message = response.choices[0].message
    
    # Step 2: Check if any tools were called
    if message.tool_calls:
        messages.append(message)
        
        # Step 3: Execute each tool
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            
            print(f"Tool called: {func_name}({func_args})")
            
            # Execute the function
            result = tool_functions[func_name](**func_args)
            
            # Add result to conversation
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })
        
        # Step 4: Send back to LLM with tool results
        final_response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
        )
        
        return final_response.choices[0].message.content
    
    return message.content

# Test
print(run_agent("What's the weather in Tehran and what's 25 times 13?"))

In this example, when the user asks about both weather and a calculation, the LLM might call both tools simultaneously! This is one of the interesting capabilities of Tool Use — parallel tool execution.

How Does the LLM Decide?

An important question: How does the LLM know when to use a tool?

Answer: From the tool descriptions and the user’s question. When a tool description says “Get current weather” and the user asks “What’s the weather like?”, the LLM recognizes the semantic match.

A few important points:

  • Write clear descriptions: “Get weather” is better than “weather”. But “Get current weather for a specific city including temperature and conditions” is best of all.
  • Describe parameters well: For example, say “City name in English, e.g. Tehran” not just “city”.
  • Control the number of tools: If you give 100 tools, the LLM gets confused. Group related tools together.

Comparing Tool Use in OpenAI and Anthropic

Two of the largest LLM providers both support Tool Use, but with some differences:

OpenAI (GPT-4o)

# OpenAI Tool Use
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=[{
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get weather",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string"}
                },
                "required": ["city"]
            }
        }
    }]
)

# Access tool call
tool_call = response.choices[0].message.tool_calls[0]
print(tool_call.function.name)       # "get_weather"
print(tool_call.function.arguments)  # {city: Tehran}

Anthropic (Claude)

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    tools=[{
        "name": "get_weather",
        "description": "Get weather",
        "input_schema": {
            "type": "object",
            "properties": {
                "city": {
                    "type": "string",
                    "description": "City name"
                }
            },
            "required": ["city"]
        }
    }],
    messages=[{"role": "user", "content": "Weather in Tehran?"}]
)

# Access tool call
for block in response.content:
    if block.type == "tool_use":
        print(block.name)   # "get_weather"
        print(block.input)  # {"city": "Tehran"}

Key Differences

  • Definition format: OpenAI uses parameters, Anthropic uses input_schema
  • Response structure: OpenAI returns tools in tool_calls, Anthropic in content with type tool_use
  • Tool results: OpenAI with role=tool, Anthropic with role=user and type tool_result
  • Parallel execution: Both support it

Building More Complex Tools

Tools can be much more complex. Let’s build a database search tool:

database_tool = {
    "type": "function",
    "function": {
        "name": "query_database",
        "description": "Search the product database. "
                       "Can filter by name, category, "
                       "price, and availability.",
        "parameters": {
            "type": "object",
            "properties": {
                "search_term": {
                    "type": "string",
                    "description": "Search term for product name"
                },
                "category": {
                    "type": "string",
                    "enum": ["electronics", "clothing", "books", "food"],
                    "description": "Product category"
                },
                "max_price": {
                    "type": "number",
                    "description": "Maximum price"
                },
                "in_stock_only": {
                    "type": "boolean",
                    "description": "Only in-stock products",
                    "default": True
                }
            },
            "required": ["search_term"]
        }
    }
}
Note: When you use enum, the LLM only selects from the specified values. This helps greatly in avoiding invalid inputs.

The Tool Use Loop Pattern

Sometimes an Agent needs to call tools multiple times to reach an answer. For example, first search, then filter results, then calculate a price. For this case, you need a loop:

def run_agent_loop(user_message: str, max_iterations: int = 10):
    messages = [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": user_message}
    ]
    
    for i in range(max_iterations):
        response = client.chat.completions.create(
            model="gpt-4o",
            messages=messages,
            tools=tools,
        )
        
        message = response.choices[0].message
        messages.append(message)
        
        # If no tool calls, the final answer is ready
        if not message.tool_calls:
            return message.content
        
        # Execute all requested tools
        for tool_call in message.tool_calls:
            func_name = tool_call.function.name
            func_args = json.loads(tool_call.function.arguments)
            result = tool_functions[func_name](**func_args)
            
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result)
            })
    
    return "Maximum iterations reached"

This pattern is very important and used in most Agent frameworks. max_iterations is a safety guard to prevent the Agent from falling into an infinite loop.

Best Practices

  • Write tool descriptions in English: LLMs work better with English descriptions, even if the user speaks another language.
  • Keep tools atomic: Each tool should do one specific thing. “get_weather” is good, “get_weather_and_send_email” is bad.
  • Keep tool output simple: Return only necessary information. A huge JSON with 100 fields confuses the LLM.
  • Return errors too: If a tool fails, tell the LLM so it can explain to the user or try a different approach.
  • Set safety limits: Always have max_iterations. Always validate tool inputs.

Tool Use Security

Tool Use gives the Agent significant power, but with power comes responsibility:

Security warning: Never use eval() directly to execute user input. Our calculator example was just for demonstration — in production, always use secure libraries like numexpr or asteval.
  • Validate inputs: Before executing any tool, check that the inputs are valid.
  • Limit access: The Agent shouldn’t have access to everything. Follow the principle of least privilege.
  • Log everything: Log every tool call. It’s necessary for debugging and auditing.
  • Human confirmation: For sensitive operations (like deleting data or making payments), get user confirmation.

Summary

Tool Use is a simple but powerful concept:

  • You define tools with JSON Schema
  • The LLM decides which tool to call and with what parameters
  • You execute the tool and return the result
  • This cycle can repeat multiple times

With Tool Use, an Agent transforms from a text-only entity into an action-oriented one that can actually get things done.

The next episode is about Agent Memory — how an Agent can remember things and use past experiences. See you next episode!