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_callsentry and reply withrole: toolmessages - 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.