LLM function calling is a powerful mechanism for giving your AI agent the ability to interact with external tools, but it opens the door to a specific kind of abuse: tools being called excessively or with malicious intent, potentially leading to service degradation or unexpected costs.

Let’s see this in action. Imagine a simple weather tool your LLM can call:

{
  "name": "get_weather",
  "description": "Get the current weather for a given location.",
  "parameters": {
    "type": "object",
    "properties": {
      "location": {
        "type": "string",
        "description": "The city and state, e.g., San Francisco, CA"
      }
    },
    "required": ["location"]
  }
}

A user might try to overload this:

User: "What’s the weather in New York? What’s the weather in London? What’s the weather in Tokyo? What’s the weather in Paris? What’s the weather in Sydney? What’s the weather in Berlin? What’s the weather in Rome? What’s the weather in Moscow? What’s the weather in Beijing? What’s the weather in Mumbai?"

Without safeguards, the LLM could happily churn out a dozen get_weather calls in rapid succession. This isn’t just inefficient; if get_weather hits an external API with rate limits, or incurs per-call charges, it becomes a significant problem.

The core problem is that the LLM, by default, has no inherent understanding of external system constraints. It sees a request to "get weather" and fulfills it. It doesn’t know if calling get_weather 100 times will cost $100, trigger an API ban, or exhaust your database connection pool.

To prevent this, you need to implement a multi-layered defense system.

1. Rate Limiting at the LLM Orchestration Layer

Before the LLM even decides to call a tool, you can limit how many tool calls are permitted per request or per user session.

  • Diagnosis: Observe the frequency of tool calls in your application logs for individual user sessions or overall.

  • Fix: Implement a simple counter in your application logic. For example, in Python using Flask:

    from flask import Flask, request, jsonify
    import time
    
    app = Flask(__name__)
    
    # Store call counts per session (simplified, use a proper cache/DB for production)
    session_calls = {}
    MAX_CALLS_PER_MINUTE = 10
    CALL_TIMEOUT = 60 # seconds
    
    @app.before_request
    def check_rate_limit():
        session_id = request.cookies.get('session_id') or request.headers.get('X-Session-ID')
        if not session_id:
            session_id = str(int(time.time())) # Basic session ID
    
        current_time = time.time()
        calls = session_calls.get(session_id, [])
    
        # Remove calls older than CALL_TIMEOUT
        calls = [t for t in calls if current_time - t < CALL_TIMEOUT]
    
        if len(calls) >= MAX_CALLS_PER_MINUTE:
            return jsonify({"error": "Rate limit exceeded. Please try again later."}), 429
    
        session_calls[session_id] = calls
        return None # Continue request
    
    # ... your LLM interaction logic ...
    # When an LLM decides to call a tool, add the current time to the session_calls
    # e.g., session_calls[session_id].append(time.time())
    
  • Why it works: This acts as a gatekeeper before the LLM’s output is even processed by your tool execution logic. It prevents a single user or a burst of requests from overwhelming downstream services by enforcing a hard cap on calls within a time window.

2. Tool-Specific Call Limits

Some tools might be more expensive or resource-intensive than others. You can apply finer-grained limits.

  • Diagnosis: Identify which specific tools are being called excessively, and understand their cost or resource implications.

  • Fix: Within your tool execution handler, check the call count for the specific tool being invoked.

    # Example within your tool execution function
    tool_call_counts = {} # Similar to session_calls, but per tool and session
    MAX_WEATHER_CALLS_PER_HOUR = 5
    
    def execute_tool(tool_name, tool_args):
        session_id = get_current_session_id() # Your session management
        tool_key = f"{session_id}:{tool_name}"
        current_time = time.time()
    
        calls = tool_call_counts.get(tool_key, [])
        calls = [t for t in calls if current_time - t < 3600] # 1 hour
    
        if tool_name == "get_weather" and len(calls) >= MAX_WEATHER_CALLS_PER_HOUR:
            raise Exception("Too many weather requests in the last hour. Please wait.")
    
        tool_call_counts[tool_key] = calls + [current_time]
        # ... proceed to call the actual tool ...
        return call_actual_weather_api(tool_args["location"])
    
  • Why it works: This allows you to protect critical or costly resources by throttling access to individual tools based on their impact, rather than a blanket limit on all tool interactions.

3. Cost-Aware Tool Selection (LLM Prompting)

You can guide the LLM itself to be more judicious. This is less about hard enforcement and more about making the LLM "aware" of costs or limits.

  • Diagnosis: Analyze LLM response logs to see if the model is unnecessarily calling tools, especially in loops or for redundant information.

  • Fix: Inject information about tool usage costs or limits into the LLM’s system prompt or context.

    # Example system prompt snippet
    system_prompt = """
    You are an AI assistant. You can use the following tools:
    - get_weather(location: str): Gets current weather. NOTE: Excessive calls to get_weather may incur charges and are limited to 5 calls per user per hour.
    - search_web(query: str): Searches the web. This tool is generally safe to use.
    
    Be mindful of tool usage. Avoid redundant calls and prioritize efficiency.
    """
    
  • Why it works: By informing the LLM about the consequences of its actions (charges, limits), you can subtly influence its decision-making process. It might then choose to summarize information, re-use previous tool results, or avoid tool calls it deems non-essential.

4. Output Validation and Sanitization

Even with limits, malicious actors might try to craft prompts that cause the LLM to output tool calls in a format that exploits your executor.

  • Diagnosis: Review logs for malformed tool call arguments or unexpected tool invocation patterns that bypass your initial checks.

  • Fix: Sanitize and validate all arguments passed to your tool execution functions. For get_weather, ensure location is a string and perhaps validate it against a known format or a list of supported locations if applicable.

    import re
    
    def execute_tool(tool_name, tool_args):
        if tool_name == "get_weather":
            location = tool_args.get("location")
            if not isinstance(location, str) or not location:
                raise ValueError("Invalid location provided for get_weather.")
            # Optional: More robust validation (e.g., regex for city, state format)
            # if not re.match(r"^[a-zA-Z\s]+, [A-Z]{2}$", location):
            #     raise ValueError("Location must be in 'City, ST' format.")
        # ... other tool validations ...
        # ... proceed to call the actual tool ...
    
  • Why it works: This prevents malformed inputs from causing errors in your tool’s underlying logic or being exploited to bypass checks. It ensures that the data fed into your tools is predictable and safe.

5. User-Level Blocking and Monitoring

For persistent abuse, you need a way to identify and block problematic users.

  • Diagnosis: Set up alerts for users who repeatedly hit rate limits across multiple tools or sessions.

  • Fix: Maintain a blacklist of user IDs or IP addresses that have exhibited abusive behavior.

    # In your rate limiting or execution logic, after an abuse is detected:
    def block_user(user_id, reason="Abuse detected"):
        # Add user_id to a persistent blacklist (e.g., Redis set, database table)
        blacklist.add(user_id)
        log_abuse(user_id, reason)
    
    def is_user_blocked(user_id):
        return user_id in blacklist
    
    # Before processing any request from a user:
    if is_user_blocked(get_current_user_id()):
        return jsonify({"error": "Your account has been blocked due to abuse."}), 403
    
  • Why it works: This is the ultimate deterrent for repeat offenders. By identifying and blocking users who consistently violate your usage policies, you protect your system from ongoing malicious activity.

The next challenge you’ll face is handling LLM hallucinations where the model thinks it has called a tool but hasn’t, or calls a tool that doesn’t exist in its defined schema.

Want structured learning?

Take the full AI Security course →