Tool calling with local models

Ollama · Practical · 14 min

Tool calling turns a local model into an agent that can look things up and act, all on your own machine.

With tool calling, the model can ask your program to run a function, for example to look up the weather or query a database. The model never runs code itself: it replies with a request, your code runs it, and you send the result back.

With the Python library you can pass ordinary functions as tools. Their names, parameter types and docstrings are turned into descriptions for the model, so write clear docstrings:

def get_temperature(city: str) -> str:
    """Get the current temperature for a city."""

The loop:

1. Call chat(model, messages, tools=[get_temperature]). 2. If response.message.tool_calls is set, each call has call.function.name and call.function.arguments (a dictionary). 3. Add the assistant message to messages, run each requested function, and add a message {"role": "tool", "tool_name": name, "content": result} for each. 4. Call chat again with the longer history. Repeat until the reply has no tool calls; that reply is the answer.

Models may request several tools at once, or one after another. Use a model with the tools capability (shown by ollama show); the docs' examples use qwen3 with thinking enabled.

Practical advice:

  • Keep the tool list short and the descriptions specific.
  • Check arguments before acting; small models sometimes pass wrong types or invent values.
  • Never let a tool do something irreversible (payments, deletions, emails) without confirmation.
  • Put a maximum number of rounds on the loop.

Frameworks such as LangChain, LlamaIndex or CrewAI can run this loop for you on Ollama models.

Example

from ollama import chat

def get_temperature(city: str) -> str:
    """Get the current temperature for a city."""
    return {"London": "15°C", "Oslo": "8°C"}.get(city, "unknown")

tools = {"get_temperature": get_temperature}
messages = [{"role": "user", "content": "Is it warmer in London or Oslo right now?"}]

for _ in range(5):                                   # bound the loop
    response = chat(model="qwen3", messages=messages, tools=list(tools.values()))
    messages.append(response.message)
    if not response.message.tool_calls:
        break                                        # final answer
    for call in response.message.tool_calls:
        fn = tools.get(call.function.name)
        result = fn(**call.function.arguments) if fn else "unknown tool"
        print("called", call.function.name, call.function.arguments, "->", result)
        messages.append({"role": "tool", "tool_name": call.function.name, "content": result})

print(response.message.content)
# called get_temperature {'city': 'London'} -> 15°C
# called get_temperature {'city': 'Oslo'} -> 8°C
# London is warmer: 15°C versus 8°C in Oslo.

Remember

  • Pass Python functions as tools=; docstrings become descriptions
  • Run each tool_calls entry and reply with role: tool messages
  • Loop until no tool calls, with a maximum number of rounds

Watch out for

  • Running whatever function name the model returns without checking it exists and that its arguments are valid.

Check yourself

  1. After the model returns tool_calls, what must your code send in the next chat request?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.ollama.com/capabilities/tool-calling