Ollama 工具调用(Tool calling)

2026-10-29 18:29 更新

模型的训练数据有截止日期,也不知道你的数据库里存了什么、现在几点、库存还剩多少。工具调用(Tool calling,也叫 function calling)就是补上这个缺口的机制:你告诉模型"有这么几个函数可用,参数长这样",模型在需要时输出"请调用 get_temperature,参数是 city=New York",你的代码去真正执行,再把结果回传给它继续回答。

关键点在于执行权始终在你手上。模型只负责"决定调什么",不负责"真的去调"。这也是它与 3.4 Ollama 决策(Decision) 的根本区别。

版本基线 v0.40.12,官方 docs.ollama.com/capabilities/tool-calling。支持工具调用的模型见 ollama.com/search?c=tools;下文示例统一用 qwen3。

一、工具的定义结构

tools 是一个数组,每一项是一个 JSON Schema 风格的函数声明:

{
  "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" }
      }
    }
  }
}

四个字段各有分工,写不好直接影响命中率:

  • name:函数名,回传结果时要靠它匹配,必须和真实函数名一致,建议蛇形命名。
  • description:最重要的一行。模型靠这句话判断"什么时候该用这个工具"。写"获取城市当前温度"比写"温度工具"有效得多。
  • parameters:标准 JSON Schema,type 用 object,字段类型用 string、integer 等。
  • required:必填参数名数组。模型漏参数时,这个列表会约束它补齐。

二、单次调用:三步走完

最简单的情况是模型只调一个工具。三次请求构成一个完整回合:

第 ① 步:带工具提问。

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"}
          }
        }
      }
    }
  ]
}'

返回的 message 里没有答案,只有 tool_calls——模型在说"我要调这个"。

第 ② 步:本地执行函数,拿到结果。

第 ③ 步:把结果塞回对话,再问一次。

## 关键:messages 里要带上 assistant 的 tool_calls,再加一条 role=tool 的结果
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
}'

第三次请求不用再传 tools 也能出结果,但保留它更安全——模型可能想再调一次。

回传格式有三个易错点:tool_calls 里的 function 要带 index(并行调用时从 0 递增);工具结果消息的 role 是 "tool",不是 "user";字段名是 tool_name 而不是 name,写错会被当成未知消息。

三、Python SDK:直接传函数对象

Python SDK 有个很省事的设计:把函数对象直接放进 tools 列表,它会读 docstring 自动生成 schema。所以写好文档字符串就等于写好了工具描述:

## pip install ollama -U
from ollama import chat

def get_temperature(city: str) -> str:
    """获取某个城市的当前温度

    Args:
        city: 城市名称

    Returns:
        该城市的当前温度
    """
    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?"}]

## 直接把函数传进 tools,SDK 自动解析成 schema
response = chat(model="qwen3", messages=messages, tools=[get_temperature], think=True)

messages.append(response.message)
if response.message.tool_calls:
    # 单工具场景:只取第一个
    call = response.message.tool_calls[0]
    result = get_temperature(**call.function.arguments)
    # 结果以 role=tool 回传
    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)

call.function.arguments 已经是解析好的 dict,用 ** 直接展开即可,不用自己 json.loads。

JavaScript 没有这层自动解析,得手写 schema:

// npm i ollama
import ollama from 'ollama'

function getTemperature(city: string): string {
  const temperatures: Record<string, string> = {
    'New York': '22°C',
    'London': '15°C',
    'Tokyo': '18°C',
  }
  return temperatures[city] ?? 'Unknown'
}

const 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' },
        },
      },
    },
  },
]

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

const response = await ollama.chat({ model: 'qwen3', messages, tools, think: true })

messages.push(response.message)
if (response.message.tool_calls?.length) {
  const call = response.message.tool_calls[0]
  const args = call.function.arguments as { city: string }
  const result = getTemperature(args.city)
  messages.push({ role: 'tool', tool_name: call.function.name, content: result })

  const finalResponse = await ollama.chat({ model: 'qwen3', messages, tools, think: true })
  console.log(finalResponse.message.content)
}

四、并行调用:一次返回多个

问一个跨多个对象的问题,模型可能一次要求调用多个工具。比如"纽约和伦敦现在的天气和温度怎么样",会得到 4 个调用(两个城市 × 温度 + 天气状况),index 从 0 到 3。

处理方式和单次一样,只是要遍历全部调用,逐个执行、逐个回传:

from ollama import chat

def get_temperature(city: str) -> str:
    """获取某个城市的当前温度"""
    return {"New York": "22°C", "London": "15°C", "Tokyo": "18°C"}.get(city, "Unknown")

def get_conditions(city: str) -> str:
    """获取某个城市的当前天气状况"""
    return {"New York": "Partly cloudy", "London": "Rainy", "Tokyo": "Sunny"}.get(city, "Unknown")

messages = [{'role': 'user', 'content': '纽约和伦敦现在的天气和温度怎么样?'}]

response = chat(model='qwen3', 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 = 'Unknown tool'    # 模型臆造了不存在的工具时的兜底
        messages.append({'role': 'tool', 'tool_name': call.function.name, 'content': str(result)})

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

那个 else: result = 'Unknown tool' 不是形式主义。模型偶尔会调用你根本没声明的函数名,不兜底会直接抛 KeyError 崩掉。

五、Agent 循环:让它自己决定调几次

真实任务往往不是"调一次就够"。比如算 (11434+12341)*412,模型得先调 add,拿到和之后再调 multiply。这就要写循环:只要还有 tool_calls 就继续,直到模型给出最终答案。

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': 'What is (11434+12341)*412?'}]

while True:
    response: ChatResponse = chat(
        model='qwen3',
        messages=messages,
        tools=[add, multiply],
        think=True,
    )
    messages.append(response.message)

    if response.message.tool_calls:
        for tc in response.message.tool_calls:
            if tc.function.name in available_functions:
                result = available_functions[tc.function.name](**tc.function.arguments)
                messages.append({'role': 'tool', 'tool_name': tc.function.name, 'content': str(result)})
    else:
        # 没有工具调用了,说明模型给出了最终答案,跳出循环
        break

两个必须加的防护:设最大轮次上限(比如 10 轮),否则模型可能陷入反复调用同一个工具的循环;用字典分发而不是 if-else 长链,工具一多就看出好处了。

JavaScript 版本结构一致:

import ollama from 'ollama'

function add(a: number, b: number): number { return a + b }
function multiply(a: number, b: number): number { return a * b }

const availableFunctions: Record<string, (a: number, b: number) => number> = { add, multiply }

const tools = [
  {
    type: 'function',
    function: {
      name: 'add',
      description: 'Add two numbers',
      parameters: {
        type: 'object',
        required: ['a', 'b'],
        properties: {
          a: { type: 'integer', description: 'The first number' },
          b: { type: 'integer', description: 'The second number' },
        },
      },
    },
  },
  {
    type: 'function',
    function: {
      name: 'multiply',
      description: 'Multiply two numbers',
      parameters: {
        type: 'object',
        required: ['a', 'b'],
        properties: {
          a: { type: 'integer', description: 'The first number' },
          b: { type: 'integer', description: 'The second number' },
        },
      },
    },
  },
]

async function agentLoop() {
  const messages = [{ role: 'user', content: 'What is (11434+12341)*412?' }]

  while (true) {
    const response = await ollama.chat({ model: 'qwen3', messages, tools, think: true })
    messages.push(response.message)

    const toolCalls = response.message.tool_calls ?? []
    if (!toolCalls.length) break

    for (const call of toolCalls) {
      const fn = availableFunctions[call.function.name]
      if (!fn) continue
      const args = call.function.arguments as { a: number; b: number }
      messages.push({ role: 'tool', tool_name: call.function.name, content: String(fn(args.a, args.b)) })
    }
  }

  console.log(messages[messages.length - 1])
}

agentLoop().catch(console.error)

六、流式 + 工具调用:必须把三者拼起来

这是最容易写错的一节。开 stream: true 后,thinking、content、tool_calls 都会以片段形式到来,你要分别累积,最后拼成一条完整的 assistant 消息回传:

from ollama import chat

def get_temperature(city: str) -> str:
    """获取某个城市的当前温度"""
    return {'New York': '22°C', 'London': '15°C'}.get(city, 'Unknown')

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

    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:
            # 工具调用也是分片来的,直接 extend,不能取值
            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 = 'Unknown tool'
        messages.append({'role': 'tool', 'tool_name': call.function.name, 'content': result})

对比非流式的写法,差别就在那条 assistant 消息:非流式可以直接 messages.append(response.message),流式必须手工拼出 thinking / content / tool_calls 三个字段。这也是 3.1 Ollama 流式输出 里反复强调"必须累积"的真正原因。

七、五条实战经验

description 决定命中率。 工具选错通常不是模型笨,而是描述太模糊。"查询订单"和"根据订单号查询订单的物流状态,返回当前所在地与预计送达时间",后者几乎不会选错。

参数尽量窄。 与其让模型传一整段自由文本,不如拆成两三个枚举型参数。自由的字符串参数越多,模型编造格式的概率越高。

工具数量控制在 10 个以内。 工具越多,schema 占的上下文越大,选错的概率也越高。真需要大量工具,先做一层意图分类(用 Decision 更划算),再决定加载哪一组。

工具结果要精简。 数据库返回的 200 行 JSON 直接回传,会把上下文挤爆。只回传模型回答所需的字段,或者先做摘要。

永远兜底未知工具名。 见第四节的 else 分支。模型偶尔会调用不存在的函数,不处理就是线上崩溃。

想把外部实时信息接进来但不想自己维护爬虫,下一篇 3.9 Ollama 网络搜索(Web search) 提供了现成的搜索与网页抓取接口,可直接作为工具挂上去。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号