← Back to DevBytes

Function Calling at Scale with vLLM: Complete Guide

Introduction to Function Calling with vLLM

Function calling has become one of the most powerful capabilities of modern large language models (LLMs). It allows models to interact with external tools, APIs, and systems by generating structured outputs that can be parsed and executed programmatically. When you combine this capability with vLLM — a high-throughput, memory-efficient inference engine — you unlock the ability to run function calling at scale, serving thousands of concurrent requests with minimal latency.

In this guide, we will explore what function calling is, why running it at scale matters, how to implement it using vLLM, and the best practices you should follow to build production-grade systems.

What Is Function Calling?

Function calling is a mechanism where an LLM is given a set of tool definitions (functions with names, descriptions, and parameter schemas) and, when prompted, the model decides whether to call one of those functions. Instead of generating free-form text, the model emits a structured JSON object that matches the function's parameter schema. The host application can then execute the function and return the result back to the model for further reasoning.

A typical function calling workflow looks like this:

This loop enables LLMs to act as intelligent agents that can fetch live data, perform calculations, query databases, and orchestrate complex workflows.

Why Function Calling at Scale Matters

Most tutorials cover function calling with a single request to an API like OpenAI. However, real-world applications — such as customer support agents, coding assistants, and autonomous workflows — require handling hundreds or thousands of concurrent users. This is where scale becomes a challenge.

Running function calling at scale introduces several concerns:

vLLM addresses these concerns through PagedAttention, continuous batching, and optimized KV cache management, making it an ideal choice for high-volume function calling workloads.

How vLLM Supports Function Calling

vLLM supports function calling through several mechanisms. The most common approaches are:

This means you can often swap your OpenAI API client to point at a vLLM server with minimal code changes.

Setting Up vLLM for Function Calling

Installation

Start by installing vLLM. It is recommended to use a Python virtual environment and a machine with a CUDA-capable GPU.

pip install vllm
pip install outlines

Launching the OpenAI-Compatible Server

The easiest way to serve function calling is through vLLM's OpenAI-compatible API server. Launch it with a model that supports tool calling, such as Qwen/Qwen2.5-7B-Instruct or meta-llama/Llama-3.1-8B-Instruct.

vllm serve Qwen/Qwen2.5-7B-Instruct \
  --port 8000 \
  --enable-auto-tool-choice \
  --tool-call-parser hermes

The --enable-auto-tool-choice flag enables automatic tool selection, and --tool-call-parser specifies how the model's output should be parsed into structured tool calls. Different models use different parsers; common options include hermes, mistral, llama3_json, and internlm.

Defining Functions and Calling Them

Basic Example with the OpenAI Client

Because vLLM exposes an OpenAI-compatible API, you can use the standard openai Python library. Here is a complete example that defines a weather function, lets the model call it, and returns the result.

from openai import OpenAI
import json

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy-key"
)

# Define the tools available to the model
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get the current weather for a given city.",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "The name of the city, e.g. San Francisco"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["city"]
            }
        }
    }
]

# Mock implementation of the function
def get_weather(city: str, unit: str = "celsius") -> dict:
    return {
        "city": city,
        "temperature": 22 if unit == "celsius" else 72,
        "condition": "sunny",
        "unit": unit
    }

# First turn: ask the model
messages = [{"role": "user", "content": "What is the weather in Tokyo?"}]

response = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",
    messages=messages,
    tools=tools,
    tool_choice="auto"
)

message = response.choices[0].message
messages.append(message)

# Check if the model wants to call a function
if message.tool_calls:
    for tool_call in message.tool_calls:
        name = tool_call.function.name
        args = json.loads(tool_call.function.arguments)
        print(f"Model requested: {name}({args})")

        # Execute the function
        if name == "get_weather":
            result = get_weather(**args)
        else:
            result = {"error": "Unknown function"}

        # Append the tool result back to the conversation
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result)
        })

    # Second turn: let the model produce a final answer
    final_response = client.chat.completions.create(
        model="Qwen/Qwen2.5-7B-Instruct",
        messages=messages,
        tools=tools
    )
    print(final_response.choices[0].message.content)

This example demonstrates the full agentic loop: the model decides to call get_weather, the application executes it, and the model uses the result to answer the user.

Scaling Up with Concurrent Requests

Using Async Batching

To handle many users simultaneously, you should use asynchronous requests. vLLM's continuous batching engine is designed to process concurrent requests efficiently — new requests are merged into active batches without waiting for previous ones to finish.

import asyncio
import json
from openai import AsyncOpenAI

async_client = AsyncOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy-key"
)

async def handle_query(user_query: str, tools: list) -> str:
    messages = [{"role": "user", "content": user_query}]
    response = await async_client.chat.completions.create(
        model="Qwen/Qwen2.5-7B-Instruct",
        messages=messages,
        tools=tools,
        tool_choice="auto"
    )
    msg = response.choices[0].message

    if msg.tool_calls:
        messages.append(msg)
        for tc in msg.tool_calls:
            args = json.loads(tc.function.arguments)
            # Replace with real function dispatch
            result = {"echo": args}
            messages.append({
                "role": "tool",
                "tool_call_id": tc.id,
                "content": json.dumps(result)
            })
        followup = await async_client.chat.completions.create(
            model="Qwen/Qwen2.5-7B-Instruct",
            messages=messages,
            tools=tools
        )
        return followup.choices[0].message.content
    return msg.content

async def main():
    queries = [
        "What is the weather in Paris?",
        "What is the weather in New York in fahrenheit?",
        "What is the weather in Sydney?",
    ]
    tasks = [handle_query(q, tools) for q in queries]
    results = await asyncio.gather(*tasks)
    for q, r in zip(queries, results):
        print(f"Q: {q}\nA: {r}\n")

asyncio.run(main())

Because vLLM uses continuous batching, all three requests are processed together in a single batch, dramatically improving throughput compared to sequential processing.

Benchmarking Throughput

To understand your system's capacity, you should benchmark it. A simple approach is to use asyncio to fire N concurrent requests and measure total time.

import time

async def benchmark(n: int = 100):
    queries = ["What is the weather in London?"] * n
    start = time.time()
    await asyncio.gather(*[handle_query(q, tools) for q in queries])
    elapsed = time.time() - start
    print(f"Completed {n} requests in {elapsed:.2f}s "
          f"({n / elapsed:.1f} req/s)")

asyncio.run(benchmark(100))

Tune the concurrency level based on your GPU memory and model size. vLLM will automatically manage batching, but extremely high concurrency can cause queueing if the KV cache is exhausted.

Ensuring Structured Output Reliability

One of the biggest challenges at scale is ensuring the model produces valid, parseable JSON. vLLM offers guided decoding to enforce structure at the token level, preventing malformed outputs before they are generated.

Using Guided JSON with the Python API

If you use vLLM directly via its Python API rather than the OpenAI-compatible server, you can pass a JSON schema to constrain generation.

from vllm import LLM, SamplingParams
from pydantic import BaseModel

class WeatherArgs(BaseModel):
    city: str
    unit: str = "celsius"

llm = LLM(model="Qwen/Qwen2.5-7B-Instruct")

sampling_params = SamplingParams(
    temperature=0.0,
    max_tokens=256,
    guided_decoding={"json": WeatherArgs.model_json_schema()}
)

prompt = "Extract the city and unit from: 'What is the weather in Berlin in fahrenheit?'"
outputs = llm.generate([prompt], sampling_params)
print(outputs[0].outputs[0].text)

This guarantees the output will conform to the WeatherArgs schema, eliminating JSON parsing errors entirely.

Using Guided JSON via the Server API

You can also pass guided_json through the OpenAI-compatible endpoint using the extra_body parameter.

response = client.chat.completions.create(
    model="Qwen/Qwen2.5-7B-Instruct",
    messages=[{"role": "user", "content": "What is the weather in Berlin in fahrenheit?"}],
    extra_body={
        "guided_json": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["city"]
        }
    }
)

Best Practices for Production

Choose the Right Model and Parser

Not all models support function calling equally well. Models specifically fine-tuned for tool use — such as Qwen2.5-Instruct, Hermes 3, and Llama 3.1 — produce more reliable structured outputs. Always match the --tool-call-parser to the model's training format.

Keep Tool Descriptions Clear and Concise

The model relies on tool descriptions to decide when and how to call functions. Write descriptions that clearly state the purpose, expected inputs, and any constraints. Ambiguous descriptions lead to incorrect or unnecessary calls.

Limit the Number of Tools

Passing too many tools increases context length and can confuse the model. Group related tools and use a retrieval step to select the most relevant subset per query. A practical limit is 10–20 tools per request.

Handle Errors Gracefully

Functions can fail — APIs time out, databases return errors, and arguments may be invalid. Always wrap function execution in error handling and return structured error messages back to the model so it can recover or ask the user for clarification.

def safe_execute(name: str, args: dict) -> dict:
    try:
        if name == "get_weather":
            return get_weather(**args)
        return {"error": f"Unknown function: {name}"}
    except TypeError as e:
        return {"error": f"Invalid arguments: {e}"}
    except Exception as e:
        return {"error": f"Execution failed: {e}"}

Set Appropriate Sampling Parameters

For function calling, use low or zero temperature to maximize determinism and reduce the chance of malformed outputs. Set a reasonable max_tokens limit to prevent runaway generation.

Monitor and Tune Concurrency

Use vLLM's built-in metrics endpoint (available at /metrics in Prometheus format) to track request latency, queue depth, and GPU utilization. Adjust your concurrency limits and consider horizontal scaling with multiple vLLM instances behind a load balancer when a single GPU is saturated.

Cache Function Results

Many function calls are repetitive (for example, weather lookups for the same city). Implement a caching layer to avoid redundant external API calls and reduce latency. This is especially important at scale where rate limits on external services can become a bottleneck.

Conclusion

Function calling transforms LLMs from passive text generators into active agents that can interact with the world. By pairing function calling with vLLM's high-throughput inference engine, you can serve agentic workloads at scale with low latency, predictable costs, and reliable structured outputs. The key to success lies in choosing the right model and parser, writing clear tool definitions, enforcing output structure with guided decoding, handling errors robustly, and continuously monitoring performance. With these practices in place, vLLM provides a powerful foundation for building production-grade AI agents that can handle thousands of concurrent users without breaking a sweat.

— Ad —

Google AdSense will appear here after approval

← Back to all articles