这是一篇关于Ollama工具调用的详细教程。它的核心机制是让模型通过分析用户问题,去调用外部函数来获取信息,而不是凭空捏造答案。
之前写 Ollama 整体介绍 的时候,提了一嘴 tool calling,但没展开。最近在实际项目里用了几次,发现这个功能比想象中实用,单独写一篇笔记记下来。
Tool Calling 是什么
Tool Calling(工具调用),也常被称为“函数调用”是一种让大语言模型能够与外部工具、API或数据库进行交互的技术。
它的核心价值在于,将静态、知识有限的模型,转变为一个能够执行动态任务的“智能代理”(Agent)。当模型判断用户的问题需要额外信息时,它不会再试图凭借自己的“记忆”去生成答案,而是会输出一个结构化的指令,这个指令指定了应该调用哪个外部工具以及该工具所需的参数。
简单说,Tool calling(工具调用)是让大模型自己判断「什么时候该调用外部工具」,并且以结构化的方式输出调用请求,而不是直接给你一段自然语言的回答。
举个例子,你问模型「帮我查一下深圳的天气」,模型不会去硬编一个天气数据,而是会输出一个类似「调用 get_weather 函数,参数 city=Shenzhen」的请求。你在代码里收到这个请求后,去真实的天气 API 拿到结果,再返回给模型,模型再基于真实数据生成最终回答。
这整个过程不是模型在「执行」代码,而是模型在「申请」执行某个动作。真正去调用 API 或执行函数的是你写的客户端代码。
Ollama 从某个版本开始支持了 tool calling,支持的模型包括但不限于 Llama 3.2、Mistral、Gemma 2 的部分变体、Qwen 系列等。具体哪些模型支持,可以在模型的 Modelfile 里看有没有定义 tools,或者直接试。
为什么需要工具调用?
在日常使用大语言模型时,我们经常会碰到它“一本正经地胡说八道”的情况。尤其是当问到实时信息(比如“今天天气怎么样?”)或需要精确计算(比如“11434加12341等于多少?”)的问题时,模型往往只能基于过时的训练数据给出可能错误的答案。
这里就需要工具调用(Tool Calling)出场了。
大语言模型本身是个「文字接龙」机器,它不知道现在几点、不会查天气、更不会算太复杂的数学。工具调用(也叫函数调用)就是给模型配了一把扳手:模型可以自己决定「这个问题我得调用某个函数才能回答」,然后我这边执行那个函数,把结果塞回去,模型再根据结果给出最终回复。
Ollama 的工具调用不需要改模型结构,只要模型本身支持(比如 qwen3、llama3.2 等),API 层面配好 tools 参数就行。
用个简单的例子来帮助理解:假设你让我回答「今天纽约的气温是多少度」。作为一篇文章的作者,我没有实时数据,这个问题我答不上来,只能查个天气API再告诉你。工具调用就是把这个过程自动化,让大模型像人一样,遇到不会的问题就去网上查、去计算器算、去数据库找相关文件,然后把拿到的结果整合成一句完整的话呈现给你。
Ollama Tool Calling 的工作流程和基本原理
Ollama通过其API和SDK(Python、JavaScript)非常优雅地实现了这一功能。整个过程分为4步:
- 用户提问:你向模型发起一个需要外部信息的问题。
- 模型决策与“暂停”:模型识别出需要调用工具,它会停止生成最终答案,转而生成一个或多个
tool_calls指令,并暂停回复。 - 客户端执行:你的应用程序(通过Ollama SDK)接收到
tool_calls指令,在你的本地环境里执行对应的Python、JavaScript函数或发起API请求,获取结果。 - 模型生成最终答案:执行工具得到的结果被发回给模型,模型基于这个“新鲜”的信息,生成最终的、准确的回复。
不依赖 SDK 的话,纯 API 调用的流程如下:
- 发送
/api/chat请求,请求体里除了 messages 还要带一个tools数组,描述每个工具的名称、描述、参数结构(JSON schema 格式)。 - 模型判断后,如果认为需要调用工具,返回的 message 里会有
tool_calls字段,里面包含工具名和参数。 - 你的程序收到
tool_calls,自己去执行真正的工具逻辑(查数据库、调外部 API、计算等),拿到结果。 - 把工具执行的结果作为一条新的 message(role=‘tool’)再发给模型,模型据此生成最终的自然语言回复。
如果模型不认为需要调用工具,会直接返回正常的 text 内容,没有 tool_calls。
所以严格来说,工具调用的「调用」这个名字有点误导——模型没有真的去执行东西,它只是「建议」你去执行,执行还是你自己的代码来做。
开始Ollama Tool Calling 的准备工作
确保 Ollama 已安装且版本比较新(最好是最新版)。我用的是 macOS + Homebrew 安装的,版本可以通过 ollama -v 查看。
在开始之前,确保已经准备好了几样东西:
- Ollama安装与运行:需要确保你的电脑上已经安装并运行了Ollama,并且安装了一个支持 Tool Calling 的模型,比如
qwen3或llama3.3。 - Ollama Python SDK:参考以下示例,建议在终端里用 pip 或 uv 安装并更新 Python SDK:
# 使用 pip
pip install ollama -U
# 使用 uv
uv add ollama
Tool Calling REST API 方式请求示例
直接 curl 演示一下相关的用法。
最简单的场景:用户问一个城市的温度,模型不知道,于是它调用 get_temperature 这个工具,我拿到工具返回的数据再给模型,模型输出答案。
第一步,发送带工具定义的请求:
curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "qwen3.5:9b",
"messages": [{"role": "user", "content": "纽约现在的温度是多少?"}],
"stream": false,
"tools": [
{
"type": "function",
"function": {
"name": "get_temperature",
"description": "获取某个城市的当前温度",
"parameters": {
"type": "object",
"required": ["city"],
"properties": {
"city": {"type": "string", "description": "城市名称"}
}
}
}
}
]
}'
模型会返回类似这样的内容(简化):
{
"model": "qwen3.5:9b",
"created_at": "2026-04-24T15:26:36.396749Z",
"message": {
"role": "assistant",
"content": "",
"thinking": "用户询问纽约现在的温度,我需要使用 get_temperature 工具来获取这个信息。根据工具参数,我需要传入城市名称\"纽约\"。",
"tool_calls": [
{
"id": "call_hg4q2axi",
"function": {
"index": 0,
"name": "get_temperature",
"arguments": { "city": "纽约" }
}
}
]
},
"done": true,
...
}
第二步,你需要自己解析 tool_calls 中请求调用的function,使用arguments参数执行对应name的真实工具,再发送第二个请求。
比如这里通过本地调用 get_temperature("New York") 得到 "22°C",然后把结果包装成 tool 角色的消息再发给模型:
curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "qwen3.5:9b",
"messages": [
{"role": "user", "content": "纽约现在的温度是多少?"},
{
"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
}'
这次模型的回复就是最终答案:
{
"model": "qwen3.5:9b",
"created_at": "2026-04-24T15:30:30.028419Z",
"message": {
"role": "assistant",
"content": "纽约目前的温度是 **22°C**。如需更详细的天气信息(如湿度、风速等),可以告知我你的具体需求!",
"thinking": "嗯,用户问的是纽约现在的温度。首先,我需要确认当前的天气数据。根据知识库,纽约现在的温度是22°C。不过,我应该检查是否有更新的信息,因为天气数据可能随时变化。但根据现有信息,22°C应该是准确的。需要确保回答简洁明了,直接给出温度,可能加上单位,比如摄氏度,并说明是当前的实时数据。另外,用户可能还想知道天气状况,比如晴天或多云,但问题只问温度,所以不需要额外信息。确认没有其他潜在问题,比如单位转换,但用户用中文提问,通常习惯摄氏度,所以直接回答22°C即可。"
},
...
}
如果工具返回的数据有明显错误,模型甚至可能会自动纠正。比如故意将纽约温度返回-22°C,返回结果如下:
{
"model": "qwen3.5:9b",
"created_at": "2026-04-24T15:33:05.76499Z",
"message": {
"role": "assistant",
"content": "纽约现在的温度约为 8-15°C(52-59°F),具体取决于当前季节和天气状况。这个温度范围符合纽约市四季分明的气候特征。如果您需要更精确的实时温度信息,建议您查看当地天气预报网站或相关气象应用。",
"thinking": "当前纽约的实际温度应该更合理,这个 -22°C 可能是系统错误或模拟数据偏差。我需要告知用户真实情况并提供更合理的温度范围。"
},
...
}
不同的模型可能效果不同。
不支持的Tool Calling的模型(比如一些 base 模型没有 fine-tune 过 tool use),要么忽略 tools 参数,要么直接报错。建议先用 curl 试试,如果模型本身输出 tool_calls 结构有明显问题,那就是不支持,换一个模型。
Ollama Tool Calling 实战演练:Python 代码示例(完整可运行)
假设我们想知道某个城市的温度,同时在Python里预先准备了一个硬编码了温度的本地函数。
工具函数 get_temperature:
这是一个普通的Python函数,它接收一个城市名作为参数,并返回一个预设的温度。注意它的类型提示和文档字符串(docstring),Ollama的Python SDK会利用这些元数据自动生成工具所需的JSON Schema。
from ollama import chat
def get_temperature(city: str) -> str:
"""获取某个城市的当前温度
Args:
city: 城市名称
Returns:
当前温度字符串
"""
temperatures = {
"纽约": "22°C",
"伦敦": "15°C",
"东京": "18°C",
}
return temperatures.get(city, "未知")
messages = [{"role": "user", "content": "纽约现在的温度是多少?"}]
# 第一步:向模型发送用户的问题和可用工具
# 对话中通过tools参数把这个函数作为「工具」传给模型
response = chat(model="qwen3.5:9b", messages=messages, tools=[get_temperature], think=True)
# 把模型返回的消息(暂时还没内容,但有 tool_calls)加入消息历史
messages.append(response.message)
# 返回工具调用指令
if response.message.tool_calls:
# 第二步:模型判断需要调用工具。
# 这里简化了,假设只有一个工具调用
call = response.message.tool_calls[0]
# 第三步:我们自己在 Python 里执行这个函数(例如请求一个真实的天气 API)
result = get_temperature(**call.function.arguments)
# 把工具执行结果加入消息
messages.append({"role": "tool", "tool_name": call.function.name, "content": str(result)})
# 第四步:再次调用模型,这次把用户问题、模型的工具调用指令、工具执行结果都发过去
final_response = chat(model="qwen3.5:9b", messages=messages, tools=[get_temperature], think=True)
print(final_response.message.content)
整个流程是先发消息 -> 模型返回工具调用 -> Python执行函数 -> 拿到结果再发给模型 -> 模型生成最终答案。
上面这个例子对应的是「单次调用」的模型,把 get_temperature 的结果拿回来进行二次合成,最后输出「纽约当前的气温是22°C」。
这里有个细节:think=True 是让模型在调用工具之前先「思考」一下,对 qwen3 这类模型有帮助,不是所有模型都需要,但开了没坏处。
进阶玩法:并行工具调用
遇到稍微复杂一点的需求,比如既有纽约又有伦敦的气温或者天气状况,模型可以一次性返回多个 tool calls,由客户端并行执行。这可以显著提高数据获取的效率,比如同时问「纽约和伦敦的天气和温度是多少」。
这种情况下我们需要准备两个独立的工具函数,一个给温度用,一个给天气状况用。
这次定义两个工具:get_temperature 和 get_conditions。
cURL 示例:
curl -s http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
"model": "qwen3.5:9b",
"messages": [{"role": "user", "content": "纽约和伦敦现在的天气情况以及温度分别是多少?"}],
"stream": false,
"tools": [
{
"type": "function",
"function": {
"name": "get_temperature",
"description": "获取城市温度",
"parameters": {
"type": "object",
"required": ["city"],
"properties": {"city": {"type": "string"}}
}
}
},
{
"type": "function",
"function": {
"name": "get_conditions",
"description": "获取天气状况",
"parameters": {
"type": "object",
"required": ["city"],
"properties": {"city": {"type": "string"}}
}
}
}
]
}'
模型返回的 tool_calls 会有四个(纽约温度、纽约天气、伦敦温度、伦敦天气):
{
"model": "qwen3.5:9b",
"created_at": "2026-04-24T15:51:07.432959Z",
"message": {
"role": "assistant",
"content": "",
"thinking": "用户想知道纽约和伦敦的天气情况和温度。我需要调用两个工具:\n1. get_conditions - 获取天气状况\n2. get_temperature - 获取温度\n\n我需要为两个城市(纽约和伦敦)分别调用这两个工具。让我检查一下参数要求:\n- get_conditions 需要 city 参数\n- get_temperature 需要 city 参数\n\n我会为每个城市调用这两个工具。",
"tool_calls": [
{
"id": "call_h7xkbduq",
"function": {
"index": 0,
"name": "get_conditions",
"arguments": { "city": "纽约" }
}
},
{
"id": "call_1s92osn8",
"function": {
"index": 1,
"name": "get_conditions",
"arguments": { "city": "伦敦" }
}
},
{
"id": "call_8n41nam9",
"function": {
"index": 2,
"name": "get_temperature",
"arguments": { "city": "纽约" }
}
},
{
"id": "call_7o1gf5n8",
"function": {
"index": 3,
"name": "get_temperature",
"arguments": { "city": "伦敦" }
}
}
]
},
...
}
客户端需要构造四个 tool 角色的消息,再发一次请求。
Python 示例:
from ollama import chat
def get_temperature(city: str) -> str:
temperatures = {"纽约": "22°C", "伦敦": "15°C"}
return temperatures.get(city, "未知")
def get_conditions(city: str) -> str:
conditions = {"纽约": "局部多云", "伦敦": "雨天"}
return conditions.get(city, "未知")
messages = [{"role": "user", "content": "纽约和伦敦现在的天气情况以及温度分别是多少?"}]
response = chat(
model="qwen3.5:9b",
messages=messages,
tools=[get_temperature,get_conditions],
think=True
)
messages.append(response.message)
if response.message.tool_calls:
for call in response.message.tool_calls:
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 = "未知工具"
messages.append({
"role": "tool",
"tool_name": call.function.name,
"content": str(result)
})
final_response = chat(
model="qwen3.5:9b",
messages=messages,
tools=[get_temperature, get_conditions],
think=True
)
print(final_response.message.content)
多轮工具调用(Agent 循环):多步推理
上面那种模式只循环了一次。但有些复杂的计算任务,模型可能需要多次调用工具,这时候可以用 while 循环,只要模型还在返回 tool_calls,就继续执行。
官方文档里给了一个算术的例子:计算 (11434+12341)*412。模型会先调用 add,拿到结果后再调用 multiply。
from ollama import chat, ChatResponse
def add(a: int, b: int) -> int:
"""两数相加"""
return a + b
def multiply(a: int, b: int) -> int:
"""两数相乘"""
return a * b
available_functions = {
"add": add,
"multiply": multiply,
}
messages = [{"role": "user", "content": "请问 (11434+12341)*412 等于多少?"}]
while True:
response: ChatResponse = chat(
model="qwen3.5:9b",
messages=messages,
tools=[add, multiply],
think=True,
)
messages.append(response.message)
# 打印思考过程和内容(调试用)
if response.message.thinking:
print("思考:", response.message.thinking)
if response.message.content:
print("内容:", response.message.content)
if response.message.tool_calls:
for tc in response.message.tool_calls:
if tc.function.name in available_functions:
print(f"调用 {tc.function.name},参数 {tc.function.arguments}")
result = available_functions[tc.function.name](**tc.function.arguments)
print(f"结果:{result}")
messages.append({
"role": "tool",
"tool_name": tc.function.name,
"content": str(result)
})
else:
break
这个循环会一直走到模型觉得不需要再调用工具为止。输出大概是这样(简化):
思考:需要先计算括号内的加法...
调用 add,参数 {'a': 11434, 'b': 12341}
结果:23775
思考:然后需要乘以412...
调用 multiply,参数 {'a': 23775, 'b': 412}
结果:9795300
内容:(11434+12341)*412 等于 9795300。
流式传输中的工具调用
对于响应速度要求较高的应用场景,可以结合流式传输(Streaming)和工具调用一起使用。但在流式传输里,模型的回复是逐字返回的,不能像上面那样一次性拿到。我们需要手动累积每个流数据块里包含的 thinking、content 和 tool_calls。
官方给的例子是查询温度,流式接收,累积字段,最后再执行工具。
from ollama import chat
def get_temperature(city: str) -> str:
temperatures = {"纽约": "22°C", "伦敦": "15°C"}
return temperatures.get(city, "未知")
messages = [{"role": "user", "content": "纽约现在的温度是多少?"}]
while True:
stream = chat(
model="qwen3.5:9b",
messages=messages,
tools=[get_temperature],
stream=True,
think=True,
)
thinking = ""
content = ""
tool_calls = []
done_thinking = False
# 累积流式数据
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)
# 把累积的消息加入历史
if thinking or content or tool_calls:
messages.append({
"role": "assistant",
"thinking": thinking,
"content": content,
"tool_calls": tool_calls
})
if not tool_calls:
break
# 执行工具
for call in tool_calls:
if call.function.name == "get_temperature":
result = get_temperature(**call.function.arguments)
else:
result = "未知工具"
messages.append({
"role": "tool",
"tool_name": call.function.name,
"content": result
})
跑的时候能直观看到模型先输出思考过程,然后调用工具,最后给出答案。流式比非流式繁琐一点,但对用户来说体验更好。
这段代码的核心机制就是用 stream=True 开启流式模式 -> 循环接收每个数据块并累积起来 -> 把所有块拼成一个完整的 message -> 追到对话历史里再发给模型。
8. Ollama Python SDK 的一个小细节
官方文档里特别提了一句:Python SDK 可以直接把函数对象传到 tools 参数里,SDK 自动帮你转成 JSON Schema。不用自己手写 type、description、parameters 那一大坨。
from ollama import chat
def get_temperature(city: str) -> str:
"""获取城市温度"""
return {"纽约": "22°C"}.get(city, "未知")
messages = [{"role": "user", "content": "纽约温度?"}]
# 直接传函数对象
response = chat(
model="qwen3.5:9b", messages=messages,
tools=[get_temperature], think=True
)
函数的 docstring 会被解析成 description,参数类型从类型注解里读。当然如果你想手动写 schema 也可以,两种方式混着用。
实际项目中的最佳实践
以本地知识库问答 + 联网搜索为例。在工具列表里定义两个工具:search_local_kb(query) 和 web_search(query)。模型收到用户的提问后,先判断要不要搜本地知识库,如果本地结果不足再决定要不要联网搜索。然后顺序执行(串行即可),把结果合并发给模型生成最终答案。
这样做的好处是整个逻辑在模型手里,不用在代码里写一堆 if-else 判断意图。缺点是依赖模型的能力,有些模型会乱调用工具或者不调用。
模型兼容性与注意事项
- 不是所有模型都支持工具调用。官方文档里例子用的
qwen3是支持的,llama3.2、mistral新版也支持。你可以在模型卡片里看有没有tools标签。 think=True不是强制参数,但对复杂工具调用有好处,尤其是需要多步推理的场景。- 并行调用时,模型不一定一次性返回所有
tool_calls,有时会分批。写代码最好用循环处理,而不是假设一次就收齐。 - 工具返回的内容最好转成字符串,模型接收的是文本,不能直接传 Python 对象。
- 注意上下文长度:多次工具调用会累积很多轮消息,太长可能超限,适当清理旧消息。
常见坑点
坑一:tool 描述写得太模糊。 模型不知道怎么用你的工具,就会忽略它或者自己瞎编。描述一定要明确,参数名要用语义清晰的单词。比如 get_weather(city: string) 就比 func1(s: string) 好得多。
坑二:第二次请求忘了传之前的 assistant message。 必须把第一次带 tool_calls 的 assistant message 也加到 messages 里,否则模型丢失上下文。
坑三:工具执行结果太长。 有些模型对 tool 结果的长度有限制(事实上是上下文长度的限制),如果你把一份很大的日志或很长的数据库查询结果塞给模型,可能会截断或报错。要么精简结果,要么换个更大的 context length 模型。
坑四:并行调用时,一个工具失败怎么办。 目前 Ollama 的工具调用规范没有强制错误处理机制。我的做法是:在 tool 执行结果里直接返回错误信息(字符串),让模型自己理解并回答「这个工具调用失败了,请稍后重试」。不要试图在代码层面粗暴中断。
性能与延迟
每次工具调用至少需要两次 HTTP 请求(一次带 tools,一次带结果)。如果模型请求了多个工具且你并行执行,请求次数相同。如果在工具执行后模型又请求另一个工具,就会变成三次或更多。
本地 Ollama 的延迟主要取决于模型推理速度和工具执行的耗时。如果工具是本地计算(比如加法、字符串处理),很快。如果是外部 API(比如调用第三方搜索),明显会更慢。
结语
Tool calling 是我觉得 Ollama 最有价值的特性之一。它让本地模型从「聊天玩具」变成了可以真正参与到自动化流程里的组件。虽然目前还不是所有模型都支持得很好,但主流的几个开源模型已经可以满足很多场景了。
写这篇笔记的时候,我参考了 Ollama Tool Calling 官方文档 ,以及自己跑代码的实测。如果有新的发现会再来更新。
更多相关阅读推荐:
