繁中 ▾

使用 OpenAI SDK 的 Ollama API 工具呼叫範例

工具呼叫讓模型要求你的程式碼執行函式,然後在回答中使用結果。下方提供使用 OpenAI SDK 針對託管端點的完整範例,以及串流輸出和導致大多數失敗的常見錯誤。

更新

模型將任務交給函式並在迴圈中接收結果的示意圖

重點摘要

  1. 將你的函式以 JSON 格式放在 tools 中;模型會回傳 tool_calls 而非文字。
  2. 你執行函式,將結果以 role tool 訊息附加,然後再次呼叫 API。
  3. 務必謹慎地解析參數,因為模型可能會回傳格式錯誤或非預期的 JSON。
  4. 串流輸出的工具呼叫以片段形式到達,你需要透過 index 將它們拼接起來。

迴圈的外觀

如果你正在尋找可針對託管端點運作的 Ollama API 工具呼叫範例,其格式為標準的 OpenAI 格式。你在 tools 陣列中描述你的函式。模型會以文字回答,或回傳一個 tool_calls 清單,列出函式名稱及其參數。模型不會執行任何程式碼。你的程式碼負責執行函式、將輸出結果回傳,然後模型會撰寫最終的回應。

這使得工具使用的一次往返需要兩個 API 請求:

  1. 傳送使用者訊息和工具定義。
  2. 接收帶有 tool_calls 的 assistant 訊息。在本地執行每個呼叫。
  3. 傳送原始訊息、assistant 訊息,以及每個呼叫對應的一則 role: "tool" 訊息及其結果。
  4. 接收最終的文字回答。

範例使用 Base URL https://api.ollamaapi.com/v1 和模型 ID uncensored。端點是託管服務,因此金鑰來自 API 金鑰頁面。如果你更改 Base 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 帶有 delta.tool_calls 條目,其中包含 index。一個 index 的第一個片段包含呼叫 ID 和函式名稱。後續片段會新增 arguments 字串的切片。你按 index 拼接切片,並在串流結束後才解析 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 進行驗證,並在失敗時將錯誤以工具訊息回傳。模型通常會在下一輪自動修正。將迴圈次數限制在幾次以內,以免頑固的錯誤無限執行。

描述過於模糊

模型透過閱讀函式名稱和描述來選擇函式。像 「取得資料」 這樣的描述讓它無從下手。說明函式的作用、輸入的含義,並在參數描述中包含範例值。

同時工具過多

每個工具定義在每個請求中都計為輸入 token。每 1M 輸入 token 為 $0.15,十個各 150 token 的工具會增加 1,500 個 token,即每次請求 $0.000225,雖然金額小但在大量時並非零。更重要的是,長清單會降低選擇的可靠性。僅提供與當前步驟相關的工具。

工具結果過大

你回傳的任何內容都會作為輸入傳回。將大型資料庫列或網頁修剪為模型所需的欄位。上下文視窗總共為 100,000 個 token,因此非常大的結果可能會擠佔對話空間。

將工具與 JSON 模式混合

JSON 模式(response_format 搭配 json_object)可用,但請用於最終回答,而非用於希望模型呼叫函式的輪次。在依賴它之前,請在自己的提示詞上測試這種組合。

從本地 Ollama 遷移及檢查事項

如果你針對本地 Ollama 伺服器建立了第一個工具迴圈,幾乎沒有變化。將 Base 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,以 delta 串流輸出,並將結果以 role 為 tool 的訊息傳回。

為什麼工具呼叫後的第二個請求會失敗?

常見原因是缺少助理訊息,或是工具訊息缺少對應的 tool_call_id。先附加包含 tool_calls 的助理回合,然後為每個呼叫傳送一則工具訊息。

模型可以在一個回合中呼叫多個工具嗎?

它可以回傳包含多個條目的清單,因此請遍歷所有條目,並為每個條目傳送一則結果。不要假設清單只有一個項目。

我需要 Ollama 函式庫才能這樣做嗎?

不需要。官方的 OpenAI SDK 就足夠了。將 Base URL 指向端點,並使用 uncensored 模型。

只差一張表單,即可取得金鑰

建立帳號、複製金鑰、更改基礎 URL。這就是整個設定過程。

取得 API 金鑰閱讀文件