TellWorldAI 世界档案
问档案English

首页›实践›实践

实践

Ollama 工具调用

Ollama 工具调用全形态:单发调用与工具结果回传、并行多工具分发、Agent 循环的终止条件,以及流式场景下分段字段的累积方法。

工具调用是本地模型从「能聊天」跨到「能干活」的那一步:模型自己判断何时需要外部工具,发起结构化调用,再把结果消化进回答。Ollama 对这套机制的支持分三种形态——单发、并行、循环——加上流式变体,这篇把每种形态的协议细节和客户端写法走一遍 [1]。

单发调用

最基本的模式是单发(single-shot):一次请求返回一个工具调用,客户端执行后把结果放进后续请求 [1]。原生 API 的工具描述就是 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"}
          }
        }
      }
    }
  ]
}'

模型回应的 assistant 消息里带 tool_calls 数组,客户端执行后把结果以 tool 角色的消息回传,注意要带 tool_name 和原始调用信息 [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
}'

Python SDK 的写法更轻:普通函数加 docstring 就能直接当工具传,SDK 自动解析成 Schema [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)

回传结果并生成最终回答 [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)

直接取 tool_calls[0] 只适合保证单调用的模型;可能返回多个调用的响应要遍历处理 [1]。

并行调用

用户问两个城市的天气和温度时,模型会一次吐出四个调用——每个城市各来一组温度和天气——这就是并行调用 [1]。Python 客户端按函数名分发执行,全部结果依次回传后才生成最终回答 [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)

并行调用的 assistant 消息里 tool_calls 按 index 0 到 3 排列,客户端回传结果的顺序要与之一致。并行形态省掉了多轮往返,但要求客户端有执行任意多个调用的能力——分发逻辑用函数名映射表写最稳 [1]。

Agent 循环

循环形态把控制权彻底交给模型:模型自己决定调几次工具、何时收尾,直到某轮响应不再包含 tool_calls 为止 [1]。文档还提了个实用技巧:在提示里告诉模型它处在循环中、可以多次调用,会有帮助 [1]。示例用加法和乘法两个函数计算 (11434+12341)*412 [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

循环主体是标准的 while 结构,终止条件就是「没有工具调用」[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

生产化这个循环还要补两样东西:最大轮数上限防止模型陷入死循环,以及异常处理让单个工具失败不至于拖垮整轮对话。每轮打印 thinking 字段能直观看到模型的规划过程 [1]。

流式工具调用

流式让等待体验好很多,但工具调用和流式叠加后有个新问题:thinking、content、tool_calls 三类字段都按增量到达,客户端必须把分段攒齐,再把攒齐的完整字段连同工具结果一起放进后续请求 [1]。请求构造与累积循环 [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

关键细节有三处:assistant 消息必须把三个字段作为整体回传,缺了 thinking 或 content 模型的上下文就断了;done_thinking 标志用来在思考转正文时打一个换行;没有工具调用时跳出循环 [1]。这个循环同时实现了「边流式边显示」和「攒齐再回传」,两个需求互不干扰。

函数直传

最后是 Python SDK 的省事特性:函数直接传进 tools 列表,SDK 从 docstring 解析出参数说明生成 Schema;需要精细控制时仍然可以显式传 JSON Schema,两种方式可以混用 [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 的质量直接决定调用准确率——Args 里的每个参数说明都会进 Schema,写得含糊模型就靠猜。把函数字典的 values() 整体传入再配合名字分发,是这页示例给出的最省代码的组合 [1]。

本文改编自 Ollama 官方文档的 Tool calling 一节[1],原文以 MIT 许可证发布。

来源

  1. 1

引用本页: Ollama 工具调用. TellWorld, https://www.tellworld.net/zh/practice/practice-ollama-tool-calling. 更新于 2026-11-14

相关动态