JA ▾

OpenAI SDK を使った Ollama API のツール呼び出し例 ツール呼び出し例

ツール呼び出しにより、モデルはコードに関数を実行させ、その結果を回答に使用できます。以下に、ホストされたエンドポイントに対して OpenAI SDK を使用した完全な例、ストリーミング、および多くの失敗を引き起こすミスを示します。

更新

モデルが関数にタスクを渡し、ループ内で結果を受け取る様子を示す図

主要ポイント

  1. 関数を JSON スキーマとして tools に送信します。モデルはテキストの代わりに tool_calls で応答します。
  2. 関数を実行し、結果を role tool メッセージとして追加し、API を再度呼び出します。
  3. モデルは形式が正しくない予期せぬ JSON を返す可能性があるため、常に防御的に引数を解析してください。
  4. ストリーミングされたツール呼び出しは断片として届くため、インデックスで結合する必要があります。

ループの形

ホストされたエンドポイントに対して動作する ollama API の関数呼び出しの例をお探しの場合は、その形は標準的な OpenAI のものです。関数は tools 配列で記述します。モデルはテキストで応答するか、関数名とその引数を含む tool_calls リストを返します。モデル自体は何もしません。あなたのコードが関数を実行し、その出力を戻し、モデルが最終的な返信を生成します。

これにより、ツール使用の 1 ラウンドトリップは 2 回の API 呼び出しになります:

  1. ユーザーメッセージとツール定義を送信します。
  2. tool_calls フィールドを持つアシスタントメッセージを受信します。各呼び出しをローカルで実行します。
  3. 元のメッセージ、アシスタントメッセージ、および各呼び出しに対して role: "tool" メッセージを 1 つずつ送信します。
  4. 最終的なテキスト応答を受信します。

例ではベース URL に https://api.ollamaapi.com/v1、モデル ID に uncensored を使用します。エンドポイントはホストサービスなので、キーは API キーページ から取得します。ローカルの Ollama サーバーでもベース URL、キー、モデル名を変更すれば同じコードが実行できます。ローカルモデルがツールをサポートしている限り、です。

完全な 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)

ここで重要な点は 3 つあります。tool_calls を含むアシスタントメッセージは、ツール結果の前に履歴に戻す必要があります。そうしないと、次のリクエストが拒否されるか、混乱します。各ツールメッセージには、応答する呼び出しの tool_call_id を記載する必要があります。また、2 番目のリクエストには 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 で 1 つのターンに複数の呼び出しを処理します。

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 で届き、ツール呼び出しは断片として届きます。各チャンクには delta.tool_calls エントリと index が含まれます。インデックスごとの最初の断片には呼び出し 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 リクエスト、1 分あたり 300 リクエストまで許可され、トライアルキーは 2 と 60 です。複数のユーザーに対して並列でツールループを実行する場合、開いているストリームを最初の数(並行リクエスト数)のカウントに含めてください。

陥りやすい罠とその解決策

形式が正しくないまたは欠落した引数

モデルはオープンウェイトモデルであり、オープンモデルは有効な JSON ではない引数を返したり、必須フィールドを省略したり、余分なフィールドを捏造することがあります。json.loads を try ブロックで囲み、スキーマに対して検証し、失敗した場合はエラーをツールメッセージとして送信します。モデルは通常次のターンで自己修正します。頑固な失敗が永遠に続かないように、ループを数回の反復で制限してください。

説明が曖昧すぎる

モデルは関数名と説明を読んで関数を選択します。「データを取得する」のような説明では手がかりがありません。関数の動作、入力の意味、パラメータの説明に例値を含めてください。

一度にツールが多すぎる

すべてのツール定義は、すべてのリクエストにおいて入力トークンとしてカウントされます。1M 入力トークンあたり $0.15 の場合、150 トークンのツールが 10 個あると 1,500 トークンが追加され、リクエストあたり $0.000225 となり、量が少ない場合は無視できるほど小さいですが、大量のリクエストではゼロではありません。より重要なのは、長いメニューでは選択の信頼性が低下することです。現在のステップに関連するツールのみを提供してください。

ツール結果が大きすぎる

返すものはすべて入力としてフィードされます。データベースの行やウェブページを、モデルが必要なフィールドに絞り込んでください。コンテキストウィンドウは合計 100,000 トークンであるため、非常に大きな結果が会話を圧迫する可能性があります。

ツールと 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 回のリクエストを行うため、コストはそれに比例します。例えば、1 回あたりの入力トークンが 1,200、出力トークンが 150 の 2,000 回のループで、各リクエストが 2 回の場合、合計 4,000 回、4.8M の入力トークンと 0.6M の出力トークンになります。合計は $1.02 ($0.72 + $0.30) です。料金ページにはチャージオプションが、ドキュメントにはサポートされるリクエストフィールドが記載されています。新規ユーザーはカード不要の $0.50 試用クレジットでこれらすべてを試すことができます。

質問と回答

エンドポイントは関数呼び出しに対応していますか?

はい。ツールと tool_choice を受け付け、tool_calls を返し、それらをデルタとしてストリーミングし、結果を role が tool のメッセージとして受け取ります。

ツール呼び出し後に 2 回目のリクエストが失敗する理由は何ですか?

一般的な原因は、アシスタントメッセージの欠落、または tool_call_id が一致しないツールメッセージです。まず tool_calls を含むアシスタントのターンを追加し、その後各呼び出しに対して 1 つずつツールメッセージを送信してください。

モデルは1回の応答で複数のツールを呼び出せますか?

複数の要素を含むリストを返す可能性があるため、それらすべてをループ処理し、それぞれに対して結果を送信してください。リストには1つの要素しか含まれないと想定しないでください。

これに Ollama ライブラリは必要ですか?

不要です。公式の OpenAI SDK で十分です。ベース URL をエンドポイントに設定し、モデル uncensored を使用してください。

キーはフォーム 1 つで入手可能です

アカウントを作成し、キーをコピーし、ベースURLを変更します。これでセットアップは完了です。

APIキーを取得ドキュメントを読む