TellWorldAI World Archive
Ask中文

Home›Practice›Practice

Practice

Tool calling with Ollama

Ollama's tool-calling forms end to end: single-shot calls with result pass-back, parallel multi-tool dispatch, agent loops with termination conditions, and accumulating streamed fields chunk by chunk.

Tool calling is the step that moves a local model from chatting to working: the model decides when it needs an external tool, issues a structured call, and folds the result back into its answer. Ollama supports the mechanism in three forms — single-shot, parallel, and looping — plus a streaming variant. This page walks the protocol details and client code for each [1].

Single-shot

The basic form fires one tool call, which the client executes and returns in a follow-up request [1]. Over the native API, tool descriptions are plain JSON Schema [1]:

curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
  "model": "qwen3",
  "messages": [{"role": "user", "content": "What is the temperature in New York?"}],
  "stream": false,
  "tools": [
    {
      "type": "function",
      "function": {
        "name": "get_temperature",
        "description": "Get the current temperature for a city",
        "parameters": {
          "type": "object",
          "required": ["city"],
          "properties": {
            "city": {"type": "string", "description": "The name of the city"}
          }
        }
      }
    }
  ]
}'

The assistant reply carries a tool_calls array; the client executes it and returns the result as a tool-role message with the tool name and original call info attached [1]:

curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
  "model": "qwen3",
  "messages": [
    {"role": "user", "content": "What is the temperature in New York?"},
    {
      "role": "assistant",
      "tool_calls": [
        {
          "type": "function",
          "function": {
            "index": 0,
            "name": "get_temperature",
            "arguments": {"city": "New York"}
          }
        }
      ]
    },
    {"role": "tool", "tool_name": "get_temperature", "content": "22°C"}
  ],
  "stream": false
}'

The Python SDK is lighter: a plain function with a docstring passes straight into the tools list, parsed into a schema automatically [1]:

from ollama import chat

def get_temperature(city: str) -> str:
  """Get the current temperature for a city
  
  Args:
    city: The name of the city

  Returns:
    The current temperature for the city
  """
  temperatures = {
    "New York": "22°C",
    "London": "15°C",
    "Tokyo": "18°C",
  }
  return temperatures.get(city, "Unknown")

messages = [{"role": "user", "content": "What is the temperature in New York?"}]

# pass functions directly as tools in the tools list or as a JSON schema
response = chat(model="qwen3", messages=messages, tools=[get_temperature], think=True)

Return the result and generate the final answer [1]:

messages.append(response.message)
if response.message.tool_calls:
  # only recommended for models which only return a single tool call
  call = response.message.tool_calls[0]
  result = get_temperature(**call.function.arguments)
  # add the tool result to the messages
  messages.append({"role": "tool", "tool_name": call.function.name, "content": str(result)})

  final_response = chat(model="qwen3", messages=messages, tools=[get_temperature], think=True)
  print(final_response.message.content)

Taking tool_calls[0] directly suits only models guaranteed to return a single call; multi-call responses deserve a loop [1].

Parallel calls

Ask for the weather and temperature in two cities and the model emits four calls at once — a temperature and conditions pair per city [1]. The Python client dispatches by function name and returns every result before the final generation [1]:

messages = [{'role': 'user', 'content': 'What are the current weather conditions and temperature in New York and London?'}]

# The python client automatically parses functions as a tool schema so we can pass them directly
# Schemas can be passed directly in the tools list as well 
response = chat(model='qwen3', messages=messages, tools=[get_temperature, get_conditions], think=True)

# add the assistant message to the messages
messages.append(response.message)
if response.message.tool_calls:
  # process each tool call 
  for call in response.message.tool_calls:
    # execute the appropriate tool
    if call.function.name == 'get_temperature':
      result = get_temperature(**call.function.arguments)
    elif call.function.name == 'get_conditions':
      result = get_conditions(**call.function.arguments)
    else:
      result = 'Unknown tool'
    # add the tool result to the messages
    messages.append({'role': 'tool',  'tool_name': call.function.name, 'content': str(result)})

  # generate the final response
  final_response = chat(model='qwen3', messages=messages, tools=[get_temperature, get_conditions], think=True)
  print(final_response.message.content)

Parallel tool_calls carry index fields 0 through 3, and the returned results should follow the same order. The parallel form saves round trips but requires client code able to execute any number of calls — a name-to-function map is the sturdiest dispatch [1].

The agent loop

The loop form hands over full control: the model decides how many calls to make and when to stop, and the loop ends when a response contains no tool_calls [1]. A practical tip from the docs: telling the model it is in a loop and may call tools multiple times helps [1]. The example computes (11434+12341)*412 with add and multiply functions [1]:

def add(a: int, b: int) -> int:
  """Add two numbers"""
  """
  Args:
    a: The first number
    b: The second number

  Returns:
    The sum of the two numbers
  """
  return a + b


def multiply(a: int, b: int) -> int:
  """Multiply two numbers"""
  """
  Args:
    a: The first number
    b: The second number

  Returns:
    The product of the two numbers
  """
  return a * b

The loop body is a standard while with one termination condition — no more tool calls [1]:

available_functions = {
  'add': add,
  'multiply': multiply,
}

messages = [{'role': 'user', 'content': 'What is (11434+12341)*412?'}]
while True:
    response: ChatResponse = chat(
        model='qwen3',
        messages=messages,
        tools=[add, multiply],
        think=True,
    )
    messages.append(response.message)
    print("Thinking: ", response.message.thinking)
    print("Content: ", response.message.content)
    if response.message.tool_calls:
        for tc in response.message.tool_calls:
            if tc.function.name in available_functions:
                print(f"Calling {tc.function.name} with arguments {tc.function.arguments}")
                result = available_functions[tc.function.name](**tc.function.arguments)
                print(f"Result: {result}")
                # add the tool result to the messages
                messages.append({'role': 'tool', 'tool_name': tc.function.name, 'content': str(result)})
    else:
        # end the loop when there are no more tool calls
        break

Productionizing this loop adds two things: a maximum-iteration cap against runaway loops, and exception handling so one failing tool does not sink the conversation. Printing thinking each round makes the model's planning visible [1].

Streaming with tools

Streaming improves the wait, but combined with tool calls it introduces a new problem: thinking, content, and tool_calls all arrive as increments, so the client must accumulate the fragments and return the complete fields together with any tool results in the follow-up request [1]. Setup and accumulation [1]:

messages = [{'role': 'user', 'content': "What is the temperature in New York?"}]

while True:
  stream = chat(
    model='qwen3',
    messages=messages,
    tools=[get_temperature],
    stream=True,
    think=True,
  )

  thinking = ''
  content = ''
  tool_calls = []
  done_thinking = False
  # accumulate the partial fields
  for chunk in stream:
    if chunk.message.thinking:
      thinking += chunk.message.thinking
      print(chunk.message.thinking, end='', flush=True)
    if chunk.message.content:
      if not done_thinking:
        done_thinking = True
        print('\n')
      content += chunk.message.content
      print(chunk.message.content, end='', flush=True)
    if chunk.message.tool_calls:
      tool_calls.extend(chunk.message.tool_calls)
      print(chunk.message.tool_calls)

  # append accumulated fields to the messages
  if thinking or content or tool_calls:
    messages.append({'role': 'assistant', 'thinking': thinking, 'content': content, 'tool_calls': tool_calls})

  if not tool_calls:
    break

Three details matter: the assistant message must return all three fields as a whole — dropping thinking or content breaks the model's context; the done_thinking flag prints one newline at the thinking-to-content transition; and no tool calls means break [1]. The loop simultaneously delivers live streaming output and intact pass-back — the two needs never conflict.

Functions as tools

The last item is the Python SDK's convenience: functions pass straight into the tools list with schemas derived from their docstrings; explicit JSON schemas remain available when fine control is needed, and both styles mix freely [1]:

from ollama import chat

def get_temperature(city: str) -> str:
  """Get the current temperature for a city
  
  Args:
    city: The name of the city

  Returns:
    The current temperature for the city
  """
  temperatures = {
    'New York': '22°C',
    'London': '15°C',
  }
  return temperatures.get(city, 'Unknown')

available_functions = {
  'get_temperature': get_temperature,
}
# directly pass the function as part of the tools list
response = chat(model='qwen3', messages=messages, tools=available_functions.values(), think=True)

Docstring quality decides call accuracy — every Args line lands in the schema, and vague descriptions leave the model guessing. Passing the dict's values() wholesale plus name-based dispatch is the least-code combination these examples demonstrate [1].

Adapted from the Tool calling section of the Ollama documentation[1], released under the MIT License.

Sources

  1. 1

Cite this page: Tool calling with Ollama. TellWorld, https://www.tellworld.net/practice/practice-ollama-tool-calling. Updated 2026-11-14

Related news

View all