ツール使用 — 関数呼び出し(function calling)とも呼ばれます — は、チャット モデルを実際に物事をこなせる存在に変えるものです。レコードを参照し、 API を叩き、計算を実行し、データベースに問い合わせる。モデル自身がコードを 実行するわけではありません。どの関数をどんな引数で呼ぶべきかを伝えてくるので、 あなたがそれを実行し、結果を返すと、モデルが回答を仕上げます。 朗報は、OpenAI の tools の形が事実上の標準であり、Brievio を 通せば、まったく同じコードが 1 つの base_url の背後で Claude と Gemini の両方を動かすことです。model 文字列を変えるだけで、ほかはすべてそのままにしておけます。
この記事は実践版です。ツールを定義し、tool_calls を読み取り、 マルチターンのループを最初から最後まで回し、並列呼び出しを処理する。 どのスニペットも OpenAI Python SDK を使って https://api.brievio.com/v1 に対してそのまま実行できます。 本番で驚かずにすむよう、モデルファミリー間で挙動が実際に異なる数少ない箇所には 印を付けておきます。
頭の中のモデル: 魔法の呼び出しではなく、ループ
関数呼び出しは一発勝負ではなく、会話です。常に同じ 4 拍子をたどります。
- あなたが送る — ユーザーのメッセージと、モデルが使ってよい ツールの一覧。
- モデルが判断する。 文章で答えるか、あるいは 1 つ以上の
tool_calls— 関数名と引数の JSON 文字列 — を返して止まる。 - あなたが関数を実行する — 自分のコードの中で実行し、その 結果を
toolメッセージとしてメッセージ一覧に追加する。 - あなたが再びモデルを呼ぶ — より長くなった履歴とともに。 モデルは結果を読み、回答するか、別のツールを求める。ツール呼び出しが なくなるまで繰り返す。
モデルはあなたのシステムに触れることは決してありません。提案するだけで、 実行の判断はあなたのコードが下します。この境界こそがツール使用のセキュリティの すべてです — モデルが送ってくる引数はどれも信頼できない入力として扱い、 フォームの入力欄と同じように検証してください。
ステップ 1 — ツールを定義し、呼び出しを読む
ツールとは {"type": "function", ...} で包んだ JSON Schema です。description フィールドは飾りではありません — モデルがいつ・ どのように呼ぶかを判断するために読む、唯一の手がかりです。新人エンジニア向けの docstring を書くつもりで書いてください。
# 標準的な OpenAI の "function" スキーマでツールを定義し、モデルが返す
# tool_calls を読み取る。Claude でも Gemini でもまったく同じ形。
from openai import OpenAI
import json
client = OpenAI(
api_key="sk-brievio-...",
base_url="https://api.brievio.com/v1",
)
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city.",
"parameters": {
"type": "object",
"properties": {
"city": {"type": "string", "description": "e.g. 'Tokyo'"},
"unit": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit.",
},
},
"required": ["city"],
},
},
}
]
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]
resp = client.chat.completions.create(
model="claude-sonnet-4-6", # "gemini-2.5-pro" に差し替えても以下のコードは同じ
messages=messages,
tools=tools,
tool_choice="auto", # 呼び出すかどうかはモデルに委ねる
)
msg = resp.choices[0].message
# モデルは文章で答えず、関数を実行するよう求めてきた。
if msg.tool_calls:
for call in msg.tool_calls:
print(call.function.name) # "get_weather"
print(call.function.arguments) # '{"city": "Tokyo", "unit": "celsius"}'
args = json.loads(call.function.arguments) # 常に JSON 文字列 — パースすること
else:
print(msg.content) # ツール不要の通常の回答ここで人がつまずく点が 2 つあります。第一に、 function.arguments は dict ではなく JSON 文字列 です — 必ず json.loads してください。第二に、モデルはツールを 呼ばないことを選ぶ場合があり、そのときは tool_calls が空で、content に通常の回答が入ります。両方に分岐させましょう。 これは model を claude-sonnet-4-6 に設定しても gemini-2.5-pro に設定しても同じです。Brievio はリクエストを 本物のファーストパーティのモデルに渡し、ネイティブのツール呼び出しを 返します — 作り替えたり偽装したりはしません。
ステップ 2 — マルチターンのループ
では往復をつなぎましょう。肝心なのはその形です。アシスタントのメッセージを返ってきたとおり厳密に追加し(呼び出しの id を保持しているため)、 続いて呼び出しごとに 1 つの tool メッセージを追加して、それぞれが 自分の tool_call_id を返すようにします。id を取り違えると次の リクエストは 400 を返します。これが両プロバイダーに対して 1 つの関数で動く ループの全体です。
# マルチターンのループ: モデルが求める -> あなたが関数を実行する ->
# その結果を返す -> モデルが最終的な回答を書く。
def run_get_weather(city: str, unit: str = "celsius") -> dict:
# 実際の実装: HTTP 呼び出し、DB 参照、なんでも。
return {"city": city, "temp": 18, "unit": unit, "sky": "clear"}
TOOL_IMPLS = {"get_weather": run_get_weather}
def answer(question: str, model: str) -> str:
messages = [{"role": "user", "content": question}]
while True:
resp = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
# ツールの要求なし -> これが最終回答。完了。
if not msg.tool_calls:
return msg.content
# 1. アシスタントのターンを返ってきたとおり厳密に追加する(次の
# メッセージが参照すべき tool_call の id を保持しているため)。
messages.append(msg)
# 2. 要求された各関数を実行し、呼び出しごとに 1 つの tool メッセージを
# 追加して、対応する tool_call_id を返す。
for call in msg.tool_calls:
fn = TOOL_IMPLS[call.function.name]
args = json.loads(call.function.arguments)
result = fn(**args)
messages.append({
"role": "tool",
"tool_call_id": call.id, # 呼び出しの id と必ず一致させる
"content": json.dumps(result), # 結果を文字列化する
})
# 3. ループ: モデルはツールの出力を見て続きを進める。
# 同じ関数が 1 つの base_url の背後で両プロバイダーに対して動作する:
print(answer("What's the weather in Tokyo?", "claude-sonnet-4-6"))
print(answer("What's the weather in Tokyo?", "gemini-2.5-pro"))この while True こそ、あなたがこれまで使ってきたあらゆる エージェントのエンジンです。モデルはツールを連鎖させられます — search を呼び、結果を読み、上位ヒットに対して get_details を呼び、それから答える — そしてこのループは特別な 場合分けなしに任意の深さを処理します。混乱したモデルが永遠に回り続けない よう、ガードレールとしてターンカウンタを追加してください。ほとんどの アプリでは 8〜10 ラウンドが妥当な上限です。
移植性について正直な注意点が 1 つ。プロトコルは Claude と Gemini で 同一ですが、挙動はクローンではありません。モデルファミリーによって 選ぶツールが違い、引数の表現が異なり、既存の知識から答えるのとツールを呼ぶのと どちらに積極的かもばらつきます。コードは変わりません。変わるのは判断です。 一方が他方に完璧に移ると決めつけず、本番投入する予定の各モデルに対して プロンプトをテストしてください。
ステップ 3 — 並列ツール呼び出し
質問が複数の独立した参照を必要とするとき — 3 都市の天気、5 つの SKU の 在庫 — 能力の高いモデルは 1 回のアシスタントターンですべての呼び出しを 返せます。あなたはそれらを(I/O バウンドな処理なら並行して)実行し、 再び問い合わせる前に id ごとに 1 つの tool メッセージを返します。
# 並列ツール呼び出し: 1 回のアシスタントターンで複数の関数を同時に
# 要求できる。あなたはそれらを(必要なら並行して)実行し、呼び出しの id ごとに
# 1 つの tool メッセージを返す。モデルが呼び出しをまとめるかどうかはまちまち —
# だから常にリストを反復処理し、ちょうど 1 件だと決めつけないこと。
messages = [{"role": "user",
"content": "Compare the weather in Tokyo, Paris and Cairo."}]
resp = client.chat.completions.create(
model="claude-sonnet-4-6",
messages=messages,
tools=tools,
tool_choice="auto",
)
msg = resp.choices[0].message
messages.append(msg)
# msg.tool_calls には今や別々の id を持つ get_weather 呼び出しが 3 件あるかもしれない。
from concurrent.futures import ThreadPoolExecutor
def handle(call):
args = json.loads(call.function.arguments)
result = TOOL_IMPLS[call.function.name](**args)
return {
"role": "tool",
"tool_call_id": call.id,
"content": json.dumps(result),
}
with ThreadPoolExecutor() as pool:
tool_msgs = list(pool.map(handle, msg.tool_calls or []))
messages.extend(tool_msgs) # 次のリクエストの前にすべての結果を追加する
final = client.chat.completions.create(
model="claude-sonnet-4-6", messages=messages, tools=tools,
)
print(final.choices[0].message.content)
# 注: モデルがまとめてではなく 1 件ずつ呼び出しを返す場合でも、前のスニペットの
# ループがそのまま処理してくれる — ただ実行ラウンドが増えるだけ。ここがモデルファミリー間でもっとも差が出る部分なので、決めつけを ハードコードしないでください。あるモデルが 1 ターンで並列に 呼び出しを出すか、それとも複数ターンにわたって 1 件ずつたどるかは、 ファミリーによって、ときにはリクエストによっても異なります。対策は単純で、 すでに上のコードに入っています。tool_calls を 反復処理し、必要ならループにラウンドを増やして回させることです。 返ってきたリストをループするコードはどちらの場合も正しく、ちょうど 1 件の 呼び出しだと決めつけるコードがバグです。同様に、厳格なスキーマの強制 (JSON が必ず妥当で、余分なキーは拒否される)も一律ではありません — どのモデルが生成したかにかかわらず、引数はサーバー側で検証し続けてください。
1 つの base_url こそが本当の勝ち筋
ゲートウェイがなければ、Claude と Gemini をサポートするとは、 2 つの SDK、2 つの認証方式、2 つのペイロード形、そして 2 通りの ツール結果の配管を意味します — 一方は Anthropic の tool_use/tool_result のコンテンツブロック、 もう一方は Google の function-call パーツ。Brievio の OpenAI 互換 エンドポイントの背後では、どちらも上で見た Chat Completions の tools 方言を話すので、モデル間の A/B テストは 1 行の差分で済み、 ツール層は一度書けば済みます。ツールのフィールドを含む完全な リクエスト/レスポンス仕様は Chat Completions のドキュメントにあり、 正確な id を載せた最新のモデル一覧は モデルページにあります。
はっきり言っておく価値があります。この価値が成り立つのは、向こう側の モデルが本物である場合に限られます。ツール呼び出しは実のところ有用な 本物らしさのシグナルです — 本物のフラッグシップは、込み入ったスキーマに 対しても妥当な引数を伴う整った tool_calls を確実に生成しますが、 安価な身代わりは JSON をしくじったりツールを無視したりしがちです。Brievio は 本物のファーストパーティのモデル(Claude Sonnet 4.6、Opus 4.7、 Gemini 2.5 Pro/Flash など)を提供し、ネイティブのツール呼び出しを尊重し、 正直なトークン数を報告します。自分で確かめたいなら、 あなたの Claude が本当に Claude かを確かめる方法をご覧ください。
現場の短いチェックリスト
- 引数は必ずパースする。
function.argumentsは文字列です。json.loadsしてから検証し、使ってください。 - id を返す。 アシスタントのメッセージをそのまま追加し、 続いて呼び出しごとに 1 つの
toolメッセージを、一致するtool_call_idとともに追加する。すべて次のリクエストの前に。 - リストをループする。 1 ターンに 1 件の呼び出しと決して 決めつけず、0 件・1 件・複数件を処理する。この習慣 1 つで、並列型でも 逐次型でも、どちらのモデルもそのまま動きます。
- ラウンドに上限を設ける。 ターンカウンタが無限の ツール呼び出しスパイラルを防ぎ、コストを抑えます。
- 何も信用しない。 引数はモデルの出力です。型・範囲・ 権限を、ユーザー入力とまったく同じように検証してください。
この 5 つを正しく押さえれば、Claude と Gemini をまたいで変更なしに動く ツール使用エージェントが手に入り、リクエストごとにコストや能力で ルーティングする選択肢も得られます。なお、失敗した 4xx/5xx 呼び出しは Brievio では課金されないので、 ツール定義を仕上げる過程で避けられないスキーマ調整の反復は無料です。 ツールの背後にどのモデルを置くか決める準備ができたら、 ゲートウェイ選定ガイド が本番で本当に効いてくるトレードオフを順に解説しています。