本地大模型进阶|Ollama Tool Calling完全教程:工具调用原理、实操与Agent循环实战

文章目录

这是一篇关于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步:

  1. 用户提问:你向模型发起一个需要外部信息的问题。
  2. 模型决策与“暂停”:模型识别出需要调用工具,它会停止生成最终答案,转而生成一个或多个 tool_calls 指令,并暂停回复。
  3. 客户端执行:你的应用程序(通过Ollama SDK)接收到 tool_calls 指令,在你的本地环境里执行对应的Python、JavaScript函数或发起API请求,获取结果。
  4. 模型生成最终答案:执行工具得到的结果被发回给模型,模型基于这个“新鲜”的信息,生成最终的、准确的回复。

不依赖 SDK 的话,纯 API 调用的流程如下:

  1. 发送 /api/chat 请求,请求体里除了 messages 还要带一个 tools 数组,描述每个工具的名称、描述、参数结构(JSON schema 格式)
  2. 模型判断后,如果认为需要调用工具,返回的 message 里会有 tool_calls 字段,里面包含工具名和参数。
  3. 你的程序收到 tool_calls,自己去执行真正的工具逻辑(查数据库、调外部 API、计算等),拿到结果。
  4. 把工具执行的结果作为一条新的 message(role=‘tool’)再发给模型,模型据此生成最终的自然语言回复。

如果模型不认为需要调用工具,会直接返回正常的 text 内容,没有 tool_calls

所以严格来说,工具调用的「调用」这个名字有点误导——模型没有真的去执行东西,它只是「建议」你去执行,执行还是你自己的代码来做。

开始Ollama Tool Calling 的准备工作

确保 Ollama 已安装且版本比较新(最好是最新版)。我用的是 macOS + Homebrew 安装的,版本可以通过 ollama -v 查看。

在开始之前,确保已经准备好了几样东西:

  • Ollama安装与运行:需要确保你的电脑上已经安装并运行了Ollama,并且安装了一个支持 Tool Calling 的模型,比如 qwen3llama3.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_temperatureget_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)工具调用一起使用。但在流式传输里,模型的回复是逐字返回的,不能像上面那样一次性拿到。我们需要手动累积每个流数据块里包含的 thinkingcontenttool_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。不用自己手写 typedescriptionparameters 那一大坨。

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.2mistral 新版也支持。你可以在模型卡片里看有没有 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 官方文档 ,以及自己跑代码的实测。如果有新的发现会再来更新。

更多相关阅读推荐:


也可以看看