中文 ▾

使用 OpenAI SDK 的 Ollama API 函数调用示例

函数调用允许模型请求你的代码运行函数,然后在回答中使用结果。下面是一个针对托管接口的完整 OpenAI SDK 示例,包含流式输出以及导致大多数失败的常见错误。

已更新

模型将任务交给函数并在循环中接收结果的示意图

要点

  1. 将函数以 JSON 架构的形式传入 tools;模型会返回 tool_calls 而非文本。
  2. 你运行该函数,将结果作为 role 为 tool 的消息追加,然后再次调用 API。
  3. 始终防御性地解析参数,因为模型可能返回格式错误或意外的 JSON。
  4. 流式输出的工具调用以片段形式到达,你必须按索引将它们拼接起来。

循环的样子

如果你在寻找针对托管接口的 ollama api 工具调用示例,其格式遵循标准的 OpenAI 规范。你在 tools 数组中描述你的函数。模型要么以文本形式回答,要么返回一个 tool_calls 列表,其中包含函数名称及其参数。模型不会执行任何操作。你的代码执行函数,将输出发送回去,然后模型生成最终回复。

这使得一次工具使用涉及两个 API 调用:

  1. 发送用户消息和工具定义。
  2. 接收带有 tool_calls 的 assistant 消息。在本地运行每次调用。
  3. 发送原始消息、assistant 消息,以及每条调用对应的一条 role: "tool" 消息及其结果。
  4. 接收最终文本答案。

示例使用基础 URL https://api.ollamaapi.com/v1 和模型 ID uncensored。该端点是一项托管服务,因此密钥来自 API 密钥页面。只要你更改基础 URL、密钥和模型名称,且本地模型支持工具,相同的代码也可以针对本地 Ollama 服务器运行。

完整的 Python 示例

该示例向模型提供一个函数 get_order_status,由一个小字典支持。实际应用中,此时会查询数据库或内部服务。

import json
from openai import OpenAI

client = OpenAI(base_url="https://api.ollamaapi.com/v1", api_key="YOUR_KEY")

ORDERS = {"A1001": "shipped", "A1002": "processing", "A1003": "delivered"}

def get_order_status(order_id: str) -> str:
    return ORDERS.get(order_id, "not found")

tools = [{
    "type": "function",
    "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
            "type": "object",
            "properties": {
                "order_id": {"type": "string", "description": "Order ID such as A1001"}
            },
            "required": ["order_id"],
        },
    },
}]

messages = [{"role": "user", "content": "Where is order A1002?"}]

first = client.chat.completions.create(
    model="uncensored",
    messages=messages,
    tools=tools,
    tool_choice="auto",
)
msg = first.choices[0].message

if msg.tool_calls:
    messages.append(msg)  # keep the assistant turn with its tool_calls
    for call in msg.tool_calls:
        args = json.loads(call.function.arguments)
        result = get_order_status(**args)
        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": result,
        })
    final = client.chat.completions.create(
        model="uncensored",
        messages=messages,
        tools=tools,
    )
    print(final.choices[0].message.content)
else:
    print(msg.content)

这里有三个细节很重要。包含 tool_calls 的 assistant 消息必须在工具结果之前放回历史记录中,否则下一次请求会被拒绝或混淆。每条 tool 消息必须携带它所回答调用的 tool_call_id。并且第二次请求仍应包含 tools,因为模型可能在看到第一个结果后希望调用另一个函数。

使用 curl 的相同请求

使用 curl 测试可以排除 SDK 的影响,这有助于当你不确定问题是出在你的代码中还是请求中时。

curl https://api.ollamaapi.com/v1/chat/completions \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "uncensored",
    "messages": [{"role": "user", "content": "Where is order A1002?"}],
    "tools": [{
      "type": "function",
      "function": {
        "name": "get_order_status",
        "description": "Look up the status of an order by its ID.",
        "parameters": {
          "type": "object",
          "properties": {"order_id": {"type": "string"}},
          "required": ["order_id"]
        }
      }
    }],
    "tool_choice": "auto"
  }'

成功的初次回复将 finish_reason 设置为 tool_calls,content 为空或 null,并包含一个 tool_calls 数组。arguments 字段是包含 JSON 的字符串,而不是嵌套对象,这就是 Python 示例调用 json.loads 的原因。

tool_choice 字段控制请求的强制程度。auto 让模型自行决定。传递一个包含函数名的对象会请求特定的函数。除非有理由,否则使用 auto,因为在不需要调用的消息上强制调用会产生虚构的参数。

Node 中的相同循环

Node 版本使用官方 openai 包并遵循相同的步骤。此版本使用 Promise.all 处理一次回复中的多次调用。

import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.ollamaapi.com/v1",
  apiKey: process.env.API_KEY,
});

const handlers = {
  get_order_status: async ({ order_id }) =>
    ({ A1001: "shipped", A1002: "processing" })[order_id] ?? "not found",
};

const tools = [{
  type: "function",
  function: {
    name: "get_order_status",
    description: "Look up the status of an order by its ID.",
    parameters: {
      type: "object",
      properties: { order_id: { type: "string" } },
      required: ["order_id"],
    },
  },
}];

const messages = [{ role: "user", content: "Where is order A1001?" }];
const first = await client.chat.completions.create({
  model: "uncensored", messages, tools,
});
const msg = first.choices[0].message;

if (msg.tool_calls?.length) {
  messages.push(msg);
  const results = await Promise.all(msg.tool_calls.map(async (call) => {
    const args = JSON.parse(call.function.arguments);
    const out = await handlers[call.function.name](args);
    return { role: "tool", tool_call_id: call.id, content: String(out) };
  }));
  messages.push(...results);
  const final = await client.chat.completions.create({
    model: "uncensored", messages, tools,
  });
  console.log(final.choices[0].message.content);
} else {
  console.log(msg.content);
}

流式工具调用

使用 stream: true 时,回复通过 SSE 到达,工具调用以片段形式到达。每个 chunk 包含一个带有 index 的 delta.tool_calls 条目。一个索引的第一个片段包含调用 ID 和函数名。后续片段添加 arguments 字符串的切片。按索引拼接切片,并在流结束后解析 JSON。

stream = client.chat.completions.create(
    model="uncensored",
    messages=[{"role": "user", "content": "Where is order A1003?"}],
    tools=tools,
    stream=True,
)

calls = {}
for chunk in stream:
    if not chunk.choices:
        continue
    delta = chunk.choices[0].delta
    for part in (delta.tool_calls or []):
        slot = calls.setdefault(part.index, {"id": "", "name": "", "args": ""})
        if part.id:
            slot["id"] = part.id
        if part.function and part.function.name:
            slot["name"] = part.function.name
        if part.function and part.function.arguments:
            slot["args"] += part.function.arguments

for slot in calls.values():
    print(slot["name"], json.loads(slot["args"]))

请记住,流式请求会一直占用其并发槽位,直到流结束。完整密钥允许同时 8 个请求和每分钟 300 个请求,试用密钥允许 2 个和 60 个。如果你为许多用户并行运行工具循环,请将每个打开的流计入第一个数字。

常见陷阱及修复方法

格式错误或缺失的参数

该模型是一个开放权重模型,开放模型有时会返回不是有效 JSON 的参数,遗漏必填字段,或虚构一个额外字段。将 json.loads 包裹在 try 块中,根据你的 schema 进行验证,失败时将错误作为 tool 消息发送。模型通常在下一轮自行纠正。将循环限制在几次迭代内,以防止顽固的失败无限运行。

描述过于模糊

模型通过阅读函数的名称和描述来选择函数。诸如“获取数据”这样的描述让它无从下手。说明函数做什么、输入的含义,并在参数描述中包含示例值。

同时工具过多

每个工具定义在每次请求中都算作输入 token。每 1M 输入 token 价格为 $0.15,十个每个 150 token 的工具会增加 1,500 个 token,即每次请求 $0.000225,虽然金额小但在大量使用时不为零。更重要的是,长长的菜单会让选择变得不可靠。只提供与当前步骤相关的工具。

工具结果过大

你返回的任何内容都会作为输入反馈。将大型数据库行或网页修剪为模型所需的字段。上下文窗口总共为 100,000 个 token,因此非常大的结果可能会挤占对话空间。

将工具与 JSON 模式混合使用

JSON 模式(response_format 配合 json_object)可用,但请将其用于最终答案,而不是用于希望模型调用函数的轮次。在依赖它之前,请在自己的提示词上测试该组合。

从本地 Ollama 迁移及检查事项

如果你针对本地 Ollama 服务器构建了第一个工具循环,几乎没有什么变化。将基础 URL 替换为 https://api.ollamaapi.com/v1,使用真实密钥,并将模型设置为 uncensored。原生 Ollama 路由如 /api/chat 在此处不存在。只有 POST /v1/chat/completions 和 GET /v1/models 可用,因此为原生 API 编写的代码需要使用 OpenAI 风格的调用。

在发布之前,运行以下简短检查清单:

  • 确认密钥在 GET /v1/models 下有效。
  • 为每次调用记录 X-Request-Id 标头,以便追踪失败的运行。
  • 如果你无人值守地运行工具循环,请监控响应标头中的 X-Balance-USD。
  • 处理 HTTP 429:在 rate_limited 或 concurrency 时等待,并在过载时遵守 Retry-After: 5。
  • 设置循环迭代的硬限制。

工具循环对每个用户问题至少发起两次请求,因此成本会随之增加。例如,2,000 次循环,每次请求包含 1,200 个输入 token 和 150 个输出 token,每次循环两次请求,总计 4,000 次请求,4.8M 输入 token 和 0.6M 输出 token。这相当于 $0.72 加上 $0.30,总计 $1.02。定价页面 提供了充值选项,文档 列出了支持的请求字段。新用户可以使用 $0.50 的试用额度来体验所有这些功能,无需绑定银行卡。

问答

该接口是否支持函数调用?

是的。它接受 tools 和 tool_choice 参数,返回 tool_calls,以增量方式流式传输,并以 role 为 tool 的消息形式接收结果。

为什么工具调用后的第二次请求会失败?

常见原因是缺少 assistant 消息,或者 tool 消息缺少匹配的 tool_call_id。请先追加包含 tool_calls 的 assistant 轮次,然后为每个调用发送一条 tool 消息。

模型能否在一次轮次中调用多个工具?

模型可以返回包含多个条目的列表,因此请遍历所有条目,并为每个条目发送一条结果。不要假设列表只包含一个条目。

为此需要 Ollama 库吗?

不需要。官方 OpenAI SDK 即可。将基础 URL 指向该端点,并使用 uncensored 模型。

只需填写一个表单即可获得密钥

创建账户,复制密钥,更改基础 URL。这就是全部设置。

获取 API 密钥阅读文档