PL ▾

Przykład wywoływania narzędzi Ollama API z użyciem OpenAI SDK

Wywoływanie narzędzi pozwala modelowi poprosić Twój kod o uruchomienie funkcji, a następnie wykorzystać wynik w odpowiedzi. Poniżej znajdziesz kompletny przykład z OpenAI SDK dla hostowanego endpointu, wraz ze strumieniowaniem i błędami powodującymi większość niepowodzeń.

Zaktualizowano

Ilustracja modelu przekazującego zadanie do funkcji i otrzymującego wynik w pętli

Kluczowe punkty

  1. Wyślij definicję funkcji jako schemat JSON w polu tools; model odpowiada wywołaniami narzędzi zamiast tekstem.
  2. Uruchamiasz funkcję, dołączasz wynik jako wiadomość narzędzia i ponownie wywołujesz API.
  3. Zawsze parsuj argumenty defensywnie, ponieważ model może zwrócić niepoprawny lub nieoczekiwany JSON.
  4. Strumieniowane wywołania narzędzi przychodzą w fragmentach, które musisz połączyć według indeksu.

Jak wygląda pętla

Jeśli szukasz działającego przykładu wywoływania narzędzi Ollama API dla hostowanego endpointu, struktura jest standardowa dla OpenAI. Opisujesz swoje funkcje w tablicy tools. Model albo odpowiada tekstem, albo zwraca listę tool_calls z nazwą funkcji i jej argumentami. Model nigdy nic nie uruchamia. Twój kod uruchamia funkcję, wysyła wynik z powrotem, a model pisze ostateczną odpowiedź.

To oznacza jeden cykl użycia narzędzia, czyli dwa zapytania API:

  1. Wyślij wiadomość użytkownika i definicje narzędzi.
  2. Otrzymaj wiadomość asystenta z tool_calls. Uruchom każde wywołanie lokalnie.
  3. Wyślij oryginalne wiadomości, wiadomość asystenta i jedną wiadomość role: "tool" dla każdego wywołania z jej wynikiem.
  4. Otrzymaj ostateczną odpowiedź tekstową.

Przykłady używają base URL https://api.ollamaapi.com/v1 i ID modelu uncensored. Endpoint to usługa hostowana, więc klucz pochodzi ze strony klucz API. Ten sam kod zadziała też na lokalnym serwerze Ollama po zmianie base URL, klucza i nazwy modelu, o ile lokalny model obsługuje narzędzia.

Kompletny przykład w Pythonie

Przykład dostarcza modelowi jedną funkcję, get_order_status, opartą na małym słowniku. W prawdziwej aplikacji w tym miejscu zapytałoby się bazę danych lub usługę wewnętrzną.

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)

Trzy szczegóły mają tu znaczenie. Wiadomość asystenta zawierająca tool_calls musi trafić z powrotem do historii przed wynikami narzędzi, w przeciwnym razie kolejne zapytanie zostanie odrzucone lub zdezorientowane. Każda wiadomość narzędzia musi zawierać tool_call_id wywołania, na które odpowiada. A drugie zapytanie powinno nadal zawierać tools, ponieważ model może chcieć wywołać kolejną funkcję po zobaczeniu pierwszego wyniku.

To samo zapytanie z curl

Testowanie z curl usuwa SDK z równania, co pomaga, gdy nie jesteś pewien, czy problem jest w Twoim kodzie, czy w zapytaniu.

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

Pierwsza udana odpowiedź ma finish_reason ustawione na tool_calls, pustą lub null content i tablicę tool_calls. Pole arguments to ciąg znaków zawierający JSON, a nie obiekt zagnieżdżony, dlatego przykład Pythona wywołuje json.loads.

Pole tool_choice kontroluje siłę żądania. auto pozwala modelowi zdecydować. Przekazanie obiektu z nazwą funkcji prosi o tę konkretną funkcję. Użyj auto, chyba że masz powód, ponieważ wymuszenie wywołania na wiadomości, która go nie potrzebuje, generuje wymyślone argumenty.

Ta sama pętla w Node

Wersja Node używa oficjalnego pakietu openai i podąża tymi samymi krokami. Ta wersja obsługuje więcej niż jedno wywołanie na turę za pomocą 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);
}

Strumieniowanie wywołań narzędzi

Z stream: true odpowiedź przychodzi przez SSE, a wywołania narzędzi przychodzą w kawałkach. Każdy fragment zawiera wpis delta.tool_calls z index. Pierwszy fragment dla indeksu zawiera ID wywołania i nazwę funkcji. Późniejsze fragmenty dodają fragmenty ciągu arguments. Łączysz fragmenty według indeksu i parsujesz JSON dopiero po zakończeniu strumienia.

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"]))

Pamiętaj, że zapytanie ze strumieniowaniem (stream) zajmuje slot współbieżności aż do zakończenia strumienia. Pełny klucz pozwala na 8 zapytań jednocześnie i 300 na minutę, klucz próbny na 2 i 60. Jeśli uruchamiasz pętle narzędzi dla wielu użytkowników równolegle, liczy się każde otwarte strumieniowanie jako jedno zapytanie z pierwszego limitu.

Pułapki i jak je naprawić

Niepoprawne lub brakujące argumenty

Model to model o otwartych wagach, a modele open czasami zwracają argumenty, które nie są poprawnym JSON, pomijają wymagane pole lub wymyślają dodatkowe. Owiń json.loads w blok try, waliduj według swojego schematu i gdy się nie powiedzie, wyślij błąd z powrotem jako wiadomość narzędzia. Model zwykle poprawi się w kolejnej turze. Ogranicz pętlę do kilku iteracji, aby uporczywny błąd nie mógł trwać w nieskończoność.

Opisy zbyt ogólne

Model wybiera funkcję, czytając jej nazwę i opis. Opis taki jak "pobiera dane" nie daje mu nic do pracy. Powiedz, co robi funkcja, co oznaczają wejścia i dołącz przykładową wartość w opisie parametru.

Za dużo narzędzi naraz

Każda definicja narzędzia liczy się jako tokeny wejściowe w każdym zapytaniu. Przy $0.15 za 1M tokenów wejściowych, dziesięć narzędzi po 150 tokenów dodaje 1500 tokenów, co daje $0.000225 na zapytanie, mało, ale nie zero przy skali. Co ważniejsze, długie menu sprawia, że wybór jest mniej niezawodny. Oferuj tylko narzędzia istotne dla bieżącego kroku.

Zbyt duże wyniki narzędzi

Wszystko, co zwrócisz, jest podawane jako dane wejściowe. Ogranicz duże wiersze bazy danych lub strony internetowe do pól potrzebnych modelowi. Okno kontekstu ma łącznie 100 000 tokenów, więc bardzo duży wynik może wypierać rozmowę.

Mieszanie narzędzi z trybem JSON

Tryb JSON (response_format z json_object) jest dostępny, ale używaj go dla ostatecznej odpowiedzi, nie dla tur, gdzie chcesz, aby model wywołał funkcję. Przetestuj kombinację na własnych promptach przed oparciem się na niej.

Przejście z lokalnego Ollama i co sprawdzić

Jeśli zbudowałeś swoją pierwszą pętlę narzędzi na lokalnym serwerze Ollama, prawie nic się nie zmienia. Zmień base URL na https://api.ollamaapi.com/v1, użyj prawdziwego klucza i ustaw model na uncensored. Natywne trasy Ollama jak /api/chat nie istnieją tutaj. Tylko POST /v1/chat/completions i GET /v1/models są dostępne, więc kod napisany dla natywnego API potrzebuje wywołania w stylu OpenAI.

Przed wdrożeniem przejdź tę krótką listę kontrolną:

  • Potwierdź, że klucz działa z GET /v1/models.
  • Loguj nagłówek X-Request-Id dla każdego wywołania, aby można było prześledzić błędne wykonanie.
  • Obserwuj X-Balance-USD w nagłówkach odpowiedzi, jeśli uruchamiasz pętle narzędzi bez nadzoru.
  • Obsłuż HTTP 429: czekaj na rate_limited lub concurrency i szanuj Retry-After: 5 przy przeciążeniu.
  • Ustaw sztywny limit iteracji pętli.

Pętla narzędzi generuje co najmniej dwa zapytania na pytanie użytkownika, więc koszty rosną wraz z ich liczbą. Na przykład 2000 pętli z 1200 tokenami wejściowymi i 150 tokenami wyjściowymi na zapytanie, po dwa zapytania każda, daje 4000 zapytań, 4,8M tokenów wejściowych i 0,6M tokenów wyjściowych. To $0,72 plus $0,30, czyli łącznie $1,02. Strona ceny zawiera opcje doładowania, a docs wymienia obsługiwane pola zapytania. Nowi użytkownicy mogą przetestować wszystko za pomocą darmowego kredytu próbnego w wysokości $0,50 bez konieczności podawania karty.

Pytania i odpowiedzi

Czy endpoint obsługuje wywoływanie funkcji?

Tak. Akceptuje narzędzia i tool_choice, zwraca tool_calls, strumieniuje je jako delta i przyjmuje wyniki jako wiadomości z rolą tool.

Dlaczego drugie zapytanie kończy się niepowodzeniem po wywołaniu narzędzia?

Najczęstszą przyczyną jest brakująca wiadomość asystenta lub wiadomość narzędzia bez pasującego tool_call_id. Najpierw dołącz krok asystenta zawierający tool_calls, a następnie jedną wiadomość narzędzia dla każdego wywołania.

Czy model może wywołać kilka narzędzi w jednym zwrocie?

Może zwrócić listę z wieloma elementami, więc przejdź pętlą po wszystkich i wyślij wynik dla każdego. Nie zakładaj, że lista ma tylko jeden element.

Czy potrzebuję biblioteki Ollama?

Nie. Oficjalne SDK OpenAI wystarczą. Ustaw base URL na endpoint i użyj modelu uncensored.

Twój klucz jest o jeden formularz stąd

Utwórz konto, skopiuj klucz, zmień base URL. To cała konfiguracja.

Pobierz klucz APIPrzeczytaj dokumentację