Join our Discord Server
Collabnix Team The Collabnix Team is a diverse collective of Docker, Kubernetes, and IoT experts united by a passion for cloud-native technologies. With backgrounds spanning across DevOps, platform engineering, cloud architecture, and container orchestration, our contributors bring together decades of combined experience from various industries and technical domains.

Function Calling with Ollama: Building Local Tool-Using AI Agents

2 min read

Ollama supports function calling (also called tool calling) for local models, letting a model decide to call a function, read the result, and keep working toward a goal instead of just returning text. This tutorial is almost entirely code: pull a model, define a tool, call it, and wire up a minimal agent loop.

Prerequisites

# install Ollama (macOS)
brew install ollama
ollama serve &

# or Linux
curl -fsSL https://ollama.com/install.sh | sh

Step 1: Pull a Tool-Capable Model

Not every model supports tool calling. Llama 3.1/3.2, Qwen2.5, Mistral Nemo, and Firefunction-v2 do.

ollama pull llama3.1
ollama pull qwen2.5

Step 2: Define a Tool Schema

A tool is a JSON Schema description of a function: name, description, and parameters.

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "Get the current weather for a city",
    "parameters": {
      "type": "object",
      "properties": {
        "city": { "type": "string", "description": "City name" },
        "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
      },
      "required": ["city"]
    }
  }
}

Step 3: Call the Chat API with curl

curl http://localhost:11434/api/chat -d '{
  "model": "llama3.1",
  "messages": [
    { "role": "user", "content": "What is the weather in Boston?" }
  ],
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
          "type": "object",
          "properties": {
            "city": { "type": "string" },
            "unit": { "type": "string", "enum": ["celsius", "fahrenheit"] }
          },
          "required": ["city"]
        }
      }
    }
  ],
  "stream": false
}'

The response includes a tool_calls array instead of a plain content string when the model decides to call your function:

{
  "message": {
    "role": "assistant",
    "content": "",
    "tool_calls": [
      {
        "function": {
          "name": "get_weather",
          "arguments": { "city": "Boston", "unit": "fahrenheit" }
        }
      }
    ]
  }
}

Step 4: The Same Call in Python

import ollama

response = ollama.chat(
    model="llama3.1",
    messages=[{"role": "user", "content": "What is the weather in Boston?"}],
    tools=[
        {
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "Get the current weather for a city",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string"},
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                    },
                    "required": ["city"],
                },
            },
        }
    ],
)

print(response["message"].get("tool_calls"))

Step 5: Execute the Tool and Send the Result Back

Ollama does not execute functions for you. You read tool_calls, run your own Python function, then append the result as a tool message and call chat again.

def get_weather(city, unit="fahrenheit"):
    # replace with a real weather API call
    return {"city": city, "temp": 61, "unit": unit, "condition": "cloudy"}

available_functions = {"get_weather": get_weather}

messages = [{"role": "user", "content": "What is the weather in Boston?"}]

response = ollama.chat(model="llama3.1", messages=messages, tools=[...])
messages.append(response["message"])

for call in response["message"].get("tool_calls", []):
    fn = available_functions[call["function"]["name"]]
    result = fn(**call["function"]["arguments"])
    messages.append({
        "role": "tool",
        "content": str(result),
    })

final = ollama.chat(model="llama3.1", messages=messages)
print(final["message"]["content"])

Step 6: Full Minimal Agent Loop

Wrap steps 4 and 5 in a loop so the model can chain multiple tool calls before giving a final answer:

import ollama

def get_weather(city, unit="fahrenheit"):
    return {"city": city, "temp": 61, "unit": unit, "condition": "cloudy"}

TOOLS = [{
    "type": "function",
    "function": {
        "name": "get_weather",
        "description": "Get the current weather for a city",
        "parameters": {
            "type": "object",
            "properties": {
                "city": {"type": "string"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
            },
            "required": ["city"],
        },
    },
}]

AVAILABLE = {"get_weather": get_weather}

def run_agent(prompt, model="llama3.1", max_turns=5):
    messages = [{"role": "user", "content": prompt}]
    for _ in range(max_turns):
        response = ollama.chat(model=model, messages=messages, tools=TOOLS)
        message = response["message"]
        messages.append(message)

        calls = message.get("tool_calls")
        if not calls:
            return message["content"]

        for call in calls:
            fn = AVAILABLE[call["function"]["name"]]
            result = fn(**call["function"]["arguments"])
            messages.append({"role": "tool", "content": str(result)})

    return "Max turns reached without a final answer."

print(run_agent("What is the weather in Boston?"))

Common Pitfalls

Small models skip the tool and answer from memory: use a model explicitly listed as tool-capable, and keep the tool description short and unambiguous.

Arguments arrive as a string instead of a dict on some models: call json.loads() on call[“function”][“arguments”] defensively before unpacking.

The loop never terminates: always cap max_turns, and return early the moment tool_calls is empty.

Ollama vs. Cloud-Managed Agents

Ollama’s tool calling gives you the raw building block: one request, one round of tool_calls, no built-in retries, memory, or sandboxing. That is a feature for local, cost-free experimentation, but for production-grade multi-step agents with hosted retries and event streaming, compare it against a managed offering before building a lot of custom orchestration around it.

Have Queries? Join https://launchpass.com/collabnix

Collabnix Team The Collabnix Team is a diverse collective of Docker, Kubernetes, and IoT experts united by a passion for cloud-native technologies. With backgrounds spanning across DevOps, platform engineering, cloud architecture, and container orchestration, our contributors bring together decades of combined experience from various industries and technical domains.
Join our Discord Server
Index