工具调用是本地模型从「能聊天」跨到「能干活」的那一步:模型自己判断何时需要外部工具,发起结构化调用,再把结果消化进回答。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 许可证发布。
来源
- 1Tool calling | Ollama docsOllama docs (MIT)
引用本页: Ollama 工具调用. TellWorld, https://www.tellworld.net/zh/practice/practice-ollama-tool-calling. 更新于 2026-11-14